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