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