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.apache.camel.Exchange; 046import org.apache.camel.ExpectedBodyTypeException; 047import org.apache.camel.Message; 048import org.apache.camel.Processor; 049import org.apache.camel.RuntimeTransformException; 050import org.apache.camel.TypeConverter; 051import org.apache.camel.converter.jaxp.StAX2SAXSource; 052import org.apache.camel.converter.jaxp.XmlConverter; 053import org.apache.camel.support.SynchronizationAdapter; 054import org.apache.camel.util.ExchangeHelper; 055import org.apache.camel.util.FileUtil; 056import org.apache.camel.util.IOHelper; 057import org.slf4j.Logger; 058import org.slf4j.LoggerFactory; 059 060import 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 * <p/> 069 * If using the static <tt>xslt</tt> methods to create a {@link org.apache.camel.builder.xml.XsltBuilder} its recommended 070 * to inject the {@link org.apache.camel.CamelContext} using the {@link #setCamelContext(org.apache.camel.CamelContext)} 071 * and call {@link #start()} to properly initialize the builder before using. 072 * 073 * @version 074 */ 075public class XsltBuilder implements Processor { 076 private static final Logger LOG = LoggerFactory.getLogger(XsltBuilder.class); 077 private Map<String, Object> parameters = new HashMap<String, Object>(); 078 private XmlConverter converter = new XmlConverter(); 079 private Templates template; 080 private volatile BlockingQueue<Transformer> transformers; 081 private ResultHandlerFactory resultHandlerFactory = new StringResultHandlerFactory(); 082 private boolean failOnNullBody = true; 083 private URIResolver uriResolver; 084 private boolean deleteOutputFile; 085 private ErrorListener errorListener; 086 private boolean allowStAX = true; 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<Transformer>(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 boolean isDeleteOutputFile() { 403 return deleteOutputFile; 404 } 405 406 public void setDeleteOutputFile(boolean deleteOutputFile) { 407 this.deleteOutputFile = deleteOutputFile; 408 } 409 410 public ErrorListener getErrorListener() { 411 return errorListener; 412 } 413 414 public void setErrorListener(ErrorListener errorListener) { 415 this.errorListener = errorListener; 416 } 417 418 // Implementation methods 419 // ------------------------------------------------------------------------- 420 private void releaseTransformer(Transformer transformer) { 421 if (transformers != null) { 422 transformer.reset(); 423 transformers.offer(transformer); 424 } 425 } 426 427 private Transformer getTransformer() throws Exception { 428 Transformer t = null; 429 if (transformers != null) { 430 t = transformers.poll(); 431 } 432 if (t == null) { 433 t = createTransformer(); 434 } 435 return t; 436 } 437 438 protected Transformer createTransformer() throws Exception { 439 return getTemplate().newTransformer(); 440 } 441 442 /** 443 * Checks whether we need an {@link InputStream} to access the message body. 444 * <p/> 445 * Depending on the content in the message body, we may not need to convert 446 * to {@link InputStream}. 447 * 448 * @param exchange the current exchange 449 * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting to {@link Source} afterwards. 450 */ 451 protected boolean isInputStreamNeeded(Exchange exchange) { 452 Object body = exchange.getIn().getBody(); 453 if (body == null) { 454 return false; 455 } 456 457 if (body instanceof InputStream) { 458 return true; 459 } else if (body instanceof Source) { 460 return false; 461 } else if (body instanceof String) { 462 return false; 463 } else if (body instanceof byte[]) { 464 return false; 465 } else if (body instanceof Node) { 466 return false; 467 } else if (exchange.getContext().getTypeConverterRegistry().lookup(Source.class, body.getClass()) != null) { 468 //there is a direct and hopefully optimized converter to Source 469 return false; 470 } 471 // yes an input stream is needed 472 return true; 473 } 474 475 /** 476 * Converts the inbound body to a {@link Source}, if the body is <b>not</b> already a {@link Source}. 477 * <p/> 478 * This implementation will prefer to source in the following order: 479 * <ul> 480 * <li>StAX - If StAX is allowed</li> 481 * <li>SAX - SAX as 2nd choice</li> 482 * <li>Stream - Stream as 3rd choice</li> 483 * <li>DOM - DOM as 4th choice</li> 484 * </ul> 485 */ 486 protected Source getSource(Exchange exchange, Object body) { 487 // body may already be a source 488 if (body instanceof Source) { 489 return (Source) body; 490 } 491 Source source = null; 492 if (body != null) { 493 if (isAllowStAX()) { 494 // try StAX if enabled 495 source = exchange.getContext().getTypeConverter().tryConvertTo(StAXSource.class, exchange, body); 496 } 497 if (source == null) { 498 // then try SAX 499 source = exchange.getContext().getTypeConverter().tryConvertTo(SAXSource.class, exchange, body); 500 } 501 if (source == null) { 502 // then try stream 503 source = exchange.getContext().getTypeConverter().tryConvertTo(StreamSource.class, exchange, body); 504 } 505 if (source == null) { 506 // and fallback to DOM 507 source = exchange.getContext().getTypeConverter().tryConvertTo(DOMSource.class, exchange, body); 508 } 509 // as the TypeConverterRegistry will look up source the converter differently if the type converter is loaded different 510 // now we just put the call of source converter at last 511 if (source == null) { 512 TypeConverter tc = exchange.getContext().getTypeConverterRegistry().lookup(Source.class, body.getClass()); 513 if (tc != null) { 514 source = tc.convertTo(Source.class, exchange, body); 515 } 516 } 517 } 518 if (source == null) { 519 if (isFailOnNullBody()) { 520 throw new ExpectedBodyTypeException(exchange, Source.class); 521 } else { 522 try { 523 source = converter.toDOMSource(converter.createDocument()); 524 } catch (ParserConfigurationException e) { 525 throw new RuntimeTransformException(e); 526 } 527 } 528 } 529 return source; 530 } 531 532 533 /** 534 * Configures the transformer with exchange specific parameters 535 */ 536 protected void configureTransformer(Transformer transformer, Exchange exchange) throws Exception { 537 if (uriResolver == null) { 538 uriResolver = new XsltUriResolver(exchange.getContext().getClassResolver(), null); 539 } 540 transformer.setURIResolver(uriResolver); 541 if (errorListener == null) { 542 // set our error listener so we can capture errors and report them back on the exchange 543 transformer.setErrorListener(new DefaultTransformErrorHandler(exchange)); 544 } else { 545 // use custom error listener 546 transformer.setErrorListener(errorListener); 547 } 548 549 transformer.clearParameters(); 550 addParameters(transformer, exchange.getProperties()); 551 addParameters(transformer, exchange.getIn().getHeaders()); 552 addParameters(transformer, getParameters()); 553 transformer.setParameter("exchange", exchange); 554 transformer.setParameter("in", exchange.getIn()); 555 transformer.setParameter("out", exchange.getOut()); 556 } 557 558 protected void addParameters(Transformer transformer, Map<String, Object> map) { 559 Set<Map.Entry<String, Object>> propertyEntries = map.entrySet(); 560 for (Map.Entry<String, Object> entry : propertyEntries) { 561 String key = entry.getKey(); 562 Object value = entry.getValue(); 563 if (value != null) { 564 LOG.trace("Transformer set parameter {} -> {}", key, value); 565 transformer.setParameter(key, value); 566 } 567 } 568 } 569 570 private static final class XsltBuilderOnCompletion extends SynchronizationAdapter { 571 private final String fileName; 572 573 private XsltBuilderOnCompletion(String fileName) { 574 this.fileName = fileName; 575 } 576 577 @Override 578 public void onDone(Exchange exchange) { 579 FileUtil.deleteFile(new File(fileName)); 580 } 581 582 @Override 583 public String toString() { 584 return "XsltBuilderOnCompletion"; 585 } 586 } 587 588}