Skip to content

Commit

Permalink
Custom XInclude attributes and resolution mechanisms (#1097)
Browse files Browse the repository at this point in the history
* Support `includeIf` and `includeUnless` in XInclude
* Resolve XIncludes in META-INF and resource roots
* Require a system property enabled to allow conditional XInclude
  • Loading branch information
novotnyr authored Jun 6, 2024
1 parent 85f83dd commit cadc5ed
Show file tree
Hide file tree
Showing 28 changed files with 512 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.jetbrains.plugin.structure.intellij.xinclude

import com.jetbrains.plugin.structure.intellij.resources.ResourceResolver
import com.jetbrains.plugin.structure.jar.META_INF
import java.nio.file.FileSystems
import java.nio.file.Path

/**
* Resolves a resource not only in the relative path, but also in the `META-INF` subdirectory.
*
* This is used to resolve plugin descriptors that are scattered across resource roots and `META-INF`
* directories, while XIncluding each other.
*/
class MetaInfResourceResolver(private val delegateResolver: ResourceResolver) : ResourceResolver {
override fun resolveResource(relativePath: String, basePath: Path): ResourceResolver.Result {
val parentPath = getParent(basePath) ?: return ResourceResolver.Result.NotFound
return delegateResolver.resolveResource(relativePath, parentPath.resolve(META_INF).resolve(relativePath))
}

private fun getParent(path: Path): Path? {
val parent: Path? = path.parent
return if (parent == null && path.fileSystem != FileSystems.getDefault()) {
path.fileSystem.rootDirectories.first()
} else {
parent
}
}
}

/**
* Resolves a resource not only in the base path, but also in the parent directory.
*
* This is used to resolve a XIncluded resource placed in the root directory being referenced
* from the `META-INF` directory.
*
*/
class InParentPathResourceResolver(private val delegateResolver: ResourceResolver) : ResourceResolver {
override fun resolveResource(relativePath: String, basePath: Path): ResourceResolver.Result {
val parentPath: Path = basePath.parent ?: return ResourceResolver.Result.NotFound
return delegateResolver.resolveResource(relativePath, parentPath)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

package com.jetbrains.plugin.structure.intellij.xinclude

import com.jetbrains.plugin.structure.base.utils.simpleName
import com.jetbrains.plugin.structure.intellij.plugin.PluginCreator
import com.jetbrains.plugin.structure.intellij.resources.CompositeResourceResolver
import com.jetbrains.plugin.structure.intellij.resources.ResourceResolver
import com.jetbrains.plugin.structure.intellij.utils.JDOMUtil
import org.jdom2.*
import java.lang.Boolean.parseBoolean
import java.nio.file.Path
import java.util.*
import java.util.regex.Pattern
Expand All @@ -18,7 +21,7 @@ import java.util.regex.Pattern
* The inspiring implementation is in IntelliJ Community class [`com.intellij.util.xmlb.JDOMXIncluder`](https://github.com/JetBrains/intellij-community/blob/master/platform/util/src/com/intellij/util/xmlb/JDOMXIncluder.java).
* This implementation provides better messages.
*/
class XIncluder private constructor(private val resourceResolver: ResourceResolver) {
class XIncluder private constructor(private val resourceResolver: ResourceResolver, private val properties: Properties) {

companion object {
@Throws(XIncluderException::class)
Expand All @@ -27,7 +30,7 @@ class XIncluder private constructor(private val resourceResolver: ResourceResolv
presentablePath: String,
resourceResolver: ResourceResolver,
documentPath: Path
): Document = XIncluder(resourceResolver).resolveXIncludes(document, presentablePath, documentPath)
): Document = XIncluder(resourceResolver, System.getProperties()).resolveXIncludes(document, presentablePath, documentPath)
}

private fun resolveXIncludes(document: Document, presentablePath: String, documentPath: Path): Document {
Expand All @@ -41,12 +44,43 @@ class XIncluder private constructor(private val resourceResolver: ResourceResolv
return Document(rootElement)
}

private fun resolveIncludeOrNonInclude(element: Element, bases: Stack<XIncludeEntry>): List<Content> =
if (isIncludeElement(element)) {
resolveXIncludeElements(element, bases)
private fun resolveIncludeOrNonInclude(element: Element, bases: Stack<XIncludeEntry>): List<Content> {
return if (isIncludeElement(element)) {
if (shouldXInclude(element, bases)) {
resolveXIncludeElements(element, bases)
} else {
emptyList()
}
} else {
listOf(resolveNonXIncludeElement(element, bases))
}
}

/**
* Handle conditional resolution of XInclude.
*
* - `includeIf`: Includes the document only if the corresponding property is set to a `true` value.
* - `includeUnless`: Includes the document if the corresponding property is either not set, or its value is `false`.
*
* Note: Although this feature is used by the Kotlin plugin, it should not be employed as a general purpose
* conditional inclusion method for other plugins.
*/
private fun shouldXInclude(element: Element, bases: Stack<XIncludeEntry>): Boolean {
val includeUnless: String? = element.getAttributeValueByLocalName(INCLUDE_UNLESS_ATTR_NAME)
val includeIf: String? = element.getAttributeValueByLocalName(INCLUDE_IF_ATTR_NAME)
if (isResolvingConditionalIncludes && includeUnless != null && includeIf != null) {
throw XIncluderException(
bases, "Cannot use '$INCLUDE_IF_ATTR_NAME' and '$INCLUDE_UNLESS_ATTR_NAME' attributes simultaneously. " +
"Specify either of these attributes or none to always include the document"
)
}

return if ((includeIf != null || includeUnless != null) && !isResolvingConditionalIncludes) {
false
} else includeIf == null && includeUnless == null
|| (includeIf != null && properties.isTrue(includeIf))
|| (includeUnless != null && properties.isFalse(includeUnless))
}

private fun resolveXIncludeElements(xincludeElement: Element, bases: Stack<XIncludeEntry>): List<Content> {
//V2 included configs can be located only in root
Expand All @@ -67,8 +101,13 @@ class XIncluder private constructor(private val resourceResolver: ResourceResolv
}

val basePath = bases.peek()!!.documentPath
val resolver = CompositeResourceResolver(mutableListOf<ResourceResolver>().apply {
add(resourceResolver)
if (basePath.isInMetaInf()) add(InParentPathResourceResolver(resourceResolver))
if (basesHaveMetaInfResolution(bases)) add(MetaInfResourceResolver(resourceResolver))
})

when (val resourceResult = resourceResolver.resolveResource(href, basePath)) {
when (val resourceResult = resolver.resolveResource(href, basePath)) {
is ResourceResolver.Result.Found -> resourceResult.use {
val remoteDocument = try {
JDOMUtil.loadDocument(it.resourceStream.buffered())
Expand All @@ -95,6 +134,15 @@ class XIncluder private constructor(private val resourceResolver: ResourceResolv
}
}

private fun Path.isInMetaInf(): Boolean {
val parent: Path? = parent
return parent?.simpleName == "META-INF"
}

private fun basesHaveMetaInfResolution(bases: Stack<XIncludeEntry>): Boolean {
return bases.any { it.documentPath.isInMetaInf() }
}

private fun resolveXIncludesOfRemoteDocument(
remoteDocument: Document,
xincludeElement: Element,
Expand Down Expand Up @@ -213,6 +261,23 @@ class XIncluder private constructor(private val resourceResolver: ResourceResolv

private fun isIncludeElement(element: Element): Boolean =
element.name == INCLUDE && element.namespace == HTTP_XINCLUDE_NAMESPACE

private fun Element.getAttributeValueByLocalName(attributeLocalName: String): String? {
val attr = this.attributes.find { it.name == attributeLocalName }
return attr?.value
}

private fun Properties.isTrue(key: String?): Boolean {
return parseBoolean(getProperty(key))
}

private fun Properties.isFalse(key: String?): Boolean {
return !parseBoolean(getProperty(key))
}

private val isResolvingConditionalIncludes: Boolean
get() = properties.isTrue(IS_RESOLVING_CONDITIONAL_INCLUDES_PROPERTY)

}

private const val HTTP_WWW_W3_ORG_2001_XINCLUDE = "http://www.w3.org/2001/XInclude"
Expand All @@ -226,4 +291,9 @@ private const val XPOINTER = "xpointer"
private val HTTP_XINCLUDE_NAMESPACE = Namespace.getNamespace(XI, HTTP_WWW_W3_ORG_2001_XINCLUDE)

private val XPOINTER_PATTERN = Pattern.compile("xpointer\\((.*)\\)")
private val XPOINTER_SELECTOR_PATTERN = Pattern.compile("/([^/]*)(/[^/]*)?/\\*")
private val XPOINTER_SELECTOR_PATTERN = Pattern.compile("/([^/]*)(/[^/]*)?/\\*")

private const val INCLUDE_UNLESS_ATTR_NAME = "includeUnless"
private const val INCLUDE_IF_ATTR_NAME = "includeIf"

const val IS_RESOLVING_CONDITIONAL_INCLUDES_PROPERTY = "com.jetbrains.plugin.structure.intellij.xinclude.isResolvingConditionalIncludes"
Loading

0 comments on commit cadc5ed

Please sign in to comment.