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