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