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