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 TypeConverter tc = exchange.getContext().getTypeConverterRegistry().lookup(Source.class, body.getClass());
460 if (tc != null) {
461 source = tc.convertTo(Source.class, body);
462 }
463 }
464
465 if (source == null && isAllowStAX()) {
466 source = exchange.getContext().getTypeConverter().tryConvertTo(StAXSource.class, exchange, body);
467 }
468 if (source == null) {
469 // then try SAX
470 source = exchange.getContext().getTypeConverter().tryConvertTo(SAXSource.class, exchange, body);
471 }
472 if (source == null) {
473 // then try stream
474 source = exchange.getContext().getTypeConverter().tryConvertTo(StreamSource.class, exchange, body);
475 }
476 if (source == null) {
477 // and fallback to DOM
478 source = exchange.getContext().getTypeConverter().tryConvertTo(DOMSource.class, exchange, body);
479 }
480 if (source == null) {
481 if (isFailOnNullBody()) {
482 throw new ExpectedBodyTypeException(exchange, Source.class);
483 } else {
484 try {
485 source = converter.toDOMSource(converter.createDocument());
486 } catch (ParserConfigurationException e) {
487 throw new RuntimeTransformException(e);
488 }
489 }
490 }
491 return source;
492 }
493
494 /**
495 * Configures the transformer with exchange specific parameters
496 */
497 protected void configureTransformer(Transformer transformer, Exchange exchange) {
498 if (uriResolver == null) {
499 uriResolver = new XsltUriResolver(exchange.getContext().getClassResolver(), null);
500 }
501 transformer.setURIResolver(uriResolver);
502 transformer.setErrorListener(new XmlErrorListener());
503
504 transformer.clearParameters();
505
506 addParameters(transformer, exchange.getProperties());
507 addParameters(transformer, exchange.getIn().getHeaders());
508 addParameters(transformer, getParameters());
509
510 transformer.setParameter("exchange", exchange);
511 transformer.setParameter("in", exchange.getIn());
512 transformer.setParameter("out", exchange.getOut());
513 }
514
515 protected void addParameters(Transformer transformer, Map<String, Object> map) {
516 Set<Map.Entry<String, Object>> propertyEntries = map.entrySet();
517 for (Map.Entry<String, Object> entry : propertyEntries) {
518 String key = entry.getKey();
519 Object value = entry.getValue();
520 if (value != null) {
521 LOG.trace("Transformer set parameter {} -> {}", key, value);
522 transformer.setParameter(key, value);
523 }
524 }
525 }
526
527 private static final class XsltBuilderOnCompletion extends SynchronizationAdapter {
528 private final String fileName;
529
530 private XsltBuilderOnCompletion(String fileName) {
531 this.fileName = fileName;
532 }
533
534 @Override
535 public void onDone(Exchange exchange) {
536 FileUtil.deleteFile(new File(fileName));
537 }
538
539 @Override
540 public String toString() {
541 return "XsltBuilderOnCompletion";
542 }
543 }
544
545 }