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