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.StringReader;
020    import java.util.List;
021    import java.util.Map;
022    
023    import javax.xml.namespace.QName;
024    import javax.xml.transform.dom.DOMSource;
025    import javax.xml.xpath.XPath;
026    import javax.xml.xpath.XPathConstants;
027    import javax.xml.xpath.XPathExpression;
028    import javax.xml.xpath.XPathExpressionException;
029    import javax.xml.xpath.XPathFactory;
030    import javax.xml.xpath.XPathFactoryConfigurationException;
031    import javax.xml.xpath.XPathFunction;
032    import javax.xml.xpath.XPathFunctionException;
033    import javax.xml.xpath.XPathFunctionResolver;
034    
035    import org.w3c.dom.Document;
036    import org.w3c.dom.Node;
037    import org.w3c.dom.NodeList;
038    
039    import org.xml.sax.InputSource;
040    
041    import org.apache.camel.Exchange;
042    import org.apache.camel.Expression;
043    import org.apache.camel.Message;
044    import org.apache.camel.Predicate;
045    import org.apache.camel.RuntimeExpressionException;
046    import org.apache.camel.converter.stream.StreamCache;
047    import org.apache.camel.spi.NamespaceAware;
048    import org.apache.camel.util.ExchangeHelper;
049    import org.apache.camel.util.MessageHelper;
050    
051    import static org.apache.camel.builder.xml.Namespaces.DEFAULT_NAMESPACE;
052    import static org.apache.camel.builder.xml.Namespaces.IN_NAMESPACE;
053    import static org.apache.camel.builder.xml.Namespaces.OUT_NAMESPACE;
054    import static org.apache.camel.builder.xml.Namespaces.isMatchingNamespaceOrEmptyNamespace;
055    import static org.apache.camel.converter.ObjectConverter.toBoolean;
056    
057    /**
058     * Creates an XPath expression builder which creates a nodeset result by default.
059     * If you want to evaluate a String expression then call {@link #stringResult()}
060     *
061     * @see XPathConstants#NODESET
062     *
063     * @version $Revision: 69509 $
064     */
065    public class XPathBuilder<E extends Exchange> implements Expression<E>, Predicate<E>, NamespaceAware {
066        private final String text;
067        private XPathFactory xpathFactory;
068        private Class documentType = Document.class;
069        // For some reason the default expression of "a/b" on a document such as
070        // <a><b>1</b><b>2</b></a>
071        // will evaluate as just "1" by default which is bizarre. So by default
072        // lets assume XPath expressions result in nodesets.
073        private Class resultType;
074        private QName resultQName = XPathConstants.NODESET;
075        private String objectModelUri;
076        private DefaultNamespaceContext namespaceContext;
077        private XPathFunctionResolver functionResolver;
078        private XPathExpression expression;
079        private MessageVariableResolver variableResolver = new MessageVariableResolver();
080        private E exchange;
081        private XPathFunction bodyFunction;
082        private XPathFunction headerFunction;
083        private XPathFunction outBodyFunction;
084        private XPathFunction outHeaderFunction;
085    
086        public XPathBuilder(String text) {
087            this.text = text;
088        }
089    
090        public static XPathBuilder xpath(String text) {
091            return new XPathBuilder(text);
092        }
093    
094        @Override
095        public String toString() {
096            return "XPath: " + text;
097        }
098    
099        public boolean matches(E exchange) {
100            Object booleanResult = evaluateAs(exchange, XPathConstants.BOOLEAN);
101            return toBoolean(booleanResult);
102        }
103    
104        public void assertMatches(String text, E exchange) throws AssertionError {
105            Object booleanResult = evaluateAs(exchange, XPathConstants.BOOLEAN);
106            if (!toBoolean(booleanResult)) {
107                throw new AssertionError(this + " failed on " + exchange + " as returned <" + booleanResult + ">");
108            }
109        }
110    
111        public Object evaluate(E exchange) {
112            Object answer = evaluateAs(exchange, resultQName);
113            if (resultType != null) {
114                return ExchangeHelper.convertToType(exchange, resultType, answer);
115            }
116            return answer;
117        }
118    
119        // Builder methods
120        // -------------------------------------------------------------------------
121    
122        /**
123         * Sets the expression result type to boolean
124         *
125         * @return the current builder
126         */
127        public XPathBuilder<E> booleanResult() {
128            resultQName = XPathConstants.BOOLEAN;
129            return this;
130        }
131    
132        /**
133         * Sets the expression result type to boolean
134         *
135         * @return the current builder
136         */
137        public XPathBuilder<E> nodeResult() {
138            resultQName = XPathConstants.NODE;
139            return this;
140        }
141    
142        /**
143         * Sets the expression result type to boolean
144         *
145         * @return the current builder
146         */
147        public XPathBuilder<E> nodeSetResult() {
148            resultQName = XPathConstants.NODESET;
149            return this;
150        }
151    
152        /**
153         * Sets the expression result type to boolean
154         *
155         * @return the current builder
156         */
157        public XPathBuilder<E> numberResult() {
158            resultQName = XPathConstants.NUMBER;
159            return this;
160        }
161    
162        /**
163         * Sets the expression result type to boolean
164         *
165         * @return the current builder
166         */
167        public XPathBuilder<E> stringResult() {
168            resultQName = XPathConstants.STRING;
169            return this;
170        }
171    
172        /**
173         * Sets the expression result type to boolean
174         *
175         * @return the current builder
176         */
177        public XPathBuilder<E> resultType(Class resultType) {
178            setResultType(resultType);
179            return this;
180        }
181    
182        /**
183         * Sets the object model URI to use
184         *
185         * @return the current builder
186         */
187        public XPathBuilder<E> objectModel(String uri) {
188            this.objectModelUri = uri;
189            return this;
190        }
191    
192        /**
193         * Sets the {@link XPathFunctionResolver} instance to use on these XPath
194         * expressions
195         *
196         * @return the current builder
197         */
198        public XPathBuilder<E> functionResolver(XPathFunctionResolver functionResolver) {
199            this.functionResolver = functionResolver;
200            return this;
201        }
202    
203        /**
204         * Registers the namespace prefix and URI with the builder so that the
205         * prefix can be used in XPath expressions
206         *
207         * @param prefix is the namespace prefix that can be used in the XPath
208         *                expressions
209         * @param uri is the namespace URI to which the prefix refers
210         * @return the current builder
211         */
212        public XPathBuilder<E> namespace(String prefix, String uri) {
213            getNamespaceContext().add(prefix, uri);
214            return this;
215        }
216    
217        /**
218         * Registers namespaces with the builder so that the registered
219         * prefixes can be used in XPath expressions
220         *
221         * @param namespaces is namespaces object that should be used in the
222         *                      XPath expression
223         * @return the current builder
224         */
225        public XPathBuilder<E> namespaces(Namespaces namespaces) {
226            namespaces.configure(this);
227            return this;
228        }
229    
230        /**
231         * Registers a variable (in the global namespace) which can be referred to
232         * from XPath expressions
233         */
234        public XPathBuilder<E> variable(String name, Object value) {
235            variableResolver.addVariable(name, value);
236            return this;
237        }
238    
239        // Properties
240        // -------------------------------------------------------------------------
241        public XPathFactory getXPathFactory() throws XPathFactoryConfigurationException {
242            if (xpathFactory == null) {
243                if (objectModelUri != null) {
244                    xpathFactory = XPathFactory.newInstance(objectModelUri);
245                }
246                xpathFactory = XPathFactory.newInstance();
247            }
248            return xpathFactory;
249        }
250    
251        public void setXPathFactory(XPathFactory xpathFactory) {
252            this.xpathFactory = xpathFactory;
253        }
254    
255        public Class getDocumentType() {
256            return documentType;
257        }
258    
259        public void setDocumentType(Class documentType) {
260            this.documentType = documentType;
261        }
262    
263        public String getText() {
264            return text;
265        }
266    
267        public QName getResultQName() {
268            return resultQName;
269        }
270    
271        public void setResultQName(QName resultQName) {
272            this.resultQName = resultQName;
273        }
274    
275        public DefaultNamespaceContext getNamespaceContext() {
276            if (namespaceContext == null) {
277                try {
278                    DefaultNamespaceContext defaultNamespaceContext = new DefaultNamespaceContext(getXPathFactory());
279                    populateDefaultNamespaces(defaultNamespaceContext);
280                    namespaceContext = defaultNamespaceContext;
281                } catch (XPathFactoryConfigurationException e) {
282                    throw new RuntimeExpressionException(e);
283                }
284            }
285            return namespaceContext;
286        }
287    
288        public void setNamespaceContext(DefaultNamespaceContext namespaceContext) {
289            this.namespaceContext = namespaceContext;
290        }
291    
292        public XPathFunctionResolver getFunctionResolver() {
293            return functionResolver;
294        }
295    
296        public void setFunctionResolver(XPathFunctionResolver functionResolver) {
297            this.functionResolver = functionResolver;
298        }
299    
300        public XPathExpression getExpression() throws XPathFactoryConfigurationException,
301            XPathExpressionException {
302            if (expression == null) {
303                expression = createXPathExpression();
304            }
305            return expression;
306        }
307    
308        public void setNamespaces(Map<String, String> namespaces) {
309            getNamespaceContext().setNamespaces(namespaces);
310        }
311    
312        public XPathFunction getBodyFunction() {
313            if (bodyFunction == null) {
314                bodyFunction = new XPathFunction() {
315                    public Object evaluate(List list) throws XPathFunctionException {
316                        if (exchange == null) {
317                            return null;
318                        }
319                        return exchange.getIn().getBody();
320                    }
321                };
322            }
323            return bodyFunction;
324        }
325    
326        public void setBodyFunction(XPathFunction bodyFunction) {
327            this.bodyFunction = bodyFunction;
328        }
329    
330        public XPathFunction getHeaderFunction() {
331            if (headerFunction == null) {
332                headerFunction = new XPathFunction() {
333                    public Object evaluate(List list) throws XPathFunctionException {
334                        if (exchange != null && !list.isEmpty()) {
335                            Object value = list.get(0);
336                            if (value != null) {
337                                return exchange.getIn().getHeader(value.toString());
338                            }
339                        }
340                        return null;
341                    }
342                };
343            }
344            return headerFunction;
345        }
346    
347        public void setHeaderFunction(XPathFunction headerFunction) {
348            this.headerFunction = headerFunction;
349        }
350    
351        public XPathFunction getOutBodyFunction() {
352            if (outBodyFunction == null) {
353                outBodyFunction = new XPathFunction() {
354                    public Object evaluate(List list) throws XPathFunctionException {
355                        if (exchange != null) {
356                            Message out = exchange.getOut(false);
357                            if (out != null) {
358                                return out.getBody();
359                            }
360                        }
361                        return null;
362                    }
363                };
364            }
365            return outBodyFunction;
366        }
367    
368        public void setOutBodyFunction(XPathFunction outBodyFunction) {
369            this.outBodyFunction = outBodyFunction;
370        }
371    
372        public XPathFunction getOutHeaderFunction() {
373            if (outHeaderFunction == null) {
374                outHeaderFunction = new XPathFunction() {
375                    public Object evaluate(List list) throws XPathFunctionException {
376                        if (exchange != null && !list.isEmpty()) {
377                            Object value = list.get(0);
378                            if (value != null) {
379                                return exchange.getOut().getHeader(value.toString());
380                            }
381                        }
382                        return null;
383                    }
384                };
385            }
386            return outHeaderFunction;
387        }
388    
389        public void setOutHeaderFunction(XPathFunction outHeaderFunction) {
390            this.outHeaderFunction = outHeaderFunction;
391        }
392    
393        public Class getResultType() {
394            return resultType;
395        }
396    
397        public void setResultType(Class resultType) {
398            this.resultType = resultType;
399            if (Number.class.isAssignableFrom(resultType)) {
400                numberResult();
401            } else if (String.class.isAssignableFrom(resultType)) {
402                stringResult();
403            } else if (Boolean.class.isAssignableFrom(resultType)) {
404                booleanResult();
405            } else if (Node.class.isAssignableFrom(resultType)) {
406                nodeResult();
407            } else if (NodeList.class.isAssignableFrom(resultType)) {
408                nodeSetResult();
409            }
410        }
411    
412        // Implementation methods
413        // -------------------------------------------------------------------------
414    
415        /**
416         * Evaluates the expression as the given result type
417         */
418        protected synchronized Object evaluateAs(E exchange, QName resultQName) {
419            this.exchange = exchange;
420            variableResolver.setExchange(exchange);
421            try {
422                Object document = getDocument(exchange);
423                if (resultQName != null) {
424                    if (document instanceof InputSource) {
425                        InputSource inputSource = (InputSource)document;
426                        return getExpression().evaluate(inputSource, resultQName);
427                    } else if (document instanceof DOMSource) {
428                        DOMSource source = (DOMSource) document;
429                        return getExpression().evaluate(source.getNode(), resultQName);
430                    } else {
431                        return getExpression().evaluate(document, resultQName);
432                    }
433                } else {
434                    if (document instanceof InputSource) {
435                        InputSource inputSource = (InputSource)document;
436                        return getExpression().evaluate(inputSource);
437                    } else if (document instanceof DOMSource) {
438                        DOMSource source = (DOMSource)document;
439                        return getExpression().evaluate(source.getNode());
440                    } else {
441                        return getExpression().evaluate(document);
442                    }
443                }
444            } catch (XPathExpressionException e) {
445                throw new InvalidXPathExpression(getText(), e);
446            } catch (XPathFactoryConfigurationException e) {
447                throw new InvalidXPathExpression(getText(), e);
448            }
449        }
450    
451        protected XPathExpression createXPathExpression() throws XPathExpressionException,
452            XPathFactoryConfigurationException {
453            XPath xPath = getXPathFactory().newXPath();
454    
455            // lets now clear any factory references to avoid keeping them around
456            xpathFactory = null;
457    
458            xPath.setNamespaceContext(getNamespaceContext());
459    
460            xPath.setXPathVariableResolver(variableResolver);
461    
462            XPathFunctionResolver parentResolver = getFunctionResolver();
463            if (parentResolver == null) {
464                parentResolver = xPath.getXPathFunctionResolver();
465            }
466            xPath.setXPathFunctionResolver(createDefaultFunctionResolver(parentResolver));
467            return xPath.compile(text);
468        }
469    
470        /**
471         * Lets populate a number of standard prefixes if they are not already there
472         */
473        protected void populateDefaultNamespaces(DefaultNamespaceContext context) {
474            setNamespaceIfNotPresent(context, "in", IN_NAMESPACE);
475            setNamespaceIfNotPresent(context, "out", OUT_NAMESPACE);
476            setNamespaceIfNotPresent(context, "env", Namespaces.ENVIRONMENT_VARIABLES);
477            setNamespaceIfNotPresent(context, "system", Namespaces.SYSTEM_PROPERTIES_NAMESPACE);
478        }
479    
480        protected void setNamespaceIfNotPresent(DefaultNamespaceContext context, String prefix, String uri) {
481            if (context != null) {
482                String current = context.getNamespaceURI(prefix);
483                if (current == null) {
484                    context.add(prefix, uri);
485                }
486            }
487        }
488    
489        protected XPathFunctionResolver createDefaultFunctionResolver(final XPathFunctionResolver parent) {
490            return new XPathFunctionResolver() {
491                public XPathFunction resolveFunction(QName qName, int argumentCount) {
492                    XPathFunction answer = null;
493                    if (parent != null) {
494                        answer = parent.resolveFunction(qName, argumentCount);
495                    }
496                    if (answer == null) {
497                        if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), IN_NAMESPACE)
498                            || isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), DEFAULT_NAMESPACE)) {
499                            String localPart = qName.getLocalPart();
500                            if (localPart.equals("body") && argumentCount == 0) {
501                                return getBodyFunction();
502                            }
503                            if (localPart.equals("header") && argumentCount == 1) {
504                                return getHeaderFunction();
505                            }
506                        }
507                        if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), OUT_NAMESPACE)) {
508                            String localPart = qName.getLocalPart();
509                            if (localPart.equals("body") && argumentCount == 0) {
510                                return getOutBodyFunction();
511                            }
512                            if (localPart.equals("header") && argumentCount == 1) {
513                                return getOutHeaderFunction();
514                            }
515                        }
516                    }
517                    return answer;
518                }
519            };
520        }
521    
522        /**
523         * Strategy method to extract the document from the exchange
524         */
525        protected Object getDocument(E exchange) {
526            Message in = exchange.getIn();
527            Class type = getDocumentType();
528            Object answer = null;
529            if (type != null) {
530                answer = in.getBody(type);
531            }
532            if (answer == null) {
533                answer = in.getBody();
534            }
535    
536            // lets try coerce some common types into something JAXP can deal with
537            if (answer instanceof String) {
538                answer = new InputSource(new StringReader(answer.toString()));
539            }
540            
541            // call the reset if the in message body is StreamCache
542            MessageHelper.resetStreamCache(exchange.getIn());
543            return answer;
544        }
545    }