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