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