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