/*
* ModeShape (http://www.modeshape.org)
*
* Licensed 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.modeshape.jcr.index.lucene.query;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Pattern;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.query.qom.Constraint;
import javax.jcr.query.qom.DynamicOperand;
import javax.jcr.query.qom.Length;
import javax.jcr.query.qom.LowerCase;
import javax.jcr.query.qom.NodeLocalName;
import javax.jcr.query.qom.NodeName;
import javax.jcr.query.qom.Not;
import javax.jcr.query.qom.PropertyExistence;
import javax.jcr.query.qom.PropertyValue;
import javax.jcr.query.qom.StaticOperand;
import javax.jcr.query.qom.UpperCase;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.LegacyNumericRangeQuery;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.MatchNoDocsQuery;
import org.apache.lucene.search.PhraseQuery;
import org.apache.lucene.search.Query;
import org.modeshape.common.annotation.Immutable;
import org.modeshape.common.annotation.ThreadSafe;
import org.modeshape.jcr.JcrI18n;
import org.modeshape.jcr.JcrLexicon;
import org.modeshape.jcr.ModeShapeLexicon;
import org.modeshape.jcr.api.query.qom.Between;
import org.modeshape.jcr.api.query.qom.Cast;
import org.modeshape.jcr.api.query.qom.NodeDepth;
import org.modeshape.jcr.api.query.qom.NodePath;
import org.modeshape.jcr.api.query.qom.Operator;
import org.modeshape.jcr.index.lucene.FieldUtil;
import org.modeshape.jcr.index.lucene.LuceneConfig;
import org.modeshape.jcr.index.lucene.LuceneIndexException;
import org.modeshape.jcr.index.lucene.LuceneIndexProviderI18n;
import org.modeshape.jcr.query.engine.QueryUtil;
import org.modeshape.jcr.query.model.And;
import org.modeshape.jcr.query.model.BindVariableName;
import org.modeshape.jcr.query.model.Comparison;
import org.modeshape.jcr.query.model.FullTextSearch;
import org.modeshape.jcr.query.model.Literal;
import org.modeshape.jcr.query.model.Or;
import org.modeshape.jcr.query.model.ReferenceValue;
import org.modeshape.jcr.query.model.Relike;
import org.modeshape.jcr.query.model.SetCriteria;
import org.modeshape.jcr.query.model.Subquery;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.NameFactory;
import org.modeshape.jcr.value.Path;
import org.modeshape.jcr.value.PathFactory;
import org.modeshape.jcr.value.PropertyType;
import org.modeshape.jcr.value.StringFactory;
import org.modeshape.jcr.value.ValueFactories;
/**
* The factory that creates a Lucene {@link Query} object from a Query Object Model {@link Constraint} object.
*
* @since 4.5
*/
@ThreadSafe
@Immutable
@SuppressWarnings("deprecation")
public class LuceneQueryFactory {
protected final PathFactory pathFactory;
protected final NameFactory nameFactory;
protected final StringFactory stringFactory;
protected final Map<String, Object> variables;
protected final ValueFactories factories;
protected final Map<String, PropertyType> propertyTypesByName;
private LuceneQueryFactory( ValueFactories factories,
Map<String, Object> variables,
Map<String, PropertyType> propertyTypesByName ) {
assert factories != null;
this.factories = factories;
this.pathFactory = factories.getPathFactory();
this.nameFactory = factories.getNameFactory();
this.stringFactory = factories.getStringFactory();
this.variables = variables != null ? variables : Collections.emptyMap();
assert propertyTypesByName != null;
this.propertyTypesByName = propertyTypesByName;
}
/**
* Creates a new query factory which can be used to produce Lucene queries for {@link org.modeshape.jcr.index.lucene.MultiColumnIndex}
* indexes.
*
* @param factories a {@link ValueFactories} instance; may not be null
* @param variables a {@link Map} instance which contains the query variables for a particular query; may be {@code null}
* @param propertyTypesByName a {@link Map} representing the columns and their types for the index definition
* for which the query should be created; may not be null.
* @return a {@link LuceneQueryFactory} instance, never {@code null}
*/
public static LuceneQueryFactory forMultiColumnIndex( ValueFactories factories,
Map<String, Object> variables,
Map<String, PropertyType> propertyTypesByName ) {
return new LuceneQueryFactory(factories, variables, propertyTypesByName);
}
/**
* Creates a new query factory which can be used to produce Lucene queries for {@link org.modeshape.jcr.index.lucene.SingleColumnIndex}
* indexes.
*
* @param factories a {@link ValueFactories} instance; may not be null
* @param variables a {@link Map} instance which contains the query variables for a particular query; may be {@code null}
* @param propertyTypesByName a {@link Map} representing the columns and their types for the index definition
* for which the query should be created; may not be null.
* @return a {@link LuceneQueryFactory} instance, never {@code null}
*/
public static LuceneQueryFactory forSingleColumnIndex( ValueFactories factories,
Map<String, Object> variables,
Map<String, PropertyType> propertyTypesByName ) {
return new SingleColumnQueryFactory(factories, variables, propertyTypesByName);
}
/**
* Creates a new query factory which can be used to produce Lucene queries for {@link org.modeshape.jcr.index.lucene.TextIndex}
* indexes.
*
* @param factories a {@link ValueFactories} instance; may not be null
* @param variables a {@link Map} instance which contains the query variables for a particular query; may be {@code null}
* @param propertyTypesByName a {@link Map} representing the columns and their types for the index definition
* for which the query should be created; may not be null.
* @param config a {@link LuceneConfig} instance required to get various information for FTS (e.g. configured analyzer); may
* not be null
* @return a {@link LuceneQueryFactory} instance, never {@code null}
*/
public static LuceneQueryFactory forTextIndex( ValueFactories factories,
Map<String, Object> variables,
Map<String, PropertyType> propertyTypesByName,
LuceneConfig config ) {
return new TextQueryFactory(factories, variables, propertyTypesByName, config);
}
/**
* Create a Lucene {@link Query} that represents the supplied Query Object Model {@link Constraint}.
*
* @param constraint the QOM constraint; never null
* @return the corresponding Query object; never null
*/
public Query createQuery( Constraint constraint ) {
if (constraint instanceof And) {
return createQuery((And) constraint);
}
if (constraint instanceof Or) {
return createQuery((Or) constraint);
}
if (constraint instanceof Not) {
return createQuery((Not) constraint);
}
if (constraint instanceof SetCriteria) {
return createQuery((SetCriteria) constraint);
}
if (constraint instanceof PropertyExistence) {
return createQuery((PropertyExistence) constraint);
}
if (constraint instanceof Between) {
return createQuery((Between) constraint);
}
if (constraint instanceof Relike) {
return createQuery((Relike) constraint);
}
if (constraint instanceof Comparison) {
return createQuery((Comparison) constraint);
}
if (constraint instanceof FullTextSearch) {
return createQuery((FullTextSearch) constraint);
}
// Should not get here ...
throw new LuceneIndexException(
"Unexpected Constraint instance: class=" + (constraint != null ? constraint.getClass() : "null")
+ " and instance=" + constraint);
}
/**
* Checks if for the query produced by this factory scores are expected or not for matching documents.
*
* @return {@code true} if scores are expected to matching documents, {@code false} otherwise
*/
public boolean scoreDocuments() {
return false;
}
protected Query createQuery( FullTextSearch constraint ) {
throw new UnsupportedOperationException("Only text indexes support FTS constraints...");
}
protected Query createQuery( Comparison comparison ) {
return createQuery(comparison.getOperand1(), comparison.operator(), comparison.getOperand2(), null);
}
protected Query createQuery( Not not ) {
return not(createQuery(not.getConstraint()));
}
protected Query createQuery( Relike relike ) {
StaticOperand op1 = relike.getOperand1();
PropertyValue op2 = relike.getOperand2();
Object relikeValue = getSingleValueFromStaticOperand(op1);
assert relikeValue != null;
String fieldName = op2.getPropertyName();
return new RelikeQuery(fieldName, relikeValue.toString());
}
protected Query createQuery( Between between ) {
DynamicOperand operand = between.getOperand();
StaticOperand lower = between.getLowerBound();
StaticOperand upper = between.getUpperBound();
boolean upperBoundIncluded = between.isUpperBoundIncluded();
boolean lowerBoundIncluded = between.isLowerBoundIncluded();
// Handle the static operands ...
Object lowerValue = getSingleValueFromStaticOperand(lower);
Object upperValue = getSingleValueFromStaticOperand(upper);
assert lowerValue != null;
assert upperValue != null;
// Only in the case of a PropertyValue and Depth will we need to do something special ...
if (operand instanceof NodeDepth) {
return createRangeQuery(depthField(), lowerValue, upperValue, lowerBoundIncluded, upperBoundIncluded);
} else if (operand instanceof PropertyValue) {
String field = ((PropertyValue) operand).getPropertyName();
PropertyType lowerType = PropertyType.discoverType(lowerValue);
PropertyType upperType = PropertyType.discoverType(upperValue);
if (upperType == lowerType) {
switch (upperType) {
case DATE:
case LONG:
case DOUBLE:
case DECIMAL:
return createRangeQuery(field, lowerValue, upperValue, lowerBoundIncluded, upperBoundIncluded);
default:
// continue on and handle as boolean query ...
}
}
}
// Otherwise, just create a boolean query ...
Operator lowerOp = lowerBoundIncluded ? Operator.GREATER_THAN_OR_EQUAL_TO : Operator.GREATER_THAN;
Operator upperOp = upperBoundIncluded ? Operator.LESS_THAN_OR_EQUAL_TO : Operator.LESS_THAN;
Query lowerQuery = createQuery(operand, lowerOp, lower, null);
Query upperQuery = createQuery(operand, upperOp, upper, null);
return booleanQuery(lowerQuery, Occur.MUST, upperQuery, Occur.MUST);
}
protected Query createRangeQuery( String field, Object lowerValue, Object upperValue, boolean includesLower,
boolean includesUpper ) {
PropertyType type = null;
PropertyType lowerType = PropertyType.discoverType(lowerValue);
PropertyType upperType = PropertyType.discoverType(upperValue);
if (lowerType != upperType) {
// the types of the bounds don't match, so nothing can be done
return new MatchNoDocsQuery();
} else {
type = lowerType;
}
switch (type) {
case DATE:
long lowerDate = factories.getLongFactory().create(lowerValue);
long upperDate = factories.getLongFactory().create(upperValue);
return LegacyNumericRangeQuery.newLongRange(field, lowerDate, upperDate, includesLower, includesUpper);
case LONG:
long lowerLong = factories.getLongFactory().create(lowerValue);
long upperLong = factories.getLongFactory().create(upperValue);
return LegacyNumericRangeQuery.newLongRange(field, lowerLong, upperLong, includesLower, includesUpper);
case DOUBLE:
double lowerDouble = factories.getDoubleFactory().create(lowerValue);
double upperDouble = factories.getDoubleFactory().create(upperValue);
return LegacyNumericRangeQuery.newDoubleRange(field, lowerDouble, upperDouble, includesLower, includesUpper);
case BOOLEAN:
int lowerInt = factories.getBooleanFactory().create(lowerValue) ? 1 : 0;
int upperInt = factories.getBooleanFactory().create(upperValue) ? 1 : 0;
return LegacyNumericRangeQuery.newIntRange(field, lowerInt, upperInt, includesLower, includesUpper);
case DECIMAL:
BigDecimal lowerDecimal = factories.getDecimalFactory().create(lowerValue);
BigDecimal upperDecimal = factories.getDecimalFactory().create(upperValue);
String lsv = FieldUtil.decimalToString(lowerDecimal);
String usv = FieldUtil.decimalToString(upperDecimal);
Query lower = null;
if (includesLower) {
lower = CompareStringQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(lsv, field, null);
} else {
lower = CompareStringQuery.createQueryForNodesWithFieldGreaterThan(lsv, field, null);
}
Query upper = null;
if (includesUpper) {
upper = CompareStringQuery.createQueryForNodesWithFieldLessThanOrEqualTo(usv, field, null);
} else {
upper = CompareStringQuery.createQueryForNodesWithFieldLessThan(usv, field, null);
}
return booleanQuery(lower, Occur.MUST, upper, Occur.MUST);
case OBJECT:
case URI:
case PATH:
case NAME:
case STRING:
case REFERENCE:
case WEAKREFERENCE:
case SIMPLEREFERENCE:
case BINARY:
throw new LuceneIndexException("Unsupported type for range query:" + type);
}
return new MatchNoDocsQuery();
}
protected Query createQuery( SetCriteria setCriteria ) {
DynamicOperand left = setCriteria.leftOperand();
int numRightOperands = setCriteria.rightOperands().size();
assert numRightOperands > 0;
if (numRightOperands == 1) {
StaticOperand rightOperand = setCriteria.rightOperands().iterator().next();
if (rightOperand instanceof Literal) {
return createQuery(left, Operator.EQUAL_TO, setCriteria.rightOperands().iterator().next(),null);
}
}
BooleanQuery.Builder builder = new BooleanQuery.Builder();
builder.setDisableCoord(true);
for (StaticOperand right : setCriteria.rightOperands()) {
if (right instanceof BindVariableName) {
// This single value is a variable name, which may evaluate to a single value or multiple values ...
BindVariableName var = (BindVariableName) right;
String bindVariableName = var.getBindVariableName();
Object value = variables.get(bindVariableName);
if (value == null) {
if (bindVariableName.startsWith(Subquery.VARIABLE_PREFIX)) {
// when subqueries are involved during the planning phase, they will not be resolved yet and therefore
// a 'null' value present in the variables map. However, since the index was called in the first place,
// we know it must apply to the constraint. Therefore, return all the docs in the index to make sure
// that during planning, this index is taken into account and can be compared with other "competing" indexes.
// Note that the real cardinality wil really be "at most" all documents (possibly less) depending on
// the actual subquery results. These will be resolved during the actual query-run time.
return new MatchAllDocsQuery();
}
throw new LuceneIndexException(JcrI18n.missingVariableValue.text(bindVariableName));
}
if (value instanceof Iterable<?>) {
for (Object resolvedValue : (Iterable<?>) value) {
if (resolvedValue == null) {
continue;
}
if (resolvedValue instanceof Object[]) {
// The row has multiple values (e.g., a multi-valued property) ...
for (Object val : (Object[]) resolvedValue) {
addQueryForSetConstraint(builder, left, val);
}
} else {
addQueryForSetConstraint(builder, left, resolvedValue);
}
}
} else {
addQueryForSetConstraint(builder, left, value);
}
} else {
Query rightQuery = createQuery(left, Operator.EQUAL_TO, right, null);
builder.add(rightQuery, Occur.SHOULD);
}
}
return builder.build();
}
private Query createQuery( Or or ) {
Query leftQuery = createQuery(or.left());
Query rightQuery = createQuery(or.right());
if (leftQuery == null) {
return rightQuery != null ? rightQuery : null;
} else if (rightQuery == null) {
return leftQuery;
}
return booleanQuery(leftQuery, Occur.SHOULD, rightQuery, Occur.SHOULD);
}
protected Query createQuery( And and ) {
Query leftQuery = createQuery(and.left());
Query rightQuery = createQuery(and.right());
if (leftQuery == null || rightQuery == null) {
return null;
}
return booleanQuery(leftQuery, Occur.MUST, rightQuery, Occur.MUST);
}
protected Query createQuery( PropertyExistence propertyExistence ) {
String field = propertyExistence.getPropertyName();
assert propertyTypesByName.containsKey(field); //or this index should not have been planned in the first place...
return new FieldExistsQuery(field);
}
private void addQueryForSetConstraint( BooleanQuery.Builder setQueryBuilder, DynamicOperand left, Object resolvedValue ) {
StaticOperand elementInRight = resolvedValue instanceof Literal ? (Literal) resolvedValue : new Literal(resolvedValue);
Query rightQuery = createQuery(left, Operator.EQUAL_TO, elementInRight, null);
setQueryBuilder.add(rightQuery, Occur.SHOULD);
}
protected Query createQuery( DynamicOperand left,
Operator operator,
StaticOperand right,
Function<String, String> caseOperation) {
// Handle the static operand ...
Object value = getSingleValueFromStaticOperand(right);
assert value != null;
// Address the dynamic operand ...
if (left instanceof PropertyValue) {
return createPropertyValueQuery((PropertyValue) left, operator, value, caseOperation);
} else if (left instanceof ReferenceValue) {
return createReferenceValueQuery((ReferenceValue) left, operator, value);
} else if (left instanceof Length) {
return createLengthQuery((Length) left, operator, value);
} else if (left instanceof LowerCase) {
LowerCase lowercase = (LowerCase) left;
return createQuery(lowercase.getOperand(), operator, right, String::toLowerCase);
} else if (left instanceof UpperCase) {
UpperCase uppercase = (UpperCase) left;
return createQuery(uppercase.getOperand(), operator, right, String::toUpperCase);
} else if (left instanceof NodeDepth) {
// this only applies to mode:depth
return longFieldQuery(depthField(), operator, value);
} else if (left instanceof NodePath) {
// this only applies to jcr:path
String field = stringFactory.create(JcrLexicon.PATH);
return pathFieldQuery(field, operator, value, caseOperation);
} else if (left instanceof NodeName) {
// this only applies to jcr:name
String field = stringFactory.create(JcrLexicon.NAME);
return nameFieldQuery(field, operator, value, caseOperation);
} else if (left instanceof NodeLocalName) {
// this only applies to mode:localName
String field = stringFactory.create(ModeShapeLexicon.LOCALNAME);
return stringFieldQuery(field, operator, value, caseOperation);
} else if (left instanceof Cast) {
Cast cast = (Cast) left;
return createQuery(cast.getOperand(), operator, right, caseOperation);
}
throw new LuceneIndexException("Unexpected DynamicOperand instance: class=" + (left != null ? left.getClass() : "null")
+ " and instance=" + left);
}
private String depthField() {
return stringFactory.create(ModeShapeLexicon.DEPTH);
}
protected Object getSingleValueFromStaticOperand( StaticOperand operand ) {
Object value = null;
if (operand instanceof Literal) {
Literal literal = (Literal) operand;
value = literal.value();
} else if (operand instanceof BindVariableName) {
BindVariableName variable = (BindVariableName) operand;
String variableName = variable.getBindVariableName();
value = variables.get(variableName);
if (value instanceof Iterable<?>) {
// We can only return one value ...
Iterator<?> iter = ((Iterable<?>) value).iterator();
if (iter.hasNext()) {
return iter.next();
}
value = null;
}
if (value == null) {
throw new LuceneIndexException(JcrI18n.missingVariableValue.text(variableName));
}
} else {
throw new IllegalArgumentException("Unknown operand type:" + operand);
}
return value;
}
protected BooleanQuery not( Query notted ) {
BooleanQuery.Builder builder = new BooleanQuery.Builder();
builder.setDisableCoord(true);
// We need at least some positive match, so get all docs ...
builder.add(new MatchAllDocsQuery(), Occur.SHOULD);
// Now apply the original query being 'NOT-ed' as a MUST_NOT occurrence ...
builder.add(notted, Occur.MUST_NOT);
return builder.build();
}
protected Query createPropertyValueQuery( PropertyValue propertyValue,
Operator operator,
Object value,
Function<String, String> caseOperation ) {
if (operator == Operator.LIKE) {
String stringValue = stringFactory.create(value);
if (!stringValue.contains("%") && !stringValue.contains("_") && !stringValue.contains("\\")) {
// The value is not a LIKE literal, so we can treat it as an '=' operator ...
operator = Operator.EQUAL_TO;
}
}
String propertyName = propertyValue.getPropertyName();
PropertyType valueType = propertyTypesByName.get(propertyName);
switch (valueType) {
case REFERENCE:
case WEAKREFERENCE:
case SIMPLEREFERENCE:
return stringFieldQuery(propertyName, operator, value, null);
case URI:
case STRING:
return stringFieldQuery(propertyName, operator, value, caseOperation);
case PATH:
return pathFieldQuery(propertyName, operator, value, caseOperation);
case NAME:
return nameFieldQuery(propertyName, operator, value, caseOperation);
case DECIMAL:
return decimalFieldQuery(propertyName, operator, value, caseOperation);
case DATE:
return dateFieldQuery(propertyName, operator, value);
case LONG:
return longFieldQuery(propertyName, operator, value);
case BOOLEAN:
return booleanFieldQuery(propertyName, operator, value);
case DOUBLE:
return doubleFieldQuery(propertyName, operator, value);
case BINARY:
case OBJECT:
default:
throw new IllegalArgumentException("Unsupported value type:" + valueType);
}
}
protected Query stringFieldQuery( String field, Operator operator, Object value, Function<String, String> caseOperation ) {
String stringValue = stringFactory.create(value);
switch (operator) {
case EQUAL_TO:
return CompareStringQuery.createQueryForNodesWithFieldEqualTo(stringValue, field, caseOperation);
case NOT_EQUAL_TO:
Query query = CompareStringQuery.createQueryForNodesWithFieldEqualTo(stringValue, field,
caseOperation);
return not(query);
case GREATER_THAN:
return CompareStringQuery.createQueryForNodesWithFieldGreaterThan(stringValue, field, caseOperation);
case GREATER_THAN_OR_EQUAL_TO:
return CompareStringQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(stringValue, field,
caseOperation);
case LESS_THAN:
return CompareStringQuery.createQueryForNodesWithFieldLessThan(stringValue, field, caseOperation);
case LESS_THAN_OR_EQUAL_TO:
return CompareStringQuery.createQueryForNodesWithFieldLessThanOrEqualTo(stringValue, field,
caseOperation);
case LIKE:
return CompareStringQuery.createQueryForNodesWithFieldLike(stringValue, field, caseOperation);
default:
throw new IllegalArgumentException("Unknown operator:" + operator);
}
}
protected Query decimalFieldQuery( String field, Operator operator, Object value, Function<String, String> caseOperation ) {
String decimalString = null;
if (operator != Operator.LIKE) {
// Decimal values are stored in a special lexicographically sortable form, so we have to
// convert the value to this ...
BigDecimal decimalValue = factories.getDecimalFactory().create(value);
decimalString = FieldUtil.decimalToString(decimalValue);
} else {
// search for LIKE as a regular string expression
decimalString = stringFactory.create(value);
}
return stringFieldQuery(field, operator, decimalString, caseOperation);
}
protected Query booleanFieldQuery( String field, Operator operator, Object value ) {
Boolean booleanValue = factories.getBooleanFactory().create(value);
if (booleanValue) {
switch (operator) {
case EQUAL_TO:
return LegacyNumericRangeQuery.newIntRange(field, 0, 1, false, true);
case NOT_EQUAL_TO:
return LegacyNumericRangeQuery.newIntRange(field, 0, 1, true, false);
case GREATER_THAN_OR_EQUAL_TO:
return LegacyNumericRangeQuery.newIntRange(field, 1, 1, true, true);
case LESS_THAN_OR_EQUAL_TO:
return LegacyNumericRangeQuery.newIntRange(field, 0, 1, true, true);
case GREATER_THAN:
// Can't be greater than 'true', per JCR spec
return new MatchNoDocsQuery();
case LESS_THAN:
// 'false' is less than 'true' ...
return LegacyNumericRangeQuery.newIntRange(field, 0, 0, true, true);
case LIKE:
// This is not supported
throw new LuceneIndexException(LuceneIndexProviderI18n.invalidOperatorForPropertyType.text(Operator.LIKE,
PropertyType.BOOLEAN));
default:
throw new IllegalArgumentException("Unknown operator:" + operator);
}
} else {
switch (operator) {
case EQUAL_TO:
return LegacyNumericRangeQuery.newIntRange(field, 0, 1, true, false);
case NOT_EQUAL_TO:
return LegacyNumericRangeQuery.newIntRange(field, 0, 1, false, true);
case GREATER_THAN_OR_EQUAL_TO:
return LegacyNumericRangeQuery.newIntRange(field, 0, 1, true, true);
case LESS_THAN_OR_EQUAL_TO:
return LegacyNumericRangeQuery.newIntRange(field, 0, 0, true, true);
case GREATER_THAN:
// 'true' is greater than 'false' ...
return LegacyNumericRangeQuery.newIntRange(field, 1, 1, true, true);
case LESS_THAN:
// Can't be less than 'false', per JCR spec
return new MatchNoDocsQuery();
case LIKE:
// This is not supported
throw new LuceneIndexException(LuceneIndexProviderI18n.invalidOperatorForPropertyType.text(Operator.LIKE,
PropertyType.BOOLEAN));
default:
throw new IllegalArgumentException("Unknown operator:" + operator);
}
}
}
protected Query longFieldQuery( String field, Operator operator, Object value ) {
Long longMinimum = Long.MIN_VALUE;
Long longMaximum = Long.MAX_VALUE;
long longValue = factories.getLongFactory().create(value);
switch (operator) {
case EQUAL_TO:
if (longValue < longMinimum || longValue > longMaximum) {
return new MatchNoDocsQuery();
}
return LegacyNumericRangeQuery.newLongRange(field, longValue, longValue, true, true);
case NOT_EQUAL_TO:
if (longValue < longMinimum || longValue > longMaximum) {
return new MatchAllDocsQuery();
}
Query lowerRange = LegacyNumericRangeQuery.newLongRange(field, longMinimum, longValue, true, false);
Query upperRange = LegacyNumericRangeQuery.newLongRange(field, longValue, longMaximum, false, true);
return booleanQuery(lowerRange, Occur.SHOULD, upperRange, Occur.SHOULD);
case GREATER_THAN:
if (longValue > longMaximum) {
return new MatchNoDocsQuery();
}
return LegacyNumericRangeQuery.newLongRange(field, longValue, longMaximum, false, true);
case GREATER_THAN_OR_EQUAL_TO:
if (longValue > longMaximum) {
return new MatchNoDocsQuery();
}
return LegacyNumericRangeQuery.newLongRange(field, longValue, longMaximum, true, true);
case LESS_THAN:
if (longValue < longMinimum) {
return new MatchNoDocsQuery();
}
return LegacyNumericRangeQuery.newLongRange(field, longMinimum, longValue, true, false);
case LESS_THAN_OR_EQUAL_TO:
if (longValue < longMinimum) {
return new MatchNoDocsQuery();
}
return LegacyNumericRangeQuery.newLongRange(field, longMinimum, longValue, true, true);
case LIKE:
throw new LuceneIndexException(LuceneIndexProviderI18n.invalidOperatorForPropertyType.text(operator,
PropertyType.LONG));
default:
throw new IllegalArgumentException("Unknown operator:" + operator);
}
}
protected Query doubleFieldQuery( String field, Operator operator, Object value ) {
double doubleValue = factories.getDoubleFactory().create(value);
Double doubleMinimum = Double.MIN_VALUE;
Double doubleMaximum = Double.MAX_VALUE;
switch (operator) {
case EQUAL_TO:
if (doubleValue < doubleMinimum || doubleValue > doubleMaximum) {
return new MatchNoDocsQuery();
}
return LegacyNumericRangeQuery.newDoubleRange(field, doubleValue, doubleValue, true, true);
case NOT_EQUAL_TO:
if (doubleValue < doubleMinimum || doubleValue > doubleMaximum) {
return new MatchAllDocsQuery();
}
Query lowerRange = LegacyNumericRangeQuery.newDoubleRange(field, doubleMinimum, doubleValue, true,
false);
Query upperRange = LegacyNumericRangeQuery.newDoubleRange(field, doubleValue, doubleMaximum, false,
true);
return booleanQuery(lowerRange, Occur.SHOULD, upperRange, Occur.SHOULD);
case GREATER_THAN:
if (doubleValue > doubleMaximum) {
return new MatchNoDocsQuery();
}
return LegacyNumericRangeQuery.newDoubleRange(field, doubleValue, doubleMaximum, false, true);
case GREATER_THAN_OR_EQUAL_TO:
if (doubleValue > doubleMaximum) {
return new MatchNoDocsQuery();
}
return LegacyNumericRangeQuery.newDoubleRange(field, doubleValue, doubleMaximum, true, true);
case LESS_THAN:
if (doubleValue < doubleMinimum) {
return new MatchNoDocsQuery();
}
return LegacyNumericRangeQuery.newDoubleRange(field, doubleMinimum, doubleValue, true, false);
case LESS_THAN_OR_EQUAL_TO:
if (doubleValue < doubleMinimum) {
return new MatchNoDocsQuery();
}
return LegacyNumericRangeQuery.newDoubleRange(field, doubleMinimum, doubleValue, true, true);
case LIKE:
// should never happen (the double conversion should've failed)
assert false;
default:
throw new IllegalArgumentException("Unknown operator:" + operator);
}
}
protected Query dateFieldQuery( String field, Operator operator, Object value ) {
Long longMinimum = Long.MIN_VALUE;
Long longMaximum = Long.MAX_VALUE;
long millis = factories.getDateFactory().create(value).getMilliseconds();
switch (operator) {
case EQUAL_TO:
if (millis < longMinimum || millis > longMaximum) {
return new MatchNoDocsQuery();
}
return LegacyNumericRangeQuery.newLongRange(field, millis, millis, true, true);
case NOT_EQUAL_TO:
if (millis < longMinimum || millis > longMaximum) {
return new MatchAllDocsQuery();
}
Query lowerRange = LegacyNumericRangeQuery.newLongRange(field, longMinimum, millis, true, false);
Query upperRange = LegacyNumericRangeQuery.newLongRange(field, millis, longMaximum, false, true);
return booleanQuery(lowerRange, Occur.SHOULD, upperRange, Occur.SHOULD);
case GREATER_THAN:
if (millis > longMaximum) {
return new MatchNoDocsQuery();
}
return LegacyNumericRangeQuery.newLongRange(field, millis, longMaximum, false, true);
case GREATER_THAN_OR_EQUAL_TO:
if (millis > longMaximum) {
return new MatchNoDocsQuery();
}
return LegacyNumericRangeQuery.newLongRange(field, millis, longMaximum, true, true);
case LESS_THAN:
if (millis < longMinimum) {
return new MatchNoDocsQuery();
}
return LegacyNumericRangeQuery.newLongRange(field, longMinimum, millis, true, false);
case LESS_THAN_OR_EQUAL_TO:
if (millis < longMinimum) {
return new MatchNoDocsQuery();
}
return LegacyNumericRangeQuery.newLongRange(field, longMinimum, millis, true, true);
case LIKE:
// should never happen (the millis conversion should've failed)
assert false;
default:
throw new IllegalArgumentException("Unknown operator:" + operator);
}
}
protected Query createReferenceValueQuery( ReferenceValue referenceValue, Operator operator, Object value ) {
String field = referenceValue.getPropertyName();
if (field != null) {
return stringFieldQuery(field, operator, value, null);
}
// we are being asked to query for all the references fields that apply to this index, so we need to collect them first
List<String> referenceFields = collectReferenceFieldNames(referenceValue);
assert !referenceFields.isEmpty(); // this can't be empty because this index was called in the first place....
if (referenceFields.size() == 1) {
return stringFieldQuery(referenceFields.get(0), operator, value, null);
} else {
BooleanQuery.Builder builder = new BooleanQuery.Builder();
builder.setDisableCoord(true);
for (String fieldName : referenceFields) {
Query fieldQuery = stringFieldQuery(fieldName, operator, value, null);
builder.add(fieldQuery, Occur.SHOULD);
}
return builder.build();
}
}
protected List<String> collectReferenceFieldNames( ReferenceValue referenceValue ) {
List<String> result = new ArrayList<>();
boolean includeWeakReferences = referenceValue.includesWeakReferences();
boolean includeSimpleReferences = referenceValue.includeSimpleReferences();
for (Map.Entry<String, PropertyType> propertyEntry : propertyTypesByName.entrySet()) {
PropertyType propertyType = propertyEntry.getValue();
switch (propertyType) {
case WEAKREFERENCE: {
if (includeWeakReferences) {
result.add(propertyEntry.getKey());
}
break;
}
case SIMPLEREFERENCE: {
if (includeSimpleReferences) {
result.add(propertyEntry.getKey());
}
break;
}
case REFERENCE: {
result.add(propertyEntry.getKey());
break;
}
}
}
return result;
}
protected Query createLengthQuery( Length propertyLength, Operator operator, Object value ) {
assert propertyLength != null;
assert value != null;
long length = factories.getLongFactory().create(value);
if (length <= 0L) {
return new MatchNoDocsQuery();
}
String field = FieldUtil.lengthField(propertyLength.getPropertyValue().getPropertyName());
switch (operator) {
case EQUAL_TO:
return LegacyNumericRangeQuery.newLongRange(field, length, length, true, true);
case NOT_EQUAL_TO:
Query upper = LegacyNumericRangeQuery.newLongRange(field, length, Long.MAX_VALUE, false, false);
Query lower = LegacyNumericRangeQuery.newLongRange(field, 0L, length, true, false);
return booleanQuery(upper, Occur.SHOULD, lower, Occur.SHOULD);
case GREATER_THAN:
return LegacyNumericRangeQuery.newLongRange(field, length, Long.MAX_VALUE, false, false);
case GREATER_THAN_OR_EQUAL_TO:
return LegacyNumericRangeQuery.newLongRange(field, length, Long.MAX_VALUE, true, false);
case LESS_THAN:
return LegacyNumericRangeQuery.newLongRange(field, 0L, length, true, false);
case LESS_THAN_OR_EQUAL_TO:
return LegacyNumericRangeQuery.newLongRange(field, 0L, length, true, true);
case LIKE:
// This is not allowed ...
throw new LuceneIndexException(LuceneIndexProviderI18n.invalidOperatorForOperand.text(operator,
propertyLength));
default: {
throw new IllegalArgumentException("Unknown operator:" + operator);
}
}
}
protected Query pathFieldQuery( String field, Operator operator, Object value, Function<String, String> caseOperation ) {
Path path = null;
if (operator != Operator.LIKE) {
path = !(value instanceof Path) ? pathFactory.create(value) : (Path) value;
}
switch (operator) {
case EQUAL_TO:
return CompareStringQuery.createQueryForNodesWithFieldEqualTo(stringFactory.create(path), field,
caseOperation);
case NOT_EQUAL_TO:
return not(CompareStringQuery.createQueryForNodesWithFieldEqualTo(stringFactory.create(path), field,
caseOperation));
case LIKE:
String likeExpression = stringFactory.create(value);
// the paths are stored in the index via stringFactory.create, which doesn't have the "1" index for SNS...
likeExpression = likeExpression.replaceAll("\\[1\\]", "");
if (likeExpression.contains("[%]")) {
// We can't use '[%]' because we only want to match digits,
// so handle this using a regex ...
String regex = likeExpression;
regex = regex.replace("[%]", "(\\[[0-9]+\\])?");
regex = regex.replaceAll("\\[(\\d+)\\]", "\\\\[$1\\\\]");
//regex = regex.replace("]", "\\]");
regex = regex.replace("*", ".*");
regex = regex.replace("%", ".*").replace("_", ".");
// Now create a regex query ...
Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
return new RegexQuery(field, pattern, caseOperation);
} else {
return CompareStringQuery.createQueryForNodesWithFieldLike(likeExpression, field, caseOperation);
}
case GREATER_THAN:
return ComparePathQuery.createQueryForNodesWithPathGreaterThan(path, field, factories, caseOperation);
case GREATER_THAN_OR_EQUAL_TO:
return ComparePathQuery.createQueryForNodesWithPathGreaterThanOrEqualTo(path, field, factories, caseOperation);
case LESS_THAN:
return ComparePathQuery.createQueryForNodesWithPathLessThan(path, field, factories, caseOperation);
case LESS_THAN_OR_EQUAL_TO:
return ComparePathQuery.createQueryForNodesWithPathLessThanOrEqualTo(path, field, factories, caseOperation);
default: {
throw new IllegalArgumentException("Unknown operator:" + operator);
}
}
}
protected Query nameFieldQuery( String field, Operator operator, Object value, Function<String, String> caseOperation ) {
Name name = null;
if (operator != Operator.LIKE) {
name = !(value instanceof Name) ? factories.getNameFactory().create(value) : (Name) value;
}
switch (operator) {
case EQUAL_TO:
return CompareNameQuery.createQueryForNodesWithNameEqualTo(name, field, factories, caseOperation);
case NOT_EQUAL_TO:
Query equalToQuery = CompareNameQuery.createQueryForNodesWithNameEqualTo(name, field, factories, caseOperation);
return not(equalToQuery);
case GREATER_THAN:
return CompareNameQuery.createQueryForNodesWithNameGreaterThan(name, field, factories, caseOperation);
case GREATER_THAN_OR_EQUAL_TO:
return CompareNameQuery.createQueryForNodesWithNameGreaterThanOrEqualTo(name, field, factories, caseOperation);
case LESS_THAN:
return CompareNameQuery.createQueryForNodesWithNameLessThan(name, field, factories, caseOperation);
case LESS_THAN_OR_EQUAL_TO:
return CompareNameQuery.createQueryForNodesWithNameLessThanOrEqualTo(name, field, factories, caseOperation);
case LIKE:
// we can only process the value as a string...
String likeExpression = stringFactory.create(value);
return CompareStringQuery.createQueryForNodesWithFieldLike(likeExpression, field, caseOperation);
default:
throw new IllegalArgumentException("Unknown operator:" + operator);
}
}
protected BooleanQuery booleanQuery( Query leftQuery, Occur leftOccur, Query rightQuery, Occur rightOccur ) {
BooleanQuery.Builder builder = new BooleanQuery.Builder();
builder.setDisableCoord(true);
builder.add(leftQuery, leftOccur);
builder.add(rightQuery, rightOccur);
return builder.build();
}
protected static class SingleColumnQueryFactory extends LuceneQueryFactory {
private SingleColumnQueryFactory( ValueFactories factories, Map<String, Object> variables,
Map<String, PropertyType> propertyTypesByName ) {
super(factories, variables, propertyTypesByName);
}
@Override
protected Query createQuery( PropertyExistence propertyExistence ) {
// since there can be only one property per indexed document, the fact that this was called means it applies to all the
// documents of the stored index
return new MatchAllDocsQuery();
}
@Override
protected List<String> collectReferenceFieldNames( ReferenceValue referenceValue ) {
// there should be just one column for these types of indexes and that column should already have the reference value
assert propertyTypesByName.size() == 1;
return Collections.singletonList(propertyName());
}
protected String propertyName() {
// these indexes can only apply to 1 single property
return propertyTypesByName.keySet().iterator().next();
}
}
protected static class TextQueryFactory extends SingleColumnQueryFactory {
private static final PhraseQuery EMPTY_PHRASE_QUERY = new PhraseQuery.Builder().build();
private final Analyzer analyzer;
private TextQueryFactory( ValueFactories factories,
Map<String, Object> variables,
Map<String, PropertyType> propertyTypesByName,
LuceneConfig config ) {
super(factories, variables, propertyTypesByName);
this.analyzer = config.getAnalyzer();
}
@Override
public boolean scoreDocuments() {
return true;
}
@Override
protected Query createQuery( FullTextSearch search ) {
String propertyName = search.getPropertyName();
if (propertyName == null) {
// the search if for * (all properties) so this query should be done for the current index's property
propertyName = propertyName();
}
StaticOperand expression = search.getFullTextSearchExpression();
Object value = getSingleValueFromStaticOperand(expression);
try {
String valueString = value instanceof Value ? ((Value) value).getString() : stringFactory.create(value);
search = search.withFullTextExpression(valueString);
return createQuery(propertyName, search.getTerm());
} catch (RepositoryException e) {
throw new LuceneIndexException(e);
}
}
protected Query createQuery( String fieldName, FullTextSearch.Term term ) {
assert fieldName != null;
if (term instanceof FullTextSearch.Conjunction) {
return createConjunctionQuery(fieldName, (FullTextSearch.Conjunction) term);
}
if (term instanceof FullTextSearch.Disjunction) {
return createDisjunctionQuery(fieldName, (FullTextSearch.Disjunction) term);
}
if (term instanceof FullTextSearch.SimpleTerm) {
return createSimpleTermQuery(fieldName, (FullTextSearch.SimpleTerm) term);
}
if (term instanceof FullTextSearch.NegationTerm) {
return createNegationTermQuery(fieldName, (FullTextSearch.NegationTerm) term);
}
throw new IllegalArgumentException("Unknown term instance:" + term);
}
private Query createNegationTermQuery( String fieldName, FullTextSearch.NegationTerm negation ) {
BooleanQuery.Builder builder = new BooleanQuery.Builder();
builder.setDisableCoord(true);
Query subQuery = createQuery(fieldName, negation.getNegatedTerm());
if (!EMPTY_PHRASE_QUERY.equals(subQuery)) {
builder.add(subQuery, Occur.MUST_NOT);
// need to add at least a positive match
builder.add(new MatchAllDocsQuery(), Occur.FILTER);
return builder.build();
} else {
return new MatchAllDocsQuery();
}
}
private Query createSimpleTermQuery( String fieldName, FullTextSearch.SimpleTerm simple ) {
try {
if (QueryUtil.hasWildcardCharacters(simple.getValue())) {
return createWildcardQuery(fieldName, simple);
}
PhraseQuery.Builder builder = new PhraseQuery.Builder();
builder.setSlop(0); // terms must be adjacent
String expression = simple.getValue();
// Run the expression through the Lucene analyzer to extract the terms ...
try (TokenStream stream = analyzer.tokenStream(fieldName, expression)) {
stream.reset();
CharTermAttribute termAttribute = stream.addAttribute(CharTermAttribute.class);
while (stream.incrementToken()) {
// The term attribute object has been modified to contain the next term ...
String analyzedTerm = termAttribute.toString();
builder.add(new Term(fieldName, analyzedTerm));
}
stream.end();
}
return builder.build();
} catch (Exception e) {
throw new LuceneIndexException(e);
}
}
private Query createDisjunctionQuery( String fieldName, FullTextSearch.Disjunction disjunction ) {
BooleanQuery.Builder builder = new BooleanQuery.Builder();
builder.setDisableCoord(true);
boolean atLeastOnePositiveClause = false;
for (FullTextSearch.Term nested : disjunction) {
if (nested instanceof FullTextSearch.NegationTerm) {
//Lucene does not have a SHOULD_NOT and MUST_NOT is too strong for disjunctions...
} else {
Query subQuery = createQuery(fieldName, nested);
if (!EMPTY_PHRASE_QUERY.equals(subQuery)) {
atLeastOnePositiveClause = true;
builder.add(subQuery, Occur.SHOULD);
}
}
}
if (!atLeastOnePositiveClause) {
// there are only MUST_NOT terms so we should add one positive for this to work
builder.add(new MatchAllDocsQuery(), Occur.FILTER);
}
return builder.build();
}
private Query createConjunctionQuery( String fieldName, FullTextSearch.Conjunction conjunction ) {
BooleanQuery.Builder builder = new BooleanQuery.Builder();
builder.setDisableCoord(true);
boolean atLeastOnePositiveClause = false;
for (FullTextSearch.Term nested : conjunction) {
if (nested instanceof FullTextSearch.NegationTerm) {
Query subQuery = createQuery(fieldName, ((FullTextSearch.NegationTerm) nested).getNegatedTerm());
if (!EMPTY_PHRASE_QUERY.equals(subQuery)) {
builder.add(subQuery, Occur.MUST_NOT);
}
} else {
Query subQuery = createQuery(fieldName, nested);
if (!EMPTY_PHRASE_QUERY.equals(subQuery)) {
atLeastOnePositiveClause = true;
builder.add(subQuery, Occur.MUST);
}
}
}
if (!atLeastOnePositiveClause) {
// there are only MUST_NOT terms so we should add one positive for this to work
builder.add(new MatchAllDocsQuery(), Occur.FILTER);
}
return builder.build();
}
private Query createWildcardQuery( final String fieldName, FullTextSearch.SimpleTerm simple ) throws ParseException {
// Use the standard parser, but instead of wildcard queries (which don't work with leading
// wildcards) we should use our like queries (which often use RegexQuery where applicable) ...
//as an alternative, for leading wildcards one could call parser.setAllowLeadingWildcard(true);
//and use the default Lucene query parser
QueryParser parser = new QueryParser(fieldName, analyzer) {
@Override
protected Query getWildcardQuery( String field, String termStr ) {
return CompareStringQuery.createQueryForNodesWithFieldLike(termStr.toLowerCase(), fieldName,
null);
}
};
String expression = simple.getValue();
// The ComplexPhraseQueryParser only understands the '?' and '*' as being wildcards ...
expression = expression.replaceAll("(?<![\\\\])_", "?");
expression = expression.replaceAll("(?<![\\\\])%", "*");
// // Replace any '-' between tokens, except when preceded or followed by a digit, '*', or '?' ...
expression = expression.replaceAll("((?<![\\d*?]))[-]((?![\\d*?]))", "$1 $2");
// Then use the parser ...
return parser.parse(expression);
}
}
}