/* * eXist Open Source Native XML Database * Copyright (C) 2001-2011 The eXist Project * http://exist-db.org * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * $Id$ */ package org.exist.xquery.functions.transform; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.dom.QName; import org.exist.dom.memtree.DocumentBuilderReceiver; import org.exist.dom.memtree.MemTreeBuilder; import org.exist.dom.persistent.DocumentImpl; import org.exist.dom.persistent.NodeProxy; import org.exist.http.servlets.ResponseWrapper; import org.exist.numbering.NodeId; import org.exist.security.PermissionDeniedException; import org.exist.storage.lock.Lock.LockMode; import org.exist.storage.serializers.EXistOutputKeys; import org.exist.storage.serializers.Serializer; import org.exist.storage.serializers.XIncludeFilter; import org.exist.util.serializer.Receiver; import org.exist.util.serializer.ReceiverToSAX; import org.exist.xmldb.XmldbURI; import org.exist.xquery.*; import org.exist.xquery.functions.response.ResponseModule; import org.exist.xquery.value.*; import org.exist.xslt.TransformerFactoryAllocator; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.xml.sax.SAXException; import javax.xml.transform.*; import javax.xml.transform.sax.SAXResult; import javax.xml.transform.sax.SAXTransformerFactory; import javax.xml.transform.sax.TemplatesHandler; import javax.xml.transform.sax.TransformerHandler; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; import java.util.Properties; /** * @author Wolfgang Meier (wolfgang@exist-db.org) */ public class Transform extends BasicFunction { public final static FunctionSignature signatures[] = { new FunctionSignature( new QName("transform", TransformModule.NAMESPACE_URI, TransformModule.PREFIX), "Applies an XSL stylesheet to the node tree passed as first argument. The stylesheet " + "is specified in the second argument. This should either be an URI or a node. If it is an " + "URI, it can either point to an external location or to an XSL stored in the db by using the " + "'xmldb:' scheme. Stylesheets are cached unless they were just created from an XML " + "fragment and not from a complete document. " + "Stylesheet parameters " + "may be passed in the third argument using an XML fragment with the following structure: " + "<parameters><param name=\"param-name1\" value=\"param-value1\"/>" + "</parameters>. There are two special parameters named \"exist:stop-on-warn\" and " + "\"exist:stop-on-error\". If set to value \"yes\", eXist will generate an XQuery error " + "if the XSL processor reports a warning or error.", new SequenceType[]{ new FunctionParameterSequenceType("node-tree", Type.NODE, Cardinality.ZERO_OR_MORE, "The source-document (node tree)"), new FunctionParameterSequenceType("stylesheet", Type.ITEM, Cardinality.EXACTLY_ONE, "The XSL stylesheet"), new FunctionParameterSequenceType("parameters", Type.NODE, Cardinality.ZERO_OR_ONE, "The transformer parameters") }, new FunctionReturnSequenceType(Type.NODE, Cardinality.ZERO_OR_ONE, "the transformed result (node tree)")), new FunctionSignature( new QName("transform", TransformModule.NAMESPACE_URI, TransformModule.PREFIX), "Applies an XSL stylesheet to the node tree passed as first argument. The stylesheet " + "is specified in the second argument. This should either be an URI or a node. If it is an " + "URI, it can either point to an external location or to an XSL stored in the db by using the " + "'xmldb:' scheme. Stylesheets are cached unless they were just created from an XML " + "fragment and not from a complete document. " + "Stylesheet parameters " + "may be passed in the third argument using an XML fragment with the following structure: " + "<parameters><param name=\"param-name\" value=\"param-value\"/>" + "</parameters>. There are two special parameters named \"exist:stop-on-warn\" and " + "\"exist:stop-on-error\". If set to value \"yes\", eXist will generate an XQuery error " + "if the XSL processor reports a warning or error. " + "The fourth argument specifies attributes to be set on the used Java TransformerFactory with the following structure: " + "<attributes><attr name=\"attr-name\" value=\"attr-value\"/></attributes>. " + "The fifth argument specifies serialization " + "options in the same way as if they " + "were passed to \"declare option exist:serialize\" expression. An additional serialization option, " + "\"xinclude-path\", is supported, which specifies a base path against which xincludes will be expanded " + "(if there are xincludes in the document). A relative path will be relative to the current " + "module load path.", new SequenceType[]{ new FunctionParameterSequenceType("node-tree", Type.NODE, Cardinality.ZERO_OR_MORE, "The source-document (node tree)"), new FunctionParameterSequenceType("stylesheet", Type.ITEM, Cardinality.EXACTLY_ONE, "The XSL stylesheet"), new FunctionParameterSequenceType("parameters", Type.NODE, Cardinality.ZERO_OR_ONE, "The transformer parameters"), new FunctionParameterSequenceType("attributes", Type.NODE, Cardinality.ZERO_OR_ONE, "Attributes to pass to the transformation factory"), new FunctionParameterSequenceType("serialization-options", Type.STRING, Cardinality.ZERO_OR_ONE, "The serialization options")}, new FunctionReturnSequenceType(Type.NODE, Cardinality.ZERO_OR_ONE, "the transformed result (node tree)")), new FunctionSignature( new QName("stream-transform", TransformModule.NAMESPACE_URI, TransformModule.PREFIX), "Applies an XSL stylesheet to the node tree passed as first argument. The parameters are the same " + "as for the transform function. stream-transform can only be used within a servlet context. Instead " + "of returning the transformed document fragment, it directly streams its output to the servlet's output stream. " + "It should thus be the last statement in the XQuery.", new SequenceType[]{ new FunctionParameterSequenceType("node-tree", Type.NODE, Cardinality.ZERO_OR_MORE, "The source-document (node tree)"), new FunctionParameterSequenceType("stylesheet", Type.ITEM, Cardinality.EXACTLY_ONE, "The XSL stylesheet"), new FunctionParameterSequenceType("parameters", Type.NODE, Cardinality.ZERO_OR_ONE, "The transformer parameters") }, new SequenceType(Type.ITEM, Cardinality.EMPTY)), new FunctionSignature( new QName("stream-transform", TransformModule.NAMESPACE_URI, TransformModule.PREFIX), "Applies an XSL stylesheet to the node tree passed as first argument. The parameters are the same " + "as for the transform function. stream-transform can only be used within a servlet context. Instead " + "of returning the transformed document fragment, it directly streams its output to the servlet's output stream. " + "It should thus be the last statement in the XQuery.", new SequenceType[]{ new FunctionParameterSequenceType("node-tree", Type.NODE, Cardinality.ZERO_OR_MORE, "The source-document (node tree)"), new FunctionParameterSequenceType("stylesheet", Type.ITEM, Cardinality.EXACTLY_ONE, "The XSL stylesheet"), new FunctionParameterSequenceType("parameters", Type.NODE, Cardinality.ZERO_OR_ONE, "The transformer parameters"), new FunctionParameterSequenceType("attributes", Type.NODE, Cardinality.ZERO_OR_ONE, "Attributes to pass to the transformation factory"), new FunctionParameterSequenceType("serialization-options", Type.STRING, Cardinality.ZERO_OR_ONE, "The serialization options")}, new SequenceType(Type.ITEM, Cardinality.EMPTY)) }; private static final Logger logger = LogManager.getLogger(Transform.class); private final Map<String, CachedStylesheet> cache = new HashMap<>(); private boolean caching = true; private boolean stopOnError = true; private boolean stopOnWarn = false; /** * @param context * @param signature */ public Transform(XQueryContext context, FunctionSignature signature) { super(context, signature); final Object property = context.getBroker().getConfiguration().getProperty(TransformerFactoryAllocator.PROPERTY_CACHING_ATTRIBUTE); if (property != null) { caching = (Boolean) property; } } /* (non-Javadoc) * @see org.exist.xquery.BasicFunction#eval(org.exist.xquery.value.Sequence[], org.exist.xquery.value.Sequence) */ public Sequence eval(Sequence[] args, Sequence contextSequence) throws XPathException { final Properties attributes = new Properties(); final Properties serializationProps = new Properties(); final Properties stylesheetParams = new Properties(); // Parameter 1 & 2 final Sequence inputNode = args[0]; final Item stylesheetItem = args[1].itemAt(0); // Parse 3rd parameter final Node options = args[2].isEmpty() ? null : ((NodeValue) args[2].itemAt(0)).getNode(); if (options != null) { stylesheetParams.putAll(parseParameters(options)); } // Parameter 4 when present if (getArgumentCount() >= 4) { final Sequence attrs = args[3]; attributes.putAll(extractAttributes(attrs)); } // Parameter 5 when present if (getArgumentCount() >= 5) { //extract serialization options final Sequence serOpts = args[4]; serializationProps.putAll(extractSerializationProperties(serOpts)); } else { context.checkOptions(serializationProps); } boolean expandXIncludes = "yes".equals(serializationProps.getProperty(EXistOutputKeys.EXPAND_XINCLUDES, "yes")); // Setup handler and error listener final TransformerHandler handler = createHandler(stylesheetItem, stylesheetParams, attributes); final TransformErrorListener errorListener = new TransformErrorListener(); handler.getTransformer().setErrorListener(errorListener); if (isCalledAs("transform")) { //transform:transform() final Transformer transformer = handler.getTransformer(); if ("org.exist.xslt.TransformerImpl".equals(transformer.getClass().getName())) { context.pushDocumentContext(); final Sequence seq = ((org.exist.xslt.Transformer) transformer).transform(args[0]); context.popDocumentContext(); return seq; } else { final ValueSequence seq = new ValueSequence(); context.pushDocumentContext(); final MemTreeBuilder builder = context.getDocumentBuilder(); final DocumentBuilderReceiver builderReceiver = new DocumentBuilderReceiver(builder, true); final SAXResult result = new SAXResult(builderReceiver); result.setLexicalHandler(builderReceiver); //preserve comments etc... from xslt output handler.setResult(result); final Receiver receiver = new ReceiverToSAX(handler); final Serializer serializer = context.getBroker().getSerializer(); serializer.reset(); try { serializer.setProperties(serializationProps); serializer.setReceiver(receiver, true); if (expandXIncludes) { String xiPath = serializationProps.getProperty(EXistOutputKeys.XINCLUDE_PATH); if (xiPath != null) { final Path f = Paths.get(xiPath).normalize(); if (!f.isAbsolute()) { xiPath = Paths.get(context.getModuleLoadPath(), xiPath).normalize().toAbsolutePath().toString(); } } else { xiPath = context.getModuleLoadPath(); } serializer.getXIncludeFilter().setModuleLoadPath(xiPath); } serializer.toSAX(inputNode, 1, inputNode.getItemCount(), false, false, 0, 0); } catch (final Exception e) { throw new XPathException(this, "Exception while transforming node: " + e.getMessage(), e); } errorListener.checkForErrors(); Node next = builder.getDocument().getFirstChild(); while (next != null) { seq.add((NodeValue) next); next = next.getNextSibling(); } context.popDocumentContext(); return seq; } } else { //transform:stream-transform() final ResponseModule myModule = (ResponseModule) context.getModule(ResponseModule.NAMESPACE_URI); // response object is read from global variable $response final Variable respVar = myModule.resolveVariable(ResponseModule.RESPONSE_VAR); if (respVar == null) { throw new XPathException(this, ErrorCodes.XPDY0002, "No response object found in the current XQuery context."); } if (respVar.getValue().getItemType() != Type.JAVA_OBJECT) { throw new XPathException(this, ErrorCodes.XPDY0002, "Variable $response is not bound to an Java object."); } final JavaObjectValue respValue = (JavaObjectValue) respVar.getValue().itemAt(0); if (!"org.exist.http.servlets.HttpResponseWrapper".equals(respValue.getObject().getClass().getName())) { throw new XPathException(this, ErrorCodes.XPDY0002, signatures[1] + " can only be used within the EXistServlet or XQueryServlet"); } final ResponseWrapper response = (ResponseWrapper) respValue.getObject(); //setup the response correctly final String mediaType = handler.getTransformer().getOutputProperty("media-type"); final String encoding = handler.getTransformer().getOutputProperty("encoding"); if (mediaType != null) { if (encoding == null) { response.setContentType(mediaType); } else { response.setContentType(mediaType + "; charset=" + encoding); } } //do the transformation try { final OutputStream os = new BufferedOutputStream(response.getOutputStream()); final StreamResult result = new StreamResult(os); handler.setResult(result); final Serializer serializer = context.getBroker().getSerializer(); serializer.reset(); Receiver receiver = new ReceiverToSAX(handler); try { serializer.setProperties(serializationProps); if (expandXIncludes) { XIncludeFilter xinclude = new XIncludeFilter(serializer, receiver); String xiPath = serializationProps.getProperty(EXistOutputKeys.XINCLUDE_PATH); if (xiPath != null) { final Path f = Paths.get(xiPath).normalize(); if (!f.isAbsolute()) { xiPath = Paths.get(context.getModuleLoadPath(), xiPath).normalize().toAbsolutePath().toString(); } } else { xiPath = context.getModuleLoadPath(); } xinclude.setModuleLoadPath(xiPath); receiver = xinclude; } serializer.setReceiver(receiver); serializer.toSAX(inputNode); } catch (final Exception e) { throw new XPathException(this, "Exception while transforming node: " + e.getMessage(), e); } errorListener.checkForErrors(); os.close(); //commit the response response.flushBuffer(); } catch (final IOException e) { throw new XPathException(this, "IO exception while transforming node: " + e.getMessage(), e); } return Sequence.EMPTY_SEQUENCE; } } /** * @param stylesheetItem * @param options * @param attributes Attributes to set on the Transformer Factory * @throws TransformerFactoryConfigurationError * @throws XPathException */ private TransformerHandler createHandler(Item stylesheetItem, Properties options, Properties attributes) throws TransformerFactoryConfigurationError, XPathException { final SAXTransformerFactory factory = TransformerFactoryAllocator.getTransformerFactory(context.getBroker().getBrokerPool()); //set any attributes for (final Map.Entry<Object, Object> attribute : attributes.entrySet()) { factory.setAttribute((String) attribute.getKey(), attribute.getValue()); } TransformerHandler handler; try { Templates templates = null; if (Type.subTypeOf(stylesheetItem.getType(), Type.NODE)) { final NodeValue stylesheetNode = (NodeValue) stylesheetItem; // if the passed node is a document node or document root element, // we construct an XMLDB URI and use the caching implementation. if (stylesheetNode.getImplementationType() == NodeValue.PERSISTENT_NODE) { final NodeProxy root = (NodeProxy) stylesheetNode; if (root.getNodeId() == NodeId.DOCUMENT_NODE || root.getNodeId().getTreeLevel() == 1) { //as this is a persistent node (e.g. a stylesheet stored in the db) //set the URI Resolver as a DatabaseResolver factory.setURIResolver(new EXistURIResolver(context.getBroker(), root.getOwnerDocument().getCollection().getURI().toString())); final String uri = XmldbURI.XMLDB_URI_PREFIX + context.getBroker().getBrokerPool().getId() + "://" + root.getOwnerDocument().getURI(); templates = getSource(factory, uri); } } if (templates == null) { if (stylesheetItem instanceof Document) { String uri = ((Document) stylesheetItem).getDocumentURI(); /* * This must be checked because in the event the stylesheet is * an in-memory document, it will cause an NPE */ if (uri != null) { uri = uri.substring(0, uri.lastIndexOf('/')); factory.setURIResolver(new EXistURIResolver(context.getBroker(), uri)); } } templates = getSource(factory, stylesheetNode); } } else { if (stylesheetItem instanceof Document) { String uri = ((Document) stylesheetItem).getDocumentURI(); /* * This must be checked because in the event the stylesheet is * an in-memory document, it will cause an NPE */ if (uri != null) { uri = uri.substring(0, uri.lastIndexOf('/')); factory.setURIResolver(new EXistURIResolver(context.getBroker(), uri)); } } final String stylesheet = stylesheetItem.getStringValue(); templates = getSource(factory, stylesheet); } handler = factory.newTransformerHandler(templates); if (options != null) { setParameters(options, handler.getTransformer()); } } catch (final TransformerConfigurationException e) { throw new XPathException(this, "Unable to set up transformer: " + e.getMessage(), e); } return handler; } private Properties extractSerializationProperties(final Sequence serOpts) throws XPathException { final Properties serializationProps = new Properties(); if (!serOpts.isEmpty()) { final String[] contents = Option.tokenize(serOpts.getStringValue()); for (String content : contents) { final String[] pair = Option.parseKeyValuePair(content); if (pair == null) { throw new XPathException(this, "Found invalid serialization option: " + content); } logger.info("Setting serialization property: " + pair[0] + " = " + pair[1]); serializationProps.setProperty(pair[0], pair[1]); } } return serializationProps; } private Properties extractAttributes(final Sequence attrs) throws XPathException { if (attrs.isEmpty()) { return new Properties(); } else { return parseElementParam(((NodeValue) attrs.itemAt(0)).getNode(), "attributes", "attr"); } } private Properties parseParameters(final Node options) throws XPathException { return parseElementParam(options, "parameters", "param"); } private Properties parseElementParam(final Node elementParam, final String container, final String param) throws XPathException { final Properties props = new Properties(); if (elementParam.getNodeType() == Node.ELEMENT_NODE && elementParam.getLocalName().equals(container)) { Node child = elementParam.getFirstChild(); while (child != null) { if (child.getNodeType() == Node.ELEMENT_NODE && child.getLocalName().equals(param)) { final Element elem = (Element) child; final String name = elem.getAttribute("name"); final String value = elem.getAttribute("value"); if (name == null || value == null) { throw new XPathException(this, "Name or value attribute missing"); } if ("exist:stop-on-warn".equals(name)) { stopOnWarn = "yes".equals(value); } else if ("exist:stop-on-error".equals(name)) { stopOnError = "yes".equals(value); } else { props.setProperty(name, value); } } child = child.getNextSibling(); } } return props; } private void setParameters(Properties parameters, Transformer handler) { for (Object o : parameters.keySet()) { final String key = (String) o; handler.setParameter(key, parameters.getProperty(key)); } } private Templates getSource(SAXTransformerFactory factory, String stylesheet) throws XPathException, TransformerConfigurationException { String base; if (stylesheet.indexOf(':') == Constants.STRING_NOT_FOUND) { Path f = Paths.get(stylesheet).normalize(); if (Files.isReadable(f)) { stylesheet = f.toUri().toASCIIString(); } else { stylesheet = context.getModuleLoadPath() + File.separatorChar + stylesheet; f = Paths.get(stylesheet).normalize(); if (Files.isReadable(f)) { stylesheet = f.toUri().toASCIIString(); } } } //TODO : use dedicated function in XmldbURI final int p = stylesheet.lastIndexOf("/"); if (p != Constants.STRING_NOT_FOUND) { base = stylesheet.substring(0, p); } else { base = stylesheet; } CachedStylesheet cached = cache.get(stylesheet); try { if (cached == null) { cached = new CachedStylesheet(factory, stylesheet, base); cache.put(stylesheet, cached); } return cached.getTemplates(); } catch (final MalformedURLException e) { LOG.debug(e.getMessage(), e); throw new XPathException(this, "Malformed URL for stylesheet: " + stylesheet, e); } catch (final IOException e) { throw new XPathException(this, "IO error while loading stylesheet: " + stylesheet, e); } } private Templates getSource(SAXTransformerFactory factory, NodeValue stylesheetRoot) throws XPathException, TransformerConfigurationException { final TemplatesHandler handler = factory.newTemplatesHandler(); try { handler.startDocument(); stylesheetRoot.toSAX(context.getBroker(), handler, null); handler.endDocument(); return handler.getTemplates(); } catch (final SAXException e) { throw new XPathException(this, "A SAX exception occurred while compiling the stylesheet: " + e.getMessage(), e); } } public static class ExternalResolver implements URIResolver { private final String baseURI; public ExternalResolver(final String base) { this.baseURI = base; } @Override public Source resolve(final String href, final String base) throws TransformerException { try { //TODO : use dedicated function in XmldbURI final URL url = new URL(baseURI + "/" + href); final URLConnection connection = url.openConnection(); return new StreamSource(connection.getInputStream()); } catch (final IOException e) { LOG.warn(e); return null; } } } //TODO: revisit this class with XmldbURI in mind private class CachedStylesheet { SAXTransformerFactory factory; long lastModified = -1; Templates templates = null; String uri; public CachedStylesheet(SAXTransformerFactory factory, String uri, String baseURI) throws TransformerConfigurationException, IOException, XPathException { this.factory = factory; this.uri = uri; if (!baseURI.startsWith(XmldbURI.EMBEDDED_SERVER_URI_PREFIX)) { factory.setURIResolver(new ExternalResolver(baseURI)); } getTemplates(); } public Templates getTemplates() throws TransformerConfigurationException, IOException, XPathException { if (uri.startsWith(XmldbURI.EMBEDDED_SERVER_URI_PREFIX)) { final String docPath = uri.substring(XmldbURI.EMBEDDED_SERVER_URI_PREFIX.length()); DocumentImpl doc = null; try { doc = context.getBroker().getXMLResource(XmldbURI.create(docPath), LockMode.READ_LOCK); if (!caching || (doc != null && (templates == null || doc.getMetadata().getLastModified() > lastModified))) { templates = getSource(doc); } lastModified = doc.getMetadata().getLastModified(); } catch (final PermissionDeniedException e) { throw new XPathException(Transform.this, "Permission denied to read stylesheet: " + uri); } finally { if (doc != null) { doc.getUpdateLock().release(LockMode.READ_LOCK); } } } else { final URL url = new URL(uri); final URLConnection connection = url.openConnection(); long modified = connection.getLastModified(); if (!caching || (templates == null || modified > lastModified || modified == 0)) { LOG.debug("compiling stylesheet " + url); try (final InputStream is = connection.getInputStream()) { templates = factory.newTemplates(new StreamSource(is)); } } lastModified = modified; } return templates; } private Templates getSource(DocumentImpl stylesheet) throws XPathException, TransformerConfigurationException { factory.setURIResolver(new EXistURIResolver(context.getBroker(), stylesheet.getCollection().getURI().toString())); final TransformErrorListener errorListener = new TransformErrorListener(); factory.setErrorListener(errorListener); final TemplatesHandler handler = factory.newTemplatesHandler(); try { handler.startDocument(); final Serializer serializer = context.getBroker().getSerializer(); serializer.reset(); serializer.setSAXHandlers(handler, null); serializer.toSAX(stylesheet); handler.endDocument(); final Templates t = handler.getTemplates(); errorListener.checkForErrors(); return t; } catch (final Exception e) { if (e instanceof XPathException) { throw (XPathException) e; } throw new XPathException(Transform.this, "An exception occurred while compiling the stylesheet: " + stylesheet.getURI() + ": " + e.getMessage(), e); } } } private class TransformErrorListener implements ErrorListener { private final static int NO_ERROR = 0; private final static int WARNING = 1; private final static int ERROR = 2; private final static int FATAL = 3; private int errorCode = NO_ERROR; private Exception exception; protected void checkForErrors() throws XPathException { switch (errorCode) { case WARNING: if (stopOnWarn) { throw new XPathException("XSL transform reported warning: " + exception.getMessage(), exception); } break; case ERROR: if (stopOnError) { throw new XPathException("XSL transform reported error: " + exception.getMessage(), exception); } break; case FATAL: throw new XPathException("XSL transform reported error: " + exception.getMessage(), exception); } } public void warning(TransformerException except) throws TransformerException { LOG.warn("XSL transform reports warning: " + except.getMessage(), except); errorCode = WARNING; exception = except; if (stopOnWarn) { throw except; } } public void error(TransformerException except) throws TransformerException { LOG.warn("XSL transform reports recoverable error: " + except.getMessage(), except); errorCode = ERROR; exception = except; if (stopOnError) { throw except; } } public void fatalError(TransformerException except) throws TransformerException { LOG.warn("XSL transform reports fatal error: " + except.getMessage(), except); errorCode = FATAL; exception = except; throw except; } } }