/* * 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 org.apache.lucene.analysis.Analyzer; import org.apache.lucene.index.Term; import org.apache.lucene.queryParser.ParseException; import org.apache.lucene.queryParser.QueryParser; import org.apache.lucene.search.BooleanClause.Occur; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.Query; import org.exoplatform.commons.utils.ISO8601; 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.ItemType; 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.util.ISO9075; import org.exoplatform.services.jcr.impl.xml.XMLChar; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import java.util.ArrayList; import java.util.Calendar; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import javax.jcr.NamespaceException; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import javax.jcr.query.InvalidQueryException; /** * 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 { public static final String NS_FN_URI = "http://www.w3.org/2005/xpath-functions"; public static final String NS_FN_OLD_URI = "http://www.w3.org/2004/10/xpath-functions"; public static final String NS_XS_URI = "http://www.w3.org/2001/XMLSchema"; /** * Logger for this class */ private static final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.LuceneQueryBuilder"); /** * Root node of the abstract query tree */ private final QueryRootNode root; /** * Session of the user executing this query */ private final SessionImpl session; /** * The shared item state manager of the workspace. */ private final ItemDataConsumer sharedItemMgr; /** * Namespace mappings to internal prefixes */ private final NamespaceMappings nsMappings; /** * Name and Path resolver */ private final LocationFactory resolver; /** * The analyzer instance to use for contains function query parsing */ private final Analyzer analyzer; /** * The property type registry. */ private final PropertyTypeRegistry propRegistry; /** * The synonym provider or <code>null</code> if none is configured. */ private final SynonymProvider synonymProvider; /** * Wether the index format is new or old. */ private final IndexFormatVersion indexFormatVersion; /** * Exceptions thrown during tree translation */ private final List<Exception> exceptions = new ArrayList<Exception>(); private final VirtualTableResolver<Query> virtualTableResolver; private IndexingConfiguration indexConfig; /** * 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 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. * @param virtualTableResolver * @throws RepositoryException */ private LuceneQueryBuilder(QueryRootNode root, SessionImpl session, ItemDataConsumer sharedItemMgr, NamespaceMappings nsMappings, Analyzer analyzer, PropertyTypeRegistry propReg, SynonymProvider synonymProvider, IndexFormatVersion indexFormatVersion, VirtualTableResolver<Query> virtualTableResolver, IndexingConfiguration indexConfig) throws RepositoryException { this.root = root; this.session = session; this.sharedItemMgr = sharedItemMgr; this.nsMappings = nsMappings; this.analyzer = analyzer; this.propRegistry = propReg; this.synonymProvider = synonymProvider; this.indexFormatVersion = indexFormatVersion; this.virtualTableResolver = virtualTableResolver; this.resolver = new LocationFactory(nsMappings); this.indexConfig = indexConfig; } /** * 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, Analyzer analyzer, PropertyTypeRegistry propReg, SynonymProvider synonymProvider, IndexFormatVersion indexFormatVersion, VirtualTableResolver<Query> virtualTableResolver, IndexingConfiguration indexConfig) throws RepositoryException { LuceneQueryBuilder builder = new LuceneQueryBuilder(root, session, sharedItemMgr, nsMappings, analyzer, propReg, synonymProvider, indexFormatVersion, virtualTableResolver, indexConfig); Query q = builder.createLuceneQuery(); if (builder.exceptions.size() > 0) { StringBuilder msg = new StringBuilder(); 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>. * @throws RepositoryException */ private Query createLuceneQuery() throws RepositoryException { return (Query)root.accept(this, null); } // ---------------------< QueryNodeVisitor interface // >----------------------- public Object visit(QueryRootNode node, Object data) throws RepositoryException { 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) throws RepositoryException { 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) throws RepositoryException { 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) throws RepositoryException { 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) { if (LOG.isTraceEnabled()) { LOG.trace("An exception occurred: " + e.getMessage()); } } return new JcrTermQuery(new Term(FieldNames.PROPERTIES, FieldNames.createNamedValue(field, value))); } public Object visit(NodeTypeQueryNode node, Object data) { try { return virtualTableResolver.resolve(node.getValue(), true); } catch (InvalidQueryException e1) { exceptions.add(e1); } catch (RepositoryException e1) { exceptions.add(e1); } return new BooleanQuery(); // // (result) // List terms = new ArrayList(); // try { // String mixinTypesField = resolver.createJCRName( // Constants.JCR_MIXINTYPES).getAsString(); // String primaryTypeField = resolver.createJCRName( // Constants.JCR_PRIMARYTYPE).getAsString(); // // 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) { // InternalQName[] superTypes = nodeTypeData // .getDeclaredSupertypeNames(); // if (Arrays.asList(superTypes).contains(base.getName())) { // String ntName = nsMappings.translateName(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 JackrabbitTermQuery((Term) terms.get(0)); // } else { // BooleanQuery b = new BooleanQuery(); // for (Iterator it = terms.iterator(); it.hasNext();) { // b.add(new JackrabbitTermQuery((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 fieldname = resolver.createJCRName(relPath.getName()).getAsString(); int idx = fieldname.indexOf(':'); fieldname = fieldname.substring(0, idx + 1) + FieldNames.FULLTEXT_PREFIX + fieldname.substring(idx + 1); } QueryParser parser = new JcrQueryParser(fieldname, analyzer, synonymProvider); Query context = parser.parse(node.getQuery()); 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--) { QPathEntry name = null; if (!elements[i].equals(RelationQueryNode.STAR_NAME_TEST)) { name = elements[i]; } // 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 NameQuery(name, indexFormatVersion, nsMappings); 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, indexFormatVersion, nsMappings); } } // finally select parent context = new ParentAxisQuery(context, null, indexFormatVersion, nsMappings); } return context; } catch (NamespaceException e) { exceptions.add(e); } catch (ParseException e) { exceptions.add(e); } catch (RepositoryException e) { LOG.error(e.getLocalizedMessage(), e); } return null; } public Object visit(PathQueryNode node, Object data) throws RepositoryException { 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 JcrTermQuery(new Term(FieldNames.UUID, Constants.ROOT_UUID)); } else if (nameTest.getName().length() == 0) { // root node context = new JcrTermQuery(new Term(FieldNames.UUID, Constants.ROOT_UUID)); } else { // then this is a node != the root node // will never match anything! BooleanQuery and = new BooleanQuery(); and.add(new JcrTermQuery(new Term(FieldNames.UUID, Constants.ROOT_UUID)), Occur.MUST); and.add(new NameQuery(nameTest, indexFormatVersion, nsMappings), 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 JcrTermQuery(new Term(FieldNames.UUID, Constants.ROOT_UUID)); } } 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) throws RepositoryException { 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()); } } } NameQuery nameTest = null; if (node.getNameTest() != null) { nameTest = new NameQuery(node.getNameTest(), indexFormatVersion, nsMappings); } if (node.getIncludeDescendants()) { if (nameTest != null) { andQuery.add(new DescendantSelfAxisQuery(context, nameTest, false, indexConfig), 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, indexConfig); andQuery = new BooleanQuery(); andQuery.add(subQuery, Occur.MUST); } } else { // todo this will traverse the whole index, optimize! // only use descendant axis if path is not //* PathQueryNode pathNode = (PathQueryNode)node.getParent(); if (pathNode.getPathSteps()[0] != node) { if (node.getIndex() == LocationStepQueryNode.NONE) { context = new DescendantSelfAxisQuery(context, false, indexConfig); andQuery.add(context, Occur.MUST); } else { context = new DescendantSelfAxisQuery(context, true, indexConfig); andQuery.add(new ChildAxisQuery(sharedItemMgr, context, null, node.getIndex(), indexFormatVersion, nsMappings, indexConfig), Occur.MUST); } } else { andQuery.add(new MatchAllDocsQuery(indexConfig), Occur.MUST); } } } } else { // name test if (nameTest != null) { andQuery.add(new ChildAxisQuery(sharedItemMgr, context, nameTest.getName(), node.getIndex(), indexFormatVersion, nsMappings, indexConfig), Occur.MUST); } else { // select child nodes andQuery.add(new ChildAxisQuery(sharedItemMgr, context, null, node.getIndex(), indexFormatVersion, nsMappings, indexConfig), Occur.MUST); } } return andQuery; } public Object visit(DerefQueryNode node, Object data) throws RepositoryException { Query context = (Query)data; if (context == null) { exceptions.add(new IllegalArgumentException("Unsupported query")); } try { String refProperty = resolver.createJCRName(node.getRefProperty()).getAsString(); if (node.getIncludeDescendants()) { Query refPropQuery = Util.createMatchAllQuery(refProperty, indexFormatVersion); context = new DescendantSelfAxisQuery(context, refPropQuery, false, indexConfig); } context = new DerefQuery(context, refProperty, node.getNameTest(), indexFormatVersion, nsMappings); // 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 (NamespaceException e) { // should never happen exceptions.add(e); } return context; } public Object visit(RelationQueryNode node, Object data) throws RepositoryException { 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() { @Override 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(); InternalQName propName; 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); propName = Constants.JCR_PRIMARYTYPE; } else { propName = relPath.getName(); } String field = ""; try { field = resolver.createJCRName(propName).getAsString(); } catch (NamespaceException 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(); query = new NameQuery(n, indexFormatVersion, nsMappings); } catch (RepositoryException e) { exceptions.add(e); return data; } } else { // will never match -> create dummy query query = new BooleanQuery(); } } 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 JcrTermQuery(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 = Util.createMatchAllQuery(field, indexFormatVersion); } 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(Util.createMatchAllQuery(field, indexFormatVersion), 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 JcrTermQuery(t); } notQuery.add(q, Occur.MUST_NOT); } // and exclude all nodes where 'field' is multi valued notQuery.add(new JcrTermQuery(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(Util.createMatchAllQuery(field, indexFormatVersion), 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 JcrTermQuery(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 JcrTermQuery(t); } and.add(q, Occur.MUST); and.add(svp, Occur.MUST); notQuery.add(and, Occur.MUST_NOT); } // todo 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(Util.createMatchAllQuery(field, indexFormatVersion)); 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], ItemType.UNKNOWN); 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"); } if (item == null) { throw new IllegalStateException("The item cannot be found"); } uuid = item.getIdentifier(); } } catch (RepositoryException e) { exceptions.add(e); } query = new SimilarityQuery(uuid, analyzer); break; case QueryConstants.OPERATION_NOT_NULL : query = Util.createMatchAllQuery(field, indexFormatVersion); break; case QueryConstants.OPERATION_SPELLCHECK : query = Util.createMatchAllQuery(field, indexFormatVersion); break; default : throw new IllegalArgumentException("Unknown relation operation: " + node.getOperation()); } } if (relPath != null && relPath.getEntries().length > 1) { // 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--) { QPathEntry name = null; if (!elements[i].equals(RelationQueryNode.STAR_NAME_TEST)) { name = elements[i]; } if (i == elements.length - 2) { // join name test with property query if there is one if (name != null) { Query nameTest = new NameQuery(name, indexFormatVersion, nsMappings); 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, indexFormatVersion, nsMappings); } } // finally select the parent of the selected nodes query = new ParentAxisQuery(query, null, indexFormatVersion, nsMappings); } 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 JcrTermQuery(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); Set<String> values = new HashSet<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.translateName(n)); LOG.debug("Coerced " + literal + " into NAME."); } catch (RepositoryException e) { if (types.length == 1) { LOG.warn("Unable to coerce '" + literal + "' into a NAME: " + e.toString()); } } catch (IllegalNameException e) { if (types.length == 1) { 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(false)); LOG.debug("Coerced " + literal + " into PATH."); } catch (RepositoryException e) { if (types.length == 1) { 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 { if (types.length == 1) { 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) { if (types.length == 1) { 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) { if (types.length == 1) { 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(false)); LOG.debug("Coerced " + literal + " into PATH."); } catch (Exception e) { if (LOG.isTraceEnabled()) { LOG.trace("An exception occurred: " + e.getMessage()); } } } if (XMLChar.isValidName(literal)) { // might be a name try { InternalQName n = session.getLocationFactory().parseJCRName(literal).getInternalName(); values.add(nsMappings.translateName(n)); LOG.debug("Coerced " + literal + " into NAME."); } catch (Exception e) { if (LOG.isTraceEnabled()) { LOG.trace("An exception occurred: " + e.getMessage()); } } } 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) { if (LOG.isTraceEnabled()) { LOG.trace("An exception occurred: " + e.getMessage()); } } } } } // if still no values use literal as is if (values.size() == 0) { values.add(literal); LOG.debug("Using literal " + literal + " as is."); } return values.toArray(new String[values.size()]); } }