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.component.xslt; 018 019import java.io.IOException; 020import java.net.URI; 021import java.net.URISyntaxException; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025import javax.xml.transform.ErrorListener; 026import javax.xml.transform.Source; 027import javax.xml.transform.TransformerException; 028import javax.xml.transform.TransformerFactory; 029import javax.xml.transform.URIResolver; 030import javax.xml.transform.sax.SAXSource; 031 032import org.xml.sax.EntityResolver; 033import org.xml.sax.InputSource; 034import org.xml.sax.SAXException; 035import org.xml.sax.XMLReader; 036import org.xml.sax.helpers.XMLReaderFactory; 037 038import org.apache.camel.CamelContext; 039import org.apache.camel.Component; 040import org.apache.camel.Exchange; 041import org.apache.camel.api.management.ManagedAttribute; 042import org.apache.camel.api.management.ManagedOperation; 043import org.apache.camel.api.management.ManagedResource; 044import org.apache.camel.builder.xml.ResultHandlerFactory; 045import org.apache.camel.builder.xml.XsltBuilder; 046import org.apache.camel.converter.jaxp.XmlConverter; 047import org.apache.camel.impl.ProcessorEndpoint; 048import org.apache.camel.spi.ClassResolver; 049import org.apache.camel.spi.Injector; 050import org.apache.camel.spi.Metadata; 051import org.apache.camel.spi.UriEndpoint; 052import org.apache.camel.spi.UriParam; 053import org.apache.camel.spi.UriPath; 054import org.apache.camel.util.EndpointHelper; 055import org.apache.camel.util.ObjectHelper; 056import org.apache.camel.util.ServiceHelper; 057import org.slf4j.Logger; 058import org.slf4j.LoggerFactory; 059 060 061/** 062 * Transforms the message using a XSLT template. 063 */ 064@ManagedResource(description = "Managed XsltEndpoint") 065@UriEndpoint(firstVersion = "1.3.0", scheme = "xslt", title = "XSLT", syntax = "xslt:resourceUri", producerOnly = true, label = "core,transformation") 066public class XsltEndpoint extends ProcessorEndpoint { 067 public static final String SAXON_TRANSFORMER_FACTORY_CLASS_NAME = "net.sf.saxon.TransformerFactoryImpl"; 068 069 private static final Logger LOG = LoggerFactory.getLogger(XsltEndpoint.class); 070 071 private volatile boolean cacheCleared; 072 private volatile XsltBuilder xslt; 073 private Map<String, Object> parameters; 074 075 @UriPath @Metadata(required = "true") 076 private String resourceUri; 077 @UriParam(defaultValue = "true") 078 private boolean contentCache = true; 079 @UriParam(label = "advanced") 080 private XmlConverter converter; 081 @UriParam(label = "advanced") 082 private String transformerFactoryClass; 083 @UriParam(label = "advanced") 084 private TransformerFactory transformerFactory; 085 @UriParam 086 private boolean saxon; 087 @UriParam(label = "advanced") 088 private Object saxonConfiguration; 089 @Metadata(label = "advanced") 090 private Map<String, Object> saxonConfigurationProperties = new HashMap<>(); 091 @Metadata(label = "advanced") 092 private Map<String, Object> saxonReaderProperties = new HashMap<>(); 093 @UriParam(label = "advanced", javaType = "java.lang.String") 094 private List<Object> saxonExtensionFunctions; 095 @UriParam(label = "advanced") 096 private ResultHandlerFactory resultHandlerFactory; 097 @UriParam(defaultValue = "true") 098 private boolean failOnNullBody = true; 099 @UriParam(defaultValue = "string") 100 private XsltOutput output = XsltOutput.string; 101 @UriParam(defaultValue = "0") 102 private int transformerCacheSize; 103 @UriParam(label = "advanced") 104 private ErrorListener errorListener; 105 @UriParam(label = "advanced") 106 private URIResolver uriResolver; 107 @UriParam(defaultValue = "true", displayName = "Allow StAX") 108 private boolean allowStAX = true; 109 @UriParam 110 private boolean deleteOutputFile; 111 @UriParam(label = "advanced") 112 private EntityResolver entityResolver; 113 114 @Deprecated 115 public XsltEndpoint(String endpointUri, Component component, XsltBuilder xslt, String resourceUri, 116 boolean cacheStylesheet) throws Exception { 117 super(endpointUri, component, xslt); 118 this.xslt = xslt; 119 this.resourceUri = resourceUri; 120 this.contentCache = cacheStylesheet; 121 } 122 123 public XsltEndpoint(String endpointUri, Component component) { 124 super(endpointUri, component); 125 } 126 127 @ManagedOperation(description = "Clears the cached XSLT stylesheet, forcing to re-load the stylesheet on next request") 128 public void clearCachedStylesheet() { 129 this.cacheCleared = true; 130 } 131 132 @ManagedAttribute(description = "Whether the XSLT stylesheet is cached") 133 public boolean isCacheStylesheet() { 134 return contentCache; 135 } 136 137 public XsltEndpoint findOrCreateEndpoint(String uri, String newResourceUri) { 138 String newUri = uri.replace(resourceUri, newResourceUri); 139 LOG.trace("Getting endpoint with URI: {}", newUri); 140 return getCamelContext().getEndpoint(newUri, XsltEndpoint.class); 141 } 142 143 @Override 144 protected void onExchange(Exchange exchange) throws Exception { 145 if (!contentCache || cacheCleared) { 146 loadResource(resourceUri); 147 } 148 super.onExchange(exchange); 149 } 150 151 public boolean isCacheCleared() { 152 return cacheCleared; 153 } 154 155 public void setCacheCleared(boolean cacheCleared) { 156 this.cacheCleared = cacheCleared; 157 } 158 159 public XsltBuilder getXslt() { 160 return xslt; 161 } 162 163 public void setXslt(XsltBuilder xslt) { 164 this.xslt = xslt; 165 } 166 167 @ManagedAttribute(description = "Path to the template") 168 public String getResourceUri() { 169 return resourceUri; 170 } 171 172 /** 173 * Path to the template. 174 * <p/> 175 * The following is supported by the default URIResolver. 176 * You can prefix with: classpath, file, http, ref, or bean. 177 * classpath, file and http loads the resource using these protocols (classpath is default). 178 * ref will lookup the resource in the registry. 179 * bean will call a method on a bean to be used as the resource. 180 * For bean you can specify the method name after dot, eg bean:myBean.myMethod 181 * 182 * @param resourceUri the resource path 183 */ 184 public void setResourceUri(String resourceUri) { 185 this.resourceUri = resourceUri; 186 } 187 188 public XmlConverter getConverter() { 189 return converter; 190 } 191 192 /** 193 * To use a custom implementation of {@link org.apache.camel.converter.jaxp.XmlConverter} 194 */ 195 public void setConverter(XmlConverter converter) { 196 this.converter = converter; 197 } 198 199 public String getTransformerFactoryClass() { 200 return transformerFactoryClass; 201 } 202 203 /** 204 * To use a custom XSLT transformer factory, specified as a FQN class name 205 */ 206 public void setTransformerFactoryClass(String transformerFactoryClass) { 207 this.transformerFactoryClass = transformerFactoryClass; 208 } 209 210 public TransformerFactory getTransformerFactory() { 211 return transformerFactory; 212 } 213 214 /** 215 * To use a custom XSLT transformer factory 216 */ 217 public void setTransformerFactory(TransformerFactory transformerFactory) { 218 this.transformerFactory = transformerFactory; 219 } 220 221 @ManagedAttribute(description = "Whether to use Saxon as the transformerFactoryClass") 222 public boolean isSaxon() { 223 return saxon; 224 } 225 226 /** 227 * Whether to use Saxon as the transformerFactoryClass. 228 * If enabled then the class net.sf.saxon.TransformerFactoryImpl. You would need to add Saxon to the classpath. 229 */ 230 public void setSaxon(boolean saxon) { 231 this.saxon = saxon; 232 } 233 234 public List<Object> getSaxonExtensionFunctions() { 235 return saxonExtensionFunctions; 236 } 237 238 /** 239 * Allows you to use a custom net.sf.saxon.lib.ExtensionFunctionDefinition. 240 * You would need to add camel-saxon to the classpath. 241 * The function is looked up in the registry, where you can comma to separate multiple values to lookup. 242 */ 243 public void setSaxonExtensionFunctions(List<Object> extensionFunctions) { 244 this.saxonExtensionFunctions = extensionFunctions; 245 } 246 247 /** 248 * Allows you to use a custom net.sf.saxon.lib.ExtensionFunctionDefinition. 249 * You would need to add camel-saxon to the classpath. 250 * The function is looked up in the registry, where you can comma to separate multiple values to lookup. 251 */ 252 public void setSaxonExtensionFunctions(String extensionFunctions) { 253 this.saxonExtensionFunctions = EndpointHelper.resolveReferenceListParameter( 254 getCamelContext(), 255 extensionFunctions, 256 Object.class 257 ); 258 } 259 260 public Object getSaxonConfiguration() { 261 return saxonConfiguration; 262 } 263 264 /** 265 * To use a custom Saxon configuration 266 */ 267 public void setSaxonConfiguration(Object saxonConfiguration) { 268 this.saxonConfiguration = saxonConfiguration; 269 } 270 271 public Map<String, Object> getSaxonConfigurationProperties() { 272 return saxonConfigurationProperties; 273 } 274 275 /** 276 * To set custom Saxon configuration properties 277 */ 278 public void setSaxonConfigurationProperties(Map<String, Object> configurationProperties) { 279 this.saxonConfigurationProperties = configurationProperties; 280 } 281 282 283 public Map<String, Object> getSaxonReaderProperties() { 284 return saxonReaderProperties; 285 } 286 287 /** 288 * To set custom Saxon Reader properties 289 */ 290 public void setSaxonReaderProperties(Map<String, Object> saxonReaderProperties) { 291 this.saxonReaderProperties = saxonReaderProperties; 292 } 293 294 295 public ResultHandlerFactory getResultHandlerFactory() { 296 return resultHandlerFactory; 297 } 298 299 /** 300 * Allows you to use a custom org.apache.camel.builder.xml.ResultHandlerFactory which is capable of 301 * using custom org.apache.camel.builder.xml.ResultHandler types. 302 */ 303 public void setResultHandlerFactory(ResultHandlerFactory resultHandlerFactory) { 304 this.resultHandlerFactory = resultHandlerFactory; 305 } 306 307 @ManagedAttribute(description = "Whether or not to throw an exception if the input body is null") 308 public boolean isFailOnNullBody() { 309 return failOnNullBody; 310 } 311 312 /** 313 * Whether or not to throw an exception if the input body is null. 314 */ 315 public void setFailOnNullBody(boolean failOnNullBody) { 316 this.failOnNullBody = failOnNullBody; 317 } 318 319 @ManagedAttribute(description = "What kind of option to use.") 320 public XsltOutput getOutput() { 321 return output; 322 } 323 324 /** 325 * Option to specify which output type to use. 326 * Possible values are: string, bytes, DOM, file. The first three options are all in memory based, where as file is streamed directly to a java.io.File. 327 * For file you must specify the filename in the IN header with the key Exchange.XSLT_FILE_NAME which is also CamelXsltFileName. 328 * Also any paths leading to the filename must be created beforehand, otherwise an exception is thrown at runtime. 329 */ 330 public void setOutput(XsltOutput output) { 331 this.output = output; 332 } 333 334 public int getTransformerCacheSize() { 335 return transformerCacheSize; 336 } 337 338 /** 339 * The number of javax.xml.transform.Transformer object that are cached for reuse to avoid calls to Template.newTransformer(). 340 */ 341 public void setTransformerCacheSize(int transformerCacheSize) { 342 this.transformerCacheSize = transformerCacheSize; 343 } 344 345 public ErrorListener getErrorListener() { 346 return errorListener; 347 } 348 349 /** 350 * Allows to configure to use a custom javax.xml.transform.ErrorListener. Beware when doing this then the default error 351 * listener which captures any errors or fatal errors and store information on the Exchange as properties is not in use. 352 * So only use this option for special use-cases. 353 */ 354 public void setErrorListener(ErrorListener errorListener) { 355 this.errorListener = errorListener; 356 } 357 358 @ManagedAttribute(description = "Cache for the resource content (the stylesheet file) when it is loaded.") 359 public boolean isContentCache() { 360 return contentCache; 361 } 362 363 /** 364 * Cache for the resource content (the stylesheet file) when it is loaded. 365 * If set to false Camel will reload the stylesheet file on each message processing. This is good for development. 366 * A cached stylesheet can be forced to reload at runtime via JMX using the clearCachedStylesheet operation. 367 */ 368 public void setContentCache(boolean contentCache) { 369 this.contentCache = contentCache; 370 } 371 372 public URIResolver getUriResolver() { 373 return uriResolver; 374 } 375 376 /** 377 * To use a custom javax.xml.transform.URIResolver 378 */ 379 public void setUriResolver(URIResolver uriResolver) { 380 this.uriResolver = uriResolver; 381 } 382 383 @ManagedAttribute(description = "Whether to allow using StAX as the javax.xml.transform.Source") 384 public boolean isAllowStAX() { 385 return allowStAX; 386 } 387 388 /** 389 * Whether to allow using StAX as the javax.xml.transform.Source. 390 */ 391 public void setAllowStAX(boolean allowStAX) { 392 this.allowStAX = allowStAX; 393 } 394 395 public boolean isDeleteOutputFile() { 396 return deleteOutputFile; 397 } 398 399 /** 400 * If you have output=file then this option dictates whether or not the output file should be deleted when the Exchange 401 * is done processing. For example suppose the output file is a temporary file, then it can be a good idea to delete it after use. 402 */ 403 public void setDeleteOutputFile(boolean deleteOutputFile) { 404 this.deleteOutputFile = deleteOutputFile; 405 } 406 407 public EntityResolver getEntityResolver() { 408 return entityResolver; 409 } 410 411 /** 412 * To use a custom org.xml.sax.EntityResolver with javax.xml.transform.sax.SAXSource. 413 */ 414 public void setEntityResolver(EntityResolver entityResolver) { 415 this.entityResolver = entityResolver; 416 } 417 418 public Map<String, Object> getParameters() { 419 return parameters; 420 } 421 422 /** 423 * Additional parameters to configure on the javax.xml.transform.Transformer. 424 */ 425 public void setParameters(Map<String, Object> parameters) { 426 this.parameters = parameters; 427 } 428 429 /** 430 * Loads the resource. 431 * 432 * @param resourceUri the resource to load 433 * @throws TransformerException is thrown if error loading resource 434 * @throws IOException is thrown if error loading resource 435 */ 436 protected void loadResource(String resourceUri) throws TransformerException, IOException { 437 LOG.trace("{} loading schema resource: {}", this, resourceUri); 438 Source source = xslt.getUriResolver().resolve(resourceUri, null); 439 if (this.saxon && this.saxonReaderProperties != null) { 440 //for Saxon we need to create XMLReader for the coming source 441 //so that the features configuration can take effect 442 source = createReaderForSource(source); 443 } 444 if (source == null) { 445 throw new IOException("Cannot load schema resource " + resourceUri); 446 } else { 447 source.setSystemId(resourceUri); 448 449 xslt.setTransformerSource(source); 450 } 451 // now loaded so clear flag 452 cacheCleared = false; 453 } 454 455 private Source createReaderForSource(Source source) { 456 try { 457 XMLReader xmlReader = XMLReaderFactory.createXMLReader(); 458 //xmlReader.setErrorHandler(new DefaultErrorHandler()); 459 for (Map.Entry<String, Object> entry : this.saxonReaderProperties.entrySet()) { 460 String key = entry.getKey(); 461 Object value = entry.getValue(); 462 try { 463 URI uri = new URI(key); 464 if (value != null 465 && (value.toString().equals("true") || (value.toString().equals("false")))) { 466 xmlReader.setFeature(uri.toString(), Boolean.valueOf(value.toString())); 467 } else if (value != null) { 468 xmlReader.setProperty(uri.toString(), value); 469 } 470 } catch (URISyntaxException e) { 471 LOG.debug("{} isn't a valid URI, so ingore it", key); 472 } 473 } 474 InputSource inputSource = SAXSource.sourceToInputSource(source); 475 return new SAXSource(xmlReader, inputSource); 476 } catch (SAXException e) { 477 LOG.info("Can't created XMLReader for source ", e); 478 return null; 479 } 480 481 } 482 483 @Override 484 protected void doStart() throws Exception { 485 super.doStart(); 486 487 final CamelContext ctx = getCamelContext(); 488 final ClassResolver resolver = ctx.getClassResolver(); 489 final Injector injector = ctx.getInjector(); 490 491 LOG.debug("{} using schema resource: {}", this, resourceUri); 492 493 this.xslt = injector.newInstance(XsltBuilder.class); 494 if (converter != null) { 495 xslt.setConverter(converter); 496 } 497 498 boolean useSaxon = false; 499 if (transformerFactoryClass == null && (saxon || saxonExtensionFunctions != null)) { 500 useSaxon = true; 501 transformerFactoryClass = SAXON_TRANSFORMER_FACTORY_CLASS_NAME; 502 } 503 504 TransformerFactory factory = transformerFactory; 505 if (factory == null && transformerFactoryClass != null) { 506 // provide the class loader of this component to work in OSGi environments 507 Class<TransformerFactory> factoryClass = resolver.resolveMandatoryClass(transformerFactoryClass, TransformerFactory.class, XsltComponent.class.getClassLoader()); 508 LOG.debug("Using TransformerFactoryClass {}", factoryClass); 509 factory = injector.newInstance(factoryClass); 510 511 if (useSaxon) { 512 XsltHelper.registerSaxonConfiguration(ctx, factoryClass, factory, saxonConfiguration); 513 XsltHelper.registerSaxonConfigurationProperties(ctx, factoryClass, factory, saxonConfigurationProperties); 514 XsltHelper.registerSaxonExtensionFunctions(ctx, factoryClass, factory, saxonExtensionFunctions); 515 } 516 } 517 518 if (factory != null) { 519 LOG.debug("Using TransformerFactory {}", factory); 520 xslt.getConverter().setTransformerFactory(factory); 521 } 522 if (resultHandlerFactory != null) { 523 xslt.setResultHandlerFactory(resultHandlerFactory); 524 } 525 if (errorListener != null) { 526 xslt.errorListener(errorListener); 527 } 528 xslt.setFailOnNullBody(failOnNullBody); 529 xslt.transformerCacheSize(transformerCacheSize); 530 xslt.setUriResolver(uriResolver); 531 xslt.setEntityResolver(entityResolver); 532 xslt.setAllowStAX(allowStAX); 533 xslt.setDeleteOutputFile(deleteOutputFile); 534 535 configureOutput(xslt, output.name()); 536 537 // any additional transformer parameters then make a copy to avoid side-effects 538 if (parameters != null) { 539 Map<String, Object> copy = new HashMap<String, Object>(parameters); 540 xslt.setParameters(copy); 541 } 542 543 // must load resource first which sets a template and do a stylesheet compilation to catch errors early 544 loadResource(resourceUri); 545 546 // the processor is the xslt builder 547 setProcessor(xslt); 548 } 549 550 protected void configureOutput(XsltBuilder xslt, String output) throws Exception { 551 if (ObjectHelper.isEmpty(output)) { 552 return; 553 } 554 555 if ("string".equalsIgnoreCase(output)) { 556 xslt.outputString(); 557 } else if ("bytes".equalsIgnoreCase(output)) { 558 xslt.outputBytes(); 559 } else if ("DOM".equalsIgnoreCase(output)) { 560 xslt.outputDOM(); 561 } else if ("file".equalsIgnoreCase(output)) { 562 xslt.outputFile(); 563 } else { 564 throw new IllegalArgumentException("Unknown output type: " + output); 565 } 566 } 567 568 @Override 569 protected void doStop() throws Exception { 570 super.doStop(); 571 ServiceHelper.stopService(xslt); 572 } 573 574}