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