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