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.
   *
   * Contributors:
   *     Jens Huebel
   *     Florent Guillaume, Nuxeo
   */
  package org.apache.chemistry.opencmis.inmemory.query;
  
  import java.io.UnsupportedEncodingException;
  import java.math.BigDecimal;
  import java.math.BigInteger;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.GregorianCalendar;
  import java.util.List;
  import java.util.Map;
  import java.util.regex.Pattern;
  
  import org.antlr.runtime.tree.Tree;
  import org.apache.chemistry.opencmis.commons.data.ObjectData;
  import org.apache.chemistry.opencmis.commons.data.ObjectList;
  import org.apache.chemistry.opencmis.commons.data.PropertyData;
  import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition;
  import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
  import org.apache.chemistry.opencmis.commons.enums.Cardinality;
  import org.apache.chemistry.opencmis.commons.enums.CmisVersion;
  import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
  import org.apache.chemistry.opencmis.commons.enums.PropertyType;
  import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
  import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
  import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectListImpl;
  import org.apache.chemistry.opencmis.commons.server.CallContext;
  import org.apache.chemistry.opencmis.inmemory.storedobj.api.Content;
  import org.apache.chemistry.opencmis.inmemory.storedobj.api.DocumentVersion;
  import org.apache.chemistry.opencmis.inmemory.storedobj.api.Filing;
  import org.apache.chemistry.opencmis.inmemory.storedobj.api.Folder;
  import org.apache.chemistry.opencmis.inmemory.storedobj.api.ObjectStore;
  import org.apache.chemistry.opencmis.inmemory.storedobj.api.StoredObject;
  import org.apache.chemistry.opencmis.inmemory.storedobj.api.VersionedDocument;
  import org.apache.chemistry.opencmis.inmemory.storedobj.impl.ContentStreamDataImpl;
  import org.apache.chemistry.opencmis.inmemory.storedobj.impl.ObjectStoreImpl;
  import org.apache.chemistry.opencmis.inmemory.types.PropertyCreationHelper;
  import org.apache.chemistry.opencmis.server.support.TypeManager;
  import org.apache.chemistry.opencmis.server.support.query.AbstractPredicateWalker;
  import org.apache.chemistry.opencmis.server.support.query.CmisQueryWalker;
  import org.apache.chemistry.opencmis.server.support.query.CmisSelector;
  import org.apache.chemistry.opencmis.server.support.query.ColumnReference;
  import org.apache.chemistry.opencmis.server.support.query.QueryObject;
  import org.apache.chemistry.opencmis.server.support.query.QueryObject.JoinSpec;
  import org.apache.chemistry.opencmis.server.support.query.QueryObject.SortSpec;
  import org.apache.chemistry.opencmis.server.support.query.QueryUtilStrict;
  import org.apache.chemistry.opencmis.server.support.query.StringUtil;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  /**
   * A processor for a CMIS query for the In-Memory server. During tree traversal
   * conditions are checked against the data contained in the central hash map
   * with all objects. In a first pass one time setup is performed, in a custom
   * walk across the query expression tree an object is checked if it matches. In
   * case of a match it is appended to a list of matching objects.
   */
  public class InMemoryQueryProcessor {
  
      private static final Logger LOG = LoggerFactory.getLogger(InMemoryQueryProcessor.class);
  
      private List<StoredObject> matches = new ArrayList<StoredObject>();
      private QueryObject queryObj;
      private Tree whereTree;
      private ObjectStoreImpl objStore;
      private List<TypeDefinition> secondaryTypeIds;
      private CallContext callContext;
      private boolean relaxedParserMode;
  
      public InMemoryQueryProcessor(ObjectStoreImpl objStore, CallContext ctx, boolean relaxedParserMode) {
          this.objStore = objStore;
          this.callContext = ctx;
          this.relaxedParserMode = relaxedParserMode;
      }
  
      /**
       * Main entry function to process a query from discovery service.
       * 
       * @param tm
       *            type manager for the given repository
       * @param objectStore
       *            object store to gets object from
       * @param user
       *            user execuing the query
       * @param repositoryId
       *            id of repository
       * @param statement
       *            query statement
       * @param searchAllVersions
       *            search in all versions of objects
       * @param includeAllowableActions
       *            include allowable actions
       * @param includeRelationships
       *            include relationships
       * @param renditionFilter
       *            include renditions
       * @param maxItems
       *            max number of items to return
       * @param skipCount
       *            items to skip
       * @return list of objects matching the query
       */
      public ObjectList query(TypeManager tm, ObjectStore objectStore, String user, String repositoryId,
              String statement, Boolean searchAllVersions, Boolean includeAllowableActions,
              IncludeRelationships includeRelationships, String renditionFilter, BigInteger maxItems, BigInteger skipCount) {
  
          processQueryAndCatchExc(statement, tm); // calls query processor
  
          // iterate over all the objects and check for each if the query matches
          for (String objectId : ((ObjectStoreImpl) objectStore).getIds()) {
              StoredObject so = objectStore.getObjectById(objectId);
              match(so, user, searchAllVersions == null ? true : searchAllVersions.booleanValue());
          }
  
          ObjectList objList = buildResultList(tm, user, includeAllowableActions, includeRelationships, renditionFilter,
                  maxItems, skipCount);
          LOG.debug("Query result, number of matching objects: " + objList.getNumItems());
          return objList;
      }
  
      /**
       * Process a query.
       * 
       * @param statement
       *            CMISQL statement to execute
       * @param tm
       *            type manager for the repository
       */
      public void processQueryAndCatchExc(String statement, TypeManager tm) {
          QueryUtilStrict queryUtil;
          if (relaxedParserMode) {
          	queryUtil = new QueryUtilStrict(statement, tm, null, true, QueryObject.ParserMode.MODE_ALLOW_RELAXED_SELECT);
          } else {
          	queryUtil = new QueryUtilStrict(statement, tm, null);
          }
          queryUtil.processStatementUsingCmisExceptions();
          CmisQueryWalker walker = queryUtil.getWalker();
          queryObj = queryUtil.getQueryObject();
          whereTree = walker.getWherePredicateTree();
          secondaryTypeIds = queryObj.getJoinedSecondaryTypes();
          doAdditionalChecks(walker);
      }
  
      /**
       * Create the list of matching objects for this query.
       * 
       * @param tm
       *            type manager for the given repository
       * @param user
       *            user execuing the query
       * @param includeAllowableActions
       *            include allowable actions
       * @param includeRelationships
       *            include relationships
       * @param renditionFilter
       *            include renditions
       * @param maxItems
       *            max number of items to return
       * @param skipCount
       *            items to skip
       * @return list of objects matching the query
       */
      public ObjectList buildResultList(TypeManager tm, String user, Boolean includeAllowableActions,
              IncludeRelationships includeRelationships, String renditionFilter, BigInteger maxItems, BigInteger skipCount) {
  
          sortMatches();
  
          ObjectListImpl res = new ObjectListImpl();
          res.setNumItems(BigInteger.valueOf(matches.size()));
          int start = 0;
          if (skipCount != null) {
              start = (int) skipCount.longValue();
          }
          if (start < 0) {
              start = 0;
          }
          if (start > matches.size()) {
              start = matches.size();
          }
          int stop = 0;
          if (maxItems != null) {
              stop = start + (int) maxItems.longValue();
          }
          if (stop <= 0 || stop > matches.size()) {
              stop = matches.size();
          }
          res.setHasMoreItems(stop < matches.size());
          if (start > 0 || stop > 0) {
              matches = matches.subList(start, stop);
          }
  
          List<ObjectData> objDataList = new ArrayList<ObjectData>();
          Map<String, String> props = queryObj.getRequestedPropertiesByAlias();
          Map<String, String> funcs = queryObj.getRequestedFuncsByAlias();
  
          for (StoredObject so : matches) {
              String queryName = queryObj.getTypes().values().iterator().next();
              TypeDefinition td = queryObj.getTypeDefinitionFromQueryName(queryName);
  
              ObjectData od = PropertyCreationHelper.getObjectDataQueryResult(callContext,tm, objStore, td, so, user, props, funcs,
                      secondaryTypeIds, includeAllowableActions, includeRelationships, renditionFilter);
              objDataList.add(od);
          }
          res.setObjects(objDataList);
          return res;
      }
  
      private boolean typeMatches(TypeDefinition td, StoredObject so) {
          String typeId = so.getTypeId();
          while (typeId != null) {
              if (typeId.equals(td.getId())) {
                  return true;
              }
              // check secondary types
              List<String> secTypeIds = so.getSecondaryTypeIds();
              for (String secTypeId : secTypeIds) {
                  if (secTypeId.equals(td.getId())) {
                      return true;
                  }
              }
              // check parent type
              TypeDefinition parentTD = queryObj.getParentType(typeId);
              typeId = parentTD == null ? null : parentTD.getId();
          }
          return false;
      }
  
      private void sortMatches() {
          final List<SortSpec> orderBy = queryObj.getOrderBys();
          if (orderBy.size() > 1) {
              LOG.warn("ORDER BY has more than one sort criterium, all but the first are ignored.");
          }
          class ResultComparator implements Comparator<StoredObject> {
  
              @Override
              @SuppressWarnings("unchecked")
              public int compare(StoredObject so1, StoredObject so2) {
                  SortSpec s = orderBy.get(0);
                  CmisSelector sel = s.getSelector();
                  int result;
  
                  if (queryObj.isPredfinedQueryName(sel.getName())) {
                      // must be SEARCH_SCORE which is currently ignored
                      result = 0;
                  } else if (sel instanceof ColumnReference) {
                      String propId = ((ColumnReference) sel).getPropertyId();
                      PropertyDefinition<?> pd = ((ColumnReference) sel).getPropertyDefinition();
  
                      boolean cmis11 = callContext.getCmisVersion() != CmisVersion.CMIS_1_0;
                      Object propVal1 = PropertyQueryUtil.getProperty(so1, propId, pd, cmis11);
                      Object propVal2 = PropertyQueryUtil.getProperty(so2, propId, pd, cmis11);
  
                      if (propVal1 == null && propVal2 == null) {
                          result = 0;
                      } else if (propVal1 == null) {
                          result = -1;
                      } else if (propVal2 == null) {
                          result = 1;
                      } else {
                          result = ((Comparable<Object>) propVal1).compareTo(propVal2);
                      }
                  } else {
                      // evaluate function here, currently ignore
                      result = 0;
                  }
                  if (!s.isAscending()) {
                      result = -result;
                  }
                  return result;
              }
          }
  
          if (orderBy.size() > 0) {
              Collections.sort(matches, new ResultComparator());
          }
  
      }
  
      /*
       * Check for each object contained in the in-memory repository if it matches
       * the current query expression. If yes add it to the list of matched
       * objects.
       */
      private void match(StoredObject so, String user, boolean searchAllVersions) {
          // first check if type is matching...
          // as we don't support joins take first type
          String queryName = queryObj.getTypes().values().iterator().next();
  
          TypeDefinition td = queryObj.getTypeDefinitionFromQueryName(queryName);
  
          // we are only interested in versions not in the series
          boolean skip = so instanceof VersionedDocument;
  
          boolean typeMatches = typeMatches(td, so);
          if (!searchAllVersions && so instanceof DocumentVersion
                  && ((DocumentVersion) so).getParentDocument().getLatestVersion(false) != so) {
              skip = true;
          }
          // ... then check expression...
          if (typeMatches && !skip) {
              evalWhereTree(whereTree, user, so);
          }
      }
  
      private void evalWhereTree(Tree node, String user, StoredObject so) {
          boolean match = true;
          if (null != node) {
              match = evalWhereNode(so, user, node);
          }
          if (match && objStore.hasReadAccess(user, so)) {
              matches.add(so); // add to list
          }
      }
  
      /*
       * For each object check if it matches and append it to match-list if it
       * does. We do here our own walking mechanism so that we can pass additional
       * parameters and define the return types.
       */
      private boolean evalWhereNode(StoredObject so, String user, Tree node) {
          return new InMemoryWhereClauseWalker(so, user).walkPredicate(node);
      }
  
      public class InMemoryWhereClauseWalker extends AbstractPredicateWalker {
  
          protected final StoredObject so;
          protected final String user;
  
          public InMemoryWhereClauseWalker(StoredObject so, String user) {
              this.so = so;
              this.user = user;
          }
  
          @Override
          public Boolean walkNot(Tree opNode, Tree node) {
              boolean hasMatched = walkPredicate(node);
              return !hasMatched;
          }
  
          @Override
          public Boolean walkAnd(Tree opNode, Tree leftNode, Tree rightNode) {
              boolean matches1 = walkPredicate(leftNode);
              boolean matches2 = walkPredicate(rightNode);
              return matches1 && matches2;
          }
  
          @Override
          public Boolean walkOr(Tree opNode, Tree leftNode, Tree rightNode) {
              boolean matches1 = walkPredicate(leftNode);
              boolean matches2 = walkPredicate(rightNode);
              return matches1 || matches2;
          }
  
          @Override
          public Boolean walkEquals(Tree opNode, Tree leftNode, Tree rightNode) {
              Integer cmp = compareTo(leftNode, rightNode);
              return cmp == null ? false : cmp == 0;
          }
  
          @Override
          public Boolean walkNotEquals(Tree opNode, Tree leftNode, Tree rightNode) {
              Integer cmp = compareTo(leftNode, rightNode);
              return cmp == null ? false : cmp != 0;
          }
  
          @Override
          public Boolean walkGreaterThan(Tree opNode, Tree leftNode, Tree rightNode) {
              Integer cmp = compareTo(leftNode, rightNode);
              return cmp == null ? false : cmp > 0;
          }
  
          @Override
          public Boolean walkGreaterOrEquals(Tree opNode, Tree leftNode, Tree rightNode) {
              Integer cmp = compareTo(leftNode, rightNode);
              return cmp == null ? false : cmp >= 0;
          }
  
          @Override
          public Boolean walkLessThan(Tree opNode, Tree leftNode, Tree rightNode) {
              Integer cmp = compareTo(leftNode, rightNode);
              return cmp == null ? false : cmp < 0;
          }
  
          @Override
          public Boolean walkLessOrEquals(Tree opNode, Tree leftNode, Tree rightNode) {
              Integer cmp = compareTo(leftNode, rightNode);
              return cmp == null ? false : cmp <= 0;
          }
  
          @Override
          public Boolean walkIn(Tree opNode, Tree colNode, Tree listNode) {
              ColumnReference colRef = getColumnReference(colNode);
              PropertyDefinition<?> pd = colRef.getPropertyDefinition();
              List<Object> literals = onLiteralList(listNode);
              boolean cmis11 = callContext.getCmisVersion() != CmisVersion.CMIS_1_0;
  
              Object prop = PropertyQueryUtil.getProperty(so, colRef.getPropertyId(), pd, cmis11);
  
              if (pd.getCardinality() != Cardinality.SINGLE) {
                  throw new IllegalStateException("Operator IN only is allowed on single-value properties ");
              } else if (prop == null) {
                  return false;
              } else {
                  return literals.contains(prop);
              }
          }
  
          @Override
          public Boolean walkNotIn(Tree opNode, Tree colNode, Tree listNode) {
              // Note just return !walkIn(node, colNode, listNode) is wrong,
              // because
              // then it evaluates to true for null values (not set properties).
              ColumnReference colRef = getColumnReference(colNode);
              PropertyDefinition<?> pd = colRef.getPropertyDefinition();
              boolean cmis11 = callContext.getCmisVersion() != CmisVersion.CMIS_1_0;
              Object prop = PropertyQueryUtil.getProperty(so, colRef.getPropertyId(), pd, cmis11);
              List<Object> literals = onLiteralList(listNode);
              if (pd.getCardinality() != Cardinality.SINGLE) {
                  throw new IllegalStateException("Operator IN only is allowed on single-value properties ");
              } else if (prop == null) {
                  return false;
              } else {
                  return !literals.contains(prop);
              }
          }
  
          @Override
          public Boolean walkInAny(Tree opNode, Tree colNode, Tree listNode) {
              ColumnReference colRef = getColumnReference(colNode);
              PropertyDefinition<?> pd = colRef.getPropertyDefinition();
              PropertyData<?> lVal = so.getProperties().get(colRef.getPropertyId());
              List<Object> literals = onLiteralList(listNode);
              if (pd.getCardinality() != Cardinality.MULTI) {
                  throw new IllegalStateException("Operator ANY...IN only is allowed on multi-value properties ");
              } else if (lVal == null) {
                  return false;
              } else {
                  List<?> props = lVal.getValues();
                  for (Object prop : props) {
                      LOG.debug("comparing with: " + prop);
                      if (literals.contains(prop)) {
                          return true;
                      }
                  }
                  return false;
              }
          }
  
          @Override
          public Boolean walkNotInAny(Tree opNode, Tree colNode, Tree listNode) {
              // Note just return !walkNotInAny(node, colNode, listNode) is
              // wrong, because
              // then it evaluates to true for null values (not set properties).
              ColumnReference colRef = getColumnReference(colNode);
              PropertyDefinition<?> pd = colRef.getPropertyDefinition();
              PropertyData<?> lVal = so.getProperties().get(colRef.getPropertyId());
              List<Object> literals = onLiteralList(listNode);
              if (pd.getCardinality() != Cardinality.MULTI) {
                  throw new IllegalStateException("Operator ANY...IN only is allowed on multi-value properties ");
              } else if (lVal == null) {
                  return false;
              } else {
                  List<?> props = lVal.getValues();
                  for (Object prop : props) {
                      LOG.debug("comparing with: " + prop);
                      if (literals.contains(prop)) {
                          return false;
                      }
                  }
                  return true;
              }
          }
  
          @Override
          public Boolean walkEqAny(Tree opNode, Tree literalNode, Tree colNode) {
              ColumnReference colRef = getColumnReference(colNode);
              PropertyDefinition<?> pd = colRef.getPropertyDefinition();
              PropertyData<?> lVal = so.getProperties().get(colRef.getPropertyId());
              Object literal = walkExpr(literalNode);
              if (pd.getCardinality() != Cardinality.MULTI) {
                  throw new IllegalStateException("Operator = ANY only is allowed on multi-value properties ");
              } else if (lVal == null) {
                  return false;
              } else {
                  List<?> props = lVal.getValues();
                  return props.contains(literal);
              }
          }
  
          @Override
          public Boolean walkIsNull(Tree opNode, Tree colNode) {
              ColumnReference colRef = getColumnReference(colNode);
              PropertyDefinition<?> pd = colRef.getPropertyDefinition();
              boolean cmis11 = callContext.getCmisVersion() != CmisVersion.CMIS_1_0;
              Object propVal = PropertyQueryUtil.getProperty(so, colRef.getPropertyId(), pd, cmis11);
              return propVal == null;
          }
  
          @Override
          public Boolean walkIsNotNull(Tree opNode, Tree colNode) {
              ColumnReference colRef = getColumnReference(colNode);
              PropertyDefinition<?> pd = colRef.getPropertyDefinition();
              boolean cmis11 = callContext.getCmisVersion() != CmisVersion.CMIS_1_0;
              Object propVal = PropertyQueryUtil.getProperty(so, colRef.getPropertyId(), pd, cmis11);
              return propVal != null;
          }
  
          @Override
          public Boolean walkLike(Tree opNode, Tree colNode, Tree stringNode) {
              Object rVal = walkExpr(stringNode);
              if (!(rVal instanceof String)) {
                  throw new IllegalStateException("LIKE operator requires String literal on right hand side.");
              }
  
              ColumnReference colRef = getColumnReference(colNode);
              PropertyDefinition<?> pd = colRef.getPropertyDefinition();
              PropertyType propType = pd.getPropertyType();
              if (propType != PropertyType.STRING && propType != PropertyType.HTML && propType != PropertyType.ID
                      && propType != PropertyType.URI) {
                  throw new IllegalStateException("Property type " + propType.value() + " is not allowed FOR LIKE");
              }
              if (pd.getCardinality() != Cardinality.SINGLE) {
                  throw new IllegalStateException("LIKE is not allowed for multi-value properties ");
              }
  
              boolean cmis11 = callContext.getCmisVersion() != CmisVersion.CMIS_1_0;
              String propVal = (String) PropertyQueryUtil.getProperty(so, colRef.getPropertyId(), pd, cmis11);
  
              if (null == propVal) {
                  return false;
              } else {
                  String pattern = translatePattern((String) rVal); // SQL to Java
                  // regex
                  // syntax
                  Pattern p = Pattern.compile(pattern);
                  return p.matcher(propVal).matches();
              }
          }
  
          @Override
          public Boolean walkNotLike(Tree opNode, Tree colNode, Tree stringNode) {
              return !walkLike(opNode, colNode, stringNode);
          }
  
          @Override
          public Boolean walkInFolder(Tree opNode, Tree qualNode, Tree paramNode) {
              if (null != qualNode) {
                  getTableReference(qualNode);
                  // just for error checking we do not evaluate this, there is
                  // only one from without join support
              }
              Object lit = walkExpr(paramNode);
              if (!(lit instanceof String)) {
                  throw new IllegalStateException("Folder id in IN_FOLDER must be of type String");
              }
              String folderId = (String) lit;
  
              // check if object is in folder
              if (so instanceof Filing) {
                  return hasParent(so, folderId, user);
              } else {
                  return false;
              }
          }
  
          @Override
          public Boolean walkInTree(Tree opNode, Tree qualNode, Tree paramNode) {
              if (null != qualNode) {
                  getTableReference(qualNode);
                  // just for error checking we do not evaluate this, there is
                  // only one from without join support
              }
              Object lit = walkExpr(paramNode);
              if (!(lit instanceof String)) {
                  throw new IllegalStateException("Folder id in IN_FOLDER must be of type String");
              }
              String folderId = (String) lit;
  
              // check if object is in folder
              if (so instanceof Filing) {
                  return hasAncestor(so, folderId, user);
              } else {
                  return false;
              }
          }
  
          protected Integer compareTo(Tree leftChild, Tree rightChild) {
              Object rVal = walkExpr(rightChild);
  
              ColumnReference colRef = getColumnReference(leftChild);
              PropertyDefinition<?> pd = colRef.getPropertyDefinition();
              boolean cmis11 = callContext.getCmisVersion() != CmisVersion.CMIS_1_0;
              Object val = PropertyQueryUtil.getProperty(so, colRef.getPropertyId(), pd, cmis11);
              if (val == null) {
                  return null;
              } else if (val instanceof List<?>) {
                  throw new IllegalStateException(
                          "You can't query operators <, <=, ==, !=, >=, > on multi-value properties ");
              } else {
                  return InMemoryQueryProcessor.this.compareTo(pd, val, rVal);
              }
          }
  
          @SuppressWarnings("unchecked")
          public List<Object> onLiteralList(Tree node) {
              return (List<Object>) walkExpr(node);
          }
  
          @Override
          protected Boolean walkTextAnd(Tree node) {
              List<Tree> terms = getChildrenAsList(node);
              for (Tree term : terms) {
                  Boolean foundOnce = walkSearchExpr(term);
                  if (foundOnce == null || !foundOnce) {
                      return false;
                  }
              }
              return true;
          }
  
          @Override
          protected Boolean walkTextOr(Tree node) {
              List<Tree> terms = getChildrenAsList(node);
              for (Tree term : terms) {
                  Boolean foundOnce = walkSearchExpr(term);
                  if (foundOnce != null && foundOnce) {
                      return true;
                  }
              }
              return false;
          }
  
          @Override
          protected Boolean walkTextMinus(Tree node) {
              return !findText(node.getChild(0).getText());
          }
  
          @Override
          protected Boolean walkTextWord(Tree node) {
              return findText(node.getText());
          }
  
          @Override
          protected Boolean walkTextPhrase(Tree node) {
              String phrase = node.getText();
              return findText(phrase.substring(1, phrase.length() - 1));
          }
  
          private List<Tree> getChildrenAsList(Tree node) {
              List<Tree> res = new ArrayList<Tree>(node.getChildCount());
              for (int i = 0; i < node.getChildCount(); i++) {
                  Tree childNnode = node.getChild(i);
                  res.add(childNnode);
              }
              return res;
          }
  
          private boolean findText(String nodeText) {
              String pattern = StringUtil.unescape(nodeText, "\\'-");
              if (null == pattern) {
                  throw new CmisInvalidArgumentException("Illegal Escape sequence in text search expression " + nodeText);
              }
  
              if (so instanceof Content && ((Content) so).hasContent()) {
                  Content cont = (Content) so;
                  ContentStreamDataImpl cdi = (ContentStreamDataImpl) cont.getContent();
                  if (cdi.getMimeType().startsWith("text/")) {
                      byte[] ba = cdi.getBytes();
                      String text;
                      try {
                          text = new String(ba, "UTF-8");
                      } catch (UnsupportedEncodingException e) {
                          throw new CmisRuntimeException("Internal error: Unsupported encoding UTF-8", e);
                      }
                      int match = text.indexOf(pattern);
                      return match >= 0;
                  } else {
                      return false;
                  }
              }
              return false;
          }
  
      }
  
      private boolean hasParent(StoredObject objInFolder, String folderId, String user) {
          List<String> parents = objStore.getParentIds(objInFolder, user);
  
          for (String parentId : parents) {
              if (folderId.equals(parentId)) {
                  return true;
              }
          }
          return false;
      }
  
      private boolean hasAncestor(StoredObject objInFolder, String folderId, String user) {
          List<String> parents = objStore.getParentIds(objInFolder, user);
  
          for (String parentId : parents) {
              if (folderId.equals(parentId)) {
                  return true;
              }
          }
          for (String parentId : parents) {
              Folder parentFolder = (Folder) objStore.getObjectById(parentId);
              if (hasAncestor(parentFolder, folderId, user)) {
                  return true;
              }
          }
          return false;
      }
  
      protected int compareTo(PropertyDefinition<?> td, Object lValue, Object rVal) {
          switch (td.getPropertyType()) {
          case BOOLEAN:
              if (rVal instanceof Boolean) {
                  return ((Boolean) lValue).compareTo((Boolean) rVal);
              } else {
                  throwIncompatibleTypesException(lValue, rVal);
              }
              break;
          case INTEGER:
              Long lLongValue = ((BigInteger) lValue).longValue();
              if (rVal instanceof Long) {
                  return (lLongValue).compareTo((Long) rVal);
              } else if (rVal instanceof Double) {
                  return Double.valueOf(((Integer) lValue).doubleValue()).compareTo((Double) rVal);
              } else {
                  throwIncompatibleTypesException(lValue, rVal);
              }
              break;
          case DATETIME:
              if (rVal instanceof GregorianCalendar) {
                  return ((GregorianCalendar) lValue).compareTo((GregorianCalendar) rVal);
              } else {
                  throwIncompatibleTypesException(lValue, rVal);
              }
              break;
          case DECIMAL:
              Double lDoubleValue = ((BigDecimal) lValue).doubleValue();
              if (rVal instanceof Double) {
                  return lDoubleValue.compareTo((Double) rVal);
              } else if (rVal instanceof Long) {
                  return lDoubleValue.compareTo(Double.valueOf(((Long) rVal)));
              } else {
                  throwIncompatibleTypesException(lValue, rVal);
              }
              break;
          case HTML:
          case URI:
          case ID:
              if (rVal instanceof String) {
                  LOG.debug("compare strings: " + lValue + " with " + rVal);
                  return ((String) lValue).compareTo((String) rVal);
              } else {
                  throwIncompatibleTypesException(lValue, rVal);
              }
              break;
          case STRING:
              if (rVal instanceof String) {
                  String unesc = StringUtil.unescape((String) rVal, null);
                  LOG.debug("compare strings: " + lValue + " with " + unesc);
                  return ((String) lValue).compareTo(unesc);
              } else {
                  throwIncompatibleTypesException(lValue, rVal);
              }
              break;
          }
          return 0;
      }
  
      private ColumnReference getColumnReference(Tree columnNode) {
          CmisSelector sel = queryObj.getColumnReference(columnNode.getTokenStartIndex());
          if (null == sel) {
              throw new IllegalStateException("Unknown property query name " + columnNode.getChild(0));
          } else if (sel instanceof ColumnReference) {
              return (ColumnReference) sel;
          } else {
              throw new IllegalStateException("Unexpected numerical value function in where clause");
          }
      }
  
      private String getTableReference(Tree tableNode) {
          String typeQueryName = queryObj.getTypeQueryName(tableNode.getText());
          if (null == typeQueryName) {
              throw new IllegalStateException("Inavlid type in IN_FOLDER() or IN_TREE(), must be in FROM list: "
                      + tableNode.getText());
          }
          return typeQueryName;
      }
  
      private void doAdditionalChecks(CmisQueryWalker walker) {
          if (walker.getNumberOfContainsClauses() > 1) {
              throw new CmisInvalidArgumentException("More than one CONTAINS clause is not allowed");
          }
          List<JoinSpec> joins = queryObj.getJoins();
          if (null == secondaryTypeIds && joins.size() > 0) {
              throw new CmisInvalidArgumentException(
                      "JOINs are not supported with the exception of secondary types");
          }
      }
  
      /**
       * Translate SQL wildcards %, _ to Java regex syntax.
       * 
       * @param wildcardString
       *            string to process
       * @return string with replaced characters
       */
      public static String translatePattern(String wildcardString) {
          int index = 0;
          int start = 0;
          String wildcard = wildcardString;
  
          StringBuilder res = new StringBuilder();
  
          while (index >= 0) {
              index = wildcard.indexOf('%', start);
              if (index < 0) {
                  res.append(wildcard.substring(start));
              } else if (index == 0 || index > 0 && wildcard.charAt(index - 1) != '\\') {
                  res.append(wildcard.substring(start, index));
                  res.append(".*");
              } else {
                  res.append(wildcard.substring(start, index + 1));
              }
              start = index + 1;
          }
          wildcard = res.toString();
  
          index = 0;
          start = 0;
          res = new StringBuilder();
  
          while (index >= 0) {
              index = wildcard.indexOf('_', start);
              if (index < 0) {
                  res.append(wildcard.substring(start));
              } else if (index == 0 || index > 0 && wildcard.charAt(index - 1) != '\\') {
                  res.append(wildcard.substring(start, index));
                  res.append(".");
              } else {
                  res.append(wildcard.substring(start, index + 1));
              }
              start = index + 1;
          }
          return res.toString();
      }
  
      private static void throwIncompatibleTypesException(Object o1, Object o2) {
          throw new CmisInvalidArgumentException("Incompatible Types to compare: " + o1 + " and " + o2);
      }
  
  }
  