1
votes

I use saxon (HE 9.9.1-6) to transform a XML to a HTML file. Saxon is used because the XSLT is version 2 and the default java classes failed.
The XSLT contains two statements to copy in the content of other files:

<xsl:value-of select="unparsed-text('file.ext')"/>

This works fine as long as the Xslt and those files are in the same directory and the xslt is given as a file source

Source xslt = new StreamSource(new File("c:/somedir/file.xsl"));

But my xslt is inside a resource directory (later on it's supposed to be packed in a jar). If I use it in that context, saxon fails to find the included files becuase it looks in the root directory of my project:

Source xslt = new StreamSource(getClass().getClassLoader().getResourceAsStream("file.xsl"));

results in:

Error evaluating (fn:unparsed-text(...)) in xsl:value-of/@select on line 22 column 66
FOUT1170: Failed to read input file: <project root directory>\included_file.css (File not found)

Is there any way that I could supply saxon with additional StreamSources for the files it needs to include? I was unable to find anything.

Ideally, I'd like something like this:

transformer.addInput(new StreamSource(getClass().getClassLoader().getResourceAsStream("inputfile.css")));

The only solution I've came up with was pretty ugly: Copy the xslt and the files it needs from the resources to a temporary directory and then do the conversion using that as a source.


Example code

I'm not knowledgeable in writing XSLT so I can only offer non-minimal example files.
The xslt and its two reuqired files (css and js) can be found here. The ones you need are the three "xrechnung" ones. Direct links: xrechnung-html.xsl, xrechnung-viewer.css, xrechnung-viewer.js.
Please put them in a resource directory (just in case, in eclipse: make a resources-folder and add it as a source directory in properties->build path).

The xml was generated by the first step of above project using it's own example files, I put it on pastebin here
(originally included directly but got character limit error)

Finally, the Java-Code including the ugly workaround:

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Comparator;

import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;

import org.xml.sax.SAXException;

import net.sf.saxon.s9api.Processor;
import net.sf.saxon.s9api.SaxonApiException;
import net.sf.saxon.s9api.Serializer;
import net.sf.saxon.s9api.Xslt30Transformer;
import net.sf.saxon.s9api.XsltCompiler;
import net.sf.saxon.s9api.XsltExecutable;

public class SaxonProblem {
    public static void main(String[] args) throws IOException, SaxonApiException, SAXException {
        Path xml = Paths.get("path/to/the.xml");
        //working(xml);
        notWorking(xml);
    }

    public static void working(Path xmlFile) throws IOException, SaxonApiException, SAXException {
        Path dir = Files.createTempDirectory("saxon");
        System.out.println("Temp dir: " + dir.toString());
        Path xsltFile = dir.resolve("xrechnung-html.xsl");

        Files.copy(SaxonProblem.class.getClassLoader().getResourceAsStream("xrechnung-html.xsl"),
                xsltFile, StandardCopyOption.REPLACE_EXISTING);
        Files.copy(SaxonProblem.class.getClassLoader().getResourceAsStream("xrechnung-viewer.css"),
                dir.resolve("xrechnung-viewer.css"), StandardCopyOption.REPLACE_EXISTING);
        Files.copy(SaxonProblem.class.getClassLoader().getResourceAsStream("xrechnung-viewer.js"),
                dir.resolve("xrechnung-viewer.js"), StandardCopyOption.REPLACE_EXISTING);

        // for the sake of brevity, the html is made where the xml was
        Path html = xmlFile.resolveSibling(xmlFile.getFileName().toString() + ".html");
        Source xslt = new StreamSource(xsltFile.toFile());
        Source xml = new StreamSource(xmlFile.toFile());

        transformXml(xml, xslt, html);

        // cleanup
        Files.walk(dir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
    }

    public static void notWorking(Path xmlFile) throws SaxonApiException, SAXException, IOException {
        // for the sake of brevity, the html is made where the xml was
        Path html = xmlFile.resolveSibling(xmlFile.getFileName().toString() + ".html");

        Source xslt = new StreamSource(SaxonProblem.class.getClassLoader().getResourceAsStream("xrechnung-html.xsl"));
        Source xml = new StreamSource(xmlFile.toFile());
        transformXml(xml, xslt, html);
    }

    public static void transformXml(Source xml, Source xslt, Path output) throws SaxonApiException, SAXException, IOException {
        Processor processor = new Processor(false);
        XsltCompiler compiler = processor.newXsltCompiler();
        XsltExecutable stylesheet = compiler.compile(xslt);
        Serializer out = processor.newSerializer(output.toFile());
        out.setOutputProperty(Serializer.Property.METHOD, "html");
        out.setOutputProperty(Serializer.Property.INDENT, "yes");
        Xslt30Transformer transformer = stylesheet.load30();
        transformer.transform(xml, out);
    }
}

Solution

Thanks to the comment by Martin Honnen and the answer by Michael Kay, I have a solution using UnparsedTextURIResolver. Does feel more like a hack, but it works and is better than my previous workaround:

Processor processor = new Processor(false);
UnparsedTextURIResolver defaultUtur = processor.getUnderlyingConfiguration().getUnparsedTextURIResolver();
processor.getUnderlyingConfiguration().setUnparsedTextURIResolver(new UnparsedTextURIResolver() {
    @Override
    public Reader resolve(URI arg0, String arg1, Configuration arg2) throws XPathException {
        if (arg0.toString().endsWith("myfilename.css")) {
            InputStream css = SaxonProblem.class.getClassLoader().getResourceAsStream("myfilename.css");
            return new InputStreamReader(css);
        }
        return defaultUtur.resolve(arg0, arg1, arg2);
    }
});
//[...]
1
If you want to resolve URIs for the doc or document function used in your XSLT code, then you can use saxonica.com/html/documentation/javadoc/net/sf/saxon/s9api/…. I think unparsed-text has its own resolver saxonica.com/html/documentation/javadoc/net/sf/saxon/lib/…. I am not quite sure whether transformer.addInput(new StreamSource(getClass().getClassLoader().getResourceAsStream("inputfile.css"))); can not simply be solved by adding and setting a global parameter. - Martin Honnen
I found the URI-resolver initially and noticed that it wass never called (at least from my XSLT). Didn't find any other resolver - but searhing for the UnparsedTextURIResolver I found tha this is set on the configuration instead on the compiler or another class I already used. Thanks! - Kadser

1 Answers

3
votes

Some suggestions:

  • Use a URI with the classpath: scheme (fairly recent addition and may not be supported on all paths where URIs are used)

  • Register an UnparsedTextResolver with the configuration; Saxon will delegate the task of finding the resource to this resolver

  • Supply the name of the containing directory as a parameter to the stylesheet, and use the resolve-uri() function to get the absolute URI