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

io.github.oliviercailloux.jaris.xml.XmlTransformerFactory Maven / Gradle / Ivy

The newest version!
package io.github.oliviercailloux.jaris.xml;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Verify.verify;
import static com.google.common.base.Verify.verifyNotNull;

import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.ByteSource;
import com.google.common.io.CharSource;
import com.google.common.io.Resources;
import io.github.oliviercailloux.jaris.collections.CollectionUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import javax.xml.transform.ErrorListener;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.URIResolver;
import javax.xml.transform.stream.StreamSource;
import net.sf.saxon.TransformerFactoryImpl;
import net.sf.saxon.jaxp.IdentityTransformer;
import net.sf.saxon.jaxp.TransformerImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 

* Instances of this class use two categories of event seriousness that can happen during parsing of * the schema or during transformation: information and problem. *

*

* An instance is either normal or {@link #pedanticTransformer pedantic}. If it is * normal, it logs information events and throws exceptions for problem events. If it is pedantic, * it throws exceptions for both information and problem events. *

* *

* XSL * messages * are considered information events iff their terminate attribute value is * no. The {@link ErrorListener errors} sent by the underlying processor are considered * information events iff they have severity warning and problem events iff they have * severity error or fatal. *

*

* The {@link KnownFactory#JDK system default factory} sometimes spits errors to the console, which * escapes the mechanism described here above, due to * a bug in the JDK. *

*/ public class XmlTransformerFactory { @SuppressWarnings("unused") private static final Logger LOGGER = LoggerFactory.getLogger(XmlTransformerFactory.class); public static final CharSource STRIP_WHITESPACE_STYLESHEET = Resources.asCharSource( Resources.getResource(XmlTransformerFactory.class, "Strip whitespace.xsl"), StandardCharsets.UTF_8); public static class OutputPropertyValue { private final Object value; public static OutputPropertyValue trueValue() { return new OutputPropertyValue(true); } public static OutputPropertyValue falseValue() { return new OutputPropertyValue(false); } public static OutputPropertyValue fromBoolean(boolean value) { return new OutputPropertyValue(value); } public static OutputPropertyValue fromInt(int value) { return new OutputPropertyValue(value); } private OutputPropertyValue(Object value) { this.value = value; } public String toOutputPropertyString() { if (value instanceof Boolean b) { return b ? "yes" : "no"; } if (value instanceof Integer i) { return Integer.toString(i); } throw new VerifyException("Unsupported value: " + value); } } /** * See https://www.w3.org/TR/2021/REC-xslt20-20210330/#serialization */ public static class OutputProperties { public static final URI XALAN_PROPERTIES_URI = URI.create("http://xml.apache.org/xslt"); /** * specifies whether the Transformer may add additional whitespace when outputting the result * tree */ public static final XmlName INDENT = XmlName.localName(OutputKeys.INDENT); public static final XmlName XALAN_INDENT_AMOUNT = XmlName.expandedName(XALAN_PROPERTIES_URI, "indent-amount"); public static final XmlName OMIT_XML_DECLARATION = XmlName.localName(OutputKeys.OMIT_XML_DECLARATION); public static OutputProperties none() { return new OutputProperties(ImmutableMap.of()); } /** * Indentation set to true; {@link KnownFactory#XALAN XALAN} indent amount set to 4. * * @return an OutputProperties object with indentation settings. */ public static OutputProperties indent() { return new OutputProperties(ImmutableMap.of(INDENT, OutputPropertyValue.trueValue(), XALAN_INDENT_AMOUNT, OutputPropertyValue.fromInt(4))); } public static OutputProperties noIndent() { return new OutputProperties(ImmutableMap.of(INDENT, OutputPropertyValue.falseValue())); } public static OutputProperties omitXmlDeclaration() { return new OutputProperties( ImmutableMap.of(OMIT_XML_DECLARATION, OutputPropertyValue.trueValue())); } public static OutputProperties fromMap(Map properties) { return new OutputProperties(properties); } private final ImmutableMap properties; public OutputProperties(Map properties) { this.properties = ImmutableMap.copyOf(properties); } public ImmutableMap asMap() { return properties; } ImmutableMap asStringMap() { return CollectionUtils.transformKeysAndValues(properties, XmlName::asFullName, ((x, s, b) -> b.toOutputPropertyString())); } } /** * Provides a transformer instance using the provided factory. * * @param factory the factory to use. * @return a transformer instance. * @see TransformerFactory#newInstance * @see TransformerFactory#newDefaultInstance */ public static XmlTransformerFactory usingFactory(TransformerFactory factory) { return generalTransformer(factory, XmlTransformErrorListener.WARNING_NOT_GRAVE_ERROR_LISTENER); } private static XmlTransformerFactory generalTransformer(TransformerFactory factory, XmlTransformErrorListener errorListener) { LOGGER.debug("Creating our transformer using factory {}.", factory); return new XmlTransformerFactory(factory, errorListener); } private final TransformerFactory factory; private final XmlTransformErrorListener errorListener; private XmlTransformerFactory(TransformerFactory tf, XmlTransformErrorListener errorListener) { this.factory = checkNotNull(tf); this.errorListener = errorListener; } /** * Returns a transformer factory that creates transformers which throw exceptions upon * encountering information events, including messages, even if specified as non-terminating. * * @return a pedantic transformer factory */ public XmlTransformerFactory pedantic() { return new XmlTransformerFactory(factory, XmlTransformErrorListener.EVERYTHING_GRAVE_ERROR_LISTENER); } TransformerFactory factory() { return factory; } /** * Returns a configured transformer that may be used to transform documents using the “identity” * transform and a default “indented” output property. * * @return a configured transformer */ public XmlTransformer usingEmptyStylesheet() { return usingStylesheetInternal(null, ImmutableMap.of(), OutputProperties.indent()); } /** * Returns a configured transformer that may be used to transform documents using the “identity” * transform. * * @param outputProperties any properties to be used with the transformer. * @return a configured transformer */ public XmlTransformer usingEmptyStylesheet(OutputProperties outputProperties) { return usingStylesheetInternal(null, ImmutableMap.of(), outputProperties); } /** * Returns a configured transformer that may be used to transform documents using the provided * stylesheet and a default “indented” output property. *

* Equivalent to {@link #usingStylesheet(ByteSource, Map)} with an empty map of parameters. *

* * @param stylesheet the stylesheet that indicates the transform to perform. * @return a configured transformer * @throws XmlException iff an error occurs when parsing the stylesheet. Wraps a * {@link TransformerConfigurationException}. */ public XmlTransformer usingStylesheet(ByteSource stylesheet) throws XmlException, IOException { return usingStylesheet(stylesheet, ImmutableMap.of(), OutputProperties.indent()); } /** * Returns a configured transformer that may be used to transform documents using the provided * stylesheet and a default “indented” output property. *

* Equivalent to {@link #usingStylesheet(CharSource, Map)} with an empty map of parameters. *

* * @param stylesheet the stylesheet that indicates the transform to perform. * @return a configured transformer * @throws XmlException iff an error occurs when parsing the stylesheet. Wraps a * {@link TransformerConfigurationException}. */ public XmlTransformer usingStylesheet(CharSource stylesheet) throws XmlException, IOException { return usingStylesheet(stylesheet, ImmutableMap.of(), OutputProperties.indent()); } /** * Returns a configured transformer that may be used to transform documents using the provided * stylesheet and a default “indented” output property. *

* Equivalent to {@link #usingStylesheet(ByteSource, Map)} with an empty map of parameters. *

* * @param stylesheet the stylesheet that indicates the transform to perform. * @return a configured transformer * @throws XmlException iff an error occurs when parsing the stylesheet. Wraps a * {@link TransformerConfigurationException}. */ public XmlTransformer usingStylesheet(URI stylesheet) throws XmlException { return usingStylesheet(stylesheet, ImmutableMap.of(), OutputProperties.indent()); } /** * Returns a configured transformer that may be used to transform documents using the provided * stylesheet parameterized with the given parameters and using a default “indented” output * property. * * @param stylesheet the stylesheet that indicates the transform to perform. * @param parameters any string parameters to be used with the given stylesheet, may be empty, * null keys or values not allowed. * @return a configured transformer * @throws XmlException iff an error occurs when parsing the stylesheet. Wraps a * {@link TransformerConfigurationException}. */ public XmlTransformer usingStylesheet(ByteSource stylesheet, Map parameters) throws XmlException, IOException { return usingStylesheet(stylesheet, parameters, OutputProperties.indent()); } /** * Returns a configured transformer that may be used to transform documents using the provided * stylesheet parameterized with the given parameters and using a default “indented” output * property. * * @param stylesheet the stylesheet that indicates the transform to perform. * @param parameters any string parameters to be used with the given stylesheet, may be empty, * null keys or values not allowed. * @return a configured transformer * @throws XmlException iff an error occurs when parsing the stylesheet. Wraps a * {@link TransformerConfigurationException}. */ public XmlTransformer usingStylesheet(CharSource stylesheet, Map parameters) throws XmlException, IOException { return usingStylesheet(stylesheet, parameters, OutputProperties.indent()); } /** * Returns a configured transformer that may be used to transform documents using the provided * stylesheet parameterized with the given parameters and using a default “indented” output * property. * * @param stylesheet the stylesheet that indicates the transform to perform. * @param parameters any string parameters to be used with the given stylesheet, may be empty, * null keys or values not allowed. * @return a configured transformer * @throws XmlException iff an error occurs when parsing the stylesheet. Wraps a * {@link TransformerConfigurationException}. */ public XmlTransformer usingStylesheet(URI stylesheet, Map parameters) throws XmlException { return usingStylesheet(stylesheet, parameters, OutputProperties.indent()); } public XmlTransformer usingStylesheet(CharSource stylesheet, Map parameters, OutputProperties outputProperties) throws XmlException, IOException { checkNotNull(stylesheet); checkNotNull(parameters); try (Reader r = stylesheet.openStream()) { return usingStylesheetInternal(new StreamSource(r), parameters, outputProperties); } } public XmlTransformer usingStylesheet(ByteSource stylesheet, Map parameters, OutputProperties outputProperties) throws XmlException, IOException { checkNotNull(stylesheet); checkNotNull(parameters); try (InputStream is = stylesheet.openStream()) { return usingStylesheetInternal(new StreamSource(is), parameters, outputProperties); } } /** * @param stylesheet will be resolved using the factory resolver and an empty base if a resolver * exists; if the resolver exists and returns null, that is an error; if it throws an * exception, it is thrown wrapped into an xmlexception. If no resolver exists, the given * stylesheet URI is considered a system id. * @param parameters * @param outputProperties * @return * @throws XmlException */ public XmlTransformer usingStylesheet(URI stylesheet, Map parameters, OutputProperties outputProperties) throws XmlException { checkNotNull(stylesheet); checkNotNull(parameters); String stylesheetStr = stylesheet.toString(); Source source; URIResolver resolver = factory.getURIResolver(); if (resolver == null) { source = new StreamSource(stylesheetStr); } else { final Source resolvedSource; try { resolvedSource = resolver.resolve(stylesheetStr, ""); } catch (TransformerException e) { throw new XmlException("Error resolving stylesheet URI.", e); } if (resolvedSource == null) { throw new XmlException( "URI resolver returned null for stylesheet URI " + stylesheetStr + "."); } source = resolvedSource; } verifyNotNull(source); return usingStylesheetInternal(source, parameters, outputProperties); } /** * Returns a configured transformer that may be used to transform documents using the provided * stylesheet parameterized with the given parameters. * * @param stylesheet the stylesheet that indicates the transform to perform. * @param parameters any string parameters to be used with the given stylesheet, may be empty, * null keys or values not allowed. * @param outputProperties any properties to be used with the transformer. * @return a configured transformer * @throws XmlException iff an error occurs when parsing the stylesheet. Wraps a * {@link TransformerConfigurationException}. */ public XmlTransformer usingStylesheet(Source stylesheet, Map parameters, OutputProperties outputProperties) throws XmlException { checkNotNull(stylesheet); checkNotNull(parameters); return usingStylesheetInternal(stylesheet, parameters, outputProperties); } /** * @param stylesheet may be null or empty, resolved already * @throws XmlException if there are errors when parsing the Source; wrapping a * {@link TransformerConfigurationException}. */ private XmlTransformer usingStylesheetInternal(Source stylesheet, Map parameters, OutputProperties outputProperties) throws XmlException { checkNotNull(parameters); checkNotNull(outputProperties); final Transformer transformer; LOGGER.debug("Obtaining transformer from stylesheet {}.", stylesheet); /* * Saxon says that this is deprecated * (https://www.saxonica.com/documentation12/index.html#!javadoc/net.sf.saxon.jaxp/ * SaxonTransformerFactory@setErrorListener), but we use it anyway. * https://saxonica.plan.io/boards/3/topics/9906 */ ErrorListener current = factory.getErrorListener(); if (stylesheet == null || stylesheet.isEmpty()) { try { factory.setErrorListener(errorListener); transformer = factory.newTransformer(); } catch (TransformerConfigurationException e) { throw new XmlException("Failed creating transformer.", e); } finally { factory.setErrorListener(current); } } else { try { factory.setErrorListener(errorListener); transformer = factory.newTransformer(stylesheet); } catch (TransformerConfigurationException e) { throw new XmlException("Could not parse the provided stylesheet.", e); } finally { factory.setErrorListener(current); } } LOGGER.debug("Obtained transformer from stylesheet {}.", stylesheet); /* This is required because of no default transmission of listeners. */ transformer.setErrorListener(errorListener); parameters.entrySet().stream() .forEach(e -> transformer.setParameter(e.getKey().asFullName(), e.getValue())); outputProperties.asStringMap().entrySet().stream() .forEach(e -> transformer.setOutputProperty(e.getKey(), e.getValue())); try { /* * https://stackoverflow.com/a/4699749. */ verify( factory instanceof TransformerFactoryImpl == transformer instanceof IdentityTransformer); if (transformer instanceof TransformerImpl saxonTransformer) { saxonTransformer.getUnderlyingXsltTransformer() .setMessageHandler(SaxonMessageHandler.newInstance()); } if (transformer instanceof TransformerImpl saxonTransformer) { if (errorListener.pedantic()) { return XmlTransformerSaxonPedanticImpl.using(saxonTransformer); } } } catch (NoClassDefFoundError e) { LOGGER.debug("Saxon not found, no special treatment.", e); } return XmlTransformerImpl.using(transformer); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy