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    public Map<String, String> getNamespaces() {
513        return namespaces;
514    }
515
516    /**
517     * Gets the {@link XPathFunction} for getting the input message body.
518     * <p/>
519     * A default function will be assigned (if no custom assigned) when either starting this builder
520     * or on first evaluation.
521     *
522     * @return the function, or <tt>null</tt> if this builder has not been started/used before.
523     */
524    public XPathFunction getBodyFunction() {
525        return bodyFunction;
526    }
527
528    private XPathFunction createBodyFunction() {
529        return new XPathFunction() {
530            @SuppressWarnings("rawtypes")
531            public Object evaluate(List list) throws XPathFunctionException {
532                return exchange.get().getIn().getBody();
533            }
534        };
535    }
536
537    public void setBodyFunction(XPathFunction bodyFunction) {
538        this.bodyFunction = bodyFunction;
539    }
540
541    /**
542     * Gets the {@link XPathFunction} for getting the input message header.
543     * <p/>
544     * A default function will be assigned (if no custom assigned) when either starting this builder
545     * or on first evaluation.
546     *
547     * @return the function, or <tt>null</tt> if this builder has not been started/used before.
548     */
549    public XPathFunction getHeaderFunction() {
550        return headerFunction;
551    }
552
553    private XPathFunction createHeaderFunction() {
554        return new XPathFunction() {
555            @SuppressWarnings("rawtypes")
556            public Object evaluate(List list) throws XPathFunctionException {
557                if (!list.isEmpty()) {
558                    Object value = list.get(0);
559                    if (value != null) {
560                        String text = exchange.get().getContext().getTypeConverter().convertTo(String.class, value);
561                        return exchange.get().getIn().getHeader(text);
562                    }
563                }
564                return null;
565            }
566        };
567    }
568
569    public void setHeaderFunction(XPathFunction headerFunction) {
570        this.headerFunction = headerFunction;
571    }
572
573    /**
574     * Gets the {@link XPathFunction} for getting the output message body.
575     * <p/>
576     * A default function will be assigned (if no custom assigned) when either starting this builder
577     * or on first evaluation.
578     *
579     * @return the function, or <tt>null</tt> if this builder has not been started/used before.
580     */
581    public XPathFunction getOutBodyFunction() {
582        return outBodyFunction;
583    }
584
585    private XPathFunction createOutBodyFunction() {
586        return new XPathFunction() {
587            @SuppressWarnings("rawtypes")
588            public Object evaluate(List list) throws XPathFunctionException {
589                if (exchange.get() != null && exchange.get().hasOut()) {
590                    return exchange.get().getOut().getBody();
591                }
592                return null;
593            }
594        };
595    }
596
597    public void setOutBodyFunction(XPathFunction outBodyFunction) {
598        this.outBodyFunction = outBodyFunction;
599    }
600
601    /**
602     * Gets the {@link XPathFunction} for getting the output message header.
603     * <p/>
604     * A default function will be assigned (if no custom assigned) when either starting this builder
605     * or on first evaluation.
606     *
607     * @return the function, or <tt>null</tt> if this builder has not been started/used before.
608     */
609    public XPathFunction getOutHeaderFunction() {
610        return outHeaderFunction;
611    }
612
613    private XPathFunction createOutHeaderFunction() {
614        return new XPathFunction() {
615            @SuppressWarnings("rawtypes")
616            public Object evaluate(List list) throws XPathFunctionException {
617                if (exchange.get() != null && !list.isEmpty()) {
618                    Object value = list.get(0);
619                    if (value != null) {
620                        String text = exchange.get().getContext().getTypeConverter().convertTo(String.class, value);
621                        return exchange.get().getOut().getHeader(text);
622                    }
623                }
624                return null;
625            }
626        };
627    }
628
629    public void setOutHeaderFunction(XPathFunction outHeaderFunction) {
630        this.outHeaderFunction = outHeaderFunction;
631    }
632
633    /**
634     * Gets the {@link XPathFunction} for getting the exchange properties.
635     * <p/>
636     * A default function will be assigned (if no custom assigned) when either starting this builder
637     * or on first evaluation.
638     *
639     * @return the function, or <tt>null</tt> if this builder has not been started/used before.
640     */
641    public XPathFunction getPropertiesFunction() {
642        return propertiesFunction;
643    }
644
645    private XPathFunction createPropertiesFunction() {
646        return new XPathFunction() {
647            @SuppressWarnings("rawtypes")
648            public Object evaluate(List list) throws XPathFunctionException {
649                if (!list.isEmpty()) {
650                    Object value = list.get(0);
651                    if (value != null) {
652                        String text = exchange.get().getContext().getTypeConverter().convertTo(String.class, value);
653                        try {
654                            // use the property placeholder resolver to lookup the property for us
655                            Object answer = exchange.get().getContext().resolvePropertyPlaceholders("{{" + text + "}}");
656                            return answer;
657                        } catch (Exception e) {
658                            throw new XPathFunctionException(e);
659                        }
660                    }
661                }
662                return null;
663            }
664        };
665    }
666
667    public void setPropertiesFunction(XPathFunction propertiesFunction) {
668        this.propertiesFunction = propertiesFunction;
669    }
670
671    /**
672     * Gets the {@link XPathFunction} for executing <a href="http://camel.apache.org/simple">simple</a>
673     * language as xpath function.
674     * <p/>
675     * A default function will be assigned (if no custom assigned) when either starting this builder
676     * or on first evaluation.
677     *
678     * @return the function, or <tt>null</tt> if this builder has not been started/used before.
679     */
680    public XPathFunction getSimpleFunction() {
681        return simpleFunction;
682    }
683
684    private XPathFunction createSimpleFunction() {
685        return new XPathFunction() {
686            @SuppressWarnings("rawtypes")
687            public Object evaluate(List list) throws XPathFunctionException {
688                if (!list.isEmpty()) {
689                    Object value = list.get(0);
690                    if (value != null) {
691                        String text = exchange.get().getContext().getTypeConverter().convertTo(String.class, value);
692                        Language simple = exchange.get().getContext().resolveLanguage("simple");
693                        Expression exp = simple.createExpression(text);
694                        Object answer = exp.evaluate(exchange.get(), Object.class);
695                        return answer;
696                    }
697                }
698                return null;
699            }
700        };
701    }
702
703    public void setSimpleFunction(XPathFunction simpleFunction) {
704        this.simpleFunction = simpleFunction;
705    }
706
707    public Class<?> getResultType() {
708        return resultType;
709    }
710
711    public void setResultType(Class<?> resultType) {
712        this.resultType = resultType;
713        if (Number.class.isAssignableFrom(resultType)) {
714            numberResult();
715        } else if (String.class.isAssignableFrom(resultType)) {
716            stringResult();
717        } else if (Boolean.class.isAssignableFrom(resultType)) {
718            booleanResult();
719        } else if (Node.class.isAssignableFrom(resultType)) {
720            nodeResult();
721        } else if (NodeList.class.isAssignableFrom(resultType)) {
722            nodeSetResult();
723        }
724    }
725
726    public void setLogNamespaces(boolean logNamespaces) {
727        this.logNamespaces = logNamespaces;
728    }
729
730    public boolean isLogNamespaces() {
731        return logNamespaces;
732    }
733
734    public String getObjectModelUri() {
735        return objectModelUri;
736    }
737
738    /**
739     * Enables Saxon on this particular XPath expression, as {@link #saxon()} sets the default static XPathFactory which may have already been initialised
740     * by previous XPath expressions
741     */
742    public void enableSaxon() {
743        this.setObjectModelUri(SAXON_OBJECT_MODEL_URI);
744    }
745
746    public void setObjectModelUri(String objectModelUri) {
747        this.objectModelUri = objectModelUri;
748    }
749
750    // Implementation methods
751    // -------------------------------------------------------------------------
752
753    protected Object evaluate(Exchange exchange) {
754        Object answer = evaluateAs(exchange, resultQName);
755        if (resultType != null) {
756            return ExchangeHelper.convertToType(exchange, resultType, answer);
757        }
758        return answer;
759    }
760
761    /**
762     * Evaluates the expression as the given result type
763     */
764    protected Object evaluateAs(Exchange exchange, QName resultQName) {
765        // pool a pre compiled expression from pool
766        XPathExpression xpathExpression = pool.poll();
767        if (xpathExpression == null) {
768            LOG.trace("Creating new XPathExpression as none was available from pool");
769            // no avail in pool then create one
770            try {
771                xpathExpression = createXPathExpression();
772            } catch (XPathExpressionException e) {
773                throw new InvalidXPathExpression(getText(), e);
774            } catch (Exception e) {
775                throw new RuntimeExpressionException("Cannot create xpath expression", e);
776            }
777        } else {
778            LOG.trace("Acquired XPathExpression from pool");
779        }
780        try {
781            if (logNamespaces && LOG.isInfoEnabled()) {
782                logNamespaces(exchange);
783            }
784            return doInEvaluateAs(xpathExpression, exchange, resultQName);
785        } finally {
786            // release it back to the pool
787            pool.add(xpathExpression);
788            LOG.trace("Released XPathExpression back to pool");
789        }
790    }
791
792    private void logNamespaces(Exchange exchange) {
793        InputStream is = null;
794        NodeList answer = null;
795        XPathExpression xpathExpression = null;
796
797        try {
798            xpathExpression = poolLogNamespaces.poll();
799            if (xpathExpression == null) {
800                xpathExpression = createTraceNamespaceExpression();
801            }
802
803            // prepare the input
804            Object document;
805            if (isInputStreamNeeded(exchange)) {
806                is = exchange.getIn().getBody(InputStream.class);
807                document = getDocument(exchange, is);
808            } else {
809                Object body = exchange.getIn().getBody();
810                document = getDocument(exchange, body);
811            }
812            // fetch all namespaces
813            if (document instanceof InputSource) {
814                InputSource inputSource = (InputSource) document;
815                answer = (NodeList) xpathExpression.evaluate(inputSource, XPathConstants.NODESET);
816            } else if (document instanceof DOMSource) {
817                DOMSource source = (DOMSource) document;
818                answer = (NodeList) xpathExpression.evaluate(source.getNode(), XPathConstants.NODESET);
819            } else {
820                answer = (NodeList) xpathExpression.evaluate(document, XPathConstants.NODESET);
821            }
822        } catch (Exception e) {
823            LOG.warn("Unable to trace discovered namespaces in XPath expression", e);
824        } finally {
825            // IOHelper can handle if is is null
826            IOHelper.close(is);
827            poolLogNamespaces.add(xpathExpression);
828        }
829
830        if (answer != null) {
831            logDiscoveredNamespaces(answer);
832        }
833    }
834
835    private void logDiscoveredNamespaces(NodeList namespaces) {
836        Map<String, HashSet<String>> map = new LinkedHashMap<String, HashSet<String>>();
837        for (int i = 0; i < namespaces.getLength(); i++) {
838            Node n = namespaces.item(i);
839            if (n.getNodeName().equals("xmlns:xml")) {
840                // skip the implicit XML namespace as it provides no value
841                continue;
842            }
843
844            String prefix = namespaces.item(i).getNodeName();
845            if (prefix.equals("xmlns")) {
846                prefix = "DEFAULT";
847            }
848
849            // add to map
850            if (!map.containsKey(prefix)) {
851                map.put(prefix, new HashSet<String>());
852            }
853            map.get(prefix).add(namespaces.item(i).getNodeValue());
854        }
855
856        LOG.info("Namespaces discovered in message: {}.", map);
857    }
858
859    protected Object doInEvaluateAs(XPathExpression xpathExpression, Exchange exchange, QName resultQName) {
860        LOG.trace("Evaluating exchange: {} as: {}", exchange, resultQName);
861
862        Object answer;
863
864        // set exchange and variable resolver as thread locals for concurrency
865        this.exchange.set(exchange);
866
867        // the underlying input stream, which we need to close to avoid locking files or other resources
868        InputStream is = null;
869        try {
870            Object document;
871
872            // Check if we need to apply the XPath expression to a header
873            if (ObjectHelper.isNotEmpty(getHeaderName())) {
874                String headerName = getHeaderName();
875                // only convert to input stream if really needed
876                if (isInputStreamNeeded(exchange, headerName)) {
877                    is = exchange.getIn().getHeader(headerName, InputStream.class);
878                    document = getDocument(exchange, is);
879                } else {
880                    Object headerObject = exchange.getIn().getHeader(getHeaderName());
881                    document = getDocument(exchange, headerObject);
882                }
883            } else {
884                // only convert to input stream if really needed
885                if (isInputStreamNeeded(exchange)) {
886                    is = exchange.getIn().getBody(InputStream.class);
887                    document = getDocument(exchange, is);
888                } else {
889                    Object body = exchange.getIn().getBody();
890                    document = getDocument(exchange, body);
891                }
892            }
893
894            if (resultQName != null) {
895                if (document instanceof InputSource) {
896                    InputSource inputSource = (InputSource) document;
897                    answer = xpathExpression.evaluate(inputSource, resultQName);
898                } else if (document instanceof DOMSource) {
899                    DOMSource source = (DOMSource) document;
900                    answer = xpathExpression.evaluate(source.getNode(), resultQName);
901                } else {
902                    answer = xpathExpression.evaluate(document, resultQName);
903                }
904            } else {
905                if (document instanceof InputSource) {
906                    InputSource inputSource = (InputSource) document;
907                    answer = xpathExpression.evaluate(inputSource);
908                } else if (document instanceof DOMSource) {
909                    DOMSource source = (DOMSource) document;
910                    answer = xpathExpression.evaluate(source.getNode());
911                } else {
912                    answer = xpathExpression.evaluate(document);
913                }
914            }
915        } catch (XPathExpressionException e) {
916            String message = getText();
917            if (ObjectHelper.isNotEmpty(getHeaderName())) {
918                message = message + " with headerName " + getHeaderName();
919            }
920            throw new InvalidXPathExpression(message, e);
921        } finally {
922            // IOHelper can handle if is is null
923            IOHelper.close(is);
924        }
925
926        if (LOG.isTraceEnabled()) {
927            LOG.trace("Done evaluating exchange: {} as: {} with result: {}", new Object[]{exchange, resultQName, answer});
928        }
929        return answer;
930    }
931
932    /**
933     * Creates a new xpath expression as there we no available in the pool.
934     * <p/>
935     * This implementation must be synchronized to ensure thread safety, as this XPathBuilder instance may not have been
936     * started prior to being used.
937     */
938    protected synchronized XPathExpression createXPathExpression() throws XPathExpressionException, XPathFactoryConfigurationException {
939        // ensure we are started
940        try {
941            start();
942        } catch (Exception e) {
943            throw new RuntimeExpressionException("Error starting XPathBuilder", e);
944        }
945
946        // XPathFactory is not thread safe
947        XPath xPath = getXPathFactory().newXPath();
948
949        if (!logNamespaces && LOG.isTraceEnabled()) {
950            LOG.trace("Creating new XPath expression in pool. Namespaces on XPath expression: {}", getNamespaceContext().toString());
951        } else if (logNamespaces && LOG.isInfoEnabled()) {
952            LOG.info("Creating new XPath expression in pool. Namespaces on XPath expression: {}", getNamespaceContext().toString());
953        }
954        xPath.setNamespaceContext(getNamespaceContext());
955        xPath.setXPathVariableResolver(getVariableResolver());
956
957        XPathFunctionResolver parentResolver = getFunctionResolver();
958        if (parentResolver == null) {
959            parentResolver = xPath.getXPathFunctionResolver();
960        }
961        xPath.setXPathFunctionResolver(createDefaultFunctionResolver(parentResolver));
962        return xPath.compile(text);
963    }
964
965    protected synchronized XPathExpression createTraceNamespaceExpression() throws XPathFactoryConfigurationException, XPathExpressionException {
966        // XPathFactory is not thread safe
967        XPath xPath = getXPathFactory().newXPath();
968        return xPath.compile(OBTAIN_ALL_NS_XPATH);
969    }
970
971    protected DefaultNamespaceContext createNamespaceContext(XPathFactory factory) {
972        DefaultNamespaceContext context = new DefaultNamespaceContext(factory);
973        populateDefaultNamespaces(context);
974        return context;
975    }
976
977    /**
978     * Populate a number of standard prefixes if they are not already there
979     */
980    protected void populateDefaultNamespaces(DefaultNamespaceContext context) {
981        setNamespaceIfNotPresent(context, "in", IN_NAMESPACE);
982        setNamespaceIfNotPresent(context, "out", OUT_NAMESPACE);
983        setNamespaceIfNotPresent(context, "env", Namespaces.ENVIRONMENT_VARIABLES);
984        setNamespaceIfNotPresent(context, "system", Namespaces.SYSTEM_PROPERTIES_NAMESPACE);
985        setNamespaceIfNotPresent(context, "function", Namespaces.FUNCTION_NAMESPACE);
986    }
987
988    protected void setNamespaceIfNotPresent(DefaultNamespaceContext context, String prefix, String uri) {
989        if (context != null) {
990            String current = context.getNamespaceURI(prefix);
991            if (current == null) {
992                context.add(prefix, uri);
993            }
994        }
995    }
996
997    protected XPathFunctionResolver createDefaultFunctionResolver(final XPathFunctionResolver parent) {
998        return new XPathFunctionResolver() {
999            public XPathFunction resolveFunction(QName qName, int argumentCount) {
1000                XPathFunction answer = null;
1001                if (parent != null) {
1002                    answer = parent.resolveFunction(qName, argumentCount);
1003                }
1004                if (answer == null) {
1005                    if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), IN_NAMESPACE)
1006                            || isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), DEFAULT_NAMESPACE)) {
1007                        String localPart = qName.getLocalPart();
1008                        if (localPart.equals("body") && argumentCount == 0) {
1009                            return getBodyFunction();
1010                        }
1011                        if (localPart.equals("header") && argumentCount == 1) {
1012                            return getHeaderFunction();
1013                        }
1014                    }
1015                    if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), OUT_NAMESPACE)) {
1016                        String localPart = qName.getLocalPart();
1017                        if (localPart.equals("body") && argumentCount == 0) {
1018                            return getOutBodyFunction();
1019                        }
1020                        if (localPart.equals("header") && argumentCount == 1) {
1021                            return getOutHeaderFunction();
1022                        }
1023                    }
1024                    if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), FUNCTION_NAMESPACE)) {
1025                        String localPart = qName.getLocalPart();
1026                        if (localPart.equals("properties") && argumentCount == 1) {
1027                            return getPropertiesFunction();
1028                        }
1029                        if (localPart.equals("simple") && argumentCount == 1) {
1030                            return getSimpleFunction();
1031                        }
1032                    }
1033                }
1034                return answer;
1035            }
1036        };
1037    }
1038
1039    /**
1040     * Checks whether we need an {@link InputStream} to access the message body.
1041     * <p/>
1042     * Depending on the content in the message body, we may not need to convert
1043     * to {@link InputStream}.
1044     *
1045     * @param exchange the current exchange
1046     * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting afterwards.
1047     */
1048    protected boolean isInputStreamNeeded(Exchange exchange) {
1049        Object body = exchange.getIn().getBody();
1050        return isInputStreamNeededForObject(exchange, body);
1051    }
1052
1053    /**
1054     * Checks whether we need an {@link InputStream} to access the message header.
1055     * <p/>
1056     * Depending on the content in the message header, we may not need to convert
1057     * to {@link InputStream}.
1058     *
1059     * @param exchange the current exchange
1060     * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting afterwards.
1061     */
1062    protected boolean isInputStreamNeeded(Exchange exchange, String headerName) {
1063        Object header = exchange.getIn().getHeader(headerName);
1064        return isInputStreamNeededForObject(exchange, header);
1065    }
1066
1067    /**
1068     * Checks whether we need an {@link InputStream} to access this object
1069     * <p/>
1070     * Depending on the content in the object, we may not need to convert
1071     * to {@link InputStream}.
1072     *
1073     * @param exchange the current exchange
1074     * @return <tt>true</tt> to convert to {@link InputStream} beforehand converting afterwards.
1075     */
1076    protected boolean isInputStreamNeededForObject(Exchange exchange, Object obj) {
1077        if (obj == null) {
1078            return false;
1079        }
1080
1081        if (obj instanceof WrappedFile) {
1082            obj = ((WrappedFile<?>) obj).getFile();
1083        }
1084        if (obj instanceof File) {
1085            // input stream is needed for File to avoid locking the file in case of errors etc
1086            return true;
1087        }
1088
1089        // input stream is not needed otherwise
1090        return false;
1091    }
1092
1093    /**
1094     * Strategy method to extract the document from the exchange.
1095     */
1096    protected Object getDocument(Exchange exchange, Object body) {
1097        try {
1098            return doGetDocument(exchange, body);
1099        } catch (Exception e) {
1100            throw ObjectHelper.wrapRuntimeCamelException(e);
1101        } finally {
1102            // call the reset if the in message body is StreamCache
1103            MessageHelper.resetStreamCache(exchange.getIn());
1104        }
1105    }
1106
1107    protected Object doGetDocument(Exchange exchange, Object body) throws Exception {
1108        if (body == null) {
1109            return null;
1110        }
1111
1112        Object answer = null;
1113
1114        Class<?> type = getDocumentType();
1115        Exception cause = null;
1116        if (type != null) {
1117            // try to get the body as the desired type
1118            try {
1119                answer = exchange.getContext().getTypeConverter().convertTo(type, exchange, body);
1120            } catch (Exception e) {
1121                // we want to store the caused exception, if we could not convert
1122                cause = e;
1123            }
1124        }
1125
1126        if (type == null && answer == null) {
1127            // fallback to get the body as is
1128            answer = body;
1129        } else if (answer == null) {
1130            // there was a type, and we could not convert to it, then fail
1131            if (cause != null) {
1132                throw cause;
1133            } else {
1134                throw new NoTypeConversionAvailableException(body, type);
1135            }
1136        }
1137
1138        return answer;
1139    }
1140
1141    private MessageVariableResolver getVariableResolver() {
1142        return variableResolver;
1143    }
1144
1145    @Override
1146    public void doStart() throws Exception {
1147        if (xpathFactory == null) {
1148            xpathFactory = createXPathFactory();
1149        }
1150        if (namespaceContext == null) {
1151            namespaceContext = createNamespaceContext(xpathFactory);
1152        }
1153        for (Map.Entry<String, String> entry : namespaces.entrySet()) {
1154            namespaceContext.add(entry.getKey(), entry.getValue());
1155        }
1156
1157        // create default functions if no custom assigned
1158        if (bodyFunction == null) {
1159            bodyFunction = createBodyFunction();
1160        }
1161        if (headerFunction == null) {
1162            headerFunction = createHeaderFunction();
1163        }
1164        if (outBodyFunction == null) {
1165            outBodyFunction = createOutBodyFunction();
1166        }
1167        if (outHeaderFunction == null) {
1168            outHeaderFunction = createOutHeaderFunction();
1169        }
1170        if (propertiesFunction == null) {
1171            propertiesFunction = createPropertiesFunction();
1172        }
1173        if (simpleFunction == null) {
1174            simpleFunction = createSimpleFunction();
1175        }
1176    }
1177
1178    @Override
1179    public void doStop() throws Exception {
1180        pool.clear();
1181        poolLogNamespaces.clear();
1182    }
1183
1184    protected synchronized XPathFactory createXPathFactory() throws XPathFactoryConfigurationException {
1185        if (objectModelUri != null) {
1186            xpathFactory = XPathFactory.newInstance(objectModelUri);
1187            LOG.info("Using objectModelUri " + objectModelUri + " when created XPathFactory {}", xpathFactory);
1188            return xpathFactory;
1189        }
1190
1191        if (defaultXPathFactory == null) {
1192            defaultXPathFactory = createDefaultXPathFactory();
1193        }
1194        return defaultXPathFactory;
1195    }
1196
1197    protected static XPathFactory createDefaultXPathFactory() throws XPathFactoryConfigurationException {
1198        XPathFactory factory = null;
1199
1200        // read system property and see if there is a factory set
1201        Properties properties = System.getProperties();
1202        for (Map.Entry<Object, Object> prop : properties.entrySet()) {
1203            String key = (String) prop.getKey();
1204            if (key.startsWith(XPathFactory.DEFAULT_PROPERTY_NAME)) {
1205                String uri = ObjectHelper.after(key, ":");
1206                if (uri != null) {
1207                    factory = XPathFactory.newInstance(uri);
1208                    LOG.info("Using system property {} with value {} when created default XPathFactory {}", new Object[]{key, uri, factory});
1209                }
1210            }
1211        }
1212
1213        if (factory == null) {
1214            factory = XPathFactory.newInstance();
1215            LOG.info("Created default XPathFactory {}", factory);
1216        }
1217
1218        return factory;
1219    }
1220
1221}