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