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.builder.xml;
018    
019    import java.io.InputStream;
020    import java.io.StringReader;
021    import java.util.List;
022    import java.util.Map;
023    import java.util.Queue;
024    import java.util.concurrent.ConcurrentLinkedQueue;
025    
026    import javax.xml.namespace.QName;
027    import javax.xml.transform.dom.DOMSource;
028    import javax.xml.xpath.XPath;
029    import javax.xml.xpath.XPathConstants;
030    import javax.xml.xpath.XPathExpression;
031    import javax.xml.xpath.XPathExpressionException;
032    import javax.xml.xpath.XPathFactory;
033    import javax.xml.xpath.XPathFactoryConfigurationException;
034    import javax.xml.xpath.XPathFunction;
035    import javax.xml.xpath.XPathFunctionException;
036    import javax.xml.xpath.XPathFunctionResolver;
037    
038    import org.w3c.dom.Document;
039    import org.w3c.dom.Node;
040    import org.w3c.dom.NodeList;
041    
042    import org.xml.sax.InputSource;
043    
044    import org.apache.camel.Exchange;
045    import org.apache.camel.Expression;
046    import org.apache.camel.Message;
047    import org.apache.camel.Predicate;
048    import org.apache.camel.RuntimeExpressionException;
049    import org.apache.camel.Service;
050    import org.apache.camel.component.bean.BeanInvocation;
051    import org.apache.camel.component.file.GenericFile;
052    import org.apache.camel.spi.NamespaceAware;
053    import org.apache.camel.util.ExchangeHelper;
054    import org.apache.camel.util.MessageHelper;
055    
056    import org.apache.commons.logging.Log;
057    import org.apache.commons.logging.LogFactory;
058    
059    import static org.apache.camel.builder.xml.Namespaces.DEFAULT_NAMESPACE;
060    import static org.apache.camel.builder.xml.Namespaces.IN_NAMESPACE;
061    import static org.apache.camel.builder.xml.Namespaces.OUT_NAMESPACE;
062    import static org.apache.camel.builder.xml.Namespaces.isMatchingNamespaceOrEmptyNamespace;
063    
064    /**
065     * Creates an XPath expression builder which creates a nodeset result by default.
066     * If you want to evaluate a String expression then call {@link #stringResult()}
067     * <p/>
068     * An XPath object is not thread-safe and not reentrant. In other words, it is the application's responsibility to make
069     * sure that one XPath object is not used from more than one thread at any given time, and while the evaluate method
070     * is invoked, applications may not recursively call the evaluate method.
071     * <p/>
072     * This implementation is thread safe by using thread locals and pooling to allow concurrency
073     *
074     * @see XPathConstants#NODESET
075     *
076     * @version $Revision: 836037 $
077     */
078    public class XPathBuilder implements Expression, Predicate, NamespaceAware, Service {
079        private static final transient Log LOG = LogFactory.getLog(XPathBuilder.class);
080        private final Queue<XPathExpression> pool = new ConcurrentLinkedQueue<XPathExpression>();
081        private final String text;
082        private final ThreadLocal<MessageVariableResolver> variableResolver = new ThreadLocal<MessageVariableResolver>();
083        private final ThreadLocal<Exchange> exchange = new ThreadLocal<Exchange>();
084    
085        private XPathFactory xpathFactory;
086        private Class<?> documentType = Document.class;
087        // For some reason the default expression of "a/b" on a document such as
088        // <a><b>1</b><b>2</b></a>
089        // will evaluate as just "1" by default which is bizarre. So by default
090        // lets assume XPath expressions result in nodesets.
091        private Class<?> resultType;
092        private QName resultQName = XPathConstants.NODESET;
093        private String objectModelUri;
094        private DefaultNamespaceContext namespaceContext;
095        private XPathFunctionResolver functionResolver;
096        private XPathFunction bodyFunction;
097        private XPathFunction headerFunction;
098        private XPathFunction outBodyFunction;
099        private XPathFunction outHeaderFunction;
100    
101        public XPathBuilder(String text) {
102            this.text = text;
103        }
104    
105        public static XPathBuilder xpath(String text) {
106            return new XPathBuilder(text);
107        }
108    
109        public static XPathBuilder xpath(String text, Class<?> resultType) {
110            XPathBuilder builder = new XPathBuilder(text);
111            builder.setResultType(resultType);
112            return builder;
113        }
114    
115        @Override
116        public String toString() {
117            return "XPath: " + text;
118        }
119    
120        public boolean matches(Exchange exchange) {
121            Object booleanResult = evaluateAs(exchange, XPathConstants.BOOLEAN);
122            return exchange.getContext().getTypeConverter().convertTo(Boolean.class, booleanResult);
123        }
124    
125        public <T> T evaluate(Exchange exchange, Class<T> type) {
126            Object result = evaluate(exchange);
127            return exchange.getContext().getTypeConverter().convertTo(type, result);
128        }
129    
130        // Builder methods
131        // -------------------------------------------------------------------------
132    
133        /**
134         * Sets the expression result type to boolean
135         *
136         * @return the current builder
137         */
138        public XPathBuilder booleanResult() {
139            resultQName = XPathConstants.BOOLEAN;
140            return this;
141        }
142    
143        /**
144         * Sets the expression result type to boolean
145         *
146         * @return the current builder
147         */
148        public XPathBuilder nodeResult() {
149            resultQName = XPathConstants.NODE;
150            return this;
151        }
152    
153        /**
154         * Sets the expression result type to boolean
155         *
156         * @return the current builder
157         */
158        public XPathBuilder nodeSetResult() {
159            resultQName = XPathConstants.NODESET;
160            return this;
161        }
162    
163        /**
164         * Sets the expression result type to boolean
165         *
166         * @return the current builder
167         */
168        public XPathBuilder numberResult() {
169            resultQName = XPathConstants.NUMBER;
170            return this;
171        }
172    
173        /**
174         * Sets the expression result type to boolean
175         *
176         * @return the current builder
177         */
178        public XPathBuilder stringResult() {
179            resultQName = XPathConstants.STRING;
180            return this;
181        }
182    
183        /**
184         * Sets the expression result type to boolean
185         *
186         * @return the current builder
187         */
188        public XPathBuilder resultType(Class<?> resultType) {
189            setResultType(resultType);
190            return this;
191        }
192    
193        /**
194         * Sets the object model URI to use
195         *
196         * @return the current builder
197         */
198        public XPathBuilder objectModel(String uri) {
199            this.objectModelUri = uri;
200            return this;
201        }
202    
203        /**
204         * Sets the {@link XPathFunctionResolver} instance to use on these XPath
205         * expressions
206         *
207         * @return the current builder
208         */
209        public XPathBuilder functionResolver(XPathFunctionResolver functionResolver) {
210            this.functionResolver = functionResolver;
211            return this;
212        }
213    
214        /**
215         * Registers the namespace prefix and URI with the builder so that the
216         * prefix can be used in XPath expressions
217         *
218         * @param prefix is the namespace prefix that can be used in the XPath
219         *                expressions
220         * @param uri is the namespace URI to which the prefix refers
221         * @return the current builder
222         */
223        public XPathBuilder namespace(String prefix, String uri) {
224            getNamespaceContext().add(prefix, uri);
225            return this;
226        }
227    
228        /**
229         * Registers namespaces with the builder so that the registered
230         * prefixes can be used in XPath expressions
231         *
232         * @param namespaces is namespaces object that should be used in the
233         *                      XPath expression
234         * @return the current builder
235         */
236        public XPathBuilder namespaces(Namespaces namespaces) {
237            namespaces.configure(this);
238            return this;
239        }
240    
241        /**
242         * Registers a variable (in the global namespace) which can be referred to
243         * from XPath expressions
244         */
245        public XPathBuilder variable(String name, Object value) {
246            getVariableResolver().addVariable(name, value);
247            return this;
248        }
249    
250        /**
251         * Configures the document type to use.
252         * <p/>
253         * The document type controls which kind of Class Camel should convert the payload
254         * to before doing the xpath evaluation.
255         * <p/>
256         * For example you can set it to {@link InputSource} to use SAX streams.
257         * By default Camel uses {@link Document} as the type.
258         *
259         * @param documentType the document type
260         * @return the current builder
261         */
262        public XPathBuilder documentType(Class<?> documentType) {
263            setDocumentType(documentType);
264            return this;
265        }
266    
267        // Properties
268        // -------------------------------------------------------------------------
269        public XPathFactory getXPathFactory() throws XPathFactoryConfigurationException {
270            if (xpathFactory == null) {
271                if (objectModelUri != null) {
272                    xpathFactory = XPathFactory.newInstance(objectModelUri);
273                }
274                xpathFactory = XPathFactory.newInstance();
275            }
276            return xpathFactory;
277        }
278    
279        public void setXPathFactory(XPathFactory xpathFactory) {
280            this.xpathFactory = xpathFactory;
281        }
282    
283        public Class<?> getDocumentType() {
284            return documentType;
285        }
286    
287        public void setDocumentType(Class<?> documentType) {
288            this.documentType = documentType;
289        }
290    
291        public String getText() {
292            return text;
293        }
294    
295        public QName getResultQName() {
296            return resultQName;
297        }
298    
299        public void setResultQName(QName resultQName) {
300            this.resultQName = resultQName;
301        }
302    
303        public DefaultNamespaceContext getNamespaceContext() {
304            if (namespaceContext == null) {
305                try {
306                    DefaultNamespaceContext defaultNamespaceContext = new DefaultNamespaceContext(getXPathFactory());
307                    populateDefaultNamespaces(defaultNamespaceContext);
308                    namespaceContext = defaultNamespaceContext;
309                } catch (XPathFactoryConfigurationException e) {
310                    throw new RuntimeExpressionException(e);
311                }
312            }
313            return namespaceContext;
314        }
315    
316        public void setNamespaceContext(DefaultNamespaceContext namespaceContext) {
317            this.namespaceContext = namespaceContext;
318        }
319    
320        public XPathFunctionResolver getFunctionResolver() {
321            return functionResolver;
322        }
323    
324        public void setFunctionResolver(XPathFunctionResolver functionResolver) {
325            this.functionResolver = functionResolver;
326        }
327    
328        public void setNamespaces(Map<String, String> namespaces) {
329            getNamespaceContext().setNamespaces(namespaces);
330        }
331    
332        public XPathFunction getBodyFunction() {
333            if (bodyFunction == null) {
334                bodyFunction = new XPathFunction() {
335                    @SuppressWarnings("unchecked")
336                    public Object evaluate(List list) throws XPathFunctionException {
337                        if (exchange == null) {
338                            return null;
339                        }
340                        return exchange.get().getIn().getBody();
341                    }
342                };
343            }
344            return bodyFunction;
345        }
346    
347        public void setBodyFunction(XPathFunction bodyFunction) {
348            this.bodyFunction = bodyFunction;
349        }
350    
351        public XPathFunction getHeaderFunction() {
352            if (headerFunction == null) {
353                headerFunction = new XPathFunction() {
354                    @SuppressWarnings("unchecked")
355                    public Object evaluate(List list) throws XPathFunctionException {
356                        if (exchange != null && !list.isEmpty()) {
357                            Object value = list.get(0);
358                            if (value != null) {
359                                return exchange.get().getIn().getHeader(value.toString());
360                            }
361                        }
362                        return null;
363                    }
364                };
365            }
366            return headerFunction;
367        }
368    
369        public void setHeaderFunction(XPathFunction headerFunction) {
370            this.headerFunction = headerFunction;
371        }
372    
373        public XPathFunction getOutBodyFunction() {
374            if (outBodyFunction == null) {
375                outBodyFunction = new XPathFunction() {
376                    @SuppressWarnings("unchecked")
377                    public Object evaluate(List list) throws XPathFunctionException {
378                        if (exchange.get() != null && exchange.get().hasOut()) {
379                            return exchange.get().getOut().getBody();
380                        }
381                        return null;
382                    }
383                };
384            }
385            return outBodyFunction;
386        }
387    
388        public void setOutBodyFunction(XPathFunction outBodyFunction) {
389            this.outBodyFunction = outBodyFunction;
390        }
391    
392        public XPathFunction getOutHeaderFunction() {
393            if (outHeaderFunction == null) {
394                outHeaderFunction = new XPathFunction() {
395                    @SuppressWarnings("unchecked")
396                    public Object evaluate(List list) throws XPathFunctionException {
397                        if (exchange.get() != null && !list.isEmpty()) {
398                            Object value = list.get(0);
399                            if (value != null) {
400                                return exchange.get().getOut().getHeader(value.toString());
401                            }
402                        }
403                        return null;
404                    }
405                };
406            }
407            return outHeaderFunction;
408        }
409    
410        public void setOutHeaderFunction(XPathFunction outHeaderFunction) {
411            this.outHeaderFunction = outHeaderFunction;
412        }
413    
414        public Class<?> getResultType() {
415            return resultType;
416        }
417    
418        public void setResultType(Class<?> resultType) {
419            this.resultType = resultType;
420            if (Number.class.isAssignableFrom(resultType)) {
421                numberResult();
422            } else if (String.class.isAssignableFrom(resultType)) {
423                stringResult();
424            } else if (Boolean.class.isAssignableFrom(resultType)) {
425                booleanResult();
426            } else if (Node.class.isAssignableFrom(resultType)) {
427                nodeResult();
428            } else if (NodeList.class.isAssignableFrom(resultType)) {
429                nodeSetResult();
430            }
431        }
432    
433        // Implementation methods
434        // -------------------------------------------------------------------------
435    
436        protected Object evaluate(Exchange exchange) {
437            Object answer = evaluateAs(exchange, resultQName);
438            if (resultType != null) {
439                return ExchangeHelper.convertToType(exchange, resultType, answer);
440            }
441            return answer;
442        }
443    
444        /**
445         * Evaluates the expression as the given result type
446         */
447        protected Object evaluateAs(Exchange exchange, QName resultQName) {
448            // pool a pre compiled expression from pool
449            XPathExpression xpathExpression = pool.poll();
450            if (xpathExpression == null) {
451                LOG.trace("Creating new XPathExpression as none was available from pool");
452                // no avail in pool then create one
453                try {
454                    xpathExpression = createXPathExpression();
455                } catch (XPathExpressionException e) {
456                    throw new InvalidXPathExpression(getText(), e);
457                } catch (Exception e) {
458                    throw new RuntimeExpressionException("Cannot create xpath expression", e);
459                }
460            } else {
461                LOG.trace("Acquired XPathExpression from pool");
462            }
463            try {
464                return doInEvaluateAs(xpathExpression, exchange, resultQName);
465            } finally {
466                // release it back to the pool
467                pool.add(xpathExpression);
468                LOG.trace("Released XPathExpression back to pool");
469            }
470        }
471    
472        protected Object doInEvaluateAs(XPathExpression xpathExpression, Exchange exchange, QName resultQName) {
473            if (LOG.isTraceEnabled()) {
474                LOG.trace("Evaluating exchange: " + exchange + " as: " + resultQName);
475            }
476    
477            Object answer;
478    
479            // set exchange and variable resolver as thread locals for concurrency
480            this.exchange.set(exchange);
481    
482            try {
483                Object document = getDocument(exchange);
484                if (resultQName != null) {
485                    if (document instanceof InputSource) {
486                        InputSource inputSource = (InputSource) document;
487                        answer = xpathExpression.evaluate(inputSource, resultQName);
488                    } else if (document instanceof DOMSource) {
489                        DOMSource source = (DOMSource) document;
490                        answer = xpathExpression.evaluate(source.getNode(), resultQName);
491                    } else {
492                        answer = xpathExpression.evaluate(document, resultQName);
493                    }
494                } else {
495                    if (document instanceof InputSource) {
496                        InputSource inputSource = (InputSource) document;
497                        answer = xpathExpression.evaluate(inputSource);
498                    } else if (document instanceof DOMSource) {
499                        DOMSource source = (DOMSource) document;
500                        answer = xpathExpression.evaluate(source.getNode());
501                    } else {
502                        answer = xpathExpression.evaluate(document);
503                    }
504                }
505            } catch (XPathExpressionException e) {
506                throw new InvalidXPathExpression(getText(), e);
507            }
508    
509            if (LOG.isTraceEnabled()) {
510                LOG.trace("Done evaluating exchange: " + exchange + " as: " + resultQName + " with result: " + answer);
511            }
512            return answer;
513        }
514    
515        protected XPathExpression createXPathExpression() throws XPathExpressionException, XPathFactoryConfigurationException {
516            XPath xPath = getXPathFactory().newXPath();
517    
518            xPath.setNamespaceContext(getNamespaceContext());
519            xPath.setXPathVariableResolver(getVariableResolver());
520    
521            XPathFunctionResolver parentResolver = getFunctionResolver();
522            if (parentResolver == null) {
523                parentResolver = xPath.getXPathFunctionResolver();
524            }
525            xPath.setXPathFunctionResolver(createDefaultFunctionResolver(parentResolver));
526            return xPath.compile(text);
527        }
528    
529        /**
530         * Lets populate a number of standard prefixes if they are not already there
531         */
532        protected void populateDefaultNamespaces(DefaultNamespaceContext context) {
533            setNamespaceIfNotPresent(context, "in", IN_NAMESPACE);
534            setNamespaceIfNotPresent(context, "out", OUT_NAMESPACE);
535            setNamespaceIfNotPresent(context, "env", Namespaces.ENVIRONMENT_VARIABLES);
536            setNamespaceIfNotPresent(context, "system", Namespaces.SYSTEM_PROPERTIES_NAMESPACE);
537        }
538    
539        protected void setNamespaceIfNotPresent(DefaultNamespaceContext context, String prefix, String uri) {
540            if (context != null) {
541                String current = context.getNamespaceURI(prefix);
542                if (current == null) {
543                    context.add(prefix, uri);
544                }
545            }
546        }
547    
548        protected XPathFunctionResolver createDefaultFunctionResolver(final XPathFunctionResolver parent) {
549            return new XPathFunctionResolver() {
550                public XPathFunction resolveFunction(QName qName, int argumentCount) {
551                    XPathFunction answer = null;
552                    if (parent != null) {
553                        answer = parent.resolveFunction(qName, argumentCount);
554                    }
555                    if (answer == null) {
556                        if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), IN_NAMESPACE)
557                            || isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), DEFAULT_NAMESPACE)) {
558                            String localPart = qName.getLocalPart();
559                            if (localPart.equals("body") && argumentCount == 0) {
560                                return getBodyFunction();
561                            }
562                            if (localPart.equals("header") && argumentCount == 1) {
563                                return getHeaderFunction();
564                            }
565                        }
566                        if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), OUT_NAMESPACE)) {
567                            String localPart = qName.getLocalPart();
568                            if (localPart.equals("body") && argumentCount == 0) {
569                                return getOutBodyFunction();
570                            }
571                            if (localPart.equals("header") && argumentCount == 1) {
572                                return getOutHeaderFunction();
573                            }
574                        }
575                    }
576                    return answer;
577                }
578            };
579        }
580    
581        /**
582         * Strategy method to extract the document from the exchange
583         */
584        @SuppressWarnings("unchecked")
585        protected Object getDocument(Exchange exchange) {
586            Message in = exchange.getIn();
587            Class type = getDocumentType();
588            Object answer = null;
589            if (type != null) {
590                answer = in.getBody(type);
591            }
592    
593            if (answer == null) {
594                answer = in.getBody();
595            }
596    
597            // lets try coerce some common types into something JAXP can deal with
598            if (answer instanceof GenericFile) {
599                // special for files so we can work with them out of the box
600                InputStream is = exchange.getContext().getTypeConverter().convertTo(InputStream.class, answer);
601                answer = new InputSource(is);
602            } else if (answer instanceof BeanInvocation) {
603                // if its a null bean invocation then handle that
604                BeanInvocation bi = exchange.getContext().getTypeConverter().convertTo(BeanInvocation.class, answer);
605                if (bi.getArgs() != null && bi.getArgs().length == 1 && bi.getArgs()[0] == null) {
606                    // its a null argument from the bean invocation so use null as answer
607                    answer = null;
608                }
609            } else if (answer instanceof String) {
610                answer = new InputSource(new StringReader(answer.toString()));
611            }
612    
613            // call the reset if the in message body is StreamCache
614            MessageHelper.resetStreamCache(exchange.getIn());
615            return answer;
616        }
617    
618        private MessageVariableResolver getVariableResolver() {
619            MessageVariableResolver resolver = variableResolver.get();
620            if (resolver == null) {
621                resolver = new MessageVariableResolver(exchange);
622                variableResolver.set(resolver);
623            }
624            return resolver;
625        }
626    
627        public void start() throws Exception {
628        }
629    
630        public void stop() throws Exception {
631            pool.clear();
632        }
633    }