Skip to content

Commit

Permalink
Support XSLT 1.0 by way of Saxon 6.5.5 and reflection
Browse files Browse the repository at this point in the history
  • Loading branch information
ndw committed Oct 26, 2021
1 parent 6d7dbba commit 49b9356
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 17 deletions.
Expand Up @@ -313,6 +313,8 @@ object XProcException {
def xcMissingHmacKey(location: Option[Location]): XProcException = stepError((36,6), location)
def xcValueNotFormUrlEncoded(value: String, location: Option[Location]): XProcException = stepError(37, value, location)
def xcVersionNotAvailable(version: String, location: Option[Location]): XProcException = stepError(38, version, location)
def xcBadXslt10Input(length: Int, location: Option[Location]): XProcException = stepError((39,1), length, location)
def xcBadXslt10Input(contentType: MediaType, location: Option[Location]): XProcException = stepError((39,2), contentType, location)

def xcCannotStore(href: URI, location: Option[Location]): XProcException = stepError(50, href, location)
def xcNotSchemaValidNvdl(href: String, message: String, location: Option[Location]): XProcException = stepError(53, List(href, message), location)
Expand Down Expand Up @@ -348,6 +350,7 @@ object XProcException {
def xcXQueryInvalidParameterType(name: QName, typename: String, location: Option[Location]): XProcException = stepError(102, List(name, typename), location)
def xcXQueryCompileError(msg: String, location: Option[Location]): XProcException = stepError(103, msg, location)
def xcXQueryEvalError(msg: String, location: Option[Location]): XProcException = stepError(104, msg, location)
def xcBadXslt10Parameter(name: QName, location: Option[Location]): XProcException = stepError(105, name, location)
def xcRejectDuplicateKeys(key: String, location: Option[Location]): XProcException = stepError(106, key, location)
def xcPrefixNotInScope(prefix: String, location: Option[Location]): XProcException = stepError(108, prefix, location)
def xcNamespaceDeleteCollision(uri: String, location: Option[Location]): XProcException = stepError(109, uri, location)
Expand Down
Expand Up @@ -127,6 +127,7 @@ object XProcConstants {
val cc_uri_resolver = new QName("cc", ns_cc, "uri-resolver")
val cc_verbose = new QName("cc", ns_cc, "verbose")
val cc_xmlcalabash = new QName("cc", ns_cc, "xmlcalabash")
val cc_xslt10_classpath = new QName("cc", ns_cc, "xslt10-classpath")

// The XML Schema type names must be defined somewhere in Saxon but...
val xs_ENTITY = new QName("xs", ns_xs, "ENTITY")
Expand Down
101 changes: 90 additions & 11 deletions src/main/scala/com/xmlcalabash/steps/Xslt.scala
@@ -1,10 +1,11 @@
package com.xmlcalabash.steps

import com.jafpl.steps.PortCardinality
import com.xmlcalabash.config.DocumentRequest
import com.xmlcalabash.exceptions.XProcException
import com.xmlcalabash.model.util.{SaxonTreeBuilder, ValueParser, XProcConstants}
import com.xmlcalabash.runtime.{BinaryNode, StaticContext, XProcMetadata, XmlPortSpecification}
import com.xmlcalabash.util.{S9Api, ValueUtils, XProcCollectionFinder}
import com.xmlcalabash.util.{MediaType, PipelineEnvironmentOptionString, S9Api, URIUtils, Urify, ValueUtils, XProcCollectionFinder, Xslt10ClassLoader, Xslt10Source}
import net.sf.saxon.Configuration
import net.sf.saxon.event.{PipelineConfiguration, Receiver}
import net.sf.saxon.expr.XPathContext
Expand All @@ -16,7 +17,9 @@ import net.sf.saxon.serialize.SerializationProperties
import net.sf.saxon.trans.XPathException
import net.sf.saxon.tree.wrapper.RebasedDocument

import java.net.URI
import java.io.{ByteArrayInputStream, ByteArrayOutputStream, OutputStream, StringReader}
import java.net.{URI, URL}
import javax.xml.transform.stream.StreamSource
import javax.xml.transform.{ErrorListener, SourceLocator, TransformerException}
import scala.collection.mutable
import scala.collection.mutable.ListBuffer
Expand All @@ -29,7 +32,7 @@ class Xslt extends DefaultXmlStep {
private val _template_name = new QName("", "template-name")
private val _output_base_uri = new QName("", "output-base-uri")

private var stylesheet = Option.empty[XdmNode]
private var stylesheet: XdmNode = _
private val inputSequence = ListBuffer.empty[XdmItem]
private val inputMetadata = ListBuffer.empty[XProcMetadata]

Expand Down Expand Up @@ -74,7 +77,7 @@ class Xslt extends DefaultXmlStep {
throw XProcException.xiThisCantHappen(s"Unexpected node type on XSLT input: ${item}", location);
}
case "stylesheet" =>
stylesheet = Some(item.asInstanceOf[XdmNode])
stylesheet = item.asInstanceOf[XdmNode]
case _ => ()
}
}
Expand Down Expand Up @@ -106,8 +109,8 @@ class Xslt extends DefaultXmlStep {
version = optionalStringBinding(XProcConstants._version)
populateDefaultCollection = booleanBinding(XProcConstants._populate_default_collection).getOrElse(populateDefaultCollection)

if (version.isEmpty && stylesheet.isDefined) {
val root = S9Api.documentElement(stylesheet.get)
if (version.isEmpty) {
val root = S9Api.documentElement(stylesheet)
version = Option(root.get.getAttributeValue(XProcConstants._version))
}

Expand All @@ -118,7 +121,7 @@ class Xslt extends DefaultXmlStep {
version.get match {
case "3.0" => xslt30()
case "2.0" => xslt20()
case "1.0" => throw XProcException.xcVersionNotAvailable(version.get, location)
case "1.0" => xslt10()
case _ => throw XProcException.xcVersionNotAvailable(version.get, location)
}
}
Expand Down Expand Up @@ -170,7 +173,7 @@ class Xslt extends DefaultXmlStep {
compiler.setSchemaAware(processor.isSchemaAware)

val exec = try {
compiler.compile(stylesheet.get.asSource())
compiler.compile(stylesheet.asSource())
} catch {
case sae: Exception =>
// Compile time exceptions are caught
Expand Down Expand Up @@ -276,9 +279,7 @@ class Xslt extends DefaultXmlStep {
transformer.setBaseOutputURI(base.toASCIIString)
}
} else {
if (stylesheet.isDefined && stylesheet.get.isInstanceOf[XdmNode]) {
transformer.setBaseOutputURI(stylesheet.get.getBaseURI.toASCIIString)
}
transformer.setBaseOutputURI(stylesheet.getBaseURI.toASCIIString)
}
}

Expand Down Expand Up @@ -383,6 +384,84 @@ class Xslt extends DefaultXmlStep {
}
}

private def xslt10(): Unit = {
if (inputSequence.length != 1) {
throw XProcException.xcBadXslt10Input(inputSequence.length, location)
}
if (!inputMetadata.head.contentType.markupContentType) {
throw XProcException.xcBadXslt10Input(inputMetadata.head.contentType, location)
}

val cp = config.config.parameters collect { case p: PipelineEnvironmentOptionString => p } filter {
_.eqname == XProcConstants.cc_xslt10_classpath.getEQName
}

val classpath = ListBuffer.empty[URL]
if (cp.nonEmpty) {
for (path <- cp) {
try {
val url = Urify.urify(path.value)
classpath += new URL(url)
} catch {
case _: Exception =>
logger.debug(s"Failed to add ${path.value} to classpath for XSLT 1.0")
}
}
}

if (classpath.isEmpty) {
throw XProcException.xcVersionNotAvailable(version.get, location)
}

val docSource = new Xslt10Source(inputSequence.head.toString, inputMetadata.head.baseURI.getOrElse(staticContext.baseURI.orNull))
val xslSource = new Xslt10Source(stylesheet.toString, stylesheet.getBaseURI)
val result = new ByteArrayOutputStream()

val params = mutable.HashMap.empty[String, String]
for ((qname, value) <- parameters) {
value match {
case atomic: XdmAtomicValue =>
params.put(qname.getClarkName, atomic.getStringValue)
case node: XdmNode =>
params.put(qname.getClarkName, node.getStringValue)
case _ =>
throw XProcException.xcBadXslt10Parameter(qname, location)
}
}

try {
val loader = new Xslt10ClassLoader(classpath.toArray)
val klass = loader.loadClass("com.xmlcalabash.steps.Xslt10")
val xslt10 = klass.getDeclaredConstructor(classOf[ClassLoader]).newInstance(loader)
val transform = klass.getMethod("transform",
classOf[Xslt10Source], classOf[Xslt10Source], classOf[OutputStream], classOf[Map[String,String]])

transform.invoke(xslt10, docSource, xslSource, result, params.toMap)

var outputURI = stylesheet.getBaseURI
if (outputBaseURI.isDefined) {
if (staticContext.baseURI.isDefined) {
outputURI = staticContext.baseURI.get.resolve(outputBaseURI.get)
} else {
outputURI = new URI(Urify.urify(outputBaseURI.get))
}
}

val req = new DocumentRequest(outputURI, MediaType.XML)
val resp = config.documentManager.parse(req, new ByteArrayInputStream(result.toByteArray))
consume(resp.value, "result", resp.props)
} catch {
case ex: XProcException =>
throw ex
case ex: Exception =>
val cause = ex.getCause
if (Option(cause).isDefined) {
throw cause
}
throw XProcException.xdStepFailed(Option(ex.getMessage).getOrElse("[no message]"), location)
}
}

override def reset(): Unit = {
super.reset()
goesBang = None
Expand Down
47 changes: 47 additions & 0 deletions src/main/scala/com/xmlcalabash/steps/Xslt10.scala
@@ -0,0 +1,47 @@
package com.xmlcalabash.steps

import com.xmlcalabash.exceptions.XProcException
import com.xmlcalabash.util.Xslt10Source

import java.io.OutputStream

class Xslt10(loader: ClassLoader) {
def transform(document: Xslt10Source, stylesheet: Xslt10Source, stream: OutputStream, params: Map[String, String]): Unit = {
val factoryClass = loader.loadClass("com.icl.saxon.TransformerFactoryImpl")
val streamResultClass = loader.loadClass("javax.xml.transform.stream.StreamResult")
val sourceClass = loader.loadClass("javax.xml.transform.Source")
val resultClass = loader.loadClass("javax.xml.transform.Result")

val factory = factoryClass.getDeclaredConstructor().newInstance()

try {
val newTransformerMethod = factory.getClass.getMethod("newTransformer", sourceClass)
val transformer = newTransformerMethod.invoke(factory, stylesheet.source)

val setParameterMethod = transformer.getClass.getMethod("setParameter", classOf[String], classOf[Any])
for ((key, value) <- params) {
setParameterMethod.invoke(transformer, key, value)
}

val streamResult = streamResultClass.getDeclaredConstructor(classOf[OutputStream]).newInstance(stream)

val transformMethod = transformer.getClass.getMethod("transform", sourceClass, resultClass)

transformMethod.invoke(transformer, document.source, streamResult)
} catch {
case ex: Exception =>
val cause = ex.getCause
if (Option(cause).isDefined) {
val message = Option(cause.getMessage).getOrElse("")
if (message.contains("Processing terminated by xsl:message")) {
throw XProcException.xcXsltUserTermination(cause.getMessage, None)
}
if (message.contains("Failed to compile")) {
throw XProcException.xcXsltCompileError(message, cause.asInstanceOf[Exception], None)
}
}

throw ex
}
}
}
10 changes: 4 additions & 6 deletions src/main/scala/com/xmlcalabash/testing/TestRunner.scala
Expand Up @@ -4,7 +4,7 @@ import com.jafpl.messages.Message
import com.xmlcalabash.XMLCalabash
import com.xmlcalabash.exceptions.TestException
import com.xmlcalabash.messages.XdmNodeItemMessage
import com.xmlcalabash.model.util.{SaxonTreeBuilder, ValueParser}
import com.xmlcalabash.model.util.{SaxonTreeBuilder, ValueParser, XProcConstants}
import com.xmlcalabash.model.xml.XMLContext
import com.xmlcalabash.runtime.{SaxonExpressionEvaluator, StaticContext, XProcLocation, XProcMetadata, XProcXPathExpression}
import com.xmlcalabash.util.{MediaType, S9Api, TypeUtils, URIUtils, Urify}
Expand Down Expand Up @@ -555,6 +555,8 @@ class TestRunner(processor: Processor, online: Boolean, regex: Option[String], t
}
}

val xmlcalabash = XMLCalabash.newInstance(processor)

var urifyFeature = Option.empty[String]
val features = node.getAttributeValue(_features)
if (features != null) {
Expand All @@ -565,10 +567,7 @@ class TestRunner(processor: Processor, online: Boolean, regex: Option[String], t
return result
}
if (features.contains("xslt-1")) {
val result = new TestResult(true) // skipped counts as a pass...
result.baseURI = node.getBaseURI
result.skipped = "XSLT 1.0 is not supported"
return result
xmlcalabash.args.config(XProcConstants.cc_xslt10_classpath, Urify.urify("src/test/resources/saxon-6.5.5.jar"))
}
if (features.contains("xquery_1_0")) {
val result = new TestResult(true) // skipped counts as a pass...
Expand Down Expand Up @@ -692,7 +691,6 @@ class TestRunner(processor: Processor, online: Boolean, regex: Option[String], t
throw new TestException("No pipeline for test")
}

val xmlcalabash = XMLCalabash.newInstance(processor)
xmlcalabash.configure()
context = new StaticContext(xmlcalabash)

Expand Down
45 changes: 45 additions & 0 deletions src/main/scala/com/xmlcalabash/util/Xslt10ClassLoader.scala
@@ -0,0 +1,45 @@
package com.xmlcalabash.util

import java.net.{URL, URLClassLoader}

// Special class loader to load Saxon 6
//
// Adapted from https://dzone.com/articles/java-classloader-handling
//
class Xslt10ClassLoader(classpath: Array[URL]) extends ClassLoader(Thread.currentThread().getContextClassLoader) {
private val childClassLoader = new ChildClassLoader(classpath, new DetectClass(this.getParent))

override protected def loadClass(name: String, resolve: Boolean): Class[_] = {
try {
childClassLoader.findClass(name)
} catch {
case _: ClassNotFoundException =>
super.loadClass(name, resolve)
}
}

private class ChildClassLoader(urls: Array[URL], parent: DetectClass) extends URLClassLoader(urls, null) {
private val realParent = parent

override def findClass(name: String): Class[_] = {
try {
val loaded = super.findLoadedClass(name)
if (loaded == null) {
super.findClass(name)
} else {
loaded
}
} catch {
case _: ClassNotFoundException =>
realParent.loadClass(name)
}
}
}

private class DetectClass(parent: ClassLoader) extends ClassLoader(parent) {
override protected def findClass(name: String): Class[_] = {
super.findClass(name)
}
}

}
21 changes: 21 additions & 0 deletions src/main/scala/com/xmlcalabash/util/Xslt10Source.scala
@@ -0,0 +1,21 @@
package com.xmlcalabash.util

import java.io.StringReader
import java.net.URI
import javax.xml.transform.stream.StreamSource

class Xslt10Source() {
private var _source: StreamSource = _

def source: StreamSource = _source

def this(document: String, baseURI: URI) = {
this()
_source = new StreamSource(new StringReader(document), baseURI.toString)
}

def this(document: URI) = {
this()
_source = new StreamSource(document.toURL.openStream(), document.toString)
}
}
Binary file added src/test/resources/saxon-6.5.5.jar
Binary file not shown.

0 comments on commit 49b9356

Please sign in to comment.