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