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