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