gt:             registerFunction("", "contains", new Contains());
context: /*
   * Copyright (C) 2005-2010 Alfresco Software Limited.
   *
   * This file is part of Alfresco
   *
   * Alfresco is free software: you can redistribute it and/or modify
   * it under the terms of the GNU Lesser General Public License as published by
   * the Free Software Foundation, either version 3 of the License, or
   * (at your option) any later version.
   *
   * Alfresco is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU Lesser General Public License for more details.
   *
   * You should have received a copy of the GNU Lesser General Public License
   * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
   */
  package org.alfresco.repo.search;
  
  import java.util.ArrayList;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Set;
  import java.util.StringTokenizer;
  
  import org.alfresco.error.AlfrescoRuntimeException;
  import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
  import org.alfresco.service.cmr.repository.ChildAssociationRef;
  import org.alfresco.service.cmr.repository.NodeRef;
  import org.alfresco.service.cmr.search.QueryParameterDefinition;
  import org.alfresco.service.cmr.search.SearchParameters;
  import org.alfresco.service.namespace.NamespacePrefixResolver;
  import org.alfresco.service.namespace.QName;
  import org.alfresco.service.namespace.QNamePattern;
  import org.alfresco.util.ISO9075;
  import org.apache.commons.logging.Log;
  import org.apache.commons.logging.LogFactory;
  import org.jaxen.BaseXPath;
  import org.jaxen.Context;
  import org.jaxen.Function;
  import org.jaxen.FunctionCallException;
  import org.jaxen.FunctionContext;
  import org.jaxen.JaxenException;
  import org.jaxen.Navigator;
  import org.jaxen.SimpleFunctionContext;
  import org.jaxen.SimpleVariableContext;
  import org.jaxen.function.BooleanFunction;
  import org.jaxen.function.CeilingFunction;
  import org.jaxen.function.ConcatFunction;
  import org.jaxen.function.ContainsFunction;
  import org.jaxen.function.CountFunction;
  import org.jaxen.function.FalseFunction;
  import org.jaxen.function.FloorFunction;
  import org.jaxen.function.IdFunction;
  import org.jaxen.function.LangFunction;
  import org.jaxen.function.LastFunction;
  import org.jaxen.function.LocalNameFunction;
  import org.jaxen.function.NameFunction;
  import org.jaxen.function.NamespaceUriFunction;
  import org.jaxen.function.NormalizeSpaceFunction;
  import org.jaxen.function.NotFunction;
  import org.jaxen.function.NumberFunction;
  import org.jaxen.function.PositionFunction;
  import org.jaxen.function.RoundFunction;
  import org.jaxen.function.StartsWithFunction;
  import org.jaxen.function.StringFunction;
  import org.jaxen.function.StringLengthFunction;
  import org.jaxen.function.SubstringAfterFunction;
  import org.jaxen.function.SubstringBeforeFunction;
  import org.jaxen.function.SubstringFunction;
  import org.jaxen.function.SumFunction;
  import org.jaxen.function.TranslateFunction;
  import org.jaxen.function.TrueFunction;
  import org.jaxen.function.ext.EndsWithFunction;
  import org.jaxen.function.ext.EvaluateFunction;
  import org.jaxen.function.ext.LowerFunction;
  import org.jaxen.function.ext.UpperFunction;
  import org.jaxen.function.xslt.DocumentFunction;
  
  /**
   * Represents an xpath statement that resolves against a
   * <code>NodeService</code>
   * 
   * @author Andy Hind
   */
  public class NodeServiceXPath extends BaseXPath
  {
      private static final long serialVersionUID = 3834032441789592882L;
  
      private static String JCR_URI = "http://www.jcp.org/jcr/1.0";
      
      private static Log logger = LogFactory.getLog(NodeServiceXPath.class);
  
      /**
       * 
       * @param xpath
       *            the xpath statement
       * @param documentNavigator
       *            the navigator that will allow the xpath to be resolved
       * @param paramDefs
       *            parameters to resolve variables required by xpath
       * @throws JaxenException
       */
      public NodeServiceXPath(String xpath, DocumentNavigator documentNavigator, QueryParameterDefinition[] paramDefs)
              throws JaxenException
      {
          super(xpath, documentNavigator);
  
          if (logger.isDebugEnabled())
          {
              StringBuilder sb = new StringBuilder();
              sb.append("Created XPath: \n")
                .append("   XPath: ").append(xpath).append("\n")
                .append("   Parameters: \n");
              for (int i = 0; paramDefs != null && i < paramDefs.length; i++)
              {
                  sb.append("      Parameter: \n")
                    .append("         name: ").append(paramDefs[i].getQName()).append("\n")
                    .append("         value: ").append(paramDefs[i].getDefault()).append("\n");
              }
              logger.debug(sb.toString());
          }
          
          // Add support for parameters
          if (paramDefs != null)
          {
              SimpleVariableContext svc = (SimpleVariableContext) this.getVariableContext();
              for (int i = 0; i < paramDefs.length; i++)
              {
                  if (!paramDefs[i].hasDefaultValue())
                  {
                      throw new AlfrescoRuntimeException("Parameter must have default value");
                  }
                  Object value = null;
                  if (paramDefs[i].getDataTypeDefinition().getName().equals(DataTypeDefinition.BOOLEAN))
                  {
                      value = Boolean.valueOf(paramDefs[i].getDefault());
                  }
                  else if (paramDefs[i].getDataTypeDefinition().getName().equals(DataTypeDefinition.DOUBLE))
                  {
                      value = Double.valueOf(paramDefs[i].getDefault());
                  }
                  else if (paramDefs[i].getDataTypeDefinition().getName().equals(DataTypeDefinition.FLOAT))
                  {
                      value = Float.valueOf(paramDefs[i].getDefault());
                  }
                  else if (paramDefs[i].getDataTypeDefinition().getName().equals(DataTypeDefinition.INT))
                  {
                      value = Integer.valueOf(paramDefs[i].getDefault());
                  }
                  else if (paramDefs[i].getDataTypeDefinition().getName().equals(DataTypeDefinition.LONG))
                  {
                      value = Long.valueOf(paramDefs[i].getDefault());
                  }
                  else
                  {
                      value = paramDefs[i].getDefault();
                  }
                  svc.setVariableValue(paramDefs[i].getQName().getNamespaceURI(), paramDefs[i].getQName().getLocalName(),
                          value);
              }
          }
  
          for (String prefix : documentNavigator.getNamespacePrefixResolver().getPrefixes())
          {
              addNamespace(prefix, documentNavigator.getNamespacePrefixResolver().getNamespaceURI(prefix));
          }
      }
      
      /**
       * Jaxen has some magic with its IdentitySet, which means that we can get different results
       * depending on whether we cache {@link ChildAssociationRef } instances or not.
       * <p>
       * So, duplicates are eliminated here before the results are returned.
       */
      @SuppressWarnings("unchecked")
      @Override
      public List selectNodes(Object arg0) throws JaxenException
      {
          if (logger.isDebugEnabled())
          {
              logger.debug("Selecting using XPath: \n" +
                      "   XPath: " + this + "\n" +
                      "   starting at: " + arg0);
          }
          
          List<Object> resultsWithDuplicates = super.selectNodes(arg0);
          
          Set<Object> set = new HashSet<Object>(resultsWithDuplicates);
          
          // now return as a list again
          List<Object> results = resultsWithDuplicates;
          results.clear();
          results.addAll(set);
          
          // done
          return results;
      }
  
      public static class FirstFunction implements Function
      {
  
          public Object call(Context context, List args) throws FunctionCallException
          {
              if (args.size() == 0)
              {
                  return evaluate(context);
              }
  
              throw new FunctionCallException("first() requires no arguments.");
          }
  
          public static Double evaluate(Context context)
          {
              return new Double(1);
          }
      }
  
      /**
       * A boolean function to determine if a node type is a subtype of another type
       */
      static class SubTypeOf implements Function
      {
          public Object call(Context context, List args) throws FunctionCallException
          {
              if (args.size() != 1)
              {
                  throw new FunctionCallException("subtypeOf() requires one argument: subtypeOf(QName typeQName)");
              }
              return evaluate(context.getNodeSet(), args.get(0), context.getNavigator());
          }
  
          public Object evaluate(List nodes, Object qnameObj, Navigator nav)
          {
              if (nodes.size() != 1)
              {
                  return false;
              }
              // resolve the qname of the type we are checking for
              String qnameStr = StringFunction.evaluate(qnameObj, nav);
              if (qnameStr.equals("*"))
              {
                  return true;
              }
              QName typeQName;
  
              if (qnameStr.startsWith("{"))
              {
                  typeQName = QName.createQName(qnameStr);
              }
              else
              {
                  typeQName = QName.createQName(qnameStr, ((DocumentNavigator) nav).getNamespacePrefixResolver());
              }
              // resolve the noderef
              NodeRef nodeRef = null;
              if (nav.isElement(nodes.get(0)))
              {
                  nodeRef = ((ChildAssociationRef) nodes.get(0)).getChildRef();
              }
              else if (nav.isAttribute(nodes.get(0)))
              {
                  nodeRef = ((DocumentNavigator.Property) nodes.get(0)).parent;
              }
  
              DocumentNavigator dNav = (DocumentNavigator) nav;
              boolean result = dNav.isSubtypeOf(nodeRef, typeQName);
              return result;
          }
      }
  
      /**
       * A boolean function to determine if a node has a given aspect
       */
      static class HasAspect implements Function
      {
          public Object call(Context context, List args) throws FunctionCallException
          {
              if (args.size() != 1)
              {
                  throw new FunctionCallException("hasAspect() requires one argument: hasAspect(QName typeQName)");
              }
              return evaluate(context.getNodeSet(), args.get(0), context.getNavigator());
          }
  
          public Object evaluate(List nodes, Object qnameObj, Navigator nav)
          {
              if (nodes.size() != 1)
              {
                  return false;
              }
              // resolve the qname of the type we are checking for
              String qnameStr = StringFunction.evaluate(qnameObj, nav);
              if (qnameStr.equals("*"))
              {
                  return true;
              }
              QName typeQName;
  
              if (qnameStr.startsWith("{"))
              {
                  typeQName = QName.createQName(qnameStr);
              }
              else
              {
                  typeQName = QName.createQName(qnameStr, ((DocumentNavigator) nav).getNamespacePrefixResolver());
              }
              // resolve the noderef
              NodeRef nodeRef = null;
              if (nav.isElement(nodes.get(0)))
              {
                  nodeRef = ((ChildAssociationRef) nodes.get(0)).getChildRef();
              }
              else if (nav.isAttribute(nodes.get(0)))
              {
                  nodeRef = ((DocumentNavigator.Property) nodes.get(0)).parent;
              }
  
              DocumentNavigator dNav = (DocumentNavigator) nav;
              boolean result = dNav.hasAspect(nodeRef, typeQName);
              return result;
          }
      }
  
      static class Deref implements Function
      {
  
          public Object call(Context context, List args) throws FunctionCallException
          {
              if (args.size() == 2)
              {
                  return evaluate(args.get(0), args.get(1), context.getNavigator());
              }
  
              throw new FunctionCallException("deref() requires two arguments.");
          }
  
          public Object evaluate(Object attributeName, Object pattern, Navigator nav)
          {
              List<Object> answer = new ArrayList<Object>();
              String attributeValue = StringFunction.evaluate(attributeName, nav);
              String patternValue = StringFunction.evaluate(pattern, nav);
  
              // TODO: Ignore the pattern for now
              // Should do a type pattern test
              if ((attributeValue != null) && (attributeValue.length() > 0))
              {
                  DocumentNavigator dNav = (DocumentNavigator) nav;
                  NodeRef nodeRef = new NodeRef(attributeValue);
                  if (patternValue.equals("*"))
                  {
                      answer.add(dNav.getNode(nodeRef));
                  }
                  else
                  {
                      QNamePattern qNamePattern = new JCRPatternMatch(patternValue, dNav.getNamespacePrefixResolver());
                      answer.addAll(dNav.getNode(nodeRef, qNamePattern));
                  }
  
              }
              return answer;
  
          }
      }
  
      /**
       * A boolean function to determine if a node property matches a pattern
       * and/or the node text matches the pattern.
       * <p>
       * The default is JSR170 compliant. The optional boolean allows searching
       * only against the property value itself.
       * <p>
       * The search is always case-insensitive.
       * 
       * @author Derek Hulley
       */
      static class Like implements Function
      {
          public Object call(Context context, List args) throws FunctionCallException
          {
              if (args.size() < 2 || args.size() > 3)
              {
                  throw new FunctionCallException("like() usage: like(@attr, 'pattern' [, includeFTS]) \n"
                          + " - includeFTS can be 'true' or 'false' \n"
                          + " - search is case-insensitive");
              }
              // default includeFTS to true
              return evaluate(context.getNodeSet(), args.get(0), args.get(1), args.size() == 2 ? Boolean.toString(true)
                      : args.get(2), context.getNavigator());
          }
  
          public Object evaluate(List nodes, Object obj, Object patternObj, Object includeFtsObj, Navigator nav)
          {
              Object attribute = null;
              if (obj instanceof List)
              {
                  List list = (List) obj;
                  if (list.isEmpty())
                  {
                      return false;
                  }
                  // do not recurse: only first list should unwrap
                  attribute = list.get(0);
              }
              if ((attribute == null) || !nav.isAttribute(attribute))
              {
                  return false;
              }
              if (nodes.size() != 1)
              {
                  return false;
              }
              if (!nav.isElement(nodes.get(0)))
              {
                  return false;
              }
              ChildAssociationRef car = (ChildAssociationRef) nodes.get(0);
              String pattern = StringFunction.evaluate(patternObj, nav);
              boolean includeFts = BooleanFunction.evaluate(includeFtsObj, nav);
              QName qname = QName.createQName(nav.getAttributeNamespaceUri(attribute), ISO9075.decode(nav
                      .getAttributeName(attribute)));
  
              DocumentNavigator dNav = (DocumentNavigator) nav;
              // JSR 170 includes full text matches
              return dNav.like(car.getChildRef(), qname, pattern, includeFts);
  
          }
      }
  
      static class Contains implements Function
      {
  
          public Object call(Context context, List args) throws FunctionCallException
          {
              if (args.size() != 1)
              {
                  throw new FunctionCallException("contains() usage: contains('pattern')");
              }
              return evaluate(context.getNodeSet(), args.get(0), context.getNavigator());
          }
  
          public Object evaluate(List nodes, Object pattern, Navigator nav)
          {
              if (nodes.size() != 1)
              {
                  return false;
              }
              QName qname = null;
              NodeRef nodeRef = null;
              if (nav.isElement(nodes.get(0)))
              {
                  qname = null; // should use all attributes and full text index
                  nodeRef = ((ChildAssociationRef) nodes.get(0)).getChildRef();
              }
              else if (nav.isAttribute(nodes.get(0)))
              {
                  qname = QName.createQName(
                          nav.getAttributeNamespaceUri(nodes.get(0)),
                          ISO9075.decode(nav.getAttributeName(nodes.get(0))));
                  nodeRef = ((DocumentNavigator.Property) nodes.get(0)).parent;
              }
  
              String patternValue = StringFunction.evaluate(pattern, nav);
              DocumentNavigator dNav = (DocumentNavigator) nav;
  
              return dNav.contains(nodeRef, qname, patternValue, SearchParameters.OR);
  
          }
      }
  
      static class JCRContains implements Function
      {
  
          public Object call(Context context, List args) throws FunctionCallException
          {
              if (args.size() == 2)
              {
                  if (context.getNavigator().isAttribute(context.getNodeSet().get(0)))
                  {
                      throw new FunctionCallException("jcr:contains() does not apply to an attribute context.");
                  }
                  return evaluate(context.getNodeSet(), args.get(0), args.get(1), context.getNavigator());
              }
  
              throw new FunctionCallException("contains() requires two argument.");
          }
  
          public Object evaluate(List nodes, Object identifier, Object pattern, Navigator nav)
          {
              if (nodes.size() != 1)
              {
                  return false;
              }
  
              QName qname = null;
              NodeRef nodeRef = null;
  
              Object target = identifier;
  
              if (identifier instanceof List)
              {
                  List list = (List) identifier;
                  if (list.isEmpty())
                  {
                      return false;
                  }
                  // do not recurse: only first list should unwrap
                  target = list.get(0);
              }
  
              if (nav.isElement(target))
              {
                  qname = null; // should use all attributes and full text index
                  nodeRef = ((ChildAssociationRef) target).getChildRef();
              }
              else if (nav.isAttribute(target))
              {
                  qname = QName.createQName(
                          nav.getAttributeNamespaceUri(target),
                          ISO9075.decode(nav.getAttributeName(target)));
                  nodeRef = ((DocumentNavigator.Property) target).parent;
              }
  
              String patternValue = StringFunction.evaluate(pattern, nav);
              DocumentNavigator dNav = (DocumentNavigator) nav;
  
              return dNav.contains(nodeRef, qname, patternValue, SearchParameters.AND);
  
          }
      }
  
      static class Score implements Function
      {
          private Double one = new Double(1);
  
          public Object call(Context context, List args) throws FunctionCallException
          {
              return evaluate(context.getNodeSet(), context.getNavigator());
          }
  
          public Object evaluate(List nodes, Navigator nav)
          {
              return one;
  
          }
      }
  
      protected FunctionContext createFunctionContext()
      {
          return XPathFunctionContext.getInstance();
      }
  
      public static class XPathFunctionContext extends SimpleFunctionContext
      {
          /**
           * Singleton implementation.
           */
          private static class Singleton
          {
              /**
               * Singleton instance.
               */
              private static XPathFunctionContext instance = new XPathFunctionContext();
          }
  
          /**
           * Retrieve the singleton instance.
           * 
           * @return the singleton instance
           */
          public static FunctionContext getInstance()
          {
              return Singleton.instance;
          }
  
          /**
           * Construct.
           * 
           * <p>
           * Construct with all core XPath functions registered.
           * </p>
           */
          public XPathFunctionContext()
          {
              // XXX could this be a HotSpot????
              registerFunction("", // namespace URI
                      "boolean", new BooleanFunction());
  
              registerFunction("", // namespace URI
                      "ceiling", new CeilingFunction());
  
              registerFunction("", // namespace URI
                      "concat", new ConcatFunction());
  
              registerFunction("", // namespace URI
                      "contains", new ContainsFunction());
  
              registerFunction("", // namespace URI
                      "count", new CountFunction());
  
              registerFunction("", // namespace URI
                      "document", new DocumentFunction());
              
              registerFunction("", // namespace URI
                      "ends-with", new EndsWithFunction());
  
              registerFunction("", // namespace URI
                      "false", new FalseFunction());
  
              registerFunction("", // namespace URI
                      "floor", new FloorFunction());
  
              registerFunction("", // namespace URI
                      "id", new IdFunction());
  
              registerFunction("", // namespace URI
                      "lang", new LangFunction());
  
              registerFunction("", // namespace URI
                      "last", new LastFunction());
  
              registerFunction("", // namespace URI
                      "local-name", new LocalNameFunction());
  
              registerFunction("", // namespace URI
                      "name", new NameFunction());
  
              registerFunction("", // namespace URI
                      "namespace-uri", new NamespaceUriFunction());
  
              registerFunction("", // namespace URI
                      "normalize-space", new NormalizeSpaceFunction());
  
              registerFunction("", // namespace URI
                      "not", new NotFunction());
  
              registerFunction("", // namespace URI
                      "number", new NumberFunction());
  
              registerFunction("", // namespace URI
                      "position", new PositionFunction());
  
              registerFunction("", // namespace URI
                      "round", new RoundFunction());
  
              registerFunction("", // namespace URI
                      "starts-with", new StartsWithFunction());
  
              registerFunction("", // namespace URI
                      "string", new StringFunction());
  
              registerFunction("", // namespace URI
                      "string-length", new StringLengthFunction());
  
              registerFunction("", // namespace URI
                      "substring-after", new SubstringAfterFunction());
  
              registerFunction("", // namespace URI
                      "substring-before", new SubstringBeforeFunction());
  
              registerFunction("", // namespace URI
                      "substring", new SubstringFunction());
  
              registerFunction("", // namespace URI
                      "sum", new SumFunction());
  
              registerFunction("", // namespace URI
                      "true", new TrueFunction());
  
              registerFunction("", // namespace URI
                      "translate", new TranslateFunction());
  
              // register extension functions
              // extension functions should go into a namespace, but which one?
              // for now, keep them in default namespace to not break any code
  
              registerFunction("", // namespace URI
                      "evaluate", new EvaluateFunction());
  
              registerFunction("", // namespace URI
                      "lower-case", new LowerFunction());
  
              registerFunction("", // namespace URI
                      "upper-case", new UpperFunction());
  
              registerFunction("", // namespace URI
                      "ends-with", new EndsWithFunction());
  
              registerFunction("", "subtypeOf", new SubTypeOf());
              registerFunction("", "hasAspect", new HasAspect());
              registerFunction("", "deref", new Deref());
              registerFunction("", "like", new Like());
  