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