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 */
017package org.apache.camel.builder.xml;
018
019import java.io.File;
020import java.io.InputStream;
021import java.util.HashSet;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Properties;
026import java.util.Queue;
027import java.util.concurrent.ConcurrentHashMap;
028import java.util.concurrent.ConcurrentLinkedQueue;
029import javax.xml.namespace.QName;
030import javax.xml.transform.dom.DOMSource;
031import javax.xml.xpath.XPath;
032import javax.xml.xpath.XPathConstants;
033import javax.xml.xpath.XPathExpression;
034import javax.xml.xpath.XPathExpressionException;
035import javax.xml.xpath.XPathFactory;
036import javax.xml.xpath.XPathFactoryConfigurationException;
037import javax.xml.xpath.XPathFunction;
038import javax.xml.xpath.XPathFunctionException;
039import javax.xml.xpath.XPathFunctionResolver;
040
041import org.w3c.dom.Document;
042import org.w3c.dom.Node;
043import org.w3c.dom.NodeList;
044import org.xml.sax.InputSource;
045
046import org.apache.camel.CamelContext;
047import org.apache.camel.Exchange;
048import org.apache.camel.Expression;
049import org.apache.camel.NoTypeConversionAvailableException;
050import org.apache.camel.Predicate;
051import org.apache.camel.RuntimeExpressionException;
052import org.apache.camel.WrappedFile;
053import org.apache.camel.impl.DefaultExchange;
054import org.apache.camel.spi.Language;
055import org.apache.camel.spi.NamespaceAware;
056import org.apache.camel.support.ServiceSupport;
057import org.apache.camel.util.ExchangeHelper;
058import org.apache.camel.util.IOHelper;
059import org.apache.camel.util.MessageHelper;
060import org.apache.camel.util.ObjectHelper;
061import org.slf4j.Logger;
062import org.slf4j.LoggerFactory;
063
064import static org.apache.camel.builder.xml.Namespaces.DEFAULT_NAMESPACE;
065import static org.apache.camel.builder.xml.Namespaces.FUNCTION_NAMESPACE;
066import static org.apache.camel.builder.xml.Namespaces.IN_NAMESPACE;
067import static org.apache.camel.builder.xml.Namespaces.OUT_NAMESPACE;
068import static org.apache.camel.builder.xml.Namespaces.isMatchingNamespaceOrEmptyNamespace;
069
070/**
071 * Creates an XPath expression builder which creates a nodeset result by default.
072 * If you want to evaluate a String expression then call {@link #stringResult()}
073 * <p/>
074 * An XPath object is not thread-safe and not reentrant. In other words, it is the application's responsibility to make
075 * sure that one XPath object is not used from more than one thread at any given time, and while the evaluate method
076 * is invoked, applications may not recursively call the evaluate method.
077 * <p/>
078 * This implementation is thread safe by using thread locals and pooling to allow concurrency.
079 * <p/>
080 * <b>Important:</b> After configuring the {@link XPathBuilder} its advised to invoke {@link #start()}
081 * to prepare the builder before using; though the builder will auto-start on first use.
082 *
083 * @see XPathConstants#NODESET
084 */
085public class XPathBuilder extends ServiceSupport implements Expression, Predicate, NamespaceAware {
086    private static final Logger LOG = LoggerFactory.getLogger(XPathBuilder.class);
087    private static final String SAXON_OBJECT_MODEL_URI = "http://saxon.sf.net/jaxp/xpath/om";
088    private static final String OBTAIN_ALL_NS_XPATH = "//*/namespace::*";
089
090    private static volatile XPathFactory defaultXPathFactory;
091
092    private final Queue<XPathExpression> pool = new ConcurrentLinkedQueue<XPathExpression>();
093    private final Queue<XPathExpression> poolLogNamespaces = new ConcurrentLinkedQueue<XPathExpression>();
094    private final String text;
095    private final ThreadLocal<Exchange> exchange = new ThreadLocal<Exchange>();
096    private final MessageVariableResolver variableResolver = new MessageVariableResolver(exchange);
097    private final Map<String, String> namespaces = new ConcurrentHashMap<String, String>();
098    private volatile XPathFactory xpathFactory;
099    private volatile Class<?> documentType = Document.class;
100    // For some reason the default expression of "a/b" on a document such as
101    // <a><b>1</b><b>2</b></a>
102    // will evaluate as just "1" by default which is bizarre. So by default
103    // let's assume XPath expressions result in nodesets.
104    private volatile Class<?> resultType;
105    private volatile QName resultQName = XPathConstants.NODESET;
106    private volatile String objectModelUri;
107    private volatile DefaultNamespaceContext namespaceContext;
108    private volatile boolean logNamespaces;
109    private volatile XPathFunctionResolver functionResolver;
110    private volatile XPathFunction bodyFunction;
111    private volatile XPathFunction headerFunction;
112    private volatile XPathFunction outBodyFunction;
113    private volatile XPathFunction outHeaderFunction;
114    private volatile XPathFunction propertiesFunction;
115    private volatile XPathFunction simpleFunction;
116    /**
117     * The name of the header we want to apply the XPath expression to, which when set will cause
118     * the xpath to be evaluated on the required header, otherwise it will be applied to the body
119     */
120    private volatile String headerName;
121
122    /**
123     * @param text The XPath expression
124     */
125    public XPathBuilder(String text) {
126        this.text = text;
127    }
128
129    /**
130     * @param text The XPath expression
131     * @return A new XPathBuilder object
132     */
133    public static XPathBuilder xpath(String text) {
134        return new XPathBuilder(text);
135    }
136
137    /**
138     * @param text       The XPath expression
139     * @param resultType The result type that the XPath expression will return.
140     * @return A new XPathBuilder object
141     */
142    public static XPathBuilder xpath(String text, Class<?> resultType) {
143        XPathBuilder builder = new XPathBuilder(text);
144        builder.setResultType(resultType);
145        return builder;
146    }
147
148    @Override
149    public String toString() {
150        return "XPath: " + text;
151    }
152
153    public boolean matches(Exchange exchange) {
154        try {
155            Object booleanResult = evaluateAs(exchange, XPathConstants.BOOLEAN);
156            return exchange.getContext().getTypeConverter().convertTo(Boolean.class, booleanResult);
157        } finally {
158            // remove the thread local after usage
159            this.exchange.remove();
160        }
161    }
162
163    public <T> T evaluate(Exchange exchange, Class<T> type) {
164        try {
165            Object result = evaluate(exchange);
166            return exchange.getContext().getTypeConverter().convertTo(type, exchange, result);
167        } finally {
168            // remove the thread local after usage
169            this.exchange.remove();
170        }
171    }
172
173    /**
174     * Matches the given xpath using the provided body.
175     *
176     * @param context the camel context
177     * @param body    the body
178     * @return <tt>true</tt> if matches, <tt>false</tt> otherwise
179     */
180    public boolean matches(CamelContext context, Object body) {
181        ObjectHelper.notNull(context, "CamelContext");
182
183        // create a dummy Exchange to use during matching
184        Exchange dummy = new DefaultExchange(context);
185        dummy.getIn().setBody(body);
186
187        try {
188            return matches(dummy);
189        } finally {
190            // remove the thread local after usage
191            exchange.remove();
192        }
193    }
194
195    /**
196     * Evaluates the given xpath using the provided body.
197     * <p/>
198     * The evaluation uses by default {@link javax.xml.xpath.XPathConstants#NODESET} as the type
199     * used during xpath evaluation. The output from xpath is then afterwards type converted
200     * using Camel's type converter to the given type.
201     * <p/>
202     * If you want to evaluate xpath using a different type, then call {@link #setResultType(Class)}
203     * prior to calling this evaluate method.
204     *
205     * @param context the camel context
206     * @param body    the body
207     * @param type    the type to return
208     * @return result of the evaluation
209     */
210    public <T> T evaluate(CamelContext context, Object body, Class<T> type) {
211        ObjectHelper.notNull(context, "CamelContext");
212
213        // create a dummy Exchange to use during evaluation
214        Exchange dummy = new DefaultExchange(context);
215        dummy.getIn().setBody(body);
216
217        try {
218            return evaluate(dummy, type);
219        } finally {
220            // remove the thread local after usage
221            exchange.remove();
222        }
223    }
224
225    /**
226     * Evaluates the given xpath using the provided body as a String return type.
227     *
228     * @param context the camel context
229     * @param body    the body
230     * @return result of the evaluation
231     */
232    public String evaluate(CamelContext context, Object body) {
233        ObjectHelper.notNull(context, "CamelContext");
234
235        // create a dummy Exchange to use during evaluation
236        Exchange dummy = new DefaultExchange(context);
237        dummy.getIn().setBody(body);
238
239        setResultQName(XPathConstants.STRING);
240        setResultType(String.class);
241        try {
242            return evaluate(dummy, String.class);
243        } finally {
244            // remove the thread local after usage
245            this.exchange.remove();
246        }
247    }
248
249    // Builder methods
250    // -------------------------------------------------------------------------
251
252    /**
253     * Sets the expression result type to {@link XPathConstants#BOOLEAN}
254     *
255     * @return the current builder
256     */
257    public XPathBuilder booleanResult() {
258        resultQName = XPathConstants.BOOLEAN;
259        return this;
260    }
261
262    /**
263     * Sets the expression result type to {@link XPathConstants#NODE}
264     *
265     * @return the current builder
266     */
267    public XPathBuilder nodeResult() {
268        resultQName = XPathConstants.NODE;
269        return this;
270    }
271
272    /**
273     * Sets the expression result type to {@link XPathConstants#NODESET}
274     *
275     * @return the current builder
276     */
277    public XPathBuilder nodeSetResult() {
278        resultQName = XPathConstants.NODESET;
279        return this;
280    }
281
282    /**
283     * Sets the expression result type to {@link XPathConstants#NUMBER}
284     *
285     * @return the current builder
286     */
287    public XPathBuilder numberResult() {
288        resultQName = XPathConstants.NUMBER;
289        return this;
290    }
291
292    /**
293     * Sets the expression result type to {@link XPathConstants#STRING}
294     *
295     * @return the current builder
296     */
297    public XPathBuilder stringResult() {
298        resultQName = XPathConstants.STRING;
299        return this;
300    }
301
302    /**
303     * Sets the expression result type to the given {@code resultType}
304     *
305     * @return the current builder
306     */
307    public XPathBuilder resultType(Class<?> resultType) {
308        setResultType(resultType);
309        return this;
310    }
311
312    /**
313     * Sets the object model URI to use
314     *
315     * @return the current builder
316     */
317    public XPathBuilder objectModel(String uri) {
318        // Careful! Setting the Object Model URI this way will set the *Default* XPath Factory, which since is a static field,
319        // will set the XPath Factory system-wide. Decide what to do, as changing this behaviour can break compatibility. Provided the setObjectModel which changes
320        // this instance's XPath Factory rather than the static field
321        this.objectModelUri = uri;
322        return this;
323    }
324
325    /**
326     * Configures to use Saxon as the XPathFactory which allows you to use XPath 2.0 functions
327     * which may not be part of the build in JDK XPath parser.
328     *
329     * @return the current builder
330     */
331    public XPathBuilder saxon() {
332        this.objectModelUri = SAXON_OBJECT_MODEL_URI;
333        return this;
334    }
335
336    /**
337     * Sets the {@link XPathFunctionResolver} instance to use on these XPath
338     * expressions
339     *
340     * @return the current builder
341     */
342    public XPathBuilder functionResolver(XPathFunctionResolver functionResolver) {
343        this.functionResolver = functionResolver;
344        return this;
345    }
346
347    /**
348     * Registers the namespace prefix and URI with the builder so that the
349     * prefix can be used in XPath expressions
350     *
351     * @param prefix is the namespace prefix that can be used in the XPath
352     *               expressions
353     * @param uri    is the namespace URI to which the prefix refers
354     * @return the current builder
355     */
356    public XPathBuilder namespace(String prefix, String uri) {
357        namespaces.put(prefix, uri);
358        return this;
359    }
360
361    /**
362     * Registers namespaces with the builder so that the registered
363     * prefixes can be used in XPath expressions
364     *
365     * @param namespaces is namespaces object that should be used in the
366     *                   XPath expression
367     * @return the current builder
368     */
369    public XPathBuilder namespaces(Namespaces namespaces) {
370        namespaces.configure(this);
371        return this;
372    }
373
374    /**
375     * Registers a variable (in the global namespace) which can be referred to
376     * from XPath expressions
377     *
378     * @param name  name of variable
379     * @param value value of variable
380     * @return the current builder
381     */
382    public XPathBuilder variable(String name, Object value) {
383        getVariableResolver().addVariable(name, value);
384        return this;
385    }
386
387    /**
388     * Configures the document type to use.
389     * <p/>
390     * The document type controls which kind of Class Camel should convert the payload
391     * to before doing the xpath evaluation.
392     * <p/>
393     * For example you can set it to {@link InputSource} to use SAX streams.
394     * By default Camel uses {@link Document} as the type.
395     *
396     * @param documentType the document type
397     * @return the current builder
398     */
399    public XPathBuilder documentType(Class<?> documentType) {
400        setDocumentType(documentType);
401        return this;
402    }
403
404    /**
405     * Configures to use the provided XPath factory.
406     * <p/>
407     * Can be used to use Saxon instead of the build in factory from the JDK.
408     *
409     * @param xpathFactory the xpath factory to use
410     * @return the current builder.
411     */
412    public XPathBuilder factory(XPathFactory xpathFactory) {
413        setXPathFactory(xpathFactory);
414        return this;
415    }
416
417    /**
418     * Activates trace logging of all discovered namespaces in the message - to simplify debugging namespace-related issues
419     * <p/>
420     * Namespaces are printed in Hashmap style <code>{xmlns:prefix=[namespaceURI], xmlns:prefix=[namespaceURI]}</code>.
421     * <p/>
422     * The implicit XML namespace is omitted (http://www.w3.org/XML/1998/namespace).
423     * XML allows for namespace prefixes to be redefined/overridden due to hierarchical scoping, i.e. prefix abc can be mapped to http://abc.com,
424     * and deeper in the document it can be mapped to http://def.com. When two prefixes are detected which are equal but are mapped to different
425     * namespace URIs, Camel will show all namespaces URIs it is mapped to in an array-style.
426     * <p/>
427     * This feature is disabled by default.
428     *
429     * @return the current builder.
430     */
431    public XPathBuilder logNamespaces() {
432        setLogNamespaces(true);
433        return this;
434    }
435
436    // Properties
437    // -------------------------------------------------------------------------
438
439    /**
440     * Gets the xpath factory, can be <tt>null</tt> if no custom factory has been assigned.
441     * <p/>
442     * A default factory will be assigned (if no custom assigned) when either starting this builder
443     * or on first evaluation.
444     *
445     * @return the factory, or <tt>null</tt> if this builder has not been started/used before.
446     */
447    public XPathFactory getXPathFactory() {
448        return xpathFactory;
449    }
450
451    public void setXPathFactory(XPathFactory xpathFactory) {
452        this.xpathFactory = xpathFactory;
453    }
454
455    public Class<?> getDocumentType() {
456        return documentType;
457    }
458
459    public void setDocumentType(Class<?> documentType) {
460        this.documentType = documentType;
461    }
462
463    public String getText() {
464        return text;
465    }
466
467    public QName getResultQName() {
468        return resultQName;
469    }
470
471    public void setResultQName(QName resultQName) {
472        this.resultQName = resultQName;
473    }
474
475    public String getHeaderName() {
476        return headerName;
477    }
478
479    public void setHeaderName(String headerName) {
480        this.headerName = headerName;
481    }
482
483    /**
484     * Gets the namespace context, can be <tt>null</tt> if no custom context has been assigned.
485     * <p/>
486     * A default context will be assigned (if no custom assigned) when either starting this builder
487     * or on first evaluation.
488     *
489     * @return the context, or <tt>null</tt> if this builder has not been started/used before.
490     */
491    public DefaultNamespaceContext getNamespaceContext() {
492        return namespaceContext;
493    }
494
495    public void setNamespaceContext(DefaultNamespaceContext namespaceContext) {
496        this.namespaceContext = namespaceContext;
497    }
498
499    public XPathFunctionResolver getFunctionResolver() {
500        return functionResolver;
501    }
502
503    public void setFunctionResolver(XPathFunctionResolver functionResolver) {
504        this.functionResolver = functionResolver;
505    }
506
507    public void setNamespaces(Map<String, String> namespaces) {
508        this.namespaces.clear();
509        this.namespaces.putAll(namespaces);
510    }
511
512    /**
513     * Gets the {@link XPathFunction} for getting the input message body.
514     * <p/>
515     * A default function will be assigned (if no custom assigned) when either starting this builder
516     * or on first evaluation.
517     *
518     * @return the function, or <tt>null</tt> if this builder has not been started/used before.
519     */
520    public XPathFunction getBodyFunction() {
521        return bodyFunction;
522    }
523
524    private XPathFunction createBodyFunction() {
525        return new XPathFunction() {
526            @SuppressWarnings("rawtypes")
527            public Object evaluate(List list) throws XPathFunctionException {
528                return exchange.get().getIn().getBody();
529            }
530        };
531    }
532
533    public void setBodyFunction(XPathFunction bodyFunction) {
534        this.bodyFunction = bodyFunction;
535    }
536
537    /**
538     * Gets the {@link XPathFunction} for getting the input message header.
539     * <p/>
540     * A default function will be assigned (if no custom assigned) when either starting this builder
541     * or on first evaluation.
542     *
543     * @return the function, or <tt>null</tt> if this builder has not been started/used before.
544     */
545    public XPathFunction getHeaderFunction() {
546        return headerFunction;
547    }
548
549    private XPathFunction createHeaderFunction() {
550        return new XPathFunction() {
551            @SuppressWarnings("rawtypes")
552            public Object evaluate(List list) throws XPathFunctionException {
553                if (!list.isEmpty()) {
554                    Object value = list.get(0);
555                    if (value != null) {
556                        String text = exchange.get().getContext().getTypeConverter().convertTo(String.class, value);
557                        return exchange.get().getIn().getHeader(text);
558                    }
559                }
560                return null;
561            }
562        };
563    }
564
565    public void setHeaderFunction(XPathFunction headerFunction) {
566        this.headerFunction = headerFunction;
567    }
568
569    /**
570     * Gets the {@link XPathFunction} for getting the output message body.
571     * <p/>
572     * A default function will be assigned (if no custom assigned) when either starting this builder
573     * or on first evaluation.
574     *
575     * @return the function, or <tt>null</tt> if this builder has not been started/used before.
576     */
577    public XPathFunction getOutBodyFunction() {
578        return outBodyFunction;
579    }
580
581    private XPathFunction createOutBodyFunction() {
582        return new XPathFunction() {
583            @SuppressWarnings("rawtypes")
584            public Object evaluate(List list) throws XPathFunctionException {
585                if (exchange.get() != null && exchange.get().hasOut()) {
586                    return exchange.get().getOut().getBody();
587                }
588                return null;
589            }
590        };
591    }
592
593    public void setOutBodyFunction(XPathFunction outBodyFunction) {
594        this.outBodyFunction = outBodyFunction;
595    }
596
597    /**
598     * Gets the {@link XPathFunction} for getting the output message header.
599     * <p/>
600     * A default function will be assigned (if no custom assigned) when either starting this builder
601     * or on first evaluation.
602     *
603     * @return the function, or <tt>null</tt> if this builder has not been started/used before.
604     */
605    public XPathFunction getOutHeaderFunction() {
606        return outHeaderFunction;
607    }
608
609    private XPathFunction createOutHeaderFunction() {
610        return new XPathFunction() {
611            @SuppressWarnings("rawtypes")
612            public Object evaluate(List list) throws XPathFunctionException {
613                if (exchange.get() != null && !list.isEmpty()) {
614                    Object value = list.get(0);
615                    if (value != null) {
616                        String text = exchange.get().getContext().getTypeConverter().convertTo(String.class, value);
617                        return exchange.get().getOut().getHeader(text);
618                    }
619                }
620                return null;
621            }
622        };
623    }
624
625    public void setOutHeaderFunction(XPathFunction outHeaderFunction) {
626        this.outHeaderFunction = outHeaderFunction;
627    }
628
629    /**
630     * Gets the {@link XPathFunction} for getting the exchange properties.
631     * <p/>
632     * A default function will be assigned (if no custom assigned) when either starting this builder
633     * or on first evaluation.
634     *
635     * @return the function, or <tt>null</tt> if this builder has not been started/used before.
636     */
637    public XPathFunction getPropertiesFunction() {
638        return propertiesFunction;
639    }
640
641    private XPathFunction createPropertiesFunction() {
642        return new XPathFunction() {
643            @SuppressWarnings("rawtypes")
644            public Object evaluate(List list) throws XPathFunctionException {
645                if (!list.isEmpty()) {
646                    Object value = list.get(0);
647                    if (value != null) {
648                        String text = exchange.get().getContext().getTypeConverter().convertTo(String.class, value);
649                        try {
650                            // use the property placeholder resolver to lookup the property for us
651                            Object answer = exchange.get().getContext().resolvePropertyPlaceholders("{{" + text + "}}");
652                            return answer;
653                        } catch (Exception e) {
654                            throw new XPathFunctionException(e);
655                        }
656                    }
657                }
658                return null;
659            }
660        };
661    }
662
663    public void setPropertiesFunction(XPathFunction propertiesFunction) {
664        this.propertiesFunction = propertiesFunction;
665    }
666
667    /**
668     * Gets the {@link XPathFunction} for executing <a href="http://camel.apache.org/simple">simple</a>
669     * language as xpath function.
670     * <p/>
671     * A default function will be assigned (if no custom assigned) when either starting this builder
672     * or on first evaluation.
673     *
674     * @return the function, or <tt>null</tt> if this builder has not been started/used before.
675     */
676    public XPathFunction getSimpleFunction() {
677        return simpleFunction;
678    }
679
680    private XPathFunction createSimpleFunction() {
681        return new XPathFunction() {
682            @SuppressWarnings("rawtypes")
683            public Object evaluate(List list) throws XPathFunctionException {
684                if (!list.isEmpty()) {
685                    Object value = list.get(0);
686                    if (value != null) {
687                        String text = exchange.get().getContext().getTypeConverter().convertTo(String.class, value);
688                        Language simple = exchange.get().getContext().resolveLanguage("simple");
689                        Expression exp = simple.createExpression(text);
690                        Object answer = exp.evaluate(exchange.get(), Object.class);
691                        return answer;
692                    }
693                }
694                return null;
695            }
696        };
697    }
698
699    public void setSimpleFunction(XPathFunction simpleFunction) {
700        this.simpleFunction = simpleFunction;
701    }
702
703    public Class<?> getResultType() {
704        return resultType;
705    }
706
707    public void setResultType(Class<?> resultType) {
708        this.resultType = resultType;
709        if (Number.class.isAssignableFrom(resultType)) {
710            numberResult();
711        } else if (String.class.isAssignableFrom(resultType)) {
712            stringResult();
713        } else if (Boolean.class.isAssignableFrom(resultType)) {
714            booleanResult();
715        } else if (Node.class.isAssignableFrom(resultType)) {
716            nodeResult();
717        } else if (NodeList.class.isAssignableFrom(resultType)) {
718            nodeSetResult();
719        }
720    }
721
722    public void setLogNamespaces(boolean logNamespaces) {
723        this.logNamespaces = logNamespaces;
724    }
725
726    public boolean isLogNamespaces() {
727        return logNamespaces;
728    }
729
730    public String getObjectModelUri() {
731        return objectModelUri;
732    }
733
734    /**
735     * Enables Saxon on this particular XPath expression, as {@link #saxon()} sets the default static XPathFactory which may have already been initialised
736     * by previous XPath expressions
737     */
738    public void enableSaxon() {
739        this.setObjectModelUri(SAXON_OBJECT_MODEL_URI);
740    }
741
742    public void setObjectModelUri(String objectModelUri) {
743        this.objectModelUri = objectModelUri;
744    }
745
746    // Implementation methods
747    // -------------------------------------------------------------------------
748
749    protected Object evaluate(Exchange exchange) {
750        Object answer = evaluateAs(exchange, resultQName);
751        if (resultType != null) {
752            return ExchangeHelper.convertToType(exchange, resultType, answer);
753        }
754        return answer;
755    }
756
757    /**
758     * Evaluates the expression as the given result type
759     */
760    protected Object evaluateAs(Exchange exchange, QName resultQName) {
761        // pool a pre compiled expression from pool
762        XPathExpression xpathExpression = pool.poll();
763        if (xpathExpression == null) {
764            LOG.trace("Creating new XPathExpression as none was available from pool");
765            // no avail in pool then create one
766            try {
767                xpathExpression = createXPathExpression();
768            } catch (XPathExpressionException e) {
769                throw new InvalidXPathExpression(getText(), e);
770            } catch (Exception e) {
771                throw new RuntimeExpressionException("Cannot create xpath expression", e);
772            }
773        } else {
774            LOG.trace("Acquired XPathExpression from pool");
775        }
776        try {
777            if (logNamespaces && LOG.isInfoEnabled()) {
778                logNamespaces(exchange);
779            }
780            return doInEvaluateAs(xpathExpression, exchange, resultQName);
781        } finally {
782            // release it back to the pool
783            pool.add(xpathExpression);
784            LOG.trace("Released XPathExpression back to pool");
785        }
786    }
787
788    private void logNamespaces(Exchange exchange) {
789        InputStream is = null;
790        NodeList answer = null;
791        XPathExpression xpathExpression = null;
792
793        try {
794            xpathExpression = poolLogNamespaces.poll();
795            if (xpathExpression == null) {
796                xpathExpression = createTraceNamespaceExpression();
797            }
798
799            // prepare the input
800            Object document;
801            if (isInputStreamNeeded(exchange)) {
802                is = exchange.getIn().getBody(InputStream.class);
803                document = getDocument(exchange, is);
804            } else {
805                Object body = exchange.getIn().getBody();
806                document = getDocument(exchange, body);
807            }
808            // fetch all namespaces
809            if (document instanceof InputSource) {
810                InputSource inputSource = (InputSource) document;
811                answer = (NodeList) xpathExpression.evaluate(inputSource, XPathConstants.NODESET);
812            } else if (document instanceof DOMSource) {
813                DOMSource source = (DOMSource) document;
814                answer = (NodeList) xpathExpression.evaluate(source.getNode(), XPathConstants.NODESET);
815            } else {
816                answer = (NodeList) xpathExpression.evaluate(document, XPathConstants.NODESET);
817            }
818        } catch (Exception e) {
819            LOG.warn("Unable to trace discovered namespaces in XPath expression", e);
820        } finally {
821            // IOHelper can handle if is is null
822            IOHelper.close(is);
823            poolLogNamespaces.add(xpathExpression);
824        }
825
826        if (answer != null) {
827            logDiscoveredNamespaces(answer);
828        }
829    }
830
831    private void logDiscoveredNamespaces(NodeList namespaces) {
832        Map<String, HashSet<String>> map = new LinkedHashMap<String, HashSet<String>>();
833        for (int i = 0; i < namespaces.getLength(); i++) {
834            Node n = namespaces.item(i);
835            if (n.getNodeName().equals("xmlns:xml")) {
836                // skip the implicit XML namespace as it provides no value
837                continue;
838            }
839
840            String prefix = namespaces.item(i).getNodeName();
841            if (prefix.equals("xmlns")) {
842                prefix = "DEFAULT";
843            }
844
845            // add to map
846            if (!map.containsKey(prefix)) {
847                map.put(prefix, new HashSet<String>());
848            }
849            map.get(prefix).add(namespaces.item(i).getNodeValue());
850        }
851
852        LOG.info("Namespaces discovered in message: {}.", map);
853    }
854
855    protected Object doInEvaluateAs(XPathExpression xpathExpression, Exchange exchange, QName resultQName) {
856        LOG.trace("Evaluating exchange: {} as: {}", exchange, resultQName);
857
858        Object answer;
859
860        // set exchange and variable resolver as thread locals for concurrency
861        this.exchange.set(exchange);
862
863        // the underlying input stream, which we need to close to avoid locking files or other resources
864        InputStream is = null;
865        try {
866            Object document;
867
868            // Check if we need to apply the XPath expression to a header
869            if (ObjectHelper.isNotEmpty(getHeaderName())) {
870                String headerName = getHeaderName();
871                // only convert to input stream if really needed
872                if (isInputStreamNeeded(exchange, headerName)) {
873                    is = exchange.getIn().getHeader(headerName, InputStream.class);
874                    document = getDocument(exchange, is);
875                } else {
876                    Object headerObject = exchange.getIn().getHeader(getHeaderName());
877                    document = getDocument(exchange, headerObject);
878                }
879            } else {
880                // only convert to input stream if really needed
881                if (isInputStreamNeeded(exchange)) {
882                    is = exchange.getIn().getBody(InputStream.class);
883                    document = getDocument(exchange, is);
884                } else {
885                    Object body = exchange.getIn().getBody();
886                    document = getDocument(exchange, body);
887                }
888            }
889
890            if (resultQName != null) {
891                if (document instanceof InputSource) {
892                    InputSource inputSource = (InputSource) document;
893                    answer = xpathExpression.evaluate(inputSource, resultQName);
894                } else if (document instanceof DOMSource) {
895                    DOMSource source = (DOMSource) document;
896                    answer = xpathExpression.evaluate(source.getNode(), resultQName);
897                } else {
898                    answer = xpathExpression.evaluate(document, resultQName);
899                }
900            } else {
901                if (document instanceof InputSource) {
902                    InputSource inputSource = (InputSource) document;
903                    answer = xpathExpression.evaluate(inputSource);
904                } else if (document instanceof DOMSource) {
905                    DOMSource source = (DOMSource) document;
906                    answer = xpathExpression.evaluate(source.getNode());
907                } else {
908                    answer = xpathExpression.evaluate(document);
909                }
910            }
911        } catch (XPathExpressionException e) {
912            String message = getText();
913            if (ObjectHelper.isNotEmpty(getHeaderName())) {
914                message = message + " with headerName " + getHeaderName();
915            }
916            throw new InvalidXPathExpression(message, e);
917        } finally {
918            // IOHelper can handle if is is null
919            IOHelper.close(is);
920        }
921
922        if (LOG.isTraceEnabled()) {
923            LOG.trace("Done evaluating exchange: {} as: {} with result: {}", new Object[]{exchange, resultQName, answer});
924        }
925        return answer;
926    }
927
928    /**
929     * Creates a new xpath expression as there we no available in the pool.
930     * <p/>
931     * This implementation must be synchronized to ensure thread safety, as this XPathBuilder instance may not have been
932     * started prior to being used.
933     */
934    protected synchronized XPathExpression createXPathExpression() throws XPathExpressionException, XPathFactoryConfigurationException {
935        // ensure we are started
936        try {
937            start();
938        } catch (Exception e) {
939            throw new RuntimeExpressionException("Error starting XPathBuilder", e);
940        }
941
942        // XPathFactory is not thread safe
943        XPath xPath = getXPathFactory().newXPath();
944
945        if (!logNamespaces && LOG.isTraceEnabled()) {
946            LOG.trace("Creating new XPath expression in pool. Namespaces on XPath expression: {}", getNamespaceContext().toString());
947        } else if (logNamespaces && LOG.isInfoEnabled()) {
948            LOG.info("Creating new XPath expression in pool. Namespaces on XPath expression: {}", getNamespaceContext().toString());
949        }
950        xPath.setNamespaceContext(getNamespaceContext());
951        xPath.setXPathVariableResolver(getVariableResolver());
952
953        XPathFunctionResolver parentResolver = getFunctionResolver();
954        if (parentResolver == null) {
955            parentResolver = xPath.getXPathFunctionResolver();
956        }
957        xPath.setXPathFunctionResolver(createDefaultFunctionResolver(parentResolver));
958        return xPath.compile(text);
959    }
960
961    protected synchronized XPathExpression createTraceNamespaceExpression() throws XPathFactoryConfigurationException, XPathExpressionException {
962        // XPathFactory is not thread safe
963        XPath xPath = getXPathFactory().newXPath();
964        return xPath.compile(OBTAIN_ALL_NS_XPATH);
965    }
966
967    protected DefaultNamespaceContext createNamespaceContext(XPathFactory factory) {
968        DefaultNamespaceContext context = new DefaultNamespaceContext(factory);
969        populateDefaultNamespaces(context);
970        return context;
971    }
972
973    /**
974     * Populate a number of standard prefixes if they are not already there
975     */
976    protected void populateDefaultNamespaces(DefaultNamespaceContext context) {
977        setNamespaceIfNotPresent(context, "in", IN_NAMESPACE);
978        setNamespaceIfNotPresent(context, "out", OUT_NAMESPACE);
979        setNamespaceIfNotPresent(context, "env", Namespaces.ENVIRONMENT_VARIABLES);
980        setNamespaceIfNotPresent(context, "system", Namespaces.SYSTEM_PROPERTIES_NAMESPACE);
981        setNamespaceIfNotPresent(context, "function", Namespaces.FUNCTION_NAMESPACE);
982    }
983
984    protected void setNamespaceIfNotPresent(DefaultNamespaceContext context, String prefix, String uri) {
985        if (context != null) {
986            String current = context.getNamespaceURI(prefix);
987            if (current == null) {
988                context.add(prefix, uri);
989            }
990        }
991    }
992
993    protected XPathFunctionResolver createDefaultFunctionResolver(final XPathFunctionResolver parent) {
994        return new XPathFunctionResolver() {
995            public XPathFunction resolveFunction(QName qName, int argumentCount) {
996                XPathFunction answer = null;
997                if (parent != null) {
998                    answer = parent.resolveFunction(qName, argumentCount);
999                }
1000                if (answer == null) {
1001                    if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), IN_NAMESPACE)
1002                            || isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), DEFAULT_NAMESPACE)) {
1003                        String localPart = qName.getLocalPart();
1004                        if (localPart.equals("body") && argumentCount == 0) {
1005                            return getBodyFunction();
1006                        }
1007                        if (localPart.equals("header") && argumentCount == 1) {
1008                            return getHeaderFunction();
1009                        }
1010                    }
1011                    if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), OUT_NAMESPACE)) {
1012                        String localPart = qName.getLocalPart();
1013                        if (localPart.equals("body") && argumentCount == 0) {
1014                            return getOutBodyFunction();
1015                        }
1016                        if (localPart.equals("header") && argumentCount == 1) {
1017                            return getOutHeaderFunction();
1018                        }
1019                    }
1020                    if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), FUNCTION_NAMESPACE)) {
1021                        String localPart = qName.getLocalPart();
1022                        if (localPart.equals("properties") && argumentCount == 1) {
1023                            return getPropertiesFunction();
1024                        }
1025                        if (localPart.equals("simple") && argumentCount == 1) {
1026                            return getSimpleFunction();
1027                        }
1028                    }
1029                }
1030                return answer;
1031            }
1032        };
1033    }
1034
1035    /**
1036     * Checks whether we need an {@link InputStream} to access the message body.
1037     * <p/>
1038     * Depending on the content in the message body, we may not need to convert
1039     * to {@link InputStream}.
1040     *
1041     * @param exchange the current exchange
1042     * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting afterwards.
1043     */
1044    protected boolean isInputStreamNeeded(Exchange exchange) {
1045        Object body = exchange.getIn().getBody();
1046        return isInputStreamNeededForObject(exchange, body);
1047    }
1048
1049    /**
1050     * Checks whether we need an {@link InputStream} to access the message header.
1051     * <p/>
1052     * Depending on the content in the message header, we may not need to convert
1053     * to {@link InputStream}.
1054     *
1055     * @param exchange the current exchange
1056     * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting afterwards.
1057     */
1058    protected boolean isInputStreamNeeded(Exchange exchange, String headerName) {
1059        Object header = exchange.getIn().getHeader(headerName);
1060        return isInputStreamNeededForObject(exchange, header);
1061    }
1062
1063    /**
1064     * Checks whether we need an {@link InputStream} to access this object
1065     * <p/>
1066     * Depending on the content in the object, we may not need to convert
1067     * to {@link InputStream}.
1068     *
1069     * @param exchange the current exchange
1070     * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting afterwards.
1071     */
1072    protected boolean isInputStreamNeededForObject(Exchange exchange, Object obj) {
1073        if (obj == null) {
1074            return false;
1075        }
1076
1077        if (obj instanceof WrappedFile) {
1078            obj = ((WrappedFile<?>) obj).getFile();
1079        }
1080        if (obj instanceof File) {
1081            // input stream is needed for File to avoid locking the file in case of errors etc
1082            return true;
1083        }
1084
1085        // input stream is not needed otherwise
1086        return false;
1087    }
1088
1089    /**
1090     * Strategy method to extract the document from the exchange.
1091     */
1092    protected Object getDocument(Exchange exchange, Object body) {
1093        try {
1094            return doGetDocument(exchange, body);
1095        } catch (Exception e) {
1096            throw ObjectHelper.wrapRuntimeCamelException(e);
1097        } finally {
1098            // call the reset if the in message body is StreamCache
1099            MessageHelper.resetStreamCache(exchange.getIn());
1100        }
1101    }
1102
1103    protected Object doGetDocument(Exchange exchange, Object body) throws Exception {
1104        if (body == null) {
1105            return null;
1106        }
1107
1108        Object answer = null;
1109
1110        Class<?> type = getDocumentType();
1111        Exception cause = null;
1112        if (type != null) {
1113            // try to get the body as the desired type
1114            try {
1115                answer = exchange.getContext().getTypeConverter().convertTo(type, exchange, body);
1116            } catch (Exception e) {
1117                // we want to store the caused exception, if we could not convert
1118                cause = e;
1119            }
1120        }
1121
1122        if (type == null && answer == null) {
1123            // fallback to get the body as is
1124            answer = body;
1125        } else if (answer == null) {
1126            // there was a type, and we could not convert to it, then fail
1127            if (cause != null) {
1128                throw cause;
1129            } else {
1130                throw new NoTypeConversionAvailableException(body, type);
1131            }
1132        }
1133
1134        return answer;
1135    }
1136
1137    private MessageVariableResolver getVariableResolver() {
1138        return variableResolver;
1139    }
1140
1141    @Override
1142    public void doStart() throws Exception {
1143        if (xpathFactory == null) {
1144            xpathFactory = createXPathFactory();
1145        }
1146        if (namespaceContext == null) {
1147            namespaceContext = createNamespaceContext(xpathFactory);
1148        }
1149        for (Map.Entry<String, String> entry : namespaces.entrySet()) {
1150            namespaceContext.add(entry.getKey(), entry.getValue());
1151        }
1152
1153        // create default functions if no custom assigned
1154        if (bodyFunction == null) {
1155            bodyFunction = createBodyFunction();
1156        }
1157        if (headerFunction == null) {
1158            headerFunction = createHeaderFunction();
1159        }
1160        if (outBodyFunction == null) {
1161            outBodyFunction = createOutBodyFunction();
1162        }
1163        if (outHeaderFunction == null) {
1164            outHeaderFunction = createOutHeaderFunction();
1165        }
1166        if (propertiesFunction == null) {
1167            propertiesFunction = createPropertiesFunction();
1168        }
1169        if (simpleFunction == null) {
1170            simpleFunction = createSimpleFunction();
1171        }
1172    }
1173
1174    @Override
1175    public void doStop() throws Exception {
1176        pool.clear();
1177        poolLogNamespaces.clear();
1178    }
1179
1180    protected synchronized XPathFactory createXPathFactory() throws XPathFactoryConfigurationException {
1181        if (objectModelUri != null) {
1182            xpathFactory = XPathFactory.newInstance(objectModelUri);
1183            LOG.info("Using objectModelUri " + objectModelUri + " when created XPathFactory {}", xpathFactory);
1184            return xpathFactory;
1185        }
1186
1187        if (defaultXPathFactory == null) {
1188            defaultXPathFactory = createDefaultXPathFactory();
1189        }
1190        return defaultXPathFactory;
1191    }
1192
1193    protected static XPathFactory createDefaultXPathFactory() throws XPathFactoryConfigurationException {
1194        XPathFactory factory = null;
1195
1196        // read system property and see if there is a factory set
1197        Properties properties = System.getProperties();
1198        for (Map.Entry<Object, Object> prop : properties.entrySet()) {
1199            String key = (String) prop.getKey();
1200            if (key.startsWith(XPathFactory.DEFAULT_PROPERTY_NAME)) {
1201                String uri = ObjectHelper.after(key, ":");
1202                if (uri != null) {
1203                    factory = XPathFactory.newInstance(uri);
1204                    LOG.info("Using system property {} with value {} when created default XPathFactory {}", new Object[]{key, uri, factory});
1205                }
1206            }
1207        }
1208
1209        if (factory == null) {
1210            factory = XPathFactory.newInstance();
1211            LOG.info("Created default XPathFactory {}", factory);
1212        }
1213
1214        return factory;
1215    }
1216
1217}