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