/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.camel.builder.xml; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.ErrorListener; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Templates; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.URIResolver; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stax.StAXSource; import javax.xml.transform.stream.StreamSource; import org.w3c.dom.Node; import org.xml.sax.EntityResolver; import org.apache.camel.Exchange; import org.apache.camel.ExpectedBodyTypeException; import org.apache.camel.Message; import org.apache.camel.Processor; import org.apache.camel.RuntimeTransformException; import org.apache.camel.TypeConverter; import org.apache.camel.converter.jaxp.StAX2SAXSource; import org.apache.camel.converter.jaxp.XmlConverter; import org.apache.camel.support.SynchronizationAdapter; import org.apache.camel.util.ExchangeHelper; import org.apache.camel.util.FileUtil; import org.apache.camel.util.IOHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.apache.camel.util.ObjectHelper.notNull; /** * Creates a <a href="http://camel.apache.org/processor.html">Processor</a> * which performs an XSLT transformation of the IN message body. * <p/> * Will by default output the result as a String. You can chose which kind of output * you want using the <tt>outputXXX</tt> methods. * * @version */ public class XsltBuilder implements Processor { private static final Logger LOG = LoggerFactory.getLogger(XsltBuilder.class); private Map<String, Object> parameters = new HashMap<String, Object>(); private XmlConverter converter = new XmlConverter(); private Templates template; private volatile BlockingQueue<Transformer> transformers; private ResultHandlerFactory resultHandlerFactory = new StringResultHandlerFactory(); private boolean failOnNullBody = true; private URIResolver uriResolver; private boolean deleteOutputFile; private ErrorListener errorListener; private boolean allowStAX = true; private EntityResolver entityResolver; public XsltBuilder() { } public XsltBuilder(Templates templates) { this.template = templates; } @Override public String toString() { return "XSLT[" + template + "]"; } public void process(Exchange exchange) throws Exception { notNull(getTemplate(), "template"); if (isDeleteOutputFile()) { // add on completion so we can delete the file when the Exchange is done String fileName = ExchangeHelper.getMandatoryHeader(exchange, Exchange.XSLT_FILE_NAME, String.class); exchange.addOnCompletion(new XsltBuilderOnCompletion(fileName)); } Transformer transformer = getTransformer(); configureTransformer(transformer, exchange); ResultHandler resultHandler = resultHandlerFactory.createResult(exchange); Result result = resultHandler.getResult(); // let's copy the headers before we invoke the transform in case they modify them Message out = exchange.getOut(); out.copyFrom(exchange.getIn()); // the underlying input stream, which we need to close to avoid locking files or other resources InputStream is = null; try { Source source; // only convert to input stream if really needed if (isInputStreamNeeded(exchange)) { is = exchange.getIn().getBody(InputStream.class); source = getSource(exchange, is); } else { Object body = exchange.getIn().getBody(); source = getSource(exchange, body); } if (source instanceof StAXSource) { // Always convert StAXSource to SAXSource. // * Xalan and Saxon-B don't support StAXSource. // * The JDK default implementation (XSLTC) doesn't handle CDATA events // (see com.sun.org.apache.xalan.internal.xsltc.trax.StAXStream2SAX). // * Saxon-HE/PE/EE seem to support StAXSource, but don't advertise this // officially (via TransformerFactory.getFeature(StAXSource.FEATURE)) source = new StAX2SAXSource(((StAXSource) source).getXMLStreamReader()); } LOG.trace("Using {} as source", source); transformer.transform(source, result); LOG.trace("Transform complete with result {}", result); resultHandler.setBody(out); } finally { releaseTransformer(transformer); // IOHelper can handle if is is null IOHelper.close(is); } } // Builder methods // ------------------------------------------------------------------------- /** * Creates an XSLT processor using the given templates instance */ public static XsltBuilder xslt(Templates templates) { return new XsltBuilder(templates); } /** * Creates an XSLT processor using the given XSLT source */ public static XsltBuilder xslt(Source xslt) throws TransformerConfigurationException { notNull(xslt, "xslt"); XsltBuilder answer = new XsltBuilder(); answer.setTransformerSource(xslt); return answer; } /** * Creates an XSLT processor using the given XSLT source */ public static XsltBuilder xslt(File xslt) throws TransformerConfigurationException { notNull(xslt, "xslt"); return xslt(new StreamSource(xslt)); } /** * Creates an XSLT processor using the given XSLT source */ public static XsltBuilder xslt(URL xslt) throws TransformerConfigurationException, IOException { notNull(xslt, "xslt"); return xslt(xslt.openStream()); } /** * Creates an XSLT processor using the given XSLT source */ public static XsltBuilder xslt(InputStream xslt) throws TransformerConfigurationException, IOException { notNull(xslt, "xslt"); return xslt(new StreamSource(xslt)); } /** * Sets the output as being a byte[] */ public XsltBuilder outputBytes() { setResultHandlerFactory(new StreamResultHandlerFactory()); return this; } /** * Sets the output as being a String */ public XsltBuilder outputString() { setResultHandlerFactory(new StringResultHandlerFactory()); return this; } /** * Sets the output as being a DOM */ public XsltBuilder outputDOM() { setResultHandlerFactory(new DomResultHandlerFactory()); return this; } /** * Sets the output as being a File where the filename * must be provided in the {@link Exchange#XSLT_FILE_NAME} header. */ public XsltBuilder outputFile() { setResultHandlerFactory(new FileResultHandlerFactory()); return this; } /** * Should the output file be deleted when the {@link Exchange} is done. * <p/> * This option should only be used if you use {@link #outputFile()} as well. */ public XsltBuilder deleteOutputFile() { this.deleteOutputFile = true; return this; } public XsltBuilder parameter(String name, Object value) { parameters.put(name, value); return this; } /** * Sets a custom URI resolver to be used */ public XsltBuilder uriResolver(URIResolver uriResolver) { setUriResolver(uriResolver); return this; } /** * Enables to allow using StAX. * <p/> * When enabled StAX is preferred as the first choice as {@link Source}. */ public XsltBuilder allowStAX() { setAllowStAX(true); return this; } /** * Used for caching {@link Transformer}s. * <p/> * By default no caching is in use. * * @param numberToCache the maximum number of transformers to cache */ public XsltBuilder transformerCacheSize(int numberToCache) { if (numberToCache > 0) { transformers = new ArrayBlockingQueue<Transformer>(numberToCache); } else { transformers = null; } return this; } /** * Uses a custom {@link javax.xml.transform.ErrorListener}. */ public XsltBuilder errorListener(ErrorListener errorListener) { setErrorListener(errorListener); return this; } // Properties // ------------------------------------------------------------------------- public Map<String, Object> getParameters() { return parameters; } public void setParameters(Map<String, Object> parameters) { this.parameters = parameters; } public void setTemplate(Templates template) { this.template = template; if (transformers != null) { transformers.clear(); } } public Templates getTemplate() { return template; } public boolean isFailOnNullBody() { return failOnNullBody; } public void setFailOnNullBody(boolean failOnNullBody) { this.failOnNullBody = failOnNullBody; } public ResultHandlerFactory getResultHandlerFactory() { return resultHandlerFactory; } public void setResultHandlerFactory(ResultHandlerFactory resultHandlerFactory) { this.resultHandlerFactory = resultHandlerFactory; } public boolean isAllowStAX() { return allowStAX; } public void setAllowStAX(boolean allowStAX) { this.allowStAX = allowStAX; } /** * Sets the XSLT transformer from a Source * * @param source the source * @throws TransformerConfigurationException is thrown if creating a XSLT transformer failed. */ public void setTransformerSource(Source source) throws TransformerConfigurationException { TransformerFactory factory = converter.getTransformerFactory(); if (errorListener != null) { factory.setErrorListener(errorListener); } else { // use a logger error listener so users can see from the logs what the error may be factory.setErrorListener(new XsltErrorListener()); } if (getUriResolver() != null) { factory.setURIResolver(getUriResolver()); } // Check that the call to newTemplates() returns a valid template instance. // In case of an xslt parse error, it will return null and we should stop the // deployment and raise an exception as the route will not be setup properly. Templates templates = factory.newTemplates(source); if (templates != null) { setTemplate(templates); } else { throw new TransformerConfigurationException("Error creating XSLT template. " + "This is most likely be caused by a XML parse error. " + "Please verify your XSLT file configured."); } } /** * Sets the XSLT transformer from a File */ public void setTransformerFile(File xslt) throws TransformerConfigurationException { setTransformerSource(new StreamSource(xslt)); } /** * Sets the XSLT transformer from a URL */ public void setTransformerURL(URL url) throws TransformerConfigurationException, IOException { notNull(url, "url"); setTransformerInputStream(url.openStream()); } /** * Sets the XSLT transformer from the given input stream */ public void setTransformerInputStream(InputStream in) throws TransformerConfigurationException, IOException { notNull(in, "InputStream"); setTransformerSource(new StreamSource(in)); } public XmlConverter getConverter() { return converter; } public void setConverter(XmlConverter converter) { this.converter = converter; } public URIResolver getUriResolver() { return uriResolver; } public void setUriResolver(URIResolver uriResolver) { this.uriResolver = uriResolver; } public void setEntityResolver(EntityResolver entityResolver) { this.entityResolver = entityResolver; } public boolean isDeleteOutputFile() { return deleteOutputFile; } public void setDeleteOutputFile(boolean deleteOutputFile) { this.deleteOutputFile = deleteOutputFile; } public ErrorListener getErrorListener() { return errorListener; } public void setErrorListener(ErrorListener errorListener) { this.errorListener = errorListener; } // Implementation methods // ------------------------------------------------------------------------- private void releaseTransformer(Transformer transformer) { if (transformers != null) { transformer.reset(); transformers.offer(transformer); } } private Transformer getTransformer() throws Exception { Transformer t = null; if (transformers != null) { t = transformers.poll(); } if (t == null) { t = createTransformer(); } return t; } protected Transformer createTransformer() throws Exception { return getTemplate().newTransformer(); } /** * Checks whether we need an {@link InputStream} to access the message body. * <p/> * Depending on the content in the message body, we may not need to convert * to {@link InputStream}. * * @param exchange the current exchange * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting to {@link Source} afterwards. */ protected boolean isInputStreamNeeded(Exchange exchange) { Object body = exchange.getIn().getBody(); if (body == null) { return false; } if (body instanceof InputStream) { return true; } else if (body instanceof Source) { return false; } else if (body instanceof String) { return false; } else if (body instanceof byte[]) { return false; } else if (body instanceof Node) { return false; } else if (exchange.getContext().getTypeConverterRegistry().lookup(Source.class, body.getClass()) != null) { //there is a direct and hopefully optimized converter to Source return false; } // yes an input stream is needed return true; } /** * Converts the inbound body to a {@link Source}, if the body is <b>not</b> already a {@link Source}. * <p/> * This implementation will prefer to source in the following order: * <ul> * <li>StAX - If StAX is allowed</li> * <li>SAX - SAX as 2nd choice</li> * <li>Stream - Stream as 3rd choice</li> * <li>DOM - DOM as 4th choice</li> * </ul> */ protected Source getSource(Exchange exchange, Object body) { // body may already be a source if (body instanceof Source) { return (Source) body; } Source source = null; if (body != null) { if (isAllowStAX()) { // try StAX if enabled source = exchange.getContext().getTypeConverter().tryConvertTo(StAXSource.class, exchange, body); } if (source == null) { // then try SAX source = exchange.getContext().getTypeConverter().tryConvertTo(SAXSource.class, exchange, body); tryAddEntityResolver((SAXSource)source); } if (source == null) { // then try stream source = exchange.getContext().getTypeConverter().tryConvertTo(StreamSource.class, exchange, body); } if (source == null) { // and fallback to DOM source = exchange.getContext().getTypeConverter().tryConvertTo(DOMSource.class, exchange, body); } // as the TypeConverterRegistry will look up source the converter differently if the type converter is loaded different // now we just put the call of source converter at last if (source == null) { TypeConverter tc = exchange.getContext().getTypeConverterRegistry().lookup(Source.class, body.getClass()); if (tc != null) { source = tc.convertTo(Source.class, exchange, body); } } } if (source == null) { if (isFailOnNullBody()) { throw new ExpectedBodyTypeException(exchange, Source.class); } else { try { source = converter.toDOMSource(converter.createDocument()); } catch (ParserConfigurationException e) { throw new RuntimeTransformException(e); } } } return source; } private void tryAddEntityResolver(SAXSource source) { //expecting source to have not null XMLReader if (this.entityResolver != null && source != null) { source.getXMLReader().setEntityResolver(this.entityResolver); } } /** * Configures the transformer with exchange specific parameters */ protected void configureTransformer(Transformer transformer, Exchange exchange) throws Exception { if (uriResolver == null) { uriResolver = new XsltUriResolver(exchange.getContext(), null); } transformer.setURIResolver(uriResolver); if (errorListener == null) { // set our error listener so we can capture errors and report them back on the exchange transformer.setErrorListener(new DefaultTransformErrorHandler(exchange)); } else { // use custom error listener transformer.setErrorListener(errorListener); } transformer.clearParameters(); addParameters(transformer, exchange.getProperties()); addParameters(transformer, exchange.getIn().getHeaders()); addParameters(transformer, getParameters()); transformer.setParameter("exchange", exchange); transformer.setParameter("in", exchange.getIn()); transformer.setParameter("out", exchange.getOut()); } protected void addParameters(Transformer transformer, Map<String, Object> map) { Set<Map.Entry<String, Object>> propertyEntries = map.entrySet(); for (Map.Entry<String, Object> entry : propertyEntries) { String key = entry.getKey(); Object value = entry.getValue(); if (value != null) { LOG.trace("Transformer set parameter {} -> {}", key, value); transformer.setParameter(key, value); } } } private static final class XsltBuilderOnCompletion extends SynchronizationAdapter { private final String fileName; private XsltBuilderOnCompletion(String fileName) { this.fileName = fileName; } @Override public void onDone(Exchange exchange) { FileUtil.deleteFile(new File(fileName)); } @Override public String toString() { return "XsltBuilderOnCompletion"; } } }