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