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