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}