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