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