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