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") 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 public XmlConverter getConverter() { 190 return converter; 191 } 192 193 /** 194 * To use a custom implementation of {@link org.apache.camel.converter.jaxp.XmlConverter} 195 */ 196 public void setConverter(XmlConverter converter) { 197 this.converter = converter; 198 } 199 200 public String getTransformerFactoryClass() { 201 return transformerFactoryClass; 202 } 203 204 /** 205 * To use a custom XSLT transformer factory, specified as a FQN class name 206 */ 207 public void setTransformerFactoryClass(String transformerFactoryClass) { 208 this.transformerFactoryClass = transformerFactoryClass; 209 } 210 211 public TransformerFactory getTransformerFactory() { 212 return transformerFactory; 213 } 214 215 /** 216 * To use a custom XSLT transformer factory 217 */ 218 public void setTransformerFactory(TransformerFactory transformerFactory) { 219 this.transformerFactory = transformerFactory; 220 } 221 222 @ManagedAttribute(description = "Whether to use Saxon as the transformerFactoryClass") 223 public boolean isSaxon() { 224 return saxon; 225 } 226 227 /** 228 * Whether to use Saxon as the transformerFactoryClass. 229 * If enabled then the class net.sf.saxon.TransformerFactoryImpl. You would need to add Saxon to the classpath. 230 */ 231 public void setSaxon(boolean saxon) { 232 this.saxon = saxon; 233 } 234 235 public List<Object> getSaxonExtensionFunctions() { 236 return saxonExtensionFunctions; 237 } 238 239 /** 240 * Allows you to use a custom net.sf.saxon.lib.ExtensionFunctionDefinition. 241 * You would need to add camel-saxon to the classpath. 242 * The function is looked up in the registry, where you can comma to separate multiple values to lookup. 243 */ 244 public void setSaxonExtensionFunctions(List<Object> extensionFunctions) { 245 this.saxonExtensionFunctions = extensionFunctions; 246 } 247 248 /** 249 * Allows you to use a custom net.sf.saxon.lib.ExtensionFunctionDefinition. 250 * You would need to add camel-saxon to the classpath. 251 * The function is looked up in the registry, where you can comma to separate multiple values to lookup. 252 */ 253 public void setSaxonExtensionFunctions(String extensionFunctions) { 254 this.saxonExtensionFunctions = EndpointHelper.resolveReferenceListParameter( 255 getCamelContext(), 256 extensionFunctions, 257 Object.class 258 ); 259 } 260 261 public Object getSaxonConfiguration() { 262 return saxonConfiguration; 263 } 264 265 /** 266 * To use a custom Saxon configuration 267 */ 268 public void setSaxonConfiguration(Object saxonConfiguration) { 269 this.saxonConfiguration = saxonConfiguration; 270 } 271 272 public Map<String, Object> getSaxonConfigurationProperties() { 273 return saxonConfigurationProperties; 274 } 275 276 /** 277 * To set custom Saxon configuration properties 278 */ 279 public void setSaxonConfigurationProperties(Map<String, Object> configurationProperties) { 280 this.saxonConfigurationProperties = configurationProperties; 281 } 282 283 284 public Map<String, Object> getSaxonReaderProperties() { 285 return saxonReaderProperties; 286 } 287 288 /** 289 * To set custom Saxon Reader properties 290 */ 291 public void setSaxonReaderProperties(Map<String, Object> saxonReaderProperties) { 292 this.saxonReaderProperties = saxonReaderProperties; 293 } 294 295 296 public ResultHandlerFactory getResultHandlerFactory() { 297 return resultHandlerFactory; 298 } 299 300 /** 301 * Allows you to use a custom org.apache.camel.builder.xml.ResultHandlerFactory which is capable of 302 * using custom org.apache.camel.builder.xml.ResultHandler types. 303 */ 304 public void setResultHandlerFactory(ResultHandlerFactory resultHandlerFactory) { 305 this.resultHandlerFactory = resultHandlerFactory; 306 } 307 308 @ManagedAttribute(description = "Whether or not to throw an exception if the input body is null") 309 public boolean isFailOnNullBody() { 310 return failOnNullBody; 311 } 312 313 /** 314 * Whether or not to throw an exception if the input body is null. 315 */ 316 public void setFailOnNullBody(boolean failOnNullBody) { 317 this.failOnNullBody = failOnNullBody; 318 } 319 320 @ManagedAttribute(description = "What kind of option to use.") 321 public XsltOutput getOutput() { 322 return output; 323 } 324 325 /** 326 * Option to specify which output type to use. 327 * 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. 328 * For file you must specify the filename in the IN header with the key Exchange.XSLT_FILE_NAME which is also CamelXsltFileName. 329 * Also any paths leading to the filename must be created beforehand, otherwise an exception is thrown at runtime. 330 */ 331 public void setOutput(XsltOutput output) { 332 this.output = output; 333 } 334 335 public int getTransformerCacheSize() { 336 return transformerCacheSize; 337 } 338 339 /** 340 * The number of javax.xml.transform.Transformer object that are cached for reuse to avoid calls to Template.newTransformer(). 341 */ 342 public void setTransformerCacheSize(int transformerCacheSize) { 343 this.transformerCacheSize = transformerCacheSize; 344 } 345 346 public ErrorListener getErrorListener() { 347 return errorListener; 348 } 349 350 /** 351 * Allows to configure to use a custom javax.xml.transform.ErrorListener. Beware when doing this then the default error 352 * listener which captures any errors or fatal errors and store information on the Exchange as properties is not in use. 353 * So only use this option for special use-cases. 354 */ 355 public void setErrorListener(ErrorListener errorListener) { 356 this.errorListener = errorListener; 357 } 358 359 @ManagedAttribute(description = "Cache for the resource content (the stylesheet file) when it is loaded.") 360 public boolean isContentCache() { 361 return contentCache; 362 } 363 364 /** 365 * Cache for the resource content (the stylesheet file) when it is loaded. 366 * If set to false Camel will reload the stylesheet file on each message processing. This is good for development. 367 * A cached stylesheet can be forced to reload at runtime via JMX using the clearCachedStylesheet operation. 368 */ 369 public void setContentCache(boolean contentCache) { 370 this.contentCache = contentCache; 371 } 372 373 public URIResolver getUriResolver() { 374 return uriResolver; 375 } 376 377 /** 378 * To use a custom javax.xml.transform.URIResolver 379 */ 380 public void setUriResolver(URIResolver uriResolver) { 381 this.uriResolver = uriResolver; 382 } 383 384 @ManagedAttribute(description = "Whether to allow using StAX as the javax.xml.transform.Source") 385 public boolean isAllowStAX() { 386 return allowStAX; 387 } 388 389 /** 390 * Whether to allow using StAX as the javax.xml.transform.Source. 391 */ 392 public void setAllowStAX(boolean allowStAX) { 393 this.allowStAX = allowStAX; 394 } 395 396 public boolean isDeleteOutputFile() { 397 return deleteOutputFile; 398 } 399 400 /** 401 * If you have output=file then this option dictates whether or not the output file should be deleted when the Exchange 402 * 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. 403 */ 404 public void setDeleteOutputFile(boolean deleteOutputFile) { 405 this.deleteOutputFile = deleteOutputFile; 406 } 407 408 public EntityResolver getEntityResolver() { 409 return entityResolver; 410 } 411 412 /** 413 * To use a custom org.xml.sax.EntityResolver with javax.xml.transform.sax.SAXSource. 414 */ 415 public void setEntityResolver(EntityResolver entityResolver) { 416 this.entityResolver = entityResolver; 417 } 418 419 public Map<String, Object> getParameters() { 420 return parameters; 421 } 422 423 /** 424 * Additional parameters to configure on the javax.xml.transform.Transformer. 425 */ 426 public void setParameters(Map<String, Object> parameters) { 427 this.parameters = parameters; 428 } 429 430 /** 431 * Loads the resource. 432 * 433 * @param resourceUri the resource to load 434 * @throws TransformerException is thrown if error loading resource 435 * @throws IOException is thrown if error loading resource 436 */ 437 protected void loadResource(String resourceUri) throws TransformerException, IOException { 438 LOG.trace("{} loading schema resource: {}", this, resourceUri); 439 Source source = xslt.getUriResolver().resolve(resourceUri, null); 440 if (this.saxon && this.saxonReaderProperties != null) { 441 //for Saxon we need to create XMLReader for the coming source 442 //so that the features configuration can take effect 443 source = createReaderForSource(source); 444 } 445 if (source == null) { 446 throw new IOException("Cannot load schema resource " + resourceUri); 447 } else { 448 xslt.setTransformerSource(source); 449 } 450 // now loaded so clear flag 451 cacheCleared = false; 452 } 453 454 private Source createReaderForSource(Source source) { 455 try { 456 XMLReader xmlReader = XMLReaderFactory.createXMLReader(); 457 //xmlReader.setErrorHandler(new DefaultErrorHandler()); 458 for (Map.Entry<String, Object> entry : this.saxonReaderProperties.entrySet()) { 459 String key = entry.getKey(); 460 Object value = entry.getValue(); 461 try { 462 URI uri = new URI(key); 463 if (value != null 464 && (value.toString().equals("true") || (value.toString().equals("false")))) { 465 xmlReader.setFeature(uri.toString(), Boolean.valueOf(value.toString())); 466 } else if (value != null) { 467 xmlReader.setProperty(uri.toString(), value); 468 } 469 } catch (URISyntaxException e) { 470 LOG.debug("{} isn't a valid URI, so ingore it", key); 471 } 472 } 473 InputSource inputSource = SAXSource.sourceToInputSource(source); 474 return new SAXSource(xmlReader, inputSource); 475 } catch (SAXException e) { 476 LOG.info("Can't created XMLReader for source ", e); 477 return null; 478 } 479 480 } 481 482 @Override 483 protected void doStart() throws Exception { 484 super.doStart(); 485 486 final CamelContext ctx = getCamelContext(); 487 final ClassResolver resolver = ctx.getClassResolver(); 488 final Injector injector = ctx.getInjector(); 489 490 LOG.debug("{} using schema resource: {}", this, resourceUri); 491 492 this.xslt = injector.newInstance(XsltBuilder.class); 493 if (converter != null) { 494 xslt.setConverter(converter); 495 } 496 497 boolean useSaxon = false; 498 if (transformerFactoryClass == null && (saxon || saxonExtensionFunctions != null)) { 499 useSaxon = true; 500 transformerFactoryClass = SAXON_TRANSFORMER_FACTORY_CLASS_NAME; 501 } 502 503 TransformerFactory factory = transformerFactory; 504 if (factory == null && transformerFactoryClass != null) { 505 // provide the class loader of this component to work in OSGi environments 506 Class<TransformerFactory> factoryClass = resolver.resolveMandatoryClass(transformerFactoryClass, TransformerFactory.class, XsltComponent.class.getClassLoader()); 507 LOG.debug("Using TransformerFactoryClass {}", factoryClass); 508 factory = injector.newInstance(factoryClass); 509 510 if (useSaxon) { 511 XsltHelper.registerSaxonConfiguration(ctx, factoryClass, factory, saxonConfiguration); 512 XsltHelper.registerSaxonConfigurationProperties(ctx, factoryClass, factory, saxonConfigurationProperties); 513 XsltHelper.registerSaxonExtensionFunctions(ctx, factoryClass, factory, saxonExtensionFunctions); 514 } 515 } 516 517 if (factory != null) { 518 LOG.debug("Using TransformerFactory {}", factory); 519 xslt.getConverter().setTransformerFactory(factory); 520 } 521 if (resultHandlerFactory != null) { 522 xslt.setResultHandlerFactory(resultHandlerFactory); 523 } 524 if (errorListener != null) { 525 xslt.errorListener(errorListener); 526 } 527 xslt.setFailOnNullBody(failOnNullBody); 528 xslt.transformerCacheSize(transformerCacheSize); 529 xslt.setUriResolver(uriResolver); 530 xslt.setEntityResolver(entityResolver); 531 xslt.setAllowStAX(allowStAX); 532 xslt.setDeleteOutputFile(deleteOutputFile); 533 534 configureOutput(xslt, output.name()); 535 536 // any additional transformer parameters then make a copy to avoid side-effects 537 if (parameters != null) { 538 Map<String, Object> copy = new HashMap<>(parameters); 539 xslt.setParameters(copy); 540 } 541 542 // must load resource first which sets a template and do a stylesheet compilation to catch errors early 543 loadResource(resourceUri); 544 545 // the processor is the xslt builder 546 setProcessor(xslt); 547 } 548 549 protected void configureOutput(XsltBuilder xslt, String output) throws Exception { 550 if (ObjectHelper.isEmpty(output)) { 551 return; 552 } 553 554 if ("string".equalsIgnoreCase(output)) { 555 xslt.outputString(); 556 } else if ("bytes".equalsIgnoreCase(output)) { 557 xslt.outputBytes(); 558 } else if ("DOM".equalsIgnoreCase(output)) { 559 xslt.outputDOM(); 560 } else if ("file".equalsIgnoreCase(output)) { 561 xslt.outputFile(); 562 } else { 563 throw new IllegalArgumentException("Unknown output type: " + output); 564 } 565 } 566 567 @Override 568 protected void doStop() throws Exception { 569 super.doStop(); 570 ServiceHelper.stopService(xslt); 571 } 572 573}