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.stream.XMLEventReader;
031import javax.xml.stream.XMLStreamReader;
032import javax.xml.transform.ErrorListener;
033import javax.xml.transform.Result;
034import javax.xml.transform.Source;
035import javax.xml.transform.Templates;
036import javax.xml.transform.Transformer;
037import javax.xml.transform.TransformerConfigurationException;
038import javax.xml.transform.TransformerFactory;
039import javax.xml.transform.URIResolver;
040import javax.xml.transform.dom.DOMSource;
041import javax.xml.transform.sax.SAXSource;
042import javax.xml.transform.stax.StAXSource;
043import javax.xml.transform.stream.StreamSource;
044
045import org.w3c.dom.Node;
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.StaxSource;
054import org.apache.camel.converter.jaxp.XmlConverter;
055import org.apache.camel.converter.jaxp.XmlErrorListener;
056import org.apache.camel.support.SynchronizationAdapter;
057import org.apache.camel.util.ExchangeHelper;
058import org.apache.camel.util.FileUtil;
059import org.apache.camel.util.IOHelper;
060import org.slf4j.Logger;
061import org.slf4j.LoggerFactory;
062
063import static org.apache.camel.util.ObjectHelper.notNull;
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<String, Object>();
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 = new XsltErrorListener();
085    private boolean allowStAX = true;
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        transformer.setErrorListener(new DefaultTransformErrorHandler());
111        ResultHandler resultHandler = resultHandlerFactory.createResult(exchange);
112        Result result = resultHandler.getResult();
113        exchange.setProperty("isXalanTransformer", isXalanTransformer(transformer));
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            LOG.trace("Using {} as source", source);
131            transformer.transform(source, result);
132            LOG.trace("Transform complete with result {}", result);
133            resultHandler.setBody(out);
134        } finally {
135            // clean up the setting on the exchange
136            
137            releaseTransformer(transformer);
138            // IOHelper can handle if is is null
139            IOHelper.close(is);
140        }
141    }
142    
143    boolean isXalanTransformer(Transformer transformer) {
144        return transformer.getClass().getName().startsWith("org.apache.xalan.transformer");
145    }
146
147    // Builder methods
148    // -------------------------------------------------------------------------
149
150    /**
151     * Creates an XSLT processor using the given templates instance
152     */
153    public static XsltBuilder xslt(Templates templates) {
154        return new XsltBuilder(templates);
155    }
156
157    /**
158     * Creates an XSLT processor using the given XSLT source
159     */
160    public static XsltBuilder xslt(Source xslt) throws TransformerConfigurationException {
161        notNull(xslt, "xslt");
162        XsltBuilder answer = new XsltBuilder();
163        answer.setTransformerSource(xslt);
164        return answer;
165    }
166
167    /**
168     * Creates an XSLT processor using the given XSLT source
169     */
170    public static XsltBuilder xslt(File xslt) throws TransformerConfigurationException {
171        notNull(xslt, "xslt");
172        return xslt(new StreamSource(xslt));
173    }
174
175    /**
176     * Creates an XSLT processor using the given XSLT source
177     */
178    public static XsltBuilder xslt(URL xslt) throws TransformerConfigurationException, IOException {
179        notNull(xslt, "xslt");
180        return xslt(xslt.openStream());
181    }
182
183    /**
184     * Creates an XSLT processor using the given XSLT source
185     */
186    public static XsltBuilder xslt(InputStream xslt) throws TransformerConfigurationException, IOException {
187        notNull(xslt, "xslt");
188        return xslt(new StreamSource(xslt));
189    }
190
191    /**
192     * Sets the output as being a byte[]
193     */
194    public XsltBuilder outputBytes() {
195        setResultHandlerFactory(new StreamResultHandlerFactory());
196        return this;
197    }
198
199    /**
200     * Sets the output as being a String
201     */
202    public XsltBuilder outputString() {
203        setResultHandlerFactory(new StringResultHandlerFactory());
204        return this;
205    }
206
207    /**
208     * Sets the output as being a DOM
209     */
210    public XsltBuilder outputDOM() {
211        setResultHandlerFactory(new DomResultHandlerFactory());
212        return this;
213    }
214
215    /**
216     * Sets the output as being a File where the filename
217     * must be provided in the {@link Exchange#XSLT_FILE_NAME} header.
218     */
219    public XsltBuilder outputFile() {
220        setResultHandlerFactory(new FileResultHandlerFactory());
221        return this;
222    }
223
224    /**
225     * Should the output file be deleted when the {@link Exchange} is done.
226     * <p/>
227     * This option should only be used if you use {@link #outputFile()} as well.
228     */
229    public XsltBuilder deleteOutputFile() {
230        this.deleteOutputFile = true;
231        return this;
232    }
233
234    public XsltBuilder parameter(String name, Object value) {
235        parameters.put(name, value);
236        return this;
237    }
238
239    /**
240     * Sets a custom URI resolver to be used
241     */
242    public XsltBuilder uriResolver(URIResolver uriResolver) {
243        setUriResolver(uriResolver);
244        return this;
245    }
246
247    /**
248     * Enables to allow using StAX.
249     * <p/>
250     * When enabled StAX is preferred as the first choice as {@link Source}.
251     */
252    public XsltBuilder allowStAX() {
253        setAllowStAX(true);
254        return this;
255    }
256    
257    
258    public XsltBuilder transformerCacheSize(int numberToCache) {
259        if (numberToCache > 0) {
260            transformers = new ArrayBlockingQueue<Transformer>(numberToCache);
261        } else {
262            transformers = null;
263        }
264        return this;
265    }
266
267    // Properties
268    // -------------------------------------------------------------------------
269
270    public Map<String, Object> getParameters() {
271        return parameters;
272    }
273
274    public void setParameters(Map<String, Object> parameters) {
275        this.parameters = parameters;
276    }
277
278    public void setTemplate(Templates template) {
279        this.template = template;
280        if (transformers != null) {
281            transformers.clear();
282        }
283    }
284    
285    public Templates getTemplate() {
286        return template;
287    }
288
289    public boolean isFailOnNullBody() {
290        return failOnNullBody;
291    }
292
293    public void setFailOnNullBody(boolean failOnNullBody) {
294        this.failOnNullBody = failOnNullBody;
295    }
296
297    public ResultHandlerFactory getResultHandlerFactory() {
298        return resultHandlerFactory;
299    }
300
301    public void setResultHandlerFactory(ResultHandlerFactory resultHandlerFactory) {
302        this.resultHandlerFactory = resultHandlerFactory;
303    }
304
305    public boolean isAllowStAX() {
306        return allowStAX;
307    }
308
309    public void setAllowStAX(boolean allowStAX) {
310        this.allowStAX = allowStAX;
311    }
312
313    /**
314     * Sets the XSLT transformer from a Source
315     *
316     * @param source  the source
317     * @throws TransformerConfigurationException is thrown if creating a XSLT transformer failed.
318     */
319    public void setTransformerSource(Source source) throws TransformerConfigurationException {
320        TransformerFactory factory = converter.getTransformerFactory();
321        factory.setErrorListener(errorListener);
322        if (getUriResolver() != null) {
323            factory.setURIResolver(getUriResolver());
324        }
325
326        // Check that the call to newTemplates() returns a valid template instance.
327        // In case of an xslt parse error, it will return null and we should stop the
328        // deployment and raise an exception as the route will not be setup properly.
329        Templates templates = factory.newTemplates(source);
330        if (templates != null) {
331            setTemplate(templates);
332        } else {
333            throw new TransformerConfigurationException("Error creating XSLT template. "
334                    + "This is most likely be caused by a XML parse error. "
335                    + "Please verify your XSLT file configured.");
336        }
337    }
338
339    /**
340     * Sets the XSLT transformer from a File
341     */
342    public void setTransformerFile(File xslt) throws TransformerConfigurationException {
343        setTransformerSource(new StreamSource(xslt));
344    }
345
346    /**
347     * Sets the XSLT transformer from a URL
348     */
349    public void setTransformerURL(URL url) throws TransformerConfigurationException, IOException {
350        notNull(url, "url");
351        setTransformerInputStream(url.openStream());
352    }
353
354    /**
355     * Sets the XSLT transformer from the given input stream
356     */
357    public void setTransformerInputStream(InputStream in) throws TransformerConfigurationException, IOException {
358        notNull(in, "InputStream");
359        setTransformerSource(new StreamSource(in));
360    }
361
362    public XmlConverter getConverter() {
363        return converter;
364    }
365
366    public void setConverter(XmlConverter converter) {
367        this.converter = converter;
368    }
369
370    public URIResolver getUriResolver() {
371        return uriResolver;
372    }
373
374    public void setUriResolver(URIResolver uriResolver) {
375        this.uriResolver = uriResolver;
376    }
377
378    public boolean isDeleteOutputFile() {
379        return deleteOutputFile;
380    }
381
382    public void setDeleteOutputFile(boolean deleteOutputFile) {
383        this.deleteOutputFile = deleteOutputFile;
384    }
385
386    public ErrorListener getErrorListener() {
387        return errorListener;
388    }
389
390    public void setErrorListener(ErrorListener errorListener) {
391        this.errorListener = errorListener;
392    }
393
394    // Implementation methods
395    // -------------------------------------------------------------------------
396    private void releaseTransformer(Transformer transformer) {
397        if (transformers != null) {
398            transformer.reset();
399            transformers.offer(transformer);
400        }
401    }
402
403    private Transformer getTransformer() throws TransformerConfigurationException {
404        Transformer t = null; 
405        if (transformers != null) {
406            t = transformers.poll();
407        }
408        if (t == null) {
409            t = getTemplate().newTransformer();
410        }
411        return t;
412    }
413
414    /**
415     * Checks whether we need an {@link InputStream} to access the message body.
416     * <p/>
417     * Depending on the content in the message body, we may not need to convert
418     * to {@link InputStream}.
419     *
420     * @param exchange the current exchange
421     * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting to {@link Source} afterwards.
422     */
423    protected boolean isInputStreamNeeded(Exchange exchange) {
424        Object body = exchange.getIn().getBody();
425        if (body == null) {
426            return false;
427        }
428
429        if (body instanceof InputStream) {
430            return true;
431        } else if (body instanceof Source) {
432            return false;
433        } else if (body instanceof String) {
434            return false;
435        } else if (body instanceof byte[]) {
436            return false;
437        } else if (body instanceof Node) {
438            return false;
439        } else if (exchange.getContext().getTypeConverterRegistry().lookup(Source.class, body.getClass()) != null) {
440            //there is a direct and hopefully optimized converter to Source 
441            return false;
442        }
443        // yes an input stream is needed
444        return true;
445    }
446
447    /**
448     * Converts the inbound body to a {@link Source}, if the body is <b>not</b> already a {@link Source}.
449     * <p/>
450     * This implementation will prefer to source in the following order:
451     * <ul>
452     *   <li>StAX - Is StAX is allowed</li>
453     *   <li>SAX - SAX as 2nd choice</li>
454     *   <li>Stream - Stream as 3rd choice</li>
455     *   <li>DOM - DOM as 4th choice</li>
456     * </ul>
457     */
458    protected Source getSource(Exchange exchange, Object body) {
459        Boolean isXalanTransformer = exchange.getProperty("isXalanTransformer", Boolean.class);
460        // body may already be a source
461        if (body instanceof Source) {
462            return (Source) body;
463        }
464        Source source = null;
465        if (body != null) {
466            if (isAllowStAX()) {
467                if (isXalanTransformer) {
468                    XMLStreamReader reader = exchange.getContext().getTypeConverter().tryConvertTo(XMLStreamReader.class, exchange, body);
469                    if (reader != null) {
470                        // create a new SAXSource with stax parser API
471                        source = new StaxSource(reader);
472                    }
473                } else {
474                    source = exchange.getContext().getTypeConverter().tryConvertTo(StAXSource.class, exchange, body);
475                }
476            }
477            if (source == null) {
478                // then try SAX
479                source = exchange.getContext().getTypeConverter().tryConvertTo(SAXSource.class, exchange, body);
480            }
481            if (source == null) {
482                // then try stream
483                source = exchange.getContext().getTypeConverter().tryConvertTo(StreamSource.class, exchange, body);
484            }
485            if (source == null) {
486                // and fallback to DOM
487                source = exchange.getContext().getTypeConverter().tryConvertTo(DOMSource.class, exchange, body);
488            }
489            // as the TypeConverterRegistry will look up source the converter differently if the type converter is loaded different
490            // now we just put the call of source converter at last
491            if (source == null) {
492                TypeConverter tc = exchange.getContext().getTypeConverterRegistry().lookup(Source.class, body.getClass());
493                if (tc != null) {
494                    source = tc.convertTo(Source.class, exchange, body);
495                }
496            }
497        }
498        if (source == null) {
499            if (isFailOnNullBody()) {
500                throw new ExpectedBodyTypeException(exchange, Source.class);
501            } else {
502                try {
503                    source = converter.toDOMSource(converter.createDocument());
504                } catch (ParserConfigurationException e) {
505                    throw new RuntimeTransformException(e);
506                }
507            }
508        }
509        return source;
510    }
511   
512
513    /**
514     * Configures the transformer with exchange specific parameters
515     */
516    protected void configureTransformer(Transformer transformer, Exchange exchange) {
517        if (uriResolver == null) {
518            uriResolver = new XsltUriResolver(exchange.getContext().getClassResolver(), null);
519        }
520        transformer.setURIResolver(uriResolver);
521        transformer.setErrorListener(new XmlErrorListener());
522
523        transformer.clearParameters();
524
525        addParameters(transformer, exchange.getProperties());
526        addParameters(transformer, exchange.getIn().getHeaders());
527        addParameters(transformer, getParameters());
528
529        transformer.setParameter("exchange", exchange);
530        transformer.setParameter("in", exchange.getIn());
531        transformer.setParameter("out", exchange.getOut());
532    }
533
534    protected void addParameters(Transformer transformer, Map<String, Object> map) {
535        Set<Map.Entry<String, Object>> propertyEntries = map.entrySet();
536        for (Map.Entry<String, Object> entry : propertyEntries) {
537            String key = entry.getKey();
538            Object value = entry.getValue();
539            if (value != null) {
540                LOG.trace("Transformer set parameter {} -> {}", key, value);
541                transformer.setParameter(key, value);
542            }
543        }
544    }
545
546    private static final class XsltBuilderOnCompletion extends SynchronizationAdapter {
547        private final String fileName;
548
549        private XsltBuilderOnCompletion(String fileName) {
550            this.fileName = fileName;
551        }
552
553        @Override
554        public void onDone(Exchange exchange) {
555            FileUtil.deleteFile(new File(fileName));
556        }
557
558        @Override
559        public String toString() {
560            return "XsltBuilderOnCompletion";
561        }
562    }
563
564}