Package org.exoplatform.services.jcr.impl.core.query.lucene

Source Code of org.exoplatform.services.jcr.impl.core.query.lucene.LuceneQueryBuilder

/*
* 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.exoplatform.services.jcr.impl.core.query.lucene;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import javax.jcr.NamespaceException;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.query.InvalidQueryException;

import org.exoplatform.services.log.Log;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.BooleanClause.Occur;

import org.exoplatform.commons.utils.ISO8601;
import org.exoplatform.services.jcr.core.nodetype.NodeTypeData;
import org.exoplatform.services.jcr.core.nodetype.NodeTypeDataManager;
import org.exoplatform.services.jcr.dataflow.ItemDataConsumer;
import org.exoplatform.services.jcr.datamodel.IllegalNameException;
import org.exoplatform.services.jcr.datamodel.IllegalPathException;
import org.exoplatform.services.jcr.datamodel.InternalQName;
import org.exoplatform.services.jcr.datamodel.ItemData;
import org.exoplatform.services.jcr.datamodel.NodeData;
import org.exoplatform.services.jcr.datamodel.QPath;
import org.exoplatform.services.jcr.datamodel.QPathEntry;
import org.exoplatform.services.jcr.impl.Constants;
import org.exoplatform.services.jcr.impl.core.LocationFactory;
import org.exoplatform.services.jcr.impl.core.SessionImpl;
import org.exoplatform.services.jcr.impl.core.query.AndQueryNode;
import org.exoplatform.services.jcr.impl.core.query.DefaultQueryNodeVisitor;
import org.exoplatform.services.jcr.impl.core.query.DerefQueryNode;
import org.exoplatform.services.jcr.impl.core.query.ExactQueryNode;
import org.exoplatform.services.jcr.impl.core.query.LocationStepQueryNode;
import org.exoplatform.services.jcr.impl.core.query.NodeTypeQueryNode;
import org.exoplatform.services.jcr.impl.core.query.NotQueryNode;
import org.exoplatform.services.jcr.impl.core.query.OrQueryNode;
import org.exoplatform.services.jcr.impl.core.query.OrderQueryNode;
import org.exoplatform.services.jcr.impl.core.query.PathQueryNode;
import org.exoplatform.services.jcr.impl.core.query.PropertyFunctionQueryNode;
import org.exoplatform.services.jcr.impl.core.query.PropertyTypeRegistry;
import org.exoplatform.services.jcr.impl.core.query.QueryConstants;
import org.exoplatform.services.jcr.impl.core.query.QueryNode;
import org.exoplatform.services.jcr.impl.core.query.QueryNodeVisitor;
import org.exoplatform.services.jcr.impl.core.query.QueryRootNode;
import org.exoplatform.services.jcr.impl.core.query.RelationQueryNode;
import org.exoplatform.services.jcr.impl.core.query.TextsearchQueryNode;
import org.exoplatform.services.jcr.impl.core.query.lucene.fulltext.ParseException;
import org.exoplatform.services.jcr.impl.core.query.lucene.fulltext.QueryParser;
import org.exoplatform.services.jcr.impl.util.ISO9075;
import org.exoplatform.services.jcr.impl.xml.XMLChar;
import org.exoplatform.services.log.ExoLogger;

/**
* Implements a query builder that takes an abstract query tree and creates a
* lucene {@link org.apache.lucene.search.Query} tree that can be executed on an
* index. todo introduce a node type hierarchy for efficient translation of
* NodeTypeQueryNode
*/
public class LuceneQueryBuilder
   implements QueryNodeVisitor
{
   /**
    * Namespace URI for xpath functions
    */
   private static final String NS_FN_PREFIX = "fn";

   public static final String NS_FN_URI = "http://www.w3.org/2005/xpath-functions";

   /**
    * Deprecated namespace URI for xpath functions
    */
   private static final String NS_FN_OLD_PREFIX = "fn_old";

   public static final String NS_FN_OLD_URI = "http://www.w3.org/2004/10/xpath-functions";

   /**
    * Namespace URI for XML schema
    */
   private static final String NS_XS_PREFIX = "xs";

   public static final String NS_XS_URI = "http://www.w3.org/2001/XMLSchema";

   /**
    * Logger for this class
    */
   private static final Log log = ExoLogger.getLogger(LuceneQueryBuilder.class);

   /**
    * Root node of the abstract query tree
    */
   private QueryRootNode root;

   /**
    * Session of the user executing this query
    */
   private SessionImpl session;

   /**
    * The shared item state manager of the workspace.
    */
   private ItemDataConsumer sharedItemMgr;

   /**
    * Namespace mappings to internal prefixes
    */
   private NamespaceMappings nsMappings;

   /**
    * Name and Path resolver
    */
   private LocationFactory resolver;

   /**
    * The analyzer instance to use for contains function query parsing
    */
   private Analyzer analyzer;

   /**
    * The property type registry.
    */
   private PropertyTypeRegistry propRegistry;

   /**
    * The synonym provider or <code>null</code> if none is configured.
    */
   private SynonymProvider synonymProvider;

   /**
    * Wether the index format is new or old.
    */
   private IndexFormatVersion indexFormatVersion;

   /**
    * Exceptions thrown during tree translation
    */
   private List<Exception> exceptions = new ArrayList<Exception>();

   private final NodeTypeDataManager nodeTypeDataManager;

   /**
    * Creates a new <code>LuceneQueryBuilder</code> instance.
    *
    * @param root the root node of the abstract query tree.
    * @param session of the user executing this query.
    * @param sharedItemMgr the shared item state manager of the workspace.
    * @param hmgr a hierarchy manager based on sharedItemMgr.
    * @param nsMappings namespace resolver for internal prefixes.
    * @param analyzer for parsing the query statement of the contains function.
    * @param propReg the property type registry.
    * @param synonymProvider the synonym provider or <code>null</code> if node is
    *          configured.
    * @param indexFormatVersion the index format version for the lucene query.
    */
   private LuceneQueryBuilder(QueryRootNode root, SessionImpl session, ItemDataConsumer sharedItemMgr,
            NamespaceMappings nsMappings, NodeTypeDataManager nodeTypeDataManager, Analyzer analyzer,
            PropertyTypeRegistry propReg, SynonymProvider synonymProvider, IndexFormatVersion indexFormatVersion)
   {
      this.root = root;
      this.session = session;
      this.sharedItemMgr = sharedItemMgr;
      this.nsMappings = nsMappings;
      this.nodeTypeDataManager = nodeTypeDataManager;
      this.analyzer = analyzer;
      this.propRegistry = propReg;
      this.synonymProvider = synonymProvider;
      this.indexFormatVersion = indexFormatVersion;
      this.resolver = new LocationFactory(nsMappings);
   }

   /**
    * Creates a lucene {@link org.apache.lucene.search.Query} tree from an
    * abstract query tree.
    *
    * @param root the root node of the abstract query tree.
    * @param session of the user executing the query.
    * @param sharedItemMgr the shared item state manager of the workspace.
    * @param nsMappings namespace resolver for internal prefixes.
    * @param analyzer for parsing the query statement of the contains function.
    * @param propReg the property type registry to lookup type information.
    * @param synonymProvider the synonym provider or <code>null</code> if node is
    *          configured.
    * @param indexFormatVersion the index format version to be used
    * @return the lucene query tree.
    * @throws RepositoryException if an error occurs during the translation.
    */
   public static Query createQuery(QueryRootNode root, SessionImpl session, ItemDataConsumer sharedItemMgr,
            NamespaceMappings nsMappings, NodeTypeDataManager nodeTypeDataManager, Analyzer analyzer,
            PropertyTypeRegistry propReg, SynonymProvider synonymProvider, IndexFormatVersion indexFormatVersion)
            throws RepositoryException
   {

      LuceneQueryBuilder builder =
               new LuceneQueryBuilder(root, session, sharedItemMgr, nsMappings, nodeTypeDataManager, analyzer, propReg,
                        synonymProvider, indexFormatVersion);

      Query q = builder.createLuceneQuery();
      if (builder.exceptions.size() > 0)
      {
         StringBuffer msg = new StringBuffer();
         for (Iterator<Exception> it = builder.exceptions.iterator(); it.hasNext();)
         {
            msg.append(it.next().toString()).append('\n');
         }
         throw new RepositoryException("Exception building query: " + msg.toString());
      }
      return q;
   }

   /**
    * Starts the tree traversal and returns the lucene
    * {@link org.apache.lucene.search.Query}.
    *
    * @return the lucene <code>Query</code>.
    */
   private Query createLuceneQuery()
   {
      return (Query) root.accept(this, null);
   }

   // ---------------------< QueryNodeVisitor interface >-----------------------

   public Object visit(QueryRootNode node, Object data)
   {
      BooleanQuery root = new BooleanQuery();

      Query wrapped = root;
      if (node.getLocationNode() != null)
      {
         wrapped = (Query) node.getLocationNode().accept(this, root);
      }

      return wrapped;
   }

   public Object visit(OrQueryNode node, Object data)
   {
      BooleanQuery orQuery = new BooleanQuery();
      Object[] result = node.acceptOperands(this, null);
      for (int i = 0; i < result.length; i++)
      {
         Query operand = (Query) result[i];
         orQuery.add(operand, Occur.SHOULD);
      }
      return orQuery;
   }

   public Object visit(AndQueryNode node, Object data)
   {
      Object[] result = node.acceptOperands(this, null);
      if (result.length == 0)
      {
         return null;
      }
      BooleanQuery andQuery = new BooleanQuery();
      for (int i = 0; i < result.length; i++)
      {
         Query operand = (Query) result[i];
         andQuery.add(operand, Occur.MUST);
      }
      return andQuery;
   }

   public Object visit(NotQueryNode node, Object data)
   {
      Object[] result = node.acceptOperands(this, null);
      if (result.length == 0)
      {
         return data;
      }
      // join the results
      BooleanQuery b = new BooleanQuery();
      for (int i = 0; i < result.length; i++)
      {
         b.add((Query) result[i], Occur.SHOULD);
      }
      // negate
      return new NotQuery(b);
   }

   public Object visit(ExactQueryNode node, Object data)
   {
      String field = "";
      String value = "";
      try
      {
         field = resolver.createJCRName(node.getPropertyName()).getAsString();
         value = resolver.createJCRName(node.getValue()).getAsString();
      }
      catch (RepositoryException e)
      {
         // will never happen, prefixes are created when unknown
         log.warn(e.getLocalizedMessage());
      }
      return new TermQuery(new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, value)));
   }

   public Object visit(NodeTypeQueryNode node, Object data)
   {

      List<Term> terms = new ArrayList<Term>();
      try
      {
         String mixinTypesField = resolver.createJCRName(Constants.JCR_MIXINTYPES).getAsString();
         String primaryTypeField = resolver.createJCRName(Constants.JCR_PRIMARYTYPE).getAsString();

         // ExtendedNodeTypeManager ntMgr =
         // session.getWorkspace().getNodeTypeManager();
         NodeTypeData base = nodeTypeDataManager.findNodeType(node.getValue());

         if (base.isMixin())
         {
            // search for nodes where jcr:mixinTypes is set to this mixin
            Term t =
                     new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(mixinTypesField, resolver
                              .createJCRName(node.getValue()).getAsString()));
            terms.add(t);
         }
         else
         {
            // search for nodes where jcr:primaryType is set to this type
            Term t =
                     new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(primaryTypeField, resolver
                              .createJCRName(node.getValue()).getAsString()));
            terms.add(t);
         }

         // now search for all node types that are derived from base
         Collection<NodeTypeData> allTypes = nodeTypeDataManager.getAllNodeTypes();
         for (NodeTypeData nodeTypeData : allTypes)
         {
            // ExtendedNodeType nt = (ExtendedNodeType) allTypes.nextNodeType();
            InternalQName[] superTypes = nodeTypeData.getDeclaredSupertypeNames();
            if (Arrays.asList(superTypes).contains(base.getName()))
            {
               String ntName = nsMappings.translatePropertyName(nodeTypeData.getName());
               Term t;
               if (nodeTypeData.isMixin())
               {
                  // search on jcr:mixinTypes
                  t = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(mixinTypesField, ntName));
               }
               else
               {
                  // search on jcr:primaryType
                  t = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(primaryTypeField, ntName));
               }
               terms.add(t);
            }
         }
      }
      catch (IllegalNameException e)
      {
         exceptions.add(e);
      }
      catch (RepositoryException e)
      {
         exceptions.add(e);
      }
      if (terms.size() == 0)
      {
         // exception occured
         return new BooleanQuery();
      }
      else if (terms.size() == 1)
      {
         return new TermQuery((Term) terms.get(0));
      }
      else
      {
         BooleanQuery b = new BooleanQuery();
         for (Iterator it = terms.iterator(); it.hasNext();)
         {
            b.add(new TermQuery((Term) it.next()), Occur.SHOULD);
         }
         return b;
      }
   }

   public Object visit(TextsearchQueryNode node, Object data)
   {
      try
      {
         QPath relPath = node.getRelativePath();
         String fieldname;
         if (relPath == null || !node.getReferencesProperty())
         {
            // fulltext on node
            fieldname = FieldNames.FULLTEXT;
         }
         else
         {
            // final path element is a property name
            InternalQName propName = relPath.getName();
            StringBuffer tmp = new StringBuffer();

            fieldname = FieldNames.createFullTextFieldName(resolver.createJCRName(propName).getAsString());
         }

         QueryParser parser = new QueryParser(fieldname, analyzer, synonymProvider);
         parser.setOperator(QueryParser.DEFAULT_OPERATOR_AND);
         // replace escaped ' with just '
         StringBuffer query = new StringBuffer();
         String textsearch = node.getQuery();
         // the default lucene query parser recognizes 'AND' and 'NOT' as
         // keywords.
         textsearch = textsearch.replaceAll("AND", "and");
         textsearch = textsearch.replaceAll("NOT", "not");
         boolean escaped = false;
         for (int i = 0; i < textsearch.length(); i++)
         {
            if (textsearch.charAt(i) == '\\')
            {
               if (escaped)
               {
                  query.append("\\\\");
                  escaped = false;
               }
               else
               {
                  escaped = true;
               }
            }
            else if (textsearch.charAt(i) == '\'')
            {
               if (escaped)
               {
                  escaped = false;
               }
               query.append(textsearch.charAt(i));
            }
            else
            {
               if (escaped)
               {
                  query.append('\\');
                  escaped = false;
               }
               query.append(textsearch.charAt(i));
            }
         }
         Query context = parser.parse(query.toString());
         if (relPath != null && (!node.getReferencesProperty() || relPath.getEntries().length > 1))
         {
            // text search on some child axis
            QPathEntry[] elements = relPath.getEntries();
            for (int i = elements.length - 1; i >= 0; i--)
            {
               String name = null;
               if (!elements[i].equals(RelationQueryNode.STAR_NAME_TEST))
               {
                  name = resolver.createJCRName(elements[i]).getAsString();
               }
               // join text search with name test
               // if path references property that's elements.length - 2
               // if path references node that's elements.length - 1
               if (name != null
                        && ((node.getReferencesProperty() && i == elements.length - 2) || (!node
                                 .getReferencesProperty() && i == elements.length - 1)))
               {
                  Query q = new TermQuery(new Term(FieldNames.LABEL, name));
                  BooleanQuery and = new BooleanQuery();
                  and.add(q, Occur.MUST);
                  and.add(context, Occur.MUST);
                  context = and;
               }
               else if ((node.getReferencesProperty() && i < elements.length - 2)
                        || (!node.getReferencesProperty() && i < elements.length - 1))
               {
                  // otherwise do a parent axis step
                  context = new ParentAxisQuery(context, name);
               }
            }
            // finally select parent
            context = new ParentAxisQuery(context, null);
         }
         return context;
      }
      catch (NamespaceException e)
      {
         exceptions.add(e);
      }
      catch (ParseException e)
      {
         exceptions.add(e);

      }
      catch (RepositoryException e)
      {
         exceptions.add(e);
      }
      return null;
   }

   public Object visit(PathQueryNode node, Object data)
   {
      Query context = null;
      LocationStepQueryNode[] steps = node.getPathSteps();
      if (steps.length > 0)
      {
         if (node.isAbsolute() && !steps[0].getIncludeDescendants())
         {
            // eat up first step
            InternalQName nameTest = steps[0].getNameTest();
            if (nameTest == null)
            {
               // this is equivalent to the root node
               context = new TermQuery(new Term(FieldNames.PARENT, ""));
            }
            else if (nameTest.getName().length() == 0)
            {
               // root node
               // context = new TermQuery(new Term(FieldNames.PARENT, ""));
               context = new TermQuery(new Term(FieldNames.UUID, Constants.ROOT_UUID));
            }
            else
            {
               // then this is a node != the root node
               // will never match anything!
               String name = "";
               try
               {
                  name = resolver.createJCRName(nameTest).getAsString();
               }
               catch (RepositoryException e)
               {
                  exceptions.add(e);
               }
               BooleanQuery and = new BooleanQuery();
               and.add(new TermQuery(new Term(FieldNames.PARENT, "")), Occur.MUST);

               and.add(new TermQuery(new Term(FieldNames.LABEL, name)), Occur.MUST);
               context = and;
            }
            LocationStepQueryNode[] tmp = new LocationStepQueryNode[steps.length - 1];
            System.arraycopy(steps, 1, tmp, 0, steps.length - 1);
            steps = tmp;
         }
         else
         {
            // path is 1) relative or 2) descendant-or-self
            // use root node as context
            context = new TermQuery(new Term(FieldNames.PARENT, ""));
         }
      }
      else
      {
         exceptions.add(new InvalidQueryException("Number of location steps must be > 0"));
      }
      // loop over steps
      for (int i = 0; i < steps.length; i++)
      {
         context = (Query) steps[i].accept(this, context);
      }
      if (data instanceof BooleanQuery)
      {
         BooleanQuery constraint = (BooleanQuery) data;
         if (constraint.getClauses().length > 0)
         {
            constraint.add(context, Occur.MUST);
            context = constraint;
         }
      }
      return context;
   }

   public Object visit(LocationStepQueryNode node, Object data)
   {
      Query context = (Query) data;
      BooleanQuery andQuery = new BooleanQuery();

      if (context == null)
      {
         exceptions.add(new IllegalArgumentException("Unsupported query"));
      }

      // predicate on step?
      Object[] predicates = node.acceptOperands(this, data);
      for (int i = 0; i < predicates.length; i++)
      {
         andQuery.add((Query) predicates[i], Occur.MUST);
      }

      // check for position predicate
      QueryNode[] pred = node.getPredicates();
      for (int i = 0; i < pred.length; i++)
      {
         if (pred[i].getType() == QueryNode.TYPE_RELATION)
         {
            RelationQueryNode pos = (RelationQueryNode) pred[i];
            if (pos.getValueType() == QueryConstants.TYPE_POSITION)
            {
               node.setIndex(pos.getPositionValue());
            }
         }
      }

      TermQuery nameTest = null;
      if (node.getNameTest() != null)
      {
         try
         {
            String internalName = resolver.createJCRName(node.getNameTest()).getAsString();
            nameTest = new TermQuery(new Term(FieldNames.LABEL, internalName));
         }
         catch (RepositoryException e)
         {
            // should never happen
            exceptions.add(e);
         }
      }

      if (node.getIncludeDescendants())
      {
         if (nameTest != null)
         {
            andQuery.add(new DescendantSelfAxisQuery(context, nameTest), Occur.MUST);
         }
         else
         {
            // descendant-or-self with nametest=*
            if (predicates.length > 0)
            {
               // if we have a predicate attached, the condition acts as
               // the sub query.

               // only use descendant axis if path is not //*
               // otherwise the query for the predicate can be used itself
               PathQueryNode pathNode = (PathQueryNode) node.getParent();
               if (pathNode.getPathSteps()[0] != node)
               {
                  Query subQuery = new DescendantSelfAxisQuery(context, andQuery, false);
                  andQuery = new BooleanQuery();
                  andQuery.add(subQuery, Occur.MUST);
               }
            }
            else
            {
               // todo this will traverse the whole index, optimize!
               Query subQuery = null;
               try
               {
                  subQuery = createMatchAllQuery(resolver.createJCRName(Constants.JCR_PRIMARYTYPE).getAsString());
               }
               catch (RepositoryException e)
               {
                  // will never happen, prefixes are created when unknown
               }
               // only use descendant axis if path is not //*
               PathQueryNode pathNode = (PathQueryNode) node.getParent();
               if (pathNode.getPathSteps()[0] != node)
               {
                  context = new DescendantSelfAxisQuery(context, subQuery);
                  andQuery.add(new ChildAxisQuery(sharedItemMgr, context, null, node.getIndex()), Occur.MUST);
               }
               else
               {
                  andQuery.add(subQuery, Occur.MUST);
               }
            }
         }
      }
      else
      {
         // name test
         if (nameTest != null)
         {
            andQuery.add(new ChildAxisQuery(sharedItemMgr, context, nameTest.getTerm().text(), node.getIndex()),
                     Occur.MUST);
         }
         else
         {
            // select child nodes
            andQuery.add(new ChildAxisQuery(sharedItemMgr, context, null, node.getIndex()), Occur.MUST);
         }
      }

      return andQuery;
   }

   public Object visit(DerefQueryNode node, Object data)
   {
      Query context = (Query) data;
      if (context == null)
      {
         exceptions.add(new IllegalArgumentException("Unsupported query"));
      }

      try
      {
         String refProperty = resolver.createJCRName(node.getRefProperty()).getAsString();
         String nameTest = null;
         if (node.getNameTest() != null)
         {
            nameTest = resolver.createJCRName(node.getNameTest()).getAsString();
         }

         if (node.getIncludeDescendants())
         {
            Query refPropQuery = createMatchAllQuery(refProperty);
            context = new DescendantSelfAxisQuery(context, refPropQuery, false);
         }

         context = new DerefQuery(context, refProperty, nameTest);

         // attach predicates
         Object[] predicates = node.acceptOperands(this, data);
         if (predicates.length > 0)
         {
            BooleanQuery andQuery = new BooleanQuery();
            for (int i = 0; i < predicates.length; i++)
            {
               andQuery.add((Query) predicates[i], Occur.MUST);
            }
            andQuery.add(context, Occur.MUST);
            context = andQuery;
         }

      }
      catch (RepositoryException e)
      {
         // should never happen
         exceptions.add(e);
      }

      return context;
   }

   public Object visit(RelationQueryNode node, Object data)
   {
      Query query;
      String[] stringValues = new String[1];
      switch (node.getValueType())
      {
         case 0 :
            // not set: either IS NULL or IS NOT NULL
            break;
         case QueryConstants.TYPE_DATE :
            stringValues[0] = DateField.dateToString(node.getDateValue());
            break;
         case QueryConstants.TYPE_DOUBLE :
            stringValues[0] = DoubleField.doubleToString(node.getDoubleValue());
            break;
         case QueryConstants.TYPE_LONG :
            stringValues[0] = LongField.longToString(node.getLongValue());
            break;
         case QueryConstants.TYPE_STRING :
            if (node.getOperation() == QueryConstants.OPERATION_EQ_GENERAL
                     || node.getOperation() == QueryConstants.OPERATION_EQ_VALUE
                     || node.getOperation() == QueryConstants.OPERATION_NE_GENERAL
                     || node.getOperation() == QueryConstants.OPERATION_NE_VALUE)
            {
               // only use coercing on non-range operations
               InternalQName propertyName = node.getRelativePath().getName();
               stringValues = getStringValues(propertyName, node.getStringValue());
            }
            else
            {
               stringValues[0] = node.getStringValue();
            }
            break;
         case QueryConstants.TYPE_POSITION :
            // ignore position. is handled in the location step
            return null;
         default :
            throw new IllegalArgumentException("Unknown relation type: " + node.getValueType());
      }

      if (node.getRelativePath() == null && node.getOperation() != QueryConstants.OPERATION_SIMILAR
               && node.getOperation() != QueryConstants.OPERATION_SPELLCHECK)
      {
         exceptions.add(new InvalidQueryException("@* not supported in predicate"));
         return data;
      }

      // get property transformation
      final int[] transform = new int[]
      {TransformConstants.TRANSFORM_NONE};
      node.acceptOperands(new DefaultQueryNodeVisitor()
      {
         public Object visit(PropertyFunctionQueryNode node, Object data)
         {
            if (node.getFunctionName().equals(PropertyFunctionQueryNode.LOWER_CASE))
            {
               transform[0] = TransformConstants.TRANSFORM_LOWER_CASE;
            }
            else if (node.getFunctionName().equals(PropertyFunctionQueryNode.UPPER_CASE))
            {
               transform[0] = TransformConstants.TRANSFORM_UPPER_CASE;
            }
            return data;
         }
      }, null);

      QPath relPath = node.getRelativePath();
      if (node.getOperation() == QueryConstants.OPERATION_SIMILAR)
      {
         // this is a bit ugly:
         // add the name of a dummy property because relPath actually
         // references a property. whereas the relPath of the similar
         // operation references a node
         relPath = QPath.makeChildPath(relPath, Constants.JCR_PRIMARYTYPE);
      }
      String field = "";
      try
      {
         field = resolver.createJCRName(relPath.getName()).getAsString();
      }
      catch (RepositoryException e)
      {
         // should never happen
         exceptions.add(e);
      }

      // support for fn:name()
      InternalQName propName = relPath.getName();
      if (propName.getNamespace().equals(NS_FN_URI) && propName.getName().equals("name()"))
      {
         if (node.getValueType() != QueryConstants.TYPE_STRING)
         {
            exceptions.add(new InvalidQueryException("Name function can "
                     + "only be used in conjunction with a string literal"));
            return data;
         }
         if (node.getOperation() != QueryConstants.OPERATION_EQ_VALUE
                  && node.getOperation() != QueryConstants.OPERATION_EQ_GENERAL)
         {
            exceptions.add(new InvalidQueryException("Name function can "
                     + "only be used in conjunction with an equals operator"));
            return data;
         }
         // check if string literal is a valid XML Name
         if (XMLChar.isValidName(node.getStringValue()))
         {
            // parse string literal as JCR Name
            try
            {
               InternalQName n =
                        session.getLocationFactory().parseJCRName(ISO9075.decode(node.getStringValue()))
                                 .getInternalName();
               String translatedQName = nsMappings.translatePropertyName(n);
               Term t = new Term(FieldNames.LABEL, translatedQName);
               query = new TermQuery(t);
            }
            catch (RepositoryException e)
            {
               exceptions.add(e);
               return data;
            }
            catch (IllegalNameException e)
            {
               exceptions.add(e);
               return data;
            }
         }
         else
         {
            // will never match -> create dummy query
            query = new TermQuery(new Term(FieldNames.UUID, "x"));
         }
      }
      else
      {
         switch (node.getOperation())
         {
            case QueryConstants.OPERATION_EQ_VALUE : // =
            case QueryConstants.OPERATION_EQ_GENERAL :
               BooleanQuery or = new BooleanQuery();
               for (int i = 0; i < stringValues.length; i++)
               {
                  Term t = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, stringValues[i]));
                  Query q;
                  if (transform[0] == TransformConstants.TRANSFORM_UPPER_CASE)
                  {
                     q = new CaseTermQuery.Upper(t);
                  }
                  else if (transform[0] == TransformConstants.TRANSFORM_LOWER_CASE)
                  {
                     q = new CaseTermQuery.Lower(t);
                  }
                  else
                  {
                     q = new TermQuery(t);
                  }
                  or.add(q, Occur.SHOULD);
               }
               query = or;
               if (node.getOperation() == QueryConstants.OPERATION_EQ_VALUE)
               {
                  query = createSingleValueConstraint(or, field);
               }
               break;
            case QueryConstants.OPERATION_GE_VALUE : // >=
            case QueryConstants.OPERATION_GE_GENERAL :
               or = new BooleanQuery();
               for (int i = 0; i < stringValues.length; i++)
               {
                  Term lower = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, stringValues[i]));
                  Term upper = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, "\uFFFF"));
                  or.add(new RangeQuery(lower, upper, true, transform[0]), Occur.SHOULD);
               }
               query = or;
               if (node.getOperation() == QueryConstants.OPERATION_GE_VALUE)
               {
                  query = createSingleValueConstraint(or, field);
               }
               break;
            case QueryConstants.OPERATION_GT_VALUE : // >
            case QueryConstants.OPERATION_GT_GENERAL :
               or = new BooleanQuery();
               for (int i = 0; i < stringValues.length; i++)
               {
                  Term lower = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, stringValues[i]));
                  Term upper = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, "\uFFFF"));
                  or.add(new RangeQuery(lower, upper, false, transform[0]), Occur.SHOULD);
               }
               query = or;
               if (node.getOperation() == QueryConstants.OPERATION_GT_VALUE)
               {
                  query = createSingleValueConstraint(or, field);
               }
               break;
            case QueryConstants.OPERATION_LE_VALUE : // <=
            case QueryConstants.OPERATION_LE_GENERAL : // <=
               or = new BooleanQuery();
               for (int i = 0; i < stringValues.length; i++)
               {
                  Term lower = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, ""));
                  Term upper = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, stringValues[i]));
                  or.add(new RangeQuery(lower, upper, true, transform[0]), Occur.SHOULD);
               }
               query = or;
               if (node.getOperation() == QueryConstants.OPERATION_LE_VALUE)
               {
                  query = createSingleValueConstraint(query, field);
               }
               break;
            case QueryConstants.OPERATION_LIKE : // LIKE
               // the like operation always has one string value.
               // no coercing, see above
               if (stringValues[0].equals("%"))
               {
                  query = createMatchAllQuery(field);
               }
               else
               {
                  query = new WildcardQuery(FieldNames.PROPERTIES, field, stringValues[0], transform[0]);
               }
               break;
            case QueryConstants.OPERATION_LT_VALUE : // <
            case QueryConstants.OPERATION_LT_GENERAL :
               or = new BooleanQuery();
               for (int i = 0; i < stringValues.length; i++)
               {
                  Term lower = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, ""));
                  Term upper = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, stringValues[i]));
                  or.add(new RangeQuery(lower, upper, false, transform[0]), Occur.SHOULD);
               }
               query = or;
               if (node.getOperation() == QueryConstants.OPERATION_LT_VALUE)
               {
                  query = createSingleValueConstraint(or, field);
               }
               break;
            case QueryConstants.OPERATION_NE_VALUE : // !=
               // match nodes with property 'field' that includes svp and mvp
               BooleanQuery notQuery = new BooleanQuery();
               notQuery.add(createMatchAllQuery(field), Occur.SHOULD);
               // exclude all nodes where 'field' has the term in question
               for (int i = 0; i < stringValues.length; i++)
               {
                  Term t = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, stringValues[i]));
                  Query q;
                  if (transform[0] == TransformConstants.TRANSFORM_UPPER_CASE)
                  {
                     q = new CaseTermQuery.Upper(t);
                  }
                  else if (transform[0] == TransformConstants.TRANSFORM_LOWER_CASE)
                  {
                     q = new CaseTermQuery.Lower(t);
                  }
                  else
                  {
                     q = new TermQuery(t);
                  }
                  notQuery.add(q, Occur.MUST_NOT);
               }
               // and exclude all nodes where 'field' is multi valued
               notQuery.add(new TermQuery(new Term(FieldNames.MVP, field)), Occur.MUST_NOT);
               query = notQuery;
               break;
            case QueryConstants.OPERATION_NE_GENERAL : // !=
               // that's:
               // all nodes with property 'field'
               // minus the nodes that have a single property 'field' that is
               // not equal to term in question
               // minus the nodes that have a multi-valued property 'field' and
               // all values are equal to term in question
               notQuery = new BooleanQuery();
               notQuery.add(createMatchAllQuery(field), Occur.SHOULD);
               for (int i = 0; i < stringValues.length; i++)
               {
                  // exclude the nodes that have the term and are single valued
                  Term t = new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, stringValues[i]));
                  Query svp = new NotQuery(new TermQuery(new Term(FieldNames.MVP, field)));
                  BooleanQuery and = new BooleanQuery();
                  Query q;
                  if (transform[0] == TransformConstants.TRANSFORM_UPPER_CASE)
                  {
                     q = new CaseTermQuery.Upper(t);
                  }
                  else if (transform[0] == TransformConstants.TRANSFORM_LOWER_CASE)
                  {
                     q = new CaseTermQuery.Lower(t);
                  }
                  else
                  {
                     q = new TermQuery(t);
                  }
                  and.add(q, Occur.MUST);
                  and.add(svp, Occur.MUST);
                  notQuery.add(and, Occur.MUST_NOT);
               }
               // above also excludes multi-valued properties that contain
               // multiple instances of only stringValues. e.g. text={foo, foo}
               query = notQuery;
               break;
            case QueryConstants.OPERATION_NULL :
               query = new NotQuery(createMatchAllQuery(field));
               break;
            case QueryConstants.OPERATION_SIMILAR :
               String uuid = "x";
               try
               {
                  // throw new UnsupportedOperationException();
                  QPath path = resolver.parseJCRPath(node.getStringValue()).getInternalPath();
                  NodeData parent = (NodeData) sharedItemMgr.getItemData(Constants.ROOT_UUID);

                  if (path.equals(Constants.ROOT_PATH))
                  {
                     uuid = Constants.ROOT_UUID;
                  }
                  else
                  {
                     QPathEntry[] relPathEntries = path.getRelPath(path.getDepth());
                     ItemData item = parent;
                     for (int i = 0; i < relPathEntries.length; i++)
                     {
                        item = sharedItemMgr.getItemData(parent, relPathEntries[i]);

                        if (item == null)
                           break;

                        if (item.isNode())
                           parent = (NodeData) item;
                        else if (i < relPathEntries.length - 1)
                           throw new IllegalPathException(
                                    "Path can not contains a property as the intermediate element");
                     }
                     uuid = item.getIdentifier();
                  }

               }
               catch (RepositoryException e)
               {
                  exceptions.add(e);
               }
               query = new SimilarityQuery(uuid, analyzer);
               break;
            case QueryConstants.OPERATION_NOT_NULL :
               query = createMatchAllQuery(field);
               break;
            case QueryConstants.OPERATION_SPELLCHECK :
               query = createMatchAllQuery(field);
               break;
            default :
               throw new IllegalArgumentException("Unknown relation operation: " + node.getOperation());
         }
      }

      if (relPath.getEntries().length > 1)
      {
         try
         {
            // child axis in relation
            QPathEntry[] elements = relPath.getEntries();
            // elements.length - 1 = property name
            // elements.length - 2 = last child axis name test
            for (int i = elements.length - 2; i >= 0; i--)
            {
               String name = null;
               if (!elements[i].equals(RelationQueryNode.STAR_NAME_TEST))
               {
                  name = resolver.createJCRName(elements[i]).getAsString();
               }
               if (i == elements.length - 2)
               {
                  // join name test with property query if there is one
                  if (name != null)
                  {
                     Query nameTest = new TermQuery(new Term(FieldNames.LABEL, name));
                     BooleanQuery and = new BooleanQuery();
                     and.add(query, Occur.MUST);
                     and.add(nameTest, Occur.MUST);
                     query = and;
                  }
                  else
                  {
                     // otherwise the query can be used as is
                  }
               }
               else
               {
                  query = new ParentAxisQuery(query, name);
               }
            }
         }
         catch (RepositoryException e)
         {
            // should never happen
            exceptions.add(e);
         }
         // finally select the parent of the selected nodes
         query = new ParentAxisQuery(query, null);
      }

      return query;
   }

   public Object visit(OrderQueryNode node, Object data)
   {
      return data;
   }

   public Object visit(PropertyFunctionQueryNode node, Object data)
   {
      return data;
   }

   // ---------------------------< internal >-----------------------------------

   /**
    * Wraps a constraint query around <code>q</code> that limits the nodes to
    * those where <code>propName</code> is the name of a single value property on
    * the node instance.
    *
    * @param q the query to wrap.
    * @param propName the name of a property that only has one value.
    * @return the wrapped query <code>q</code>.
    */
   private Query createSingleValueConstraint(Query q, String propName)
   {
      // get nodes with multi-values in propName
      Query mvp = new TermQuery(new Term(FieldNames.MVP, propName));
      // now negate, that gives the nodes that have propName as single
      // values but also all others
      Query svp = new NotQuery(mvp);
      // now join the two, which will result in those nodes where propName
      // only contains a single value. This works because q already restricts
      // the result to those nodes that have a property propName
      BooleanQuery and = new BooleanQuery();
      and.add(q, Occur.MUST);
      and.add(svp, Occur.MUST);
      return and;
   }

   /**
    * Returns an array of String values to be used as a term to lookup the search
    * index for a String <code>literal</code> of a certain property name. This
    * method will lookup the <code>propertyName</code> in the node type registry
    * trying to find out the {@link javax.jcr.PropertyType}s. If no property type
    * is found looking up node type information, this method will guess the
    * property type.
    *
    * @param propertyName the name of the property in the relation.
    * @param literal the String literal in the relation.
    * @return the String values to use as term for the query.
    */
   private String[] getStringValues(InternalQName propertyName, String literal)
   {
      PropertyTypeRegistry.TypeMapping[] types = propRegistry.getPropertyTypes(propertyName);
      List<String> values = new ArrayList<String>();
      for (int i = 0; i < types.length; i++)
      {
         switch (types[i].type)
         {
            case PropertyType.NAME :
               // try to translate name
               try
               {
                  InternalQName n = session.getLocationFactory().parseJCRName(literal).getInternalName();
                  values.add(nsMappings.translatePropertyName(n));
                  log.debug("Coerced " + literal + " into NAME.");
               }
               catch (RepositoryException e)
               {
                  log.warn("Unable to coerce '" + literal + "' into a NAME: " + e.toString());
               }
               catch (IllegalNameException e)
               {
                  log.warn("Unable to coerce '" + literal + "' into a NAME: " + e.toString());
               }
               break;
            case PropertyType.PATH :
               // try to translate path
               try
               {
                  QPath p = session.getLocationFactory().parseJCRPath(literal).getInternalPath();
                  values.add(resolver.createJCRPath(p).getAsString(true));
                  log.debug("Coerced " + literal + " into PATH.");
               }
               catch (RepositoryException e)
               {
                  log.warn("Unable to coerce '" + literal + "' into a PATH: " + e.toString());
               }
               break;
            case PropertyType.DATE :
               // try to parse date
               Calendar c = ISO8601.parse(literal);
               if (c != null)
               {
                  values.add(DateField.timeToString(c.getTimeInMillis()));
                  log.debug("Coerced " + literal + " into DATE.");
               }
               else
               {
                  log.warn("Unable to coerce '" + literal + "' into a DATE.");
               }
               break;
            case PropertyType.DOUBLE :
               // try to parse double
               try
               {
                  double d = Double.parseDouble(literal);
                  values.add(DoubleField.doubleToString(d));
                  log.debug("Coerced " + literal + " into DOUBLE.");
               }
               catch (NumberFormatException e)
               {
                  log.warn("Unable to coerce '" + literal + "' into a DOUBLE: " + e.toString());
               }
               break;
            case PropertyType.LONG :
               // try to parse long
               try
               {
                  long l = Long.parseLong(literal);
                  values.add(LongField.longToString(l));
                  log.debug("Coerced " + literal + " into LONG.");
               }
               catch (NumberFormatException e)
               {
                  log.warn("Unable to coerce '" + literal + "' into a LONG: " + e.toString());
               }
               break;
            case PropertyType.STRING :
               values.add(literal);
               log.debug("Using literal " + literal + " as is.");
               break;
         }
      }
      if (values.size() == 0)
      {
         // use literal as is then try to guess other types
         values.add(literal);

         // try to guess property type
         if (literal.indexOf('/') > -1)
         {
            // might be a path
            try
            {
               QPath p = session.getLocationFactory().parseJCRPath(literal).getInternalPath();
               values.add(resolver.createJCRPath(p).getAsString(true));
               log.debug("Coerced " + literal + " into PATH.");
            }
            catch (Exception e)
            {
               // not a path
            }
         }
         if (XMLChar.isValidName(literal))
         {
            // might be a name
            try
            {
               InternalQName n = session.getLocationFactory().parseJCRName(literal).getInternalName();
               values.add(nsMappings.translatePropertyName(n));
               log.debug("Coerced " + literal + " into NAME.");
            }
            catch (Exception e)
            {
               // not a name
            }
         }
         if (literal.indexOf(':') > -1)
         {
            // is it a date?
            Calendar c = ISO8601.parse(literal);
            if (c != null)
            {
               values.add(DateField.timeToString(c.getTimeInMillis()));
               log.debug("Coerced " + literal + " into DATE.");
            }
         }
         else
         {
            // long or double are possible at this point
            try
            {
               values.add(LongField.longToString(Long.parseLong(literal)));
               log.debug("Coerced " + literal + " into LONG.");
            }
            catch (NumberFormatException e)
            {
               // not a long
               // try double
               try
               {
                  values.add(DoubleField.doubleToString(Double.parseDouble(literal)));
                  log.debug("Coerced " + literal + " into DOUBLE.");
               }
               catch (NumberFormatException e1)
               {
                  // not a double
               }
            }
         }
      }
      // if still no values use literal as is
      if (values.size() == 0)
      {
         values.add(literal);
         log.debug("Using literal " + literal + " as is.");
      }
      return (String[]) values.toArray(new String[values.size()]);
   }

   /**
    * Depending on the index format this method returns a query that matches all
    * nodes that have a property named 'field'
    *
    * @param field
    * @return Query that matches all nodes that have a property named 'field'
    */
   private final Query createMatchAllQuery(String field)
   {
      if (indexFormatVersion.getVersion() >= IndexFormatVersion.V2.getVersion())
      {
         // new index format style
         return new TermQuery(new Term(FieldNames.PROPERTIES_SET, field));
      }
      else
      {
         return new MatchAllQuery(field);
      }
   }
}
TOP

Related Classes of org.exoplatform.services.jcr.impl.core.query.lucene.LuceneQueryBuilder

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.