content: /*
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to You under the Apache License, Version 2.0
   * (the "License"); you may not use this file except in compliance with
   * the License.  You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package org.apache.camel.language.xpath;
  
  import java.io.File;
  import java.io.InputStream;
  import java.util.HashSet;
  import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Properties;
  import java.util.Queue;
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.concurrent.ConcurrentLinkedQueue;
  import java.util.concurrent.locks.Lock;
  import java.util.concurrent.locks.ReentrantLock;
  
  import javax.xml.namespace.QName;
  import javax.xml.parsers.ParserConfigurationException;
  import javax.xml.transform.dom.DOMSource;
  import javax.xml.transform.sax.SAXSource;
  import javax.xml.xpath.XPath;
  import javax.xml.xpath.XPathConstants;
  import javax.xml.xpath.XPathExpression;
  import javax.xml.xpath.XPathExpressionException;
  import javax.xml.xpath.XPathFactory;
  import javax.xml.xpath.XPathFactoryConfigurationException;
  import javax.xml.xpath.XPathFunction;
  import javax.xml.xpath.XPathFunctionException;
  import javax.xml.xpath.XPathFunctionResolver;
  
  import org.w3c.dom.Document;
  import org.w3c.dom.Node;
  import org.w3c.dom.NodeList;
  
  import org.xml.sax.InputSource;
  
  import org.apache.camel.CamelContext;
  import org.apache.camel.CamelContextAware;
  import org.apache.camel.Exchange;
  import org.apache.camel.Expression;
  import org.apache.camel.NoTypeConversionAvailableException;
  import org.apache.camel.Predicate;
  import org.apache.camel.RuntimeCamelException;
  import org.apache.camel.RuntimeExpressionException;
  import org.apache.camel.WrappedFile;
  import org.apache.camel.spi.ExpressionResultTypeAware;
  import org.apache.camel.spi.Language;
  import org.apache.camel.spi.NamespaceAware;
  import org.apache.camel.support.DefaultExchange;
  import org.apache.camel.support.ExchangeHelper;
  import org.apache.camel.support.MessageHelper;
  import org.apache.camel.support.builder.Namespaces;
  import org.apache.camel.support.builder.xml.XMLConverterHelper;
  import org.apache.camel.support.service.ServiceSupport;
  import org.apache.camel.util.IOHelper;
  import org.apache.camel.util.ObjectHelper;
  import org.apache.camel.util.StringHelper;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import static org.apache.camel.support.builder.Namespaces.*;
  
  /**
   * Creates an XPath expression builder which creates a nodeset result by default. If you want to evaluate a String
   * expression then call {@link #stringResult()}
   * <p/>
   * An XPath object is not thread-safe and not reentrant. In other words, it is the application's responsibility to make
   * sure that one XPath object is not used from more than one thread at any given time, and while the evaluate method is
   * invoked, applications may not recursively call the evaluate method.
   * <p/>
   * This implementation is thread safe by using thread locals and pooling to allow concurrency.
   * <p/>
   * <b>Important:</b> After configuring the {@link XPathBuilder} its advised to invoke {@link #start()} to prepare the
   * builder before using; though the builder will auto-start on first use.
   *
   * @see XPathConstants#NODESET
   */
  public class XPathBuilder extends ServiceSupport
          implements CamelContextAware, Expression, Predicate,
          NamespaceAware, ExpressionResultTypeAware {
      private static final Logger LOG = LoggerFactory.getLogger(XPathBuilder.class);
      private static final String SAXON_OBJECT_MODEL_URI = "http://saxon.sf.net/jaxp/xpath/om";
      private static final String SAXON_FACTORY_CLASS_NAME = "net.sf.saxon.xpath.XPathFactoryImpl";
      private static final String OBTAIN_ALL_NS_XPATH = "//*/namespace::*";
  
      private static volatile XPathFactory defaultXPathFactory;
  
      private CamelContext camelContext;
      private final Lock lock = new ReentrantLock();
      private final Queue<XPathExpression> pool = new ConcurrentLinkedQueue<>();
      private final Queue<XPathExpression> poolLogNamespaces = new ConcurrentLinkedQueue<>();
      private final String text;
      private final ThreadLocal<Exchange> exchange = new ThreadLocal<>();
      private final MessageVariableResolver variableResolver = new MessageVariableResolver(exchange);
      private final Map<String, String> namespaces = new ConcurrentHashMap<>();
      private boolean preCompile = true;
      private boolean threadSafety;
      private volatile XPathFactory xpathFactory;
      private volatile Class<?> documentType = Document.class;
      // For some reason the default expression of "a/b" on a document such as
      // <a><b>1</b><b>2</b></a>
      // will evaluate as just "1" by default which is bizarre. So by default
      // let's assume XPath expressions result in nodesets.
      private volatile Class<?> resultType;
      private volatile QName resultQName = XPathConstants.NODESET;
      private volatile boolean useSaxon;
      private volatile String objectModelUri;
      private volatile String factoryClassName;
      private volatile DefaultNamespaceContext namespaceContext;
      private volatile boolean logNamespaces;
      private volatile XPathFunctionResolver functionResolver;
      private volatile XPathFunction bodyFunction;
      private volatile XPathFunction headerFunction;
      private volatile XPathFunction outBodyFunction;
      private volatile XPathFunction outHeaderFunction;
      private volatile XPathFunction propertiesFunction;
      private volatile XPathFunction simpleFunction;
      private volatile Expression source;
  
      /**
       * @param text The XPath expression
       */
      public XPathBuilder(String text) {
          this.text = text;
      }
  
      /**
       * @param  text The XPath expression
       * @return      A new XPathBuilder object
       */
      public static XPathBuilder xpath(String text) {
          return new XPathBuilder(text);
      }
  
      /**
       * @param  text       The XPath expression
       * @param  resultType The result type that the XPath expression will return.
       * @return            A new XPathBuilder object
       */
      public static XPathBuilder xpath(String text, Class<?> resultType) {
          XPathBuilder builder = new XPathBuilder(text);
          if (resultType != null) {
              builder.setResultType(resultType);
          }
          return builder;
      }
  
      @Override
      public void init(CamelContext context) {
          if (preCompile && pool.isEmpty()) {
              LOG.trace("PreCompiling new XPathExpression and adding to pool during initialization");
              try {
                  XPathExpression xpathExpression = createXPathExpression();
                  pool.add(xpathExpression);
              } catch (XPathExpressionException e) {
                  throw RuntimeCamelException.wrapRuntimeException(e);
              }
          }
      }
  
      @Override
      public String toString() {
          return "XPath: " + text;
      }
  
      @Override
      public CamelContext getCamelContext() {
          return camelContext;
      }
  
      @Override
      public void setCamelContext(CamelContext camelContext) {
          this.camelContext = camelContext;
      }
  
      @Override
      public boolean matches(Exchange exchange) {
          try {
              Object booleanResult = evaluateAs(exchange, XPathConstants.BOOLEAN);
              return exchange.getContext().getTypeConverter().convertTo(Boolean.class, booleanResult);
          } finally {
              // remove the thread local after usage
              this.exchange.remove();
          }
      }
  
      @Override
      public <T> T evaluate(Exchange exchange, Class<T> type) {
          try {
              Object result = evaluate(exchange);
              return exchange.getContext().getTypeConverter().convertTo(type, exchange, result);
          } finally {
              // remove the thread local after usage
              this.exchange.remove();
          }
      }
  
      /**
       * Matches the given xpath using the provided body.
       *
       * @param  context the camel context
       * @param  body    the body
       * @return         <tt>true</tt> if matches, <tt>false</tt> otherwise
       */
      public boolean matches(CamelContext context, Object body) {
          ObjectHelper.notNull(context, "CamelContext");
  
          // create a dummy Exchange to use during matching
          Exchange dummy = new DefaultExchange(context);
          dummy.getIn().setBody(body);
  
          try {
              return matches(dummy);
          } finally {
              // remove the thread local after usage
              exchange.remove();
          }
      }
  
      /**
       * Evaluates the given xpath using the provided body.
       * <p/>
       * The evaluation uses by default {@link javax.xml.xpath.XPathConstants#NODESET} as the type used during xpath
       * evaluation. The output from xpath is then afterwards type converted using Camel's type converter to the given
       * type.
       * <p/>
       * If you want to evaluate xpath using a different type, then call {@link #setResultType(Class)} prior to calling
       * this evaluate method.
       *
       * @param  context the camel context
       * @param  body    the body
       * @param  type    the type to return
       * @return         result of the evaluation
       */
      public <T> T evaluate(CamelContext context, Object body, Class<T> type) {
          ObjectHelper.notNull(context, "CamelContext");
  
          // create a dummy Exchange to use during evaluation
          Exchange dummy = new DefaultExchange(context);
          dummy.getIn().setBody(body);
  
          try {
              return evaluate(dummy, type);
          } finally {
              // remove the thread local after usage
              exchange.remove();
          }
      }
  
      /**
       * Evaluates the given xpath using the provided body as a String return type.
       *
       * @param  context the camel context
       * @param  body    the body
       * @return         result of the evaluation
       */
      public String evaluate(CamelContext context, Object body) {
          ObjectHelper.notNull(context, "CamelContext");
  
          // create a dummy Exchange to use during evaluation
          Exchange dummy = new DefaultExchange(context);
          dummy.getIn().setBody(body);
  
          setResultQName(XPathConstants.STRING);
          setResultType(String.class);
          try {
              return evaluate(dummy, String.class);
          } finally {
              // remove the thread local after usage
              this.exchange.remove();
          }
      }
  
      // Builder methods
      // -------------------------------------------------------------------------
  
      /**
       * Sets the expression result type to {@link XPathConstants#BOOLEAN}
       *
       * @return the current builder
       */
      public XPathBuilder booleanResult() {
          resultQName = XPathConstants.BOOLEAN;
          return this;
      }
  
      /**
       * Sets the expression result type to {@link XPathConstants#NODE}
       *
       * @return the current builder
       */
      public XPathBuilder nodeResult() {
          resultQName = XPathConstants.NODE;
          return this;
      }
  
      /**
       * Sets the expression result type to {@link XPathConstants#NODESET}
       *
       * @return the current builder
       */
      public XPathBuilder nodeSetResult() {
          resultQName = XPathConstants.NODESET;
          return this;
      }
  
      /**
       * Sets the expression result type to {@link XPathConstants#NUMBER}
       *
       * @return the current builder
       */
      public XPathBuilder numberResult() {
          resultQName = XPathConstants.NUMBER;
          return this;
      }
  
      /**
       * Sets the expression result type to {@link XPathConstants#STRING}
       *
       * @return the current builder
       */
      public XPathBuilder stringResult() {
          resultQName = XPathConstants.STRING;
          return this;
      }
  
      /**
       * Sets the expression result type to the given {@code resultType}
       *
       * @return the current builder
       */
      public XPathBuilder resultType(Class<?> resultType) {
          setResultType(resultType);
          return this;
      }
  
      /**
       * Sets the object model URI to use
       *
       * @return the current builder
       */
      public XPathBuilder objectModel(String uri) {
          // Careful! Setting the Object Model URI this way will set the *Default*
          // XPath Factory, which since is a static field,
          // will set the XPath Factory system-wide. Decide what to do, as
          // changing this behaviour can break compatibility. Provided the
          // setObjectModel which changes
          // this instance's XPath Factory rather than the static field
          this.objectModelUri = uri;
          return this;
      }
  
      /**
       * Sets the factory class name to use
       *
       * @return the current builder
       */
      public XPathBuilder factoryClassName(String factoryClassName) {
          this.factoryClassName = factoryClassName;
          return this;
      }
  
      /**
       * Configures to use Saxon as the XPathFactory which allows you to use XPath 2.0 functions which may not be part of
       * the build in JDK XPath parser.
       *
       * @return the current builder
       */
      public XPathBuilder saxon() {
          this.useSaxon = true;
          this.objectModelUri = SAXON_OBJECT_MODEL_URI;
          this.factoryClassName = SAXON_FACTORY_CLASS_NAME;
          return this;
      }
  
      /**
       * Sets the {@link XPathFunctionResolver} instance to use on these XPath expressions
       *
       * @return the current builder
       */
      public XPathBuilder functionResolver(XPathFunctionResolver functionResolver) {
          this.functionResolver = functionResolver;
          return this;
      }
  
      /**
       * Registers the namespace prefix and URI with the builder so that the prefix can be used in XPath expressions
       *
       * @param  prefix is the namespace prefix that can be used in the XPath expressions
       * @param  uri    is the namespace URI to which the prefix refers
       * @return        the current builder
       */
      public XPathBuilder namespace(String prefix, String uri) {
          namespaces.put(prefix, uri);
          return this;
      }
  
      /**
       * Registers namespaces with the builder so that the registered prefixes can be used in XPath expressions
       *
       * @param  namespaces is namespaces object that should be used in the XPath expression
       * @return            the current builder
       */
      public XPathBuilder namespaces(Namespaces namespaces) {
          namespaces.configure(this);
          return this;
      }
  
      /**
       * Registers a variable (in the global namespace) which can be referred to from XPath expressions
       *
       * @param  name  name of variable
       * @param  value value of variable
       * @return       the current builder
       */
      public XPathBuilder variable(String name, Object value) {
          getVariableResolver().addVariable(name, value);
          return this;
      }
  
      /**
       * Configures the document type to use.
       * <p/>
       * The document type controls which kind of Class Camel should convert the payload to before doing the xpath
       * evaluation.
       * <p/>
       * For example you can set it to {@link InputSource} to use SAX streams. By default Camel uses {@link Document} as
       * the type.
       *
       * @param  documentType the document type
       * @return              the current builder
       */
      public XPathBuilder documentType(Class<?> documentType) {
          setDocumentType(documentType);
          return this;
      }
  
      /**
       * Configures to use the provided XPath factory.
       * <p/>
       * Can be used to use Saxon instead of the build in factory from the JDK.
       *
       * @param  xpathFactory the xpath factory to use
       * @return              the current builder.
       */
      public XPathBuilder factory(XPathFactory xpathFactory) {
          setXPathFactory(xpathFactory);
          return this;
      }
  
      /**
       * Activates trace logging of all discovered namespaces in the message - to simplify debugging namespace-related
       * issues
       * <p/>
       * Namespaces are printed in Hashmap style <code>{xmlns:prefix=[namespaceURI], xmlns:prefix=[namespaceURI]}</code>.
       * <p/>
       * The implicit XML namespace is omitted (http://www.w3.org/XML/1998/namespace). XML allows for namespace prefixes
       * to be redefined/overridden due to hierarchical scoping, i.e. prefix abc can be mapped to http://abc.com, 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 namespace URIs, Camel will show all namespaces URIs it is mapped to in an array-style.
       * <p/>
       * This feature is disabled by default.
       *
       * @return the current builder.
       */
      public XPathBuilder logNamespaces() {
          setLogNamespaces(true);
          return this;
      }
  
      /**
       * Whether to enable thread-safety for the returned result of the xpath expression. This applies to when using
       * NODESET as the result type, and the returned set has multiple elements. In this situation there can be
       * thread-safety issues if you process the NODESET concurrently such as from a Camel Splitter EIP in parallel
       * processing mode. This option prevents concurrency issues by doing defensive copies of the nodes.
       * <p/>
       * It is recommended to turn this option on if you are using camel-saxon or Saxon in your application. Saxon has
       * thread-safety issues which can be prevented by turning this option on.
       * <p/>
       * Thread-safety is disabled by default
       *
       * @return the current builder.
       */
      public XPathBuilder threadSafety(boolean threadSafety) {
          setThreadSafety(threadSafety);
          return this;
      }
  
      /**
       * Whether to enable pre-compiling the xpath expression during initialization phase. pre-compile is enabled by
       * default.
       *
       * This can be used to turn off, for example in cases the compilation phase is desired at the starting phase, such
       * as if the application is ahead of time compiled (for example with camel-quarkus) which would then load the xpath
       * factory of the built operating system, and not a JVM runtime.
       *
       * @return the current builder.
       */
      public XPathBuilder preCompile(boolean preCompile) {
          setPreCompile(preCompile);
          return this;
      }
  
      // Properties
      // -------------------------------------------------------------------------
  
      /**
       * Gets the xpath factory, can be <tt>null</tt> if no custom factory has been assigned.
       * <p/>
       * A default factory will be assigned (if no custom assigned) when either starting this builder or on first
       * evaluation.
       *
       * @return the factory, or <tt>null</tt> if this builder has not been started/used before.
       */
      public XPathFactory getXPathFactory() {
          return xpathFactory;
      }
  
      public void setXPathFactory(XPathFactory xpathFactory) {
          this.xpathFactory = xpathFactory;
      }
  
      public Class<?> getDocumentType() {
          return documentType;
      }
  
      public void setDocumentType(Class<?> documentType) {
          this.documentType = documentType;
      }
  
      public String getText() {
          return text;
      }
  
      public QName getResultQName() {
          return resultQName;
      }
  
      public void setResultQName(QName resultQName) {
          this.resultQName = resultQName;
      }
  
      public boolean isThreadSafety() {
          return threadSafety;
      }
  
      public void setThreadSafety(boolean threadSafety) {
          this.threadSafety = threadSafety;
      }
  
      public boolean isPreCompile() {
          return preCompile;
      }
  
      public void setPreCompile(boolean preCompile) {
          this.preCompile = preCompile;
      }
  
      /**
       * Gets the namespace context, can be <tt>null</tt> if no custom context has been assigned.
       * <p/>
       * A default context will be assigned (if no custom assigned) when either starting this builder or on first
       * evaluation.
       *
       * @return the context, or <tt>null</tt> if this builder has not been started/used before.
       */
      public DefaultNamespaceContext getNamespaceContext() {
          return namespaceContext;
      }
  
      public void setNamespaceContext(DefaultNamespaceContext namespaceContext) {
          this.namespaceContext = namespaceContext;
      }
  
      public XPathFunctionResolver getFunctionResolver() {
          return functionResolver;
      }
  
      public void setFunctionResolver(XPathFunctionResolver functionResolver) {
          this.functionResolver = functionResolver;
      }
  
      @Override
      public void setNamespaces(Map<String, String> namespaces) {
          this.namespaces.clear();
          this.namespaces.putAll(namespaces);
      }
  
      @Override
      public Map<String, String> getNamespaces() {
          return namespaces;
      }
  
      /**
       * Gets the {@link XPathFunction} for getting the input message body.
       * <p/>
       * A default function will be assigned (if no custom assigned) when either starting this builder or on first
       * evaluation.
       *
       * @return the function, or <tt>null</tt> if this builder has not been started/used before.
       */
      public XPathFunction getBodyFunction() {
          return bodyFunction;
      }
  
      private XPathFunction createBodyFunction() {
          return new XPathFunction() {
              @SuppressWarnings("rawtypes")
              public Object evaluate(List list) throws XPathFunctionException {
                  return exchange.get().getIn().getBody();
              }
          };
      }
  
      public void setBodyFunction(XPathFunction bodyFunction) {
          this.bodyFunction = bodyFunction;
      }
  
      /**
       * Gets the {@link XPathFunction} for getting the input message header.
       * <p/>
       * A default function will be assigned (if no custom assigned) when either starting this builder or on first
       * evaluation.
       *
       * @return the function, or <tt>null</tt> if this builder has not been started/used before.
       */
      public XPathFunction getHeaderFunction() {
          return headerFunction;
      }
  
      private XPathFunction createHeaderFunction() {
          return new XPathFunction() {
              @SuppressWarnings("rawtypes")
              public Object evaluate(List list) throws XPathFunctionException {
                  if (!list.isEmpty()) {
                      Object value = list.get(0);
                      if (value != null) {
                          String headerText = exchange.get().getContext().getTypeConverter().convertTo(String.class, value);
                          return exchange.get().getIn().getHeader(headerText);
                      }
                  }
                  return null;
              }
          };
      }
  
      public void setHeaderFunction(XPathFunction headerFunction) {
          this.headerFunction = headerFunction;
      }
  
      /**
       * Gets the {@link XPathFunction} for getting the output message body.
       * <p/>
       * A default function will be assigned (if no custom assigned) when either starting this builder or on first
       * evaluation.
       *
       * @return the function, or <tt>null</tt> if this builder has not been started/used before.
       */
      public XPathFunction getOutBodyFunction() {
          return outBodyFunction;
      }
  
      private XPathFunction createOutBodyFunction() {
          return new XPathFunction() {
              @SuppressWarnings("rawtypes")
              public Object evaluate(List list) throws XPathFunctionException {
                  if (exchange.get() != null && exchange.get().hasOut()) {
                      return exchange.get().getOut().getBody();
                  }
                  return null;
              }
          };
      }
  
      public void setOutBodyFunction(XPathFunction outBodyFunction) {
          this.outBodyFunction = outBodyFunction;
      }
  
      /**
       * Gets the {@link XPathFunction} for getting the output message header.
       * <p/>
       * A default function will be assigned (if no custom assigned) when either starting this builder or on first
       * evaluation.
       *
       * @return the function, or <tt>null</tt> if this builder has not been started/used before.
       */
      public XPathFunction getOutHeaderFunction() {
          return outHeaderFunction;
      }
  
      private XPathFunction createOutHeaderFunction() {
          return new XPathFunction() {
              @SuppressWarnings("rawtypes")
              public Object evaluate(List list) throws XPathFunctionException {
                  if (exchange.get() != null && !list.isEmpty()) {
                      Object value = list.get(0);
                      if (value != null) {
                          String headerText = exchange.get().getContext().getTypeConverter().convertTo(String.class, value);
                          return exchange.get().getOut().getHeader(headerText);
                      }
                  }
                  return null;
              }
          };
      }
  
      public void setOutHeaderFunction(XPathFunction outHeaderFunction) {
          this.outHeaderFunction = outHeaderFunction;
      }
  
      /**
       * Gets the {@link XPathFunction} for getting the exchange properties.
       * <p/>
       * A default function will be assigned (if no custom assigned) when either starting this builder or on first
       * evaluation.
       *
       * @return the function, or <tt>null</tt> if this builder has not been started/used before.
       */
      public XPathFunction getPropertiesFunction() {
          return propertiesFunction;
      }
  
      private XPathFunction createPropertiesFunction() {
          return new XPathFunction() {
              @SuppressWarnings("rawtypes")
              public Object evaluate(List list) throws XPathFunctionException {
                  if (!list.isEmpty()) {
                      Object value = list.get(0);
                      if (value != null) {
                          String propertyText = exchange.get().getContext().getTypeConverter().convertTo(String.class, value);
                          try {
                              // use the property placeholder resolver to lookup
                              // the property for us
                              return exchange.get().getContext().resolvePropertyPlaceholders("{{" + propertyText + "}}");
                          } catch (Exception e) {
                              throw new XPathFunctionException(e);
                          }
                      }
                  }
                  return null;
              }
          };
      }
  
      public void setPropertiesFunction(XPathFunction propertiesFunction) {
          this.propertiesFunction = propertiesFunction;
      }
  
      /**
       * Gets the {@link XPathFunction} for executing <a href="http://camel.apache.org/simple">simple</a> language as
       * xpath function.
       * <p/>
       * A default function will be assigned (if no custom assigned) when either starting this builder or on first
       * evaluation.
       *
       * @return the function, or <tt>null</tt> if this builder has not been started/used before.
       */
      public XPathFunction getSimpleFunction() {
          return simpleFunction;
      }
  
      private XPathFunction createSimpleFunction() {
          return new XPathFunction() {
              @SuppressWarnings("rawtypes")
              public Object evaluate(List list) throws XPathFunctionException {
                  if (!list.isEmpty()) {
                      Object value = list.get(0);
                      if (value != null) {
                          String exprText = exchange.get().getContext().getTypeConverter().convertTo(String.class, value);
                          Language simple = exchange.get().getContext().resolveLanguage("simple");
                          Expression exp = simple.createExpression(exprText);
                          return exp.evaluate(exchange.get(), Object.class);
                      }
                  }
                  return null;
              }
          };
      }
  
      public void setSimpleFunction(XPathFunction simpleFunction) {
          this.simpleFunction = simpleFunction;
      }
  
      public Expression getSource() {
          return source;
      }
  
      public void setSource(Expression source) {
          this.source = source;
      }
  
      @Override
      public String getExpressionText() {
          return text;
      }
  
      @Override
      public Class<?> getResultType() {
          return resultType;
      }
  
      public void setResultType(Class<?> resultType) {
          this.resultType = resultType;
          if (Number.class.isAssignableFrom(resultType)) {
              numberResult();
          } else if (String.class.isAssignableFrom(resultType)) {
              stringResult();
          } else if (Boolean.class.isAssignableFrom(resultType)) {
              booleanResult();
          } else if (Node.class.isAssignableFrom(resultType)) {
              nodeResult();
          } else if (NodeList.class.isAssignableFrom(resultType)) {
              nodeSetResult();
          }
      }
  
      public void setLogNamespaces(boolean logNamespaces) {
          this.logNamespaces = logNamespaces;
      }
  
      public boolean isLogNamespaces() {
          return logNamespaces;
      }
  
      /**
       * Enables Saxon on this particular XPath expression, as {@link #saxon()} sets the default static XPathFactory which
       * may have already been initialised by previous XPath expressions
       */
      public void enableSaxon() {
          this.setObjectModelUri(SAXON_OBJECT_MODEL_URI);
          this.setFactoryClassName(SAXON_FACTORY_CLASS_NAME);
      }
  
      /**
       * Whether to enable Saxon on this particular XPath expression.
       */
      public void setUseSaxon(boolean useSaxon) {
          this.useSaxon = useSaxon;
          if (useSaxon) {
              enableSaxon();
          }
      }
  
      public boolean isUseSaxon() {
          return useSaxon;
      }
  
      public String getObjectModelUri() {
          return objectModelUri;
      }
  
      public void setObjectModelUri(String objectModelUri) {
          this.objectModelUri = objectModelUri;
      }
  
      public String getFactoryClassName() {
          return factoryClassName;
      }
  
      public void setFactoryClassName(String factoryClassName) {
          this.factoryClassName = factoryClassName;
      }
  
      public Object evaluate(Exchange exchange) {
          Object answer = evaluateAs(exchange, resultQName);
          if (resultType != null) {
              return ExchangeHelper.convertToType(exchange, resultType, answer);
          }
          return answer;
      }
  
      // Implementation methods
      // -------------------------------------------------------------------------
  
      /**
       * Evaluates the expression as the given result type
       */
      protected Object evaluateAs(Exchange exchange, QName resultQName) {
          // pool a pre compiled expression from pool
          XPathExpression xpathExpression = pool.poll();
          if (xpathExpression == null) {
              LOG.trace("Creating new XPathExpression as none was available from pool");
              // no avail in pool then create one
              try {
                  xpathExpression = createXPathExpression();
              } catch (XPathExpressionException e) {
                  throw new InvalidXPathException(getText(), e);
              } catch (Exception e) {
                  throw new RuntimeExpressionException("Cannot create xpath expression", e);
              }
          } else {
              LOG.trace("Acquired XPathExpression from pool");
          }
          try {
              if (logNamespaces && LOG.isInfoEnabled()) {
                  logNamespaces(exchange);
              }
              return doInEvaluateAs(xpathExpression, exchange, resultQName);
          } finally {
              // release it back to the pool
              pool.add(xpathExpression);
              LOG.trace("Released XPathExpression back to pool");
          }
      }
  
      private void logNamespaces(Exchange exchange) {
          InputStream is = null;
          NodeList answer = null;
          XPathExpression xpathExpression = null;
  
          try {
              xpathExpression = poolLogNamespaces.poll();
              if (xpathExpression == null) {
                  xpathExpression = createTraceNamespaceExpression();
              }
  
              // prepare the input
              Object document;
              if (isInputStreamNeeded(exchange)) {
                  is = exchange.getIn().getBody(InputStream.class);
                  document = getDocument(exchange, is);
              } else {
                  Object body = exchange.getIn().getBody();
                  document = getDocument(exchange, body);
              }
              // fetch all namespaces
              if (document instanceof InputSource) {
                  InputSource inputSource = (InputSource) document;
                  answer = (NodeList) xpathExpression.evaluate(inputSource, XPathConstants.NODESET);
              } else if (document instanceof DOMSource) {
                  DOMSource source = (DOMSource) document;
                  answer = (NodeList) xpathExpression.evaluate(source.getNode(), XPathConstants.NODESET);
              } else if (document instanceof SAXSource) {
                  SAXSource source = (SAXSource) document;
                  // since its a SAXSource it may not return an NodeList (for
                  // example if using Saxon)
                  Object result = xpathExpression.evaluate(source.getInputSource(), XPathConstants.NODESET);
                  if (result instanceof NodeList) {
                      answer = (NodeList) result;
                  } else {
                      answer = null;
                  }
              } else {
                  answer = (NodeList) xpathExpression.evaluate(document, XPathConstants.NODESET);
              }
          } catch (Exception e) {
              LOG.warn("Unable to trace discovered namespaces in XPath expression", e);
          } finally {
              // IOHelper can handle if is is null
              IOHelper.close(is);
              poolLogNamespaces.add(xpathExpression);
          }
  
          if (answer != null) {
              logDiscoveredNamespaces(answer);
          }
      }
  
      private void logDiscoveredNamespaces(NodeList namespaces) {
          Map<String, HashSet<String>> map = new LinkedHashMap<>();
          for (int i = 0; i < namespaces.getLength(); i++) {
              Node n = namespaces.item(i);
              if (n.getNodeName().equals("xmlns:xml")) {
                  // skip the implicit XML namespace as it provides no value
                  continue;
              }
  
              String prefix = namespaces.item(i).getNodeName();
              if (prefix.equals("xmlns")) {
                  prefix = "DEFAULT";
              }
  
              // add to map
              map.computeIfAbsent(prefix, k -> new HashSet<>())
                      .add(namespaces.item(i).getNodeValue());
          }
  
          LOG.info("Namespaces discovered in message: {}.", map);
      }
  
      protected Object doInEvaluateAs(XPathExpression xpathExpression, Exchange exchange, QName resultQName) {
          LOG.trace("Evaluating exchange: {} as: {}", exchange, resultQName);
  
          Object answer;
  
          // set exchange and variable resolver as thread locals for concurrency
          this.exchange.set(exchange);
  
          Object payload = source != null ? source.evaluate(exchange, Object.class) : exchange.getMessage().getBody();
          Object document;
          InputStream is = null;
          if (isInputStreamNeededForObject(payload)) {
              is = exchange.getContext().getTypeConverter().tryConvertTo(InputStream.class, exchange, payload);
              document = getDocument(exchange, is);
          } else {
              document = getDocument(exchange, payload);
          }
          try {
              if (resultQName != null) {
                  if (document == null) {
                      document = new XMLConverterHelper().createDocument();
                  }
                  if (document instanceof InputSource) {
                      InputSource inputSource = (InputSource) document;
                      answer = xpathExpression.evaluate(inputSource, resultQName);
                  } else if (document instanceof DOMSource) {
                      DOMSource source = (DOMSource) document;
                      answer = xpathExpression.evaluate(source.getNode(), resultQName);
                  } else {
                      answer = xpathExpression.evaluate(document, resultQName);
                  }
              } else {
                  if (document instanceof InputSource) {
                      InputSource inputSource = (InputSource) document;
                      answer = xpathExpression.evaluate(inputSource);
                  } else if (document instanceof DOMSource) {
                      DOMSource source = (DOMSource) document;
                      answer = xpathExpression.evaluate(source.getNode());
                  } else {
                      answer = xpathExpression.evaluate(document);
                  }
              }
          } catch (ParserConfigurationException e) {
              String message = getText();
              throw new RuntimeCamelException(message, e);
          } catch (XPathExpressionException e) {
              String message = getText();
              throw new InvalidXPathException(message, e);
          } finally {
              IOHelper.close(is);
          }
  
          if (threadSafety && answer instanceof NodeList) {
              try {
                  NodeList list = (NodeList) answer;
  
                  // when the result is NodeList and has 1 or more elements, then it is not thread-safe to use concurrently,
                  // and we need to clone each node and build a thread-safe list to be used instead
                  boolean threadSafetyNeeded = list.getLength() >= 1;
                  if (threadSafetyNeeded) {
                      answer = new ThreadSafeNodeList(list);
                      if (LOG.isDebugEnabled()) {
                          LOG.debug("Created thread-safe result from: {} as: {}", list.getClass().getName(),
                                  answer.getClass().getName());
                      }
                  }
              } catch (Exception e) {
                  throw RuntimeCamelException.wrapRuntimeCamelException(e);
              }
          }
  
          if (LOG.isTraceEnabled()) {
              LOG.trace("Done evaluating exchange: {} as: {} with result: {}", exchange, resultQName, answer);
          }
          return answer;
      }
  
      /**
       * Creates a new xpath expression as there we no available in the pool.
       * <p/>
       * This implementation must be synchronized to ensure thread safety, as this XPathBuilder instance may not have been
       * started prior to being used.
       */
      protected XPathExpression createXPathExpression()
              throws XPathExpressionException {
          lock.lock();
          try {
              // ensure we are started
              try {
                  start();
              } catch (Exception e) {
                  throw new RuntimeExpressionException("Error starting XPathBuilder", e);
              }
  
              // XPathFactory is not thread safe
              XPath xPath = getXPathFactory().newXPath();
  
              if (!logNamespaces && LOG.isTraceEnabled()) {
                  LOG.trace("Creating new XPath expression in pool. Namespaces on XPath expression: {}", getNamespaceContext());
              } else if (logNamespaces && LOG.isInfoEnabled()) {
                  LOG.info("Creating new XPath expression in pool. Namespaces on XPath expression: {}", getNamespaceContext());
              }
              xPath.setNamespaceContext(getNamespaceContext());
              xPath.setXPathVariableResolver(getVariableResolver());
  
              XPathFunctionResolver parentResolver = getFunctionResolver();
              if (parentResolver == null) {
                  parentResolver = xPath.getXPathFunctionResolver();
              }
              xPath.setXPathFunctionResolver(createDefaultFunctionResolver(parentResolver));
              return xPath.compile(text);
          } finally {
              lock.unlock();
          }
      }
  
      protected XPathExpression createTraceNamespaceExpression()
              throws XPathExpressionException {
          lock.lock();
          try {
              // XPathFactory is not thread safe
              XPath xPath = getXPathFactory().newXPath();
              return xPath.compile(OBTAIN_ALL_NS_XPATH);
          } finally {
              lock.unlock();
          }
      }
  
      protected DefaultNamespaceContext createNamespaceContext(XPathFactory factory) {
          DefaultNamespaceContext context = new DefaultNamespaceContext(factory);
          populateDefaultNamespaces(context);
          return context;
      }
  
      /**
       * Populate a number of standard prefixes if they are not already there
       */
      protected void populateDefaultNamespaces(DefaultNamespaceContext context) {
          setNamespaceIfNotPresent(context, "in", IN_NAMESPACE);
          setNamespaceIfNotPresent(context, "out", OUT_NAMESPACE);
          setNamespaceIfNotPresent(context, "env", Namespaces.ENVIRONMENT_VARIABLES);
          setNamespaceIfNotPresent(context, "system", Namespaces.SYSTEM_PROPERTIES_NAMESPACE);
          setNamespaceIfNotPresent(context, "function", Namespaces.FUNCTION_NAMESPACE);
      }
  
      protected void setNamespaceIfNotPresent(DefaultNamespaceContext context, String prefix, String uri) {
          if (context != null) {
              String current = context.getNamespaceURI(prefix);
              if (current == null) {
                  context.add(prefix, uri);
              }
          }
      }
  
      protected XPathFunctionResolver createDefaultFunctionResolver(final XPathFunctionResolver parent) {
          return new XPathFunctionResolver() {
              public XPathFunction resolveFunction(QName qName, int argumentCount) {
                  XPathFunction answer = null;
                  if (parent != null) {
                      answer = parent.resolveFunction(qName, argumentCount);
                  }
                  if (answer == null) {
                      if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), IN_NAMESPACE)
                              || isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), DEFAULT_NAMESPACE)) {
                          String localPart = qName.getLocalPart();
                          if (localPart.equals("body") && argumentCount == 0) {
                              return getBodyFunction();
                          }
                          if (localPart.equals("header") && argumentCount == 1) {
                              return getHeaderFunction();
                          }
                      }
                      if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), OUT_NAMESPACE)) {
                          String localPart = qName.getLocalPart();
                          if (localPart.equals("body") && argumentCount == 0) {
                              return getOutBodyFunction();
                          }
                          if (localPart.equals("header") && argumentCount == 1) {
                              return getOutHeaderFunction();
                          }
                      }
                      if (isMatchingNamespaceOrEmptyNamespace(qName.getNamespaceURI(), FUNCTION_NAMESPACE)) {
                          String localPart = qName.getLocalPart();
                          if (localPart.equals("properties") && argumentCount == 1) {
                              return getPropertiesFunction();
                          }
                          if (localPart.equals("simple") && argumentCount == 1) {
                              return getSimpleFunction();
                          }
                      }
                  }
                  return answer;
              }
          };
      }
  
      /**
       * Checks whether we need an {@link InputStream} to access the message body.
       * <p/>
       * Depending on the content in the message body, we may not need to convert to {@link InputStream}.
       *
       * @param  exchange the current exchange
       * @return          <tt>true</tt> to convert to {@link InputStream} beforehand converting afterwards.
       */
      protected boolean isInputStreamNeeded(Exchange exchange) {
          Object body = exchange.getIn().getBody();
          return isInputStreamNeededForObject(body);
      }
  
      /**
       * Checks whether we need an {@link InputStream} to access this object
       * <p/>
       * Depending on the content in the object, we may not need to convert to {@link InputStream}.
       *
       * @param  obj the object
       * @return     <tt>true</tt> to convert to {@link InputStream} beforehand converting afterwards.
       */
      protected boolean isInputStreamNeededForObject(Object obj) {
          if (obj == null) {
              return false;
          }
  
          if (obj instanceof WrappedFile) {
              obj = ((WrappedFile<?>) obj).getFile();
          }
          if (obj instanceof File) {
              // input stream is needed for File to avoid locking the file in case
              // of errors etc
              return true;
          }
  
          // input stream is not needed otherwise
          return false;
      }
  
      /**
       * Strategy method to extract the document from the exchange.
       */
      protected Object getDocument(Exchange exchange, Object body) {
          try {
              return doGetDocument(exchange, body);
          } catch (Exception e) {
              throw RuntimeCamelException.wrapRuntimeCamelException(e);
          } finally {
              // call the reset if the in message body is StreamCache
              MessageHelper.resetStreamCache(exchange.getIn());
          }
      }
  
      protected Object doGetDocument(Exchange exchange, Object body) throws Exception {
          if (body == null) {
              return null;
          }
  
          Object answer = null;
  
          Class<?> type = getDocumentType();
          Exception cause = null;
          if (type != null) {
              // try to get the body as the desired type
              try {
                  answer = exchange.getContext().getTypeConverter().convertTo(type, exchange, body);
              } catch (Exception e) {
                  // we want to store the caused exception, if we could not
                  // convert
                  cause = e;
              }
          }
  
          if (type == null && answer == null) {
              // fallback to get the body as is
              answer = body;
          } else if (answer == null) {
              // there was a type, and we could not convert to it, then fail
              if (cause != null) {
                  throw cause;
              } else {
                  throw new NoTypeConversionAvailableException(body, type);
              }
          }
  
          return answer;
      }
  
      private MessageVariableResolver getVariableResolver() {
          return variableResolver;
      }
  
      @Override
      public void doStart() throws Exception {
          if (xpathFactory == null) {
              xpathFactory = createXPathFactory();
          }
          if (namespaceContext == null) {
              namespaceContext = createNamespaceContext(xpathFactory);
          }
          for (Map.Entry<String, String> entry : namespaces.entrySet()) {
              namespaceContext.add(entry.getKey(), entry.getValue());
          }
  
          // create default functions if no custom assigned
          if (bodyFunction == null) {
              bodyFunction = createBodyFunction();
          }
          if (headerFunction == null) {
              headerFunction = createHeaderFunction();
          }
          if (outBodyFunction == null) {
              outBodyFunction = createOutBodyFunction();
          }
          if (outHeaderFunction == null) {
              outHeaderFunction = createOutHeaderFunction();
          }
          if (propertiesFunction == null) {
              propertiesFunction = createPropertiesFunction();
          }
          if (simpleFunction == null) {
              simpleFunction = createSimpleFunction();
          }
      }
  
      @Override
      public void doStop() throws Exception {
          pool.clear();
          poolLogNamespaces.clear();
      }
  
      protected XPathFactory createXPathFactory() throws XPathFactoryConfigurationException {
          lock.lock();
          try {
              if (objectModelUri != null) {
                  String xpathFactoryClassName = factoryClassName;
                  if (objectModelUri.equals(SAXON_OBJECT_MODEL_URI)
                          && (xpathFactoryClassName == null || SAXON_FACTORY_CLASS_NAME.equals(xpathFactoryClassName))) {
                      // from Saxon 9.7 onwards you should favour to create the class
                      // directly
                      // https://www.saxonica.com/html/documentation/xpath-api/jaxp-xpath/factory.html
                      try {
                          if (camelContext != null) {
                              Class<XPathFactory> clazz
                                      = camelContext.getClassResolver().resolveClass(SAXON_FACTORY_CLASS_NAME,
                                              XPathFactory.class);
                              if (clazz != null) {
                                  LOG.debug("Creating Saxon XPathFactory using class: {})", clazz);
                                  xpathFactory = camelContext.getInjector().newInstance(clazz);
                                  LOG.info("Created Saxon XPathFactory: {}", xpathFactory);
                              }
                          }
                      } catch (Exception e) {
                          LOG.warn("Attempted to create Saxon XPathFactory by creating a new instance of {}" +
                                   " failed. Will fallback and create XPathFactory using JDK API. This exception is ignored (stacktrace in DEBUG logging level).",
                                  SAXON_FACTORY_CLASS_NAME);
                          LOG.debug("Error creating Saxon XPathFactory. This exception is ignored.", e);
                      }
                  }
  
                  if (xpathFactory == null) {
                      LOG.debug("Creating XPathFactory from objectModelUri: {}", objectModelUri);
                      xpathFactory = ObjectHelper.isEmpty(xpathFactoryClassName)
                              ? XPathFactory.newInstance(objectModelUri)
                              : XPathFactory.newInstance(objectModelUri, xpathFactoryClassName, null);
                      LOG.info("Created XPathFactory: {} from objectModelUri: {}", xpathFactory, objectModelUri);
                  }
  
                  return xpathFactory;
              }
  
              if (defaultXPathFactory == null) {
                  defaultXPathFactory = createDefaultXPathFactory();
              }
              return defaultXPathFactory;
          } finally {
              lock.unlock();
          }
      }
  
      protected static XPathFactory createDefaultXPathFactory() throws XPathFactoryConfigurationException {
          XPathFactory factory = null;
  
          // read system property and see if there is a factory set
          Properties properties = System.getProperties();
          for (Map.Entry<Object, Object> prop : properties.entrySet()) {
              String key = (String) prop.getKey();
              if (key.startsWith(XPathFactory.DEFAULT_PROPERTY_NAME)) {
                  String uri = StringHelper.after(key, ":");
                  if (uri != null) {
                      factory = XPathFactory.newInstance(uri);
                      LOG.info("Using system property {} with value {} when created default XPathFactory {}", key, uri, factory);
                  }
              }
          }
  
          if (factory == null) {
              factory = XPathFactory.newInstance();
              LOG.info("Created default XPathFactory {}", factory);
          }
  
          return factory;
      }
  
  }
  