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.util.toolbox;
018
019import java.io.IOException;
020import java.util.concurrent.RejectedExecutionException;
021
022import javax.xml.transform.Source;
023import javax.xml.transform.TransformerException;
024import javax.xml.transform.TransformerFactory;
025import javax.xml.transform.URIResolver;
026
027import org.w3c.dom.Document;
028
029import org.apache.camel.CamelContext;
030import org.apache.camel.CamelContextAware;
031import org.apache.camel.Exchange;
032import org.apache.camel.builder.xml.XsltBuilder;
033import org.apache.camel.builder.xml.XsltUriResolver;
034import org.apache.camel.component.xslt.XsltEndpoint;
035import org.apache.camel.component.xslt.XsltOutput;
036import org.apache.camel.processor.aggregate.AggregationStrategy;
037import org.apache.camel.support.ServiceSupport;
038import org.apache.camel.util.ObjectHelper;
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041
042/**
043 * The XSLT Aggregation Strategy enables you to use XSL stylesheets to aggregate messages.
044 * <p>
045 * Since XSLT does not directly support providing multiple XML payloads as an input, this aggregator injects
046 * the new incoming XML document (<tt>newExchange</tt>) into the <tt>oldExchange</tt> as an exchange property of
047 * type {@link Document}. The old exchange therefore remains accessible as the root context.
048 * This exchange property can then be accessed from your XSLT by declaring an {@code <xsl:param />} at the top
049 * of your stylesheet:
050 *
051 * <code>
052 *     <xsl:param name="new-exchange" />
053 *
054 *     <xsl:template match="/">
055 *         <abc>
056 *             <xsl:copy-of select="/ElementFromOldExchange" />
057 *             <xsl:copy-of select="$new-exchange/ElementFromNewExchange" />
058 *         </abc>
059 *     </xsl:template>
060 * </code>
061 *
062 * The exchange property name defaults to <tt>new-exchange</tt> but can be
063 * changed through {@link #setPropertyName(String)}.
064 * <p>
065 * Some code bits have been copied from the {@link org.apache.camel.component.xslt.XsltEndpoint}.
066 */
067public class XsltAggregationStrategy extends ServiceSupport implements AggregationStrategy, CamelContextAware {
068
069    private static final Logger LOG = LoggerFactory.getLogger(XsltAggregationStrategy.class);
070    private static final String DEFAULT_PROPERTY_NAME = "new-exchange";
071
072    private volatile XsltBuilder xslt;
073    private volatile URIResolver uriResolver;
074    private CamelContext camelContext;
075
076    private String propertyName;
077    private String xslFile;
078    private String transformerFactoryClass;
079    private XsltOutput output = XsltOutput.string;
080
081    /**
082     * Constructor.
083     *
084     * @param xslFileLocation location of the XSL transformation
085     */
086    public XsltAggregationStrategy(String xslFileLocation) {
087        this.xslFile = xslFileLocation;
088    }
089
090    @Override
091    public CamelContext getCamelContext() {
092        return camelContext;
093    }
094
095    @Override
096    public void setCamelContext(CamelContext camelContext) {
097        this.camelContext = camelContext;
098    }
099
100    @Override
101    public Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
102        // guard against unlikely NPE
103        if (newExchange == null) {
104            return oldExchange;
105        }
106
107        // in the first call to this aggregation, do not call the XSLT but instead store the
108        // incoming exchange
109        if (oldExchange == null) {
110            return newExchange;
111        }
112
113        if (!isRunAllowed()) {
114            throw new RejectedExecutionException();
115        }
116
117        try {
118            oldExchange.setProperty(propertyName, newExchange.getIn().getBody(Document.class));
119            xslt.process(oldExchange);
120            return oldExchange;
121        } catch (Throwable e) {
122            oldExchange.setException(e);
123        }
124
125        return oldExchange;
126    }
127
128    public void setOutput(XsltOutput output) {
129        this.output = output;
130    }
131
132    public void setXslt(XsltBuilder xslt) {
133        this.xslt = xslt;
134    }
135
136    public void setUriResolver(URIResolver uriResolver) {
137        this.uriResolver = uriResolver;
138    }
139
140    public void setTransformerFactoryClass(String transformerFactoryClass) {
141        this.transformerFactoryClass = transformerFactoryClass;
142    }
143
144    public String getPropertyName() {
145        return propertyName;
146    }
147
148    public void setPropertyName(String propertyName) {
149        this.propertyName = propertyName;
150    }
151
152    @Deprecated
153    protected void initialize(CamelContext context) throws Exception {
154        this.camelContext = context;
155    }
156
157    protected void configureOutput(XsltBuilder xslt, String output) throws Exception {
158        if (ObjectHelper.isEmpty(output)) {
159            return;
160        }
161
162        if ("string".equalsIgnoreCase(output)) {
163            xslt.outputString();
164        } else if ("bytes".equalsIgnoreCase(output)) {
165            xslt.outputBytes();
166        } else if ("DOM".equalsIgnoreCase(output)) {
167            xslt.outputDOM();
168        } else if ("file".equalsIgnoreCase(output)) {
169            xslt.outputFile();
170        } else {
171            throw new IllegalArgumentException("Unknown output type: " + output);
172        }
173    }
174
175    /**
176     * Loads the resource.
177     *
178     * @param resourceUri the resource to load
179     * @throws TransformerException is thrown if error loading resource
180     * @throws IOException          is thrown if error loading resource
181     */
182    protected void loadResource(String resourceUri) throws TransformerException, IOException {
183        LOG.trace("{} loading schema resource: {}", this, resourceUri);
184        Source source = xslt.getUriResolver().resolve(resourceUri, null);
185        if (source == null) {
186            throw new IOException("Cannot load schema resource " + resourceUri);
187        } else {
188            xslt.setTransformerSource(source);
189        }
190    }
191
192    // --- fluent builders ---
193    public static XsltAggregationStrategy create(String xslFile) {
194        return new XsltAggregationStrategy(xslFile);
195    }
196
197    public XsltAggregationStrategy withPropertyName(String propertyName) {
198        setPropertyName(propertyName);
199        return this;
200    }
201
202    public XsltAggregationStrategy withOutput(XsltOutput output) {
203        setOutput(output);
204        return this;
205    }
206
207    public XsltAggregationStrategy withUriResolver(URIResolver resolver) {
208        setUriResolver(resolver);
209        return this;
210    }
211
212    public XsltAggregationStrategy withTransformerFactoryClass(String clazz) {
213        setTransformerFactoryClass(clazz);
214        return this;
215    }
216
217    public XsltAggregationStrategy withSaxon() {
218        setTransformerFactoryClass(XsltEndpoint.SAXON_TRANSFORMER_FACTORY_CLASS_NAME);
219        return this;
220    }
221
222    @Override
223    protected void doStart() throws Exception {
224        ObjectHelper.notNull(camelContext, "CamelContext", this);
225
226        // set the default property name if not set
227        this.propertyName = ObjectHelper.isNotEmpty(propertyName) ? propertyName : DEFAULT_PROPERTY_NAME;
228
229        // initialize the XsltBuilder
230        this.xslt = camelContext.getInjector().newInstance(XsltBuilder.class);
231
232        if (transformerFactoryClass != null) {
233            Class<?> factoryClass = camelContext.getClassResolver().resolveMandatoryClass(transformerFactoryClass,
234                XsltAggregationStrategy.class.getClassLoader());
235            TransformerFactory factory = (TransformerFactory) camelContext.getInjector().newInstance(factoryClass);
236            xslt.getConverter().setTransformerFactory(factory);
237        }
238
239        if (uriResolver == null) {
240            uriResolver = new XsltUriResolver(camelContext, xslFile);
241        }
242
243        xslt.setUriResolver(uriResolver);
244        xslt.setFailOnNullBody(true);
245        xslt.transformerCacheSize(0);
246        xslt.setAllowStAX(true);
247
248        configureOutput(xslt, output.name());
249        loadResource(xslFile);
250    }
251
252    @Override
253    protected void doStop() throws Exception {
254        // noop
255    }
256}