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     */
017    package org.apache.camel.component.xquery;
018    
019    import java.io.ByteArrayOutputStream;
020    import java.io.File;
021    import java.io.FileNotFoundException;
022    import java.io.IOException;
023    import java.io.InputStream;
024    import java.io.Reader;
025    import java.io.StringWriter;
026    import java.net.URL;
027    import java.util.Collection;
028    import java.util.HashMap;
029    import java.util.List;
030    import java.util.Map;
031    import java.util.Properties;
032    import java.util.Set;
033    import java.util.concurrent.atomic.AtomicBoolean;
034    
035    import javax.xml.transform.Result;
036    import javax.xml.transform.Source;
037    import javax.xml.transform.dom.DOMResult;
038    import javax.xml.transform.stream.StreamResult;
039    
040    import org.w3c.dom.Node;
041    
042    import net.sf.saxon.Configuration;
043    import net.sf.saxon.om.DocumentInfo;
044    import net.sf.saxon.om.Item;
045    import net.sf.saxon.om.SequenceIterator;
046    import net.sf.saxon.query.DynamicQueryContext;
047    import net.sf.saxon.query.StaticQueryContext;
048    import net.sf.saxon.query.XQueryExpression;
049    import net.sf.saxon.trans.XPathException;
050    
051    import org.apache.camel.Exchange;
052    import org.apache.camel.Expression;
053    import org.apache.camel.Message;
054    import org.apache.camel.NoTypeConversionAvailableException;
055    import org.apache.camel.Predicate;
056    import org.apache.camel.Processor;
057    import org.apache.camel.RuntimeExpressionException;
058    import org.apache.camel.converter.IOConverter;
059    import org.apache.camel.converter.jaxp.BytesSource;
060    import org.apache.camel.converter.jaxp.StringSource;
061    import org.apache.camel.converter.jaxp.XmlConverter;
062    import org.apache.camel.spi.NamespaceAware;
063    import org.apache.camel.util.MessageHelper;
064    import org.apache.camel.util.ObjectHelper;
065    import org.apache.commons.logging.Log;
066    import org.apache.commons.logging.LogFactory;
067    
068    /**
069     * Creates an XQuery builder
070     *
071     * @version $Revision: 3358 $
072     */
073    public abstract class XQueryBuilder implements Expression<Exchange>, Predicate<Exchange>, NamespaceAware, Processor {
074        private static final transient Log LOG = LogFactory.getLog(XQueryBuilder.class);
075        private Configuration configuration;
076        private XQueryExpression expression;
077        private StaticQueryContext staticQueryContext;
078        private Map<String, Object> parameters = new HashMap<String, Object>();
079        private Map<String, String> namespacePrefixes = new HashMap<String, String>();
080        private XmlConverter converter = new XmlConverter();
081        private ResultFormat resultsFormat = ResultFormat.DOM;
082        private Properties properties = new Properties();
083        private Class resultType;
084        private final AtomicBoolean initialized = new AtomicBoolean(false);
085    
086        @Override
087        public String toString() {
088            return "XQuery[" + expression + "]";
089        }
090    
091        public void process(Exchange exchange) throws Exception {
092            Object body = evaluate(exchange);
093            exchange.getOut(true).setBody(body);
094    
095            // propogate headers
096            exchange.getOut().getHeaders().putAll(exchange.getIn().getHeaders());
097        }
098    
099        public Object evaluate(Exchange exchange) {
100            try {
101                if (LOG.isDebugEnabled()) {
102                    LOG.debug("Evaluation " + expression + " for exchange: " + exchange);
103                }
104    
105                if (resultType != null) {
106                    if (resultType.equals(String.class)) {
107                        return evaluateAsString(exchange);
108                    } else if (resultType.isAssignableFrom(Collection.class)) {
109                        return evaluateAsList(exchange);
110                    } else if (resultType.isAssignableFrom(Node.class)) {
111                        return evaluateAsDOM(exchange);
112                    } else {
113                        throw new IllegalArgumentException("ResultType: " + resultType.getCanonicalName() + " not supported");
114                    }
115                }
116                switch (resultsFormat) {
117                case Bytes:
118                    return evaluateAsBytes(exchange);
119                case BytesSource:
120                    return evaluateAsBytesSource(exchange);
121                case DOM:
122                    return evaluateAsDOM(exchange);
123                case List:
124                    return evaluateAsList(exchange);
125                case StringSource:
126                    return evaluateAsStringSource(exchange);
127                case String:
128                default:
129                    return evaluateAsString(exchange);
130                }
131            } catch (Exception e) {
132                throw new RuntimeExpressionException(e);
133            }
134        }
135    
136        public List evaluateAsList(Exchange exchange) throws Exception {
137            initialize(exchange);
138    
139            return getExpression().evaluate(createDynamicContext(exchange));
140        }
141    
142        public Object evaluateAsStringSource(Exchange exchange) throws Exception {
143            initialize(exchange);
144    
145            String text = evaluateAsString(exchange);
146            return new StringSource(text);
147        }
148    
149        public Object evaluateAsBytesSource(Exchange exchange) throws Exception {
150            initialize(exchange);
151    
152            byte[] bytes = evaluateAsBytes(exchange);
153            return new BytesSource(bytes);
154        }
155    
156        public Node evaluateAsDOM(Exchange exchange) throws Exception {
157            initialize(exchange);
158    
159            DOMResult result = new DOMResult();
160            DynamicQueryContext context = createDynamicContext(exchange);
161            XQueryExpression expression = getExpression();
162            expression.pull(context, result, properties);
163            return result.getNode();
164        }
165    
166        public byte[] evaluateAsBytes(Exchange exchange) throws Exception {
167            initialize(exchange);
168    
169            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
170            Result result = new StreamResult(buffer);
171            getExpression().pull(createDynamicContext(exchange), result, properties);
172            byte[] bytes = buffer.toByteArray();
173            return bytes;
174        }
175    
176        public String evaluateAsString(Exchange exchange) throws Exception {
177            initialize(exchange);
178    
179            StringWriter buffer = new StringWriter();
180            SequenceIterator iter = getExpression().iterator(createDynamicContext(exchange));
181            for (Item item = iter.next(); item != null; item = iter.next()) {
182                buffer.append(item.getStringValueCS());
183            }
184            return buffer.toString();
185        }
186    
187        public boolean matches(Exchange exchange) {
188            try {
189                List list = evaluateAsList(exchange);
190                return matches(exchange, list);
191            } catch (Exception e) {
192                throw new RuntimeExpressionException(e);
193            }
194        }
195    
196        public void assertMatches(String text, Exchange exchange) throws AssertionError {
197            try {
198                List list = evaluateAsList(exchange);
199                if (!matches(exchange, list)) {
200                    throw new AssertionError(this + " failed on " + exchange + " as evaluated: " + list);
201                }
202            } catch (Exception e) {
203                throw new AssertionError(e);
204            }
205        }
206    
207        // Static helper methods
208        //-------------------------------------------------------------------------
209        public static XQueryBuilder xquery(final String queryText) {
210            return new XQueryBuilder() {
211                protected XQueryExpression createQueryExpression(StaticQueryContext staticQueryContext)
212                    throws XPathException {
213                    return staticQueryContext.compileQuery(queryText);
214                }
215            };
216        }
217    
218        public static XQueryBuilder xquery(final Reader reader) {
219            return new XQueryBuilder() {
220                protected XQueryExpression createQueryExpression(StaticQueryContext staticQueryContext)
221                    throws XPathException, IOException {
222                    return staticQueryContext.compileQuery(reader);
223                }
224            };
225        }
226    
227        public static XQueryBuilder xquery(final InputStream in, final String characterSet) {
228            return new XQueryBuilder() {
229                protected XQueryExpression createQueryExpression(StaticQueryContext staticQueryContext)
230                    throws XPathException, IOException {
231                    return staticQueryContext.compileQuery(in, characterSet);
232                }
233            };
234        }
235    
236        public static XQueryBuilder xquery(File file, String characterSet) throws FileNotFoundException {
237            return xquery(IOConverter.toInputStream(file), characterSet);
238        }
239    
240        public static XQueryBuilder xquery(URL url, String characterSet) throws IOException {
241            return xquery(IOConverter.toInputStream(url), characterSet);
242        }
243    
244        public static XQueryBuilder xquery(File file) throws FileNotFoundException {
245            return xquery(IOConverter.toInputStream(file), ObjectHelper.getDefaultCharacterSet());
246        }
247    
248        public static XQueryBuilder xquery(URL url) throws IOException {
249            return xquery(IOConverter.toInputStream(url), ObjectHelper.getDefaultCharacterSet());
250        }
251    
252        // Fluent API
253        // -------------------------------------------------------------------------
254        public XQueryBuilder parameter(String name, Object value) {
255            parameters.put(name, value);
256            return this;
257        }
258    
259        public XQueryBuilder namespace(String prefix, String uri) {
260            namespacePrefixes.put(prefix, uri);
261            // more namespace, we must re initialize
262            initialized.set(false);
263            return this;
264        }
265    
266        public XQueryBuilder resultType(Class resultType) {
267            setResultType(resultType);
268            return this;
269        }
270    
271        public XQueryBuilder asBytes() {
272            setResultsFormat(ResultFormat.Bytes);
273            return this;
274        }
275    
276        public XQueryBuilder asBytesSource() {
277            setResultsFormat(ResultFormat.BytesSource);
278            return this;
279        }
280    
281        public XQueryBuilder asDOM() {
282            setResultsFormat(ResultFormat.DOM);
283            return this;
284        }
285    
286        public XQueryBuilder asDOMSource() {
287            setResultsFormat(ResultFormat.DOMSource);
288            return this;
289        }
290    
291        public XQueryBuilder asList() {
292            setResultsFormat(ResultFormat.List);
293            return this;
294        }
295    
296        public XQueryBuilder asString() {
297            setResultsFormat(ResultFormat.String);
298            return this;
299        }
300    
301        public XQueryBuilder asStringSource() {
302            setResultsFormat(ResultFormat.StringSource);
303            return this;
304        }
305    
306        // Properties
307        // -------------------------------------------------------------------------
308    
309        /**
310         * Configures the namespace context from the given DOM element
311         */
312        public void setNamespaces(Map<String, String> namespaces) {
313            namespacePrefixes.putAll(namespaces);
314            // more namespace, we must re initialize
315            initialized.set(false);
316        }
317    
318        public XQueryExpression getExpression() throws IOException, XPathException {
319            return expression;
320        }
321    
322        public Configuration getConfiguration() {
323            return configuration;
324        }
325    
326        public void setConfiguration(Configuration configuration) {
327            this.configuration = configuration;
328            // change configuration, we must re intialize
329            initialized.set(false);
330        }
331    
332        public StaticQueryContext getStaticQueryContext() throws XPathException {
333            return staticQueryContext;
334        }
335    
336        public void setStaticQueryContext(StaticQueryContext staticQueryContext) {
337            this.staticQueryContext = staticQueryContext;
338            // change context, we must re intialize
339            initialized.set(false);
340        }
341    
342        public Map<String, Object> getParameters() {
343            return parameters;
344        }
345    
346        public void setParameters(Map<String, Object> parameters) {
347            this.parameters = parameters;
348        }
349    
350        public Properties getProperties() {
351            return properties;
352        }
353    
354        public void setProperties(Properties properties) {
355            this.properties = properties;
356        }
357    
358        public ResultFormat getResultsFormat() {
359            return resultsFormat;
360        }
361    
362        public void setResultsFormat(ResultFormat resultsFormat) {
363            this.resultsFormat = resultsFormat;
364        }
365    
366        public Class getResultType() {
367            return resultType;
368        }
369    
370        public void setResultType(Class resultType) {
371            this.resultType = resultType;
372        }
373    
374        // Implementation methods
375        // -------------------------------------------------------------------------
376    
377        /**
378         * A factory method to create the XQuery expression
379         */
380        protected abstract XQueryExpression createQueryExpression(StaticQueryContext staticQueryContext)
381            throws XPathException, IOException;
382    
383        /**
384         * Creates a dynamic context for the given exchange
385         */
386        protected DynamicQueryContext createDynamicContext(Exchange exchange) throws Exception {
387            Configuration config = getConfiguration();
388            DynamicQueryContext dynamicQueryContext = new DynamicQueryContext(config);
389    
390            Message in = exchange.getIn();
391            Source source = null;
392            try {
393                Item item = in.getBody(Item.class);
394                dynamicQueryContext.setContextItem(item);
395            } catch (NoTypeConversionAvailableException e) {
396                try {
397                    source = in.getBody(Source.class);
398                } catch (NoTypeConversionAvailableException e2) {
399                    if (LOG.isDebugEnabled()) {
400                        LOG.debug("No body available on exchange so using an empty document: " + exchange);
401                    }
402                    source = converter.toSource(converter.createDocument());
403                }
404                DocumentInfo doc = getStaticQueryContext().buildDocument(source);
405                dynamicQueryContext.setContextItem(doc);
406            }
407            
408            configureQuery(dynamicQueryContext, exchange);
409            // call the reset if the in message body is StreamCache
410            MessageHelper.resetStreamCache(exchange.getIn());
411            return dynamicQueryContext;
412        }
413    
414        /**
415         * Configures the dynamic context with exchange specific parameters
416         */
417        protected void configureQuery(DynamicQueryContext dynamicQueryContext, Exchange exchange)
418            throws Exception {
419            addParameters(dynamicQueryContext, exchange.getProperties());
420            addParameters(dynamicQueryContext, exchange.getIn().getHeaders(), "in.headers.");
421            dynamicQueryContext.setParameter("in.body", exchange.getIn().getBody());
422            addParameters(dynamicQueryContext, getParameters());
423    
424            dynamicQueryContext.setParameter("exchange", exchange);
425            if (exchange.getOut(false) != null && exchange.getPattern().isOutCapable()) {
426                dynamicQueryContext.setParameter("out.body", exchange.getOut().getBody());
427                addParameters(dynamicQueryContext, exchange.getOut().getHeaders(), "out.headers.");
428            }
429        }
430        
431        protected void addParameters(DynamicQueryContext dynamicQueryContext, Map<String, Object> map) {
432            addParameters(dynamicQueryContext, map, "");        
433        }
434    
435        protected void addParameters(DynamicQueryContext dynamicQueryContext, Map<String, Object> map, String parameterPrefix) {
436            Set<Map.Entry<String, Object>> propertyEntries = map.entrySet();
437            for (Map.Entry<String, Object> entry : propertyEntries) {
438                dynamicQueryContext.setParameter(parameterPrefix + entry.getKey(), entry.getValue());
439            }
440        }
441    
442        protected boolean matches(Exchange exchange, List results) {
443            return ObjectHelper.matches(results);
444        }
445    
446        /**
447         * Initializes this builder - <b>Must be invoked before evaluation</b>.
448         */
449        protected synchronized void initialize(Exchange exchange) throws XPathException, IOException {
450            // must use synchronized for concurrency issues and only let it initialize once
451            if (!initialized.get()) {
452                if (LOG.isDebugEnabled()) {
453                    LOG.debug("Initializing XQueryBuilder " + this);
454                }
455                configuration = new Configuration();
456                configuration.setHostLanguage(Configuration.XQUERY);
457    
458                staticQueryContext = new StaticQueryContext(getConfiguration());
459                Set<Map.Entry<String, String>> entries = namespacePrefixes.entrySet();
460                for (Map.Entry<String, String> entry : entries) {
461                    String prefix = entry.getKey();
462                    String uri = entry.getValue();
463                    staticQueryContext.declarePassiveNamespace(prefix, uri, false);
464                    staticQueryContext.setInheritNamespaces(true);
465                }
466    
467                expression = createQueryExpression(staticQueryContext);
468    
469                initialized.set(true);
470            }
471    
472            // let the configuration be accessible on the exchange as its shared for this evaulation
473            // and can be needed for 3rd part type converters or in some other situations
474            exchange.setProperty("CamelSaxonConfiguration", configuration);
475        }
476    
477    }