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     */
017    package org.apache.camel.builder.xml;
018    
019    import java.io.File;
020    import java.io.IOException;
021    import java.io.InputStream;
022    import java.net.URL;
023    import java.util.HashMap;
024    import java.util.Map;
025    import java.util.Set;
026    import java.util.concurrent.ArrayBlockingQueue;
027    import java.util.concurrent.BlockingQueue;
028    
029    import javax.xml.parsers.ParserConfigurationException;
030    import javax.xml.stream.XMLEventReader;
031    import javax.xml.stream.XMLStreamReader;
032    import javax.xml.transform.ErrorListener;
033    import javax.xml.transform.Result;
034    import javax.xml.transform.Source;
035    import javax.xml.transform.Templates;
036    import javax.xml.transform.Transformer;
037    import javax.xml.transform.TransformerConfigurationException;
038    import javax.xml.transform.TransformerFactory;
039    import javax.xml.transform.URIResolver;
040    import javax.xml.transform.dom.DOMSource;
041    import javax.xml.transform.sax.SAXSource;
042    import javax.xml.transform.stax.StAXSource;
043    import javax.xml.transform.stream.StreamSource;
044    
045    import org.w3c.dom.Node;
046    
047    import org.apache.camel.Exchange;
048    import org.apache.camel.ExpectedBodyTypeException;
049    import org.apache.camel.Message;
050    import org.apache.camel.Processor;
051    import org.apache.camel.RuntimeTransformException;
052    import org.apache.camel.TypeConverter;
053    import org.apache.camel.converter.jaxp.StaxSource;
054    import org.apache.camel.converter.jaxp.XmlConverter;
055    import org.apache.camel.converter.jaxp.XmlErrorListener;
056    import org.apache.camel.support.SynchronizationAdapter;
057    import org.apache.camel.util.ExchangeHelper;
058    import org.apache.camel.util.FileUtil;
059    import org.apache.camel.util.IOHelper;
060    import org.slf4j.Logger;
061    import org.slf4j.LoggerFactory;
062    
063    import 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     */
074    public 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    }