All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jetbrains.plugin.structure.intellij.xinclude.XIncluder.kt Maven / Gradle / Ivy

Go to download

Library for parsing JetBrains IDE plugins. Can be used to verify that plugin complies with the JetBrains Marketplace requirements.

There is a newer version: 3.290
Show newest version
/*
 * Copyright 2000-2020 JetBrains s.r.o. and other contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
 */

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

import com.jetbrains.plugin.structure.intellij.plugin.PluginCreator
import com.jetbrains.plugin.structure.intellij.resources.ResourceResolver
import com.jetbrains.plugin.structure.intellij.utils.JDOMUtil
import org.jdom2.*
import java.nio.file.Path
import java.util.*
import java.util.regex.Pattern

/**
 * Resolves all `` references in xml documents using the provided path resolver.
 *
 * 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) {

  companion object {
    @Throws(XIncluderException::class)
    fun resolveXIncludes(
      document: Document,
      presentablePath: String,
      resourceResolver: ResourceResolver,
      documentPath: Path
    ): Document = XIncluder(resourceResolver).resolveXIncludes(document, presentablePath, documentPath)
  }

  private fun resolveXIncludes(document: Document, presentablePath: String, documentPath: Path): Document {
    val startEntry = XIncludeEntry(presentablePath, documentPath)
    if (isIncludeElement(document.rootElement)) {
      throw XIncluderException(listOf(startEntry), "Invalid root element ${document.rootElement.getElementNameAndAttributes()}")
    }
    val bases = Stack()
    bases.push(startEntry)
    val rootElement = resolveNonXIncludeElement(document.rootElement, bases)
    return Document(rootElement)
  }

  private fun resolveIncludeOrNonInclude(element: Element, bases: Stack): List =
    if (isIncludeElement(element)) {
      resolveXIncludeElements(element, bases)
    } else {
      listOf(resolveNonXIncludeElement(element, bases))
    }

  private fun resolveXIncludeElements(xincludeElement: Element, bases: Stack): List {
    //V2 included configs can be located only in root
    val href = xincludeElement.getAttributeValue(HREF).let { if (PluginCreator.v2ModulePrefix.matches(it)) "/$it" else it}
    val presentableXInclude = xincludeElement.getElementNameAndAttributes()
    if (href.isNullOrEmpty()) {
      throw XIncluderException(bases, "Missing or empty 'href' attribute in $presentableXInclude")
    }

    val parseAttribute = xincludeElement.getAttributeValue(PARSE)
    if (parseAttribute != null && parseAttribute != XML) {
      throw XIncluderException(bases, "Attribute 'parse' must be 'xml' but was '$parseAttribute' in $presentableXInclude")
    }

    val baseAttribute = xincludeElement.getAttributeValue(BASE, Namespace.XML_NAMESPACE)
    if (baseAttribute != null) {
      throw XIncluderException(bases, "'base' attribute of xi:include is not supported!")
    }

    val basePath = bases.peek()!!.documentPath

    when (val resourceResult = resourceResolver.resolveResource(href, basePath)) {
      is ResourceResolver.Result.Found -> resourceResult.use {
        val remoteDocument = try {
          JDOMUtil.loadDocument(it.resourceStream.buffered())
        } catch (e: Exception) {
          throw XIncluderException(bases, "Invalid document '$href' referenced in $presentableXInclude", e)
        }

        val xincludeEntry = XIncludeEntry(href, resourceResult.path)
        val xIncludeElements = resolveXIncludesOfRemoteDocument(remoteDocument, xincludeElement, xincludeEntry, bases)
        val startComment = Comment("Start $presentableXInclude")
        val endComment = Comment("End $presentableXInclude")
        return listOf(startComment) + xIncludeElements + listOf(endComment)
      }
      is ResourceResolver.Result.NotFound -> {
        val fallbackElement = xincludeElement.getChild("fallback", xincludeElement.namespace)
        if (fallbackElement != null) {
          return emptyList()
        }
        throw XIncluderException(bases, "Not found document '$href' referenced in $presentableXInclude.  element is not provided.")
      }
      is ResourceResolver.Result.Failed -> {
        throw XIncluderException(bases, "Failed to load document referenced in $presentableXInclude", resourceResult.exception)
      }
    }
  }

  private fun resolveXIncludesOfRemoteDocument(
    remoteDocument: Document,
    xincludeElement: Element,
    xincludeEntry: XIncludeEntry,
    bases: Stack
  ): List {
    val presentableXInclude = xincludeElement.getElementNameAndAttributes()
    checkCyclicReference(xincludeEntry, bases)

    if (!remoteDocument.hasRootElement()) {
      throw XIncluderException(bases, "Remote root element is not set for document referenced in $presentableXInclude")
    }

    if (remoteDocument.content.count { it is Element } > 1) {
      throw XIncluderException(bases, "Multiple root elements in document referenced in $presentableXInclude")
    }

    bases.push(xincludeEntry)
    val remoteContents = try {
      resolveIncludeOrNonInclude(remoteDocument.rootElement, bases)
    } finally {
      bases.pop()
    }

    if (remoteContents.isEmpty()) {
      return emptyList()
    }

    if (remoteContents.size > 1) {
      throw XIncluderException(bases, "Multiple elements referenced in $presentableXInclude")
    }

    val remoteRootElement = remoteContents.single() as? Element
      ?: throw XIncluderException(bases, "Root element, not '${remoteContents.single().cType}', must have been resolved in $presentableXInclude")

    return selectContents(xincludeElement, xincludeEntry, remoteRootElement, bases)
  }

  private fun checkCyclicReference(xincludeEntry: XIncludeEntry, bases: Stack) {
    val index = bases.indexOf(xincludeEntry)
    if (index >= 0) {
      val cycle = bases.drop(index) + listOf(xincludeEntry)
      val prefix = bases.take(index + 1)
      throw XIncluderException(prefix, "Circular includes: " + cycle.joinToString(separator = " -> ") { it.presentablePath })
    }
  }

  private fun resolveNonXIncludeElement(element: Element, bases: Stack): Element {
    val result = Element(element.name, element.namespace)
    if (element.hasAttributes()) {
      for (attribute in element.attributes) {
        result.setAttribute(attribute.clone())
      }
    }

    if (element.hasAdditionalNamespaces()) {
      for (additionalNamespace in element.additionalNamespaces) {
        result.addNamespaceDeclaration(additionalNamespace)
      }
    }

    for (content in element.content) {
      if (content is Element) {
        result.addContent(resolveIncludeOrNonInclude(content, bases))
      } else {
        result.addContent(content.clone())
      }
    }

    return result
  }

  private fun selectContents(
    xincludeElement: Element,
    xincludeEntry: XIncludeEntry,
    remoteRootElement: Element,
    bases: Stack
  ): List {
    val xPointer = xincludeElement.getAttributeValue(XPOINTER)
      ?: return remoteRootElement.content.toList().map { it.detach() }

    val pointerMatcher = XPOINTER_PATTERN.matcher(xPointer)
    if (!pointerMatcher.matches()) {
      throw XIncluderException(bases, "Invalid xpointer value in ${xincludeElement.getElementNameAndAttributes()}")
    }

    val pointerSelector = pointerMatcher.group(1)

    val selectorMatcher = XPOINTER_SELECTOR_PATTERN.matcher(pointerSelector)
    if (!selectorMatcher.matches()) {
      throw XIncluderException(bases, "Invalid xpointer selector value in ${xincludeElement.getElementNameAndAttributes()}")
    }

    val rootTagName = selectorMatcher.group(1)

    if (remoteRootElement.name != rootTagName) {
      return emptyList()
    }

    val subTagName = selectorMatcher.group(2)?.drop(1)
    val selectedChildren = if (subTagName != null) {
      val child = remoteRootElement.getChild(subTagName)
        ?: throw XIncluderException(bases, "No elements are selected in document '${xincludeEntry.presentablePath}' referenced in ${xincludeElement.getElementNameAndAttributes()}")
      child.content
    } else {
      remoteRootElement.content
    }.toList()

    selectedChildren.forEach { it.detach() }
    return selectedChildren
  }

  private fun Element.getElementNameAndAttributes(): String {
    return "<$qualifiedName " + attributes.joinToString { "${it.name}=\"${it.value}\"" } + "/>"
  }

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

private const val HTTP_WWW_W3_ORG_2001_XINCLUDE = "http://www.w3.org/2001/XInclude"
private const val XI = "xi"
private const val INCLUDE = "include"
private const val HREF = "href"
private const val BASE = "base"
private const val PARSE = "parse"
private const val XML = "xml"
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("/([^/]*)(/[^/]*)?/\\*")




© 2015 - 2024 Weber Informatics LLC | Privacy Policy