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