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