001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.camel.builder.xml;
018
019import java.io.File;
020import java.io.IOException;
021import java.io.InputStream;
022import java.lang.reflect.Method;
023import java.net.URL;
024import java.util.HashMap;
025import java.util.Map;
026import java.util.Set;
027import java.util.concurrent.ArrayBlockingQueue;
028import java.util.concurrent.BlockingQueue;
029import javax.xml.parsers.ParserConfigurationException;
030import javax.xml.stream.XMLStreamReader;
031import javax.xml.transform.ErrorListener;
032import javax.xml.transform.Result;
033import javax.xml.transform.Source;
034import javax.xml.transform.Templates;
035import javax.xml.transform.Transformer;
036import javax.xml.transform.TransformerConfigurationException;
037import javax.xml.transform.TransformerFactory;
038import javax.xml.transform.URIResolver;
039import javax.xml.transform.dom.DOMSource;
040import javax.xml.transform.sax.SAXSource;
041import javax.xml.transform.stax.StAXSource;
042import javax.xml.transform.stream.StreamSource;
043
044import org.w3c.dom.Node;
045
046import org.apache.camel.CamelContext;
047import org.apache.camel.CamelContextAware;
048import org.apache.camel.Exchange;
049import org.apache.camel.ExpectedBodyTypeException;
050import org.apache.camel.Message;
051import org.apache.camel.Processor;
052import org.apache.camel.RuntimeTransformException;
053import org.apache.camel.TypeConverter;
054import org.apache.camel.converter.jaxp.StaxSource;
055import org.apache.camel.converter.jaxp.XmlConverter;
056import org.apache.camel.support.ServiceSupport;
057import org.apache.camel.support.SynchronizationAdapter;
058import org.apache.camel.util.ExchangeHelper;
059import org.apache.camel.util.FileUtil;
060import org.apache.camel.util.IOHelper;
061import org.apache.camel.util.ObjectHelper;
062import org.slf4j.Logger;
063import org.slf4j.LoggerFactory;
064
065import static org.apache.camel.util.ObjectHelper.notNull;
066
067/**
068 * Creates a <a href="http://camel.apache.org/processor.html">Processor</a>
069 * which performs an XSLT transformation of the IN message body.
070 * <p/>
071 * Will by default output the result as a String. You can chose which kind of output
072 * you want using the <tt>outputXXX</tt> methods.
073 * <p/>
074 * If using the static <tt>xslt</tt> methods to create a {@link org.apache.camel.builder.xml.XsltBuilder} its recommended
075 * to inject the {@link org.apache.camel.CamelContext} using the {@link #setCamelContext(org.apache.camel.CamelContext)}
076 * and call {@link #start()} to properly initialize the builder before using.
077 *
078 * @version 
079 */
080public class XsltBuilder extends ServiceSupport implements Processor, CamelContextAware {
081    private static final Logger LOG = LoggerFactory.getLogger(XsltBuilder.class);
082    private CamelContext camelContext;
083    private Map<String, Object> parameters = new HashMap<String, Object>();
084    private XmlConverter converter = new XmlConverter();
085    private Templates template;
086    private volatile BlockingQueue<Transformer> transformers;
087    private ResultHandlerFactory resultHandlerFactory = new StringResultHandlerFactory();
088    private boolean failOnNullBody = true;
089    private URIResolver uriResolver;
090    private boolean deleteOutputFile;
091    private ErrorListener errorListener;
092    private boolean allowStAX = true;
093    private volatile Method setMessageEmitterMethod;
094    private volatile Class<?> saxonReceiverClass;
095    private volatile Class<?> saxonWarnerClass;
096
097    public XsltBuilder() {
098    }
099
100    public XsltBuilder(Templates templates) {
101        this.template = templates;
102    }
103
104    @Override
105    public String toString() {
106        return "XSLT[" + template + "]";
107    }
108
109    public void process(Exchange exchange) throws Exception {
110        notNull(getTemplate(), "template");
111
112        if (isDeleteOutputFile()) {
113            // add on completion so we can delete the file when the Exchange is done
114            String fileName = ExchangeHelper.getMandatoryHeader(exchange, Exchange.XSLT_FILE_NAME, String.class);
115            exchange.addOnCompletion(new XsltBuilderOnCompletion(fileName));
116        }
117
118        Transformer transformer = getTransformer();
119        configureTransformer(transformer, exchange);
120
121        ResultHandler resultHandler = resultHandlerFactory.createResult(exchange);
122        Result result = resultHandler.getResult();
123        exchange.setProperty("isXalanTransformer", isXalanTransformer(transformer));
124        // let's copy the headers before we invoke the transform in case they modify them
125        Message out = exchange.getOut();
126        out.copyFrom(exchange.getIn());
127
128        // the underlying input stream, which we need to close to avoid locking files or other resources
129        InputStream is = null;
130        try {
131            Source source;
132            // only convert to input stream if really needed
133            if (isInputStreamNeeded(exchange)) {
134                is = exchange.getIn().getBody(InputStream.class);
135                source = getSource(exchange, is);
136            } else {
137                Object body = exchange.getIn().getBody();
138                source = getSource(exchange, body);
139            }
140            LOG.trace("Using {} as source", source);
141            transformer.transform(source, result);
142            LOG.trace("Transform complete with result {}", result);
143            resultHandler.setBody(out);
144        } finally {
145            releaseTransformer(transformer);
146            // IOHelper can handle if is is null
147            IOHelper.close(is);
148        }
149    }
150    
151    boolean isXalanTransformer(Transformer transformer) {
152        return transformer.getClass().getName().startsWith("org.apache.xalan.transformer");
153    }
154
155    boolean isSaxonTransformer(Transformer transformer) {
156        return transformer.getClass().getName().startsWith("net.sf.saxon");
157    }
158
159    // Builder methods
160    // -------------------------------------------------------------------------
161
162    /**
163     * Creates an XSLT processor using the given templates instance
164     */
165    public static XsltBuilder xslt(Templates templates) {
166        return new XsltBuilder(templates);
167    }
168
169    /**
170     * Creates an XSLT processor using the given XSLT source
171     */
172    public static XsltBuilder xslt(Source xslt) throws TransformerConfigurationException {
173        notNull(xslt, "xslt");
174        XsltBuilder answer = new XsltBuilder();
175        answer.setTransformerSource(xslt);
176        return answer;
177    }
178
179    /**
180     * Creates an XSLT processor using the given XSLT source
181     */
182    public static XsltBuilder xslt(File xslt) throws TransformerConfigurationException {
183        notNull(xslt, "xslt");
184        return xslt(new StreamSource(xslt));
185    }
186
187    /**
188     * Creates an XSLT processor using the given XSLT source
189     */
190    public static XsltBuilder xslt(URL xslt) throws TransformerConfigurationException, IOException {
191        notNull(xslt, "xslt");
192        return xslt(xslt.openStream());
193    }
194
195    /**
196     * Creates an XSLT processor using the given XSLT source
197     */
198    public static XsltBuilder xslt(InputStream xslt) throws TransformerConfigurationException, IOException {
199        notNull(xslt, "xslt");
200        return xslt(new StreamSource(xslt));
201    }
202
203    /**
204     * Sets the output as being a byte[]
205     */
206    public XsltBuilder outputBytes() {
207        setResultHandlerFactory(new StreamResultHandlerFactory());
208        return this;
209    }
210
211    /**
212     * Sets the output as being a String
213     */
214    public XsltBuilder outputString() {
215        setResultHandlerFactory(new StringResultHandlerFactory());
216        return this;
217    }
218
219    /**
220     * Sets the output as being a DOM
221     */
222    public XsltBuilder outputDOM() {
223        setResultHandlerFactory(new DomResultHandlerFactory());
224        return this;
225    }
226
227    /**
228     * Sets the output as being a File where the filename
229     * must be provided in the {@link Exchange#XSLT_FILE_NAME} header.
230     */
231    public XsltBuilder outputFile() {
232        setResultHandlerFactory(new FileResultHandlerFactory());
233        return this;
234    }
235
236    /**
237     * Should the output file be deleted when the {@link Exchange} is done.
238     * <p/>
239     * This option should only be used if you use {@link #outputFile()} as well.
240     */
241    public XsltBuilder deleteOutputFile() {
242        this.deleteOutputFile = true;
243        return this;
244    }
245
246    public XsltBuilder parameter(String name, Object value) {
247        parameters.put(name, value);
248        return this;
249    }
250
251    /**
252     * Sets a custom URI resolver to be used
253     */
254    public XsltBuilder uriResolver(URIResolver uriResolver) {
255        setUriResolver(uriResolver);
256        return this;
257    }
258
259    /**
260     * Enables to allow using StAX.
261     * <p/>
262     * When enabled StAX is preferred as the first choice as {@link Source}.
263     */
264    public XsltBuilder allowStAX() {
265        setAllowStAX(true);
266        return this;
267    }
268
269    /**
270     * Used for caching {@link Transformer}s.
271     * <p/>
272     * By default no caching is in use.
273     *
274     * @param numberToCache  the maximum number of transformers to cache
275     */
276    public XsltBuilder transformerCacheSize(int numberToCache) {
277        if (numberToCache > 0) {
278            transformers = new ArrayBlockingQueue<Transformer>(numberToCache);
279        } else {
280            transformers = null;
281        }
282        return this;
283    }
284
285    /**
286     * Uses a custom {@link javax.xml.transform.ErrorListener}.
287     */
288    public XsltBuilder errorListener(ErrorListener errorListener) {
289        setErrorListener(errorListener);
290        return this;
291    }
292
293        // Properties
294    // -------------------------------------------------------------------------
295
296    public Map<String, Object> getParameters() {
297        return parameters;
298    }
299
300    public void setParameters(Map<String, Object> parameters) {
301        this.parameters = parameters;
302    }
303
304    public void setTemplate(Templates template) {
305        this.template = template;
306        if (transformers != null) {
307            transformers.clear();
308        }
309    }
310    
311    public Templates getTemplate() {
312        return template;
313    }
314
315    public boolean isFailOnNullBody() {
316        return failOnNullBody;
317    }
318
319    public void setFailOnNullBody(boolean failOnNullBody) {
320        this.failOnNullBody = failOnNullBody;
321    }
322
323    public ResultHandlerFactory getResultHandlerFactory() {
324        return resultHandlerFactory;
325    }
326
327    public void setResultHandlerFactory(ResultHandlerFactory resultHandlerFactory) {
328        this.resultHandlerFactory = resultHandlerFactory;
329    }
330
331    public boolean isAllowStAX() {
332        return allowStAX;
333    }
334
335    public void setAllowStAX(boolean allowStAX) {
336        this.allowStAX = allowStAX;
337    }
338
339    /**
340     * Sets the XSLT transformer from a Source
341     *
342     * @param source  the source
343     * @throws TransformerConfigurationException is thrown if creating a XSLT transformer failed.
344     */
345    public void setTransformerSource(Source source) throws TransformerConfigurationException {
346        TransformerFactory factory = converter.getTransformerFactory();
347        if (errorListener != null) {
348            factory.setErrorListener(errorListener);
349        } else {
350            // use a logger error listener so users can see from the logs what the error may be
351            factory.setErrorListener(new XsltErrorListener());
352        }
353        if (getUriResolver() != null) {
354            factory.setURIResolver(getUriResolver());
355        }
356
357        // Check that the call to newTemplates() returns a valid template instance.
358        // In case of an xslt parse error, it will return null and we should stop the
359        // deployment and raise an exception as the route will not be setup properly.
360        Templates templates = factory.newTemplates(source);
361        if (templates != null) {
362            setTemplate(templates);
363        } else {
364            throw new TransformerConfigurationException("Error creating XSLT template. "
365                    + "This is most likely be caused by a XML parse error. "
366                    + "Please verify your XSLT file configured.");
367        }
368    }
369
370    /**
371     * Sets the XSLT transformer from a File
372     */
373    public void setTransformerFile(File xslt) throws TransformerConfigurationException {
374        setTransformerSource(new StreamSource(xslt));
375    }
376
377    /**
378     * Sets the XSLT transformer from a URL
379     */
380    public void setTransformerURL(URL url) throws TransformerConfigurationException, IOException {
381        notNull(url, "url");
382        setTransformerInputStream(url.openStream());
383    }
384
385    /**
386     * Sets the XSLT transformer from the given input stream
387     */
388    public void setTransformerInputStream(InputStream in) throws TransformerConfigurationException, IOException {
389        notNull(in, "InputStream");
390        setTransformerSource(new StreamSource(in));
391    }
392
393    public XmlConverter getConverter() {
394        return converter;
395    }
396
397    public void setConverter(XmlConverter converter) {
398        this.converter = converter;
399    }
400
401    public URIResolver getUriResolver() {
402        return uriResolver;
403    }
404
405    public void setUriResolver(URIResolver uriResolver) {
406        this.uriResolver = uriResolver;
407    }
408
409    public boolean isDeleteOutputFile() {
410        return deleteOutputFile;
411    }
412
413    public void setDeleteOutputFile(boolean deleteOutputFile) {
414        this.deleteOutputFile = deleteOutputFile;
415    }
416
417    public ErrorListener getErrorListener() {
418        return errorListener;
419    }
420
421    public void setErrorListener(ErrorListener errorListener) {
422        this.errorListener = errorListener;
423    }
424
425    // Implementation methods
426    // -------------------------------------------------------------------------
427    private void releaseTransformer(Transformer transformer) {
428        if (transformers != null) {
429            transformer.reset();
430            transformers.offer(transformer);
431        }
432    }
433
434    private Transformer getTransformer() throws Exception {
435        Transformer t = null; 
436        if (transformers != null) {
437            t = transformers.poll();
438        }
439        if (t == null) {
440            t = createTransformer();
441        }
442        return t;
443    }
444
445    protected Transformer createTransformer() throws Exception {
446        Transformer t = getTemplate().newTransformer();
447
448        // special for saxon as we need to call setMessageEmitter on the transformer to hook from saxon to the JAXP errorListener
449        // so we can get notified if any errors happen during transformation
450        // see details at: https://stackoverflow.com/questions/4695489/capture-xslmessage-output-in-java
451        if (isSaxonTransformer(t) && setMessageEmitterMethod != null) {
452            Object warner = getCamelContext().getInjector().newInstance(saxonWarnerClass);
453            setMessageEmitterMethod.invoke(t, warner);
454        }
455
456        return t;
457    }
458
459    /**
460     * Checks whether we need an {@link InputStream} to access the message body.
461     * <p/>
462     * Depending on the content in the message body, we may not need to convert
463     * to {@link InputStream}.
464     *
465     * @param exchange the current exchange
466     * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting to {@link Source} afterwards.
467     */
468    protected boolean isInputStreamNeeded(Exchange exchange) {
469        Object body = exchange.getIn().getBody();
470        if (body == null) {
471            return false;
472        }
473
474        if (body instanceof InputStream) {
475            return true;
476        } else if (body instanceof Source) {
477            return false;
478        } else if (body instanceof String) {
479            return false;
480        } else if (body instanceof byte[]) {
481            return false;
482        } else if (body instanceof Node) {
483            return false;
484        } else if (exchange.getContext().getTypeConverterRegistry().lookup(Source.class, body.getClass()) != null) {
485            //there is a direct and hopefully optimized converter to Source 
486            return false;
487        }
488        // yes an input stream is needed
489        return true;
490    }
491
492    /**
493     * Converts the inbound body to a {@link Source}, if the body is <b>not</b> already a {@link Source}.
494     * <p/>
495     * This implementation will prefer to source in the following order:
496     * <ul>
497     *   <li>StAX - Is StAX is allowed</li>
498     *   <li>SAX - SAX as 2nd choice</li>
499     *   <li>Stream - Stream as 3rd choice</li>
500     *   <li>DOM - DOM as 4th choice</li>
501     * </ul>
502     */
503    protected Source getSource(Exchange exchange, Object body) {
504        Boolean isXalanTransformer = exchange.getProperty("isXalanTransformer", Boolean.class);
505        // body may already be a source
506        if (body instanceof Source) {
507            return (Source) body;
508        }
509        Source source = null;
510        if (body != null) {
511            if (isAllowStAX()) {
512                if (isXalanTransformer) {
513                    XMLStreamReader reader = exchange.getContext().getTypeConverter().tryConvertTo(XMLStreamReader.class, exchange, body);
514                    if (reader != null) {
515                        // create a new SAXSource with stax parser API
516                        source = new StaxSource(reader);
517                    }
518                } else {
519                    source = exchange.getContext().getTypeConverter().tryConvertTo(StAXSource.class, exchange, body);
520                }
521            }
522            if (source == null) {
523                // then try SAX
524                source = exchange.getContext().getTypeConverter().tryConvertTo(SAXSource.class, exchange, body);
525            }
526            if (source == null) {
527                // then try stream
528                source = exchange.getContext().getTypeConverter().tryConvertTo(StreamSource.class, exchange, body);
529            }
530            if (source == null) {
531                // and fallback to DOM
532                source = exchange.getContext().getTypeConverter().tryConvertTo(DOMSource.class, exchange, body);
533            }
534            // as the TypeConverterRegistry will look up source the converter differently if the type converter is loaded different
535            // now we just put the call of source converter at last
536            if (source == null) {
537                TypeConverter tc = exchange.getContext().getTypeConverterRegistry().lookup(Source.class, body.getClass());
538                if (tc != null) {
539                    source = tc.convertTo(Source.class, exchange, body);
540                }
541            }
542        }
543        if (source == null) {
544            if (isFailOnNullBody()) {
545                throw new ExpectedBodyTypeException(exchange, Source.class);
546            } else {
547                try {
548                    source = converter.toDOMSource(converter.createDocument());
549                } catch (ParserConfigurationException e) {
550                    throw new RuntimeTransformException(e);
551                }
552            }
553        }
554        return source;
555    }
556   
557
558    /**
559     * Configures the transformer with exchange specific parameters
560     */
561    protected void configureTransformer(Transformer transformer, Exchange exchange) throws Exception {
562        if (uriResolver == null) {
563            uriResolver = new XsltUriResolver(exchange.getContext().getClassResolver(), null);
564        }
565        transformer.setURIResolver(uriResolver);
566        if (errorListener == null) {
567            // set our error listener so we can capture errors and report them back on the exchange
568            transformer.setErrorListener(new DefaultTransformErrorHandler(exchange));
569        } else {
570            // use custom error listener
571            transformer.setErrorListener(errorListener);
572        }
573
574        transformer.clearParameters();
575        addParameters(transformer, exchange.getProperties());
576        addParameters(transformer, exchange.getIn().getHeaders());
577        addParameters(transformer, getParameters());
578        transformer.setParameter("exchange", exchange);
579        transformer.setParameter("in", exchange.getIn());
580        transformer.setParameter("out", exchange.getOut());
581    }
582
583    protected void addParameters(Transformer transformer, Map<String, Object> map) {
584        Set<Map.Entry<String, Object>> propertyEntries = map.entrySet();
585        for (Map.Entry<String, Object> entry : propertyEntries) {
586            String key = entry.getKey();
587            Object value = entry.getValue();
588            if (value != null) {
589                LOG.trace("Transformer set parameter {} -> {}", key, value);
590                transformer.setParameter(key, value);
591            }
592        }
593    }
594
595    public CamelContext getCamelContext() {
596        return camelContext;
597    }
598
599    public void setCamelContext(CamelContext camelContext) {
600        this.camelContext = camelContext;
601    }
602
603    @Override
604    protected void doStart() throws Exception {
605        ObjectHelper.notNull(camelContext, "camelContext", this);
606
607        // create a transformer to see if its saxon, as we then need to do some initial preparation
608        Transformer t = getTemplate().newTransformer();
609
610        if (isSaxonTransformer(t)) {
611            // pre-load saxon classes as we need to call the setMessageEmitter on the transformer to hook saxon to use the JAXP
612            // error listener, so we can capture errors and xsl:message outputs which end users may define in the xslt files
613            try {
614                saxonReceiverClass = getCamelContext().getClassResolver().resolveMandatoryClass("net.sf.saxon.event.Receiver");
615                saxonWarnerClass = getCamelContext().getClassResolver().resolveMandatoryClass("net.sf.saxon.serialize.MessageWarner");
616                setMessageEmitterMethod = t.getClass().getMethod("setMessageEmitter", saxonReceiverClass);
617            } catch (Exception e) {
618                throw new IllegalStateException("Error pre-loading Saxon classes. Make sure you have saxon on the classpath,"
619                        + " and the classloader can load the following two classes: net.sf.saxon.event.Receiver, net.sf.saxon.serialize.MessageWarner.", e);
620            }
621        }
622    }
623
624    @Override
625    protected void doStop() throws Exception {
626        // noop
627    }
628
629    private static final class XsltBuilderOnCompletion extends SynchronizationAdapter {
630        private final String fileName;
631
632        private XsltBuilderOnCompletion(String fileName) {
633            this.fileName = fileName;
634        }
635
636        @Override
637        public void onDone(Exchange exchange) {
638            FileUtil.deleteFile(new File(fileName));
639        }
640
641        @Override
642        public String toString() {
643            return "XsltBuilderOnCompletion";
644        }
645    }
646
647}