/*
* 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()]);
}
}