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