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