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