Java tutorial
/* * ModeShape (http://www.modeshape.org) * See the COPYRIGHT.txt file distributed with this work for information * regarding copyright ownership. Some portions may be licensed * to Red Hat, Inc. under one or more contributor license agreements. * See the AUTHORS.txt file in the distribution for a full listing of * individual contributors. * * ModeShape is free software. Unless otherwise indicated, all code in ModeShape * is licensed to you under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * ModeShape is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.modeshape.jcr.query.lucene.basic; import java.math.BigDecimal; import java.util.regex.Pattern; import javax.jcr.query.qom.Length; import javax.jcr.query.qom.NodeLocalName; import javax.jcr.query.qom.NodeName; import javax.jcr.query.qom.PropertyValue; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.index.Term; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanClause.Occur; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.NumericRangeQuery; import org.apache.lucene.search.PrefixQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.regex.JavaUtilRegexCapabilities; import org.apache.lucene.search.regex.RegexQuery; import org.apache.lucene.util.Version; import org.hibernate.search.SearchFactory; import org.modeshape.jcr.api.JcrConstants; 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.api.value.DateTime; import org.modeshape.jcr.query.QueryContext; import org.modeshape.jcr.query.lucene.CaseOperations; import org.modeshape.jcr.query.lucene.CaseOperations.CaseOperation; import org.modeshape.jcr.query.lucene.CompareLengthQuery; import org.modeshape.jcr.query.lucene.CompareNameQuery; import org.modeshape.jcr.query.lucene.ComparePathQuery; import org.modeshape.jcr.query.lucene.CompareStringQuery; import org.modeshape.jcr.query.lucene.FieldUtil; import org.modeshape.jcr.query.lucene.LuceneQueryFactory; import org.modeshape.jcr.query.lucene.MatchNoneQuery; import org.modeshape.jcr.query.lucene.basic.NodeInfoIndex.FieldName; import org.modeshape.jcr.query.model.ReferenceValue; import org.modeshape.jcr.query.model.SelectorName; import org.modeshape.jcr.query.validate.Schemata; import org.modeshape.jcr.value.Path; import org.modeshape.jcr.value.PropertyType; import org.modeshape.jcr.value.ValueFormatException; /** * The {@link LuceneQueryFactory} customization that produces {@link Query} objects based upon the {@link BasicLuceneSchema}. */ public class BasicLuceneQueryFactory extends LuceneQueryFactory { protected static final int MIN_DEPTH = 0; protected static final int MAX_DEPTH = 1000; protected static final int MIN_SNS_INDEX = 1; protected static final int MAX_SNS_INDEX = 10000000; // assume there won't be more than 10M same-name-siblings /** * @param context * @param searchFactory * @param version the Lucene version */ public BasicLuceneQueryFactory(QueryContext context, SearchFactory searchFactory, Version version) { super(context, searchFactory, version); } protected final String pathAsString(Path path) { assert path != null; if (path.isRoot()) return "/"; StringBuilder sb = new StringBuilder(); for (Path.Segment segment : path) { sb.append('/'); sb.append(stringFactory.create(segment.getName())); sb.append('['); sb.append(segment.getIndex()); sb.append(']'); } return sb.toString(); } @Override protected Analyzer getFullTextSearchAnalyzer() { return searchFactory.getAnalyzer(NodeInfo.class); } /** * {@inheritDoc} * <p> * If a property name is provided, then the resulting field name is generated from the * {@link NodeInfoIndex.FieldName#FULL_TEXT_PREFIX full-text prefix} and the property name. Otherwise, the result is the field * name where the full-text terms for the entire node are indexed. * </p> */ @Override protected String fullTextFieldName(String propertyName) { return propertyName == null ? FieldName.FULL_TEXT : FieldName.FULL_TEXT_PREFIX + propertyName; } @Override protected Query findAllNodesBelow(Path ancestorPath) { // Find the path of the parent ... String stringifiedPath = pathAsString(ancestorPath); if (!ancestorPath.isRoot()) { // Append a '/' to the parent path, and we'll only get decendants ... stringifiedPath = stringifiedPath + '/'; } // Create a prefix query ... return new PrefixQuery(new Term(FieldName.PATH, stringifiedPath)); } @Override protected Query findAllNodesAtOrBelow(Path ancestorPath) { if (ancestorPath.isRoot()) { return new MatchAllDocsQuery(); } // Find the path of the parent ... String stringifiedPath = pathAsString(ancestorPath); // Do not append a '/' to the parent path ... otherwise we won't get the parent node // Create a prefix query ... return new PrefixQuery(new Term(FieldName.PATH, stringifiedPath)); } @Override protected Query findChildNodes(Path parentPath) { // Create a query to find all descendants ... Query descendants = findAllNodesBelow(parentPath); // And another to find all nodes at the depth of the children ... int childrenDepth = parentPath.size() + 1; Query depthQuery = NumericRangeQuery.newIntRange(FieldName.DEPTH, childrenDepth, childrenDepth, true, true); // Now combine ... BooleanQuery combinedQuery = new BooleanQuery(); combinedQuery.add(descendants, Occur.MUST); combinedQuery.add(depthQuery, Occur.MUST); return combinedQuery; } @Override protected Query findNodeAt(Path path) { if (path.isRoot()) { // Look for the root node using the depth (which is hopefully the fastest) ... return NumericRangeQuery.newIntRange(FieldName.DEPTH, 0, 0, true, true); } String stringifiedPath = pathAsString(path); return new TermQuery(new Term(FieldName.PATH, stringifiedPath)); } @Override protected Query findNodesLike(SelectorName selectorName, String fieldName, String likeExpression, CaseOperation caseOperation) { if (caseOperation == null) caseOperation = CaseOperations.AS_IS; return CompareStringQuery.createQueryForNodesWithFieldLike(likeExpression, fieldName, factories, caseOperation); } protected Query findNodesLike(String fieldName, String likeExpression, CaseOperation caseOperation) { if (caseOperation == null) caseOperation = CaseOperations.AS_IS; return CompareStringQuery.createQueryForNodesWithFieldLike(likeExpression, fieldName, factories, caseOperation); } @Override protected Query findNodesWith(SelectorName selectorName, Length propertyLength, Operator operator, Object value) { assert propertyLength != null; assert value != null; PropertyValue propertyValue = propertyLength.getPropertyValue(); String field = stringFactory.create(propertyValue.getPropertyName()); long length = factories.getLongFactory().create(value).longValue(); if (length <= 0L) return new MatchNoneQuery(); if (JcrConstants.JCR_NAME.equals(field) || JcrConstants.JCR_PATH.equals(field) || JcrConstants.MODE_LOCAL_NAME.equals(field)) { // We can actually use the stored field ... switch (operator) { case EQUAL_TO: return CompareLengthQuery.createQueryForNodesWithFieldEqualTo(length, field, factories); case NOT_EQUAL_TO: return CompareLengthQuery.createQueryForNodesWithFieldNotEqualTo(length, field, factories); case GREATER_THAN: return CompareLengthQuery.createQueryForNodesWithFieldGreaterThan(length, field, factories); case GREATER_THAN_OR_EQUAL_TO: return CompareLengthQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(length, field, factories); case LESS_THAN: return CompareLengthQuery.createQueryForNodesWithFieldLessThan(length, field, factories); case LESS_THAN_OR_EQUAL_TO: return CompareLengthQuery.createQueryForNodesWithFieldLessThanOrEqualTo(length, field, factories); case LIKE: // This is not allowed ... assert false; break; } } else { // We should use the LONG field that begins with ':len:' ... field = FieldName.LENGTH_PREFIX + field; switch (operator) { case EQUAL_TO: return NumericRangeQuery.newLongRange(field, length, length, true, true); case NOT_EQUAL_TO: Query upper = NumericRangeQuery.newLongRange(field, length, Long.MAX_VALUE, false, false); Query lower = NumericRangeQuery.newLongRange(field, 0L, length, true, false); BooleanQuery query = new BooleanQuery(); query.add(new BooleanClause(upper, Occur.SHOULD)); query.add(new BooleanClause(lower, Occur.SHOULD)); return query; case GREATER_THAN: return NumericRangeQuery.newLongRange(field, length, Long.MAX_VALUE, false, false); case GREATER_THAN_OR_EQUAL_TO: return NumericRangeQuery.newLongRange(field, length, Long.MAX_VALUE, true, false); case LESS_THAN: return NumericRangeQuery.newLongRange(field, 0L, length, true, false); case LESS_THAN_OR_EQUAL_TO: return NumericRangeQuery.newLongRange(field, 0L, length, true, true); case LIKE: // This is not allowed ... assert false; break; } } return null; } @Override protected Query findNodesWith(SelectorName selectorName, PropertyValue propertyValue, Operator operator, Object value, CaseOperation caseOperation) { if (caseOperation == null) caseOperation = CaseOperations.AS_IS; String field = propertyValue.getPropertyName(); Schemata.Column metadata = getMetadataFor(selectorName, field); if (metadata != null) { PropertyType requiredType = metadata.getRequiredType(); PropertyType valueType = PropertyType.discoverType(value); // The supplied value might not match the required type. If it doesn't, then the client issuing the query // has different expectations on what values are stored in the index. If the types are different, then // we should compute a query based upon the required type (which converts the supplied value) *and* // a query based upon the actual type; and we can OR these together. Query query1 = findNodesWith(selectorName, propertyValue, operator, value, caseOperation, requiredType, metadata); if (requiredType == valueType) { return query1; } // Otherwise the types are different, so build the same query using the actual type ... Query query2 = findNodesWith(selectorName, propertyValue, operator, value, caseOperation, valueType, metadata); if (query1.equals(query2)) return query1; if (operator == Operator.NOT_EQUAL_TO) { // We actually want to AND the negated results ... BooleanQuery result = new BooleanQuery(); result.add(new BooleanClause(query1, Occur.MUST)); result.add(new BooleanClause(query2, Occur.MUST)); return result; } BooleanQuery result = new BooleanQuery(); result.add(new BooleanClause(query1, Occur.SHOULD)); result.add(new BooleanClause(query2, Occur.SHOULD)); return result; } assert metadata == null; if (!(value instanceof String)) { // This is due to an explicit cast, so treat it as the actual value ... PropertyType type = PropertyType.discoverType(value); return findNodesWith(selectorName, propertyValue, operator, value, caseOperation, type, metadata); } if (NodeInfoIndex.FieldName.WORKSPACE.equals(field)) { String strValue = stringFactory.create(value); return findNodesWith(selectorName, propertyValue, operator, strValue, caseOperation, PropertyType.STRING, null); } // Otherwise, the metadata is null and the value is a string. We can't find metadata if the property is residual, // and since the value is a string, we may be able to represent the value using different types. So rather than // determining the type from the string value, we can try converting the value to the different types and see // which ones work. If there are multiple conversions (including string), then we can OR them together. BooleanQuery orOfValues = new BooleanQuery(); boolean checkBoolean = false; boolean checkDate = true; try { Long lValue = factories.getLongFactory().create(value); Query query = findNodesWith(selectorName, propertyValue, operator, lValue, caseOperation, PropertyType.LONG, null); if (query != null) { orOfValues.add(query, Occur.SHOULD); } checkBoolean = lValue.longValue() == 1L || lValue.longValue() == 0L; checkDate = false; // no need to check the date, as we'd just convert it to a long and we've already added that } catch (ValueFormatException e) { // Not a long value ... } try { Double dValue = factories.getDoubleFactory().create(value); Query query = findNodesWith(selectorName, propertyValue, operator, dValue, caseOperation, PropertyType.DOUBLE, null); if (query != null) { orOfValues.add(query, Occur.SHOULD); } } catch (ValueFormatException e) { // Not a long value ... } if (checkBoolean) { try { Boolean b = factories.getBooleanFactory().create(value); Query query = findNodesWith(selectorName, propertyValue, operator, b, caseOperation, PropertyType.BOOLEAN, null); if (query != null) { orOfValues.add(query, Occur.SHOULD); } } catch (ValueFormatException e) { // Not a long value ... } } if (checkDate) { try { DateTime date = factories.getDateFactory().create(value); Query query = findNodesWith(selectorName, propertyValue, operator, date, caseOperation, PropertyType.DATE, null); if (query != null) { orOfValues.add(query, Occur.SHOULD); } } catch (ValueFormatException e) { // Not a long value ... } } // Finally treat it as a string ... String strValue = stringFactory.create(value); Query strQuery = findNodesWith(selectorName, propertyValue, operator, strValue, caseOperation, PropertyType.STRING, null); if (orOfValues.clauses().isEmpty()) { return strQuery; } orOfValues.add(strQuery, Occur.SHOULD); return orOfValues; } protected Query findNodesWith(SelectorName selectorName, PropertyValue propertyValue, Operator operator, Object value, CaseOperation caseOperation, PropertyType valueType, Schemata.Column metadata) { if (caseOperation == null) caseOperation = CaseOperations.AS_IS; String field = propertyValue.getPropertyName(); if (valueType == PropertyType.OBJECT) { // There is no known/prescribed property type, so match our criteria based upon the type of value we have ... valueType = PropertyType.discoverType(value); } if (operator == Operator.LIKE) { String stringValue = stringFactory.create(value); if (stringValue.indexOf('%') != -1 || stringValue.indexOf('_') != -1 || stringValue.indexOf('\\') != -1) { // This value is not a literal value ... valueType = PropertyType.STRING; } else { // The value is not a LIKE literal, so we can treat it as an '=' operator ... operator = Operator.EQUAL_TO; } } switch (valueType) { case REFERENCE: case WEAKREFERENCE: case UUID: case PATH: case NAME: case URI: case STRING: String stringValue = stringFactory.create(value); if (value instanceof Path) { stringValue = pathAsString((Path) value); } switch (operator) { case EQUAL_TO: return CompareStringQuery.createQueryForNodesWithFieldEqualTo(stringValue, field, factories, caseOperation); case NOT_EQUAL_TO: Query query = CompareStringQuery.createQueryForNodesWithFieldEqualTo(stringValue, field, factories, caseOperation); return not(query); case GREATER_THAN: return CompareStringQuery.createQueryForNodesWithFieldGreaterThan(stringValue, field, factories, caseOperation); case GREATER_THAN_OR_EQUAL_TO: return CompareStringQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(stringValue, field, factories, caseOperation); case LESS_THAN: return CompareStringQuery.createQueryForNodesWithFieldLessThan(stringValue, field, factories, caseOperation); case LESS_THAN_OR_EQUAL_TO: return CompareStringQuery.createQueryForNodesWithFieldLessThanOrEqualTo(stringValue, field, factories, caseOperation); case LIKE: return findNodesLike(selectorName, field, stringValue, caseOperation); } break; case DECIMAL: // 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); stringValue = FieldUtil.decimalToString(decimalValue); // Now we can just create the query ... switch (operator) { case EQUAL_TO: return CompareStringQuery.createQueryForNodesWithFieldEqualTo(stringValue, field, factories, caseOperation); case NOT_EQUAL_TO: Query query = CompareStringQuery.createQueryForNodesWithFieldEqualTo(stringValue, field, factories, caseOperation); return not(query); case GREATER_THAN: return CompareStringQuery.createQueryForNodesWithFieldGreaterThan(stringValue, field, factories, caseOperation); case GREATER_THAN_OR_EQUAL_TO: return CompareStringQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(stringValue, field, factories, caseOperation); case LESS_THAN: return CompareStringQuery.createQueryForNodesWithFieldLessThan(stringValue, field, factories, caseOperation); case LESS_THAN_OR_EQUAL_TO: return CompareStringQuery.createQueryForNodesWithFieldLessThanOrEqualTo(stringValue, field, factories, caseOperation); case LIKE: return findNodesLike(selectorName, field, stringValue, caseOperation); } break; case DATE: Long longMinimum = Long.MIN_VALUE; Long longMaximum = Long.MAX_VALUE; if (metadata != null) { longMinimum = (Long) metadata.getMinimum(); longMaximum = (Long) metadata.getMaximum(); if (longMinimum == null) longMinimum = Long.MIN_VALUE; if (longMaximum == null) longMaximum = Long.MAX_VALUE; } long date = factories.getLongFactory().create(value); switch (operator) { case EQUAL_TO: if (date < longMinimum || date > longMaximum) return new MatchNoneQuery(); return NumericRangeQuery.newLongRange(field, date, date, true, true); case NOT_EQUAL_TO: if (date < longMinimum || date > longMaximum) return new MatchAllDocsQuery(); Query lowerRange = NumericRangeQuery.newLongRange(field, longMinimum, date, true, false); Query upperRange = NumericRangeQuery.newLongRange(field, date, longMaximum, false, true); BooleanQuery query = new BooleanQuery(); query.add(lowerRange, Occur.SHOULD); query.add(upperRange, Occur.SHOULD); return query; case GREATER_THAN: if (date > longMaximum) return new MatchNoneQuery(); return NumericRangeQuery.newLongRange(field, date, longMaximum, false, true); case GREATER_THAN_OR_EQUAL_TO: if (date > longMaximum) return new MatchNoneQuery(); return NumericRangeQuery.newLongRange(field, date, longMaximum, true, true); case LESS_THAN: if (date < longMinimum) return new MatchNoneQuery(); return NumericRangeQuery.newLongRange(field, longMinimum, date, true, false); case LESS_THAN_OR_EQUAL_TO: if (date < longMinimum) return new MatchNoneQuery(); return NumericRangeQuery.newLongRange(field, longMinimum, date, true, true); case LIKE: // This is not allowed ... assert false; return null; } break; case LONG: if (metadata != null) { longMinimum = (Long) metadata.getMinimum(); longMaximum = (Long) metadata.getMaximum(); if (longMinimum == null) longMinimum = Long.MIN_VALUE; if (longMaximum == null) longMaximum = Long.MAX_VALUE; } else { longMinimum = Long.MIN_VALUE; longMaximum = Long.MAX_VALUE; } long longValue = factories.getLongFactory().create(value); switch (operator) { case EQUAL_TO: if (longValue < longMinimum || longValue > longMaximum) return new MatchNoneQuery(); return NumericRangeQuery.newLongRange(field, longValue, longValue, true, true); case NOT_EQUAL_TO: if (longValue < longMinimum || longValue > longMaximum) return new MatchNoneQuery(); Query lowerRange = NumericRangeQuery.newLongRange(field, longMinimum, longValue, true, false); Query upperRange = NumericRangeQuery.newLongRange(field, longValue, longMaximum, false, true); BooleanQuery query = new BooleanQuery(); query.add(lowerRange, Occur.SHOULD); query.add(upperRange, Occur.SHOULD); return query; case GREATER_THAN: if (longValue > longMaximum) return new MatchNoneQuery(); return NumericRangeQuery.newLongRange(field, longValue, longMaximum, false, true); case GREATER_THAN_OR_EQUAL_TO: if (longValue > longMaximum) return new MatchNoneQuery(); return NumericRangeQuery.newLongRange(field, longValue, longMaximum, true, true); case LESS_THAN: if (longValue < longMinimum) return new MatchNoneQuery(); return NumericRangeQuery.newLongRange(field, longMinimum, longValue, true, false); case LESS_THAN_OR_EQUAL_TO: if (longValue < longMinimum) return new MatchNoneQuery(); return NumericRangeQuery.newLongRange(field, longMinimum, longValue, true, true); case LIKE: // This is not allowed ... assert false; return null; } break; case BOOLEAN: boolean booleanValue = factories.getBooleanFactory().create(value); if (booleanValue) { switch (operator) { case EQUAL_TO: return NumericRangeQuery.newIntRange(field, 0, 1, false, true); case NOT_EQUAL_TO: return NumericRangeQuery.newIntRange(field, 0, 1, true, false); case GREATER_THAN_OR_EQUAL_TO: return NumericRangeQuery.newIntRange(field, 1, 1, true, true); case LESS_THAN_OR_EQUAL_TO: return NumericRangeQuery.newIntRange(field, 0, 1, true, true); case GREATER_THAN: // Can't be greater than 'true', per JCR spec return new MatchNoneQuery(); case LESS_THAN: // 'false' is less than 'true' ... return NumericRangeQuery.newIntRange(field, 0, 0, true, true); case LIKE: // This is not allowed ... assert false; return null; } } else { switch (operator) { case EQUAL_TO: return NumericRangeQuery.newIntRange(field, 0, 1, true, false); case NOT_EQUAL_TO: return NumericRangeQuery.newIntRange(field, 0, 1, false, true); case GREATER_THAN_OR_EQUAL_TO: return NumericRangeQuery.newIntRange(field, 0, 1, true, true); case LESS_THAN_OR_EQUAL_TO: return NumericRangeQuery.newIntRange(field, 0, 0, true, true); case GREATER_THAN: // 'true' is greater than 'false' ... return NumericRangeQuery.newIntRange(field, 1, 1, true, true); case LESS_THAN: // Can't be less than 'false', per JCR spec return new MatchNoneQuery(); case LIKE: // This is not allowed ... assert false; return null; } } break; case DOUBLE: double doubleValue = factories.getDoubleFactory().create(value); Double doubleMinimum = Double.MIN_VALUE; Double doubleMaximum = Double.MAX_VALUE; if (metadata != null) { doubleMinimum = (Double) metadata.getMinimum(); doubleMaximum = (Double) metadata.getMaximum(); if (doubleMinimum == null) doubleMinimum = Double.MIN_VALUE; if (doubleMaximum == null) doubleMaximum = Double.MAX_VALUE; } switch (operator) { case EQUAL_TO: if (doubleValue < doubleMinimum || doubleValue > doubleMaximum) return new MatchNoneQuery(); return NumericRangeQuery.newDoubleRange(field, doubleValue, doubleValue, true, true); case NOT_EQUAL_TO: if (doubleValue < doubleMinimum || doubleValue > doubleMaximum) return new MatchAllDocsQuery(); Query lowerRange = NumericRangeQuery.newDoubleRange(field, doubleMinimum, doubleValue, true, false); Query upperRange = NumericRangeQuery.newDoubleRange(field, doubleValue, doubleMaximum, false, true); BooleanQuery query = new BooleanQuery(); query.add(lowerRange, Occur.SHOULD); query.add(upperRange, Occur.SHOULD); return query; case GREATER_THAN: if (doubleValue > doubleMaximum) return new MatchNoneQuery(); return NumericRangeQuery.newDoubleRange(field, doubleValue, doubleMaximum, false, true); case GREATER_THAN_OR_EQUAL_TO: if (doubleValue > doubleMaximum) return new MatchNoneQuery(); return NumericRangeQuery.newDoubleRange(field, doubleValue, doubleMaximum, true, true); case LESS_THAN: if (doubleValue < doubleMinimum) return new MatchNoneQuery(); return NumericRangeQuery.newDoubleRange(field, doubleMinimum, doubleValue, true, false); case LESS_THAN_OR_EQUAL_TO: if (doubleValue < doubleMinimum) return new MatchNoneQuery(); return NumericRangeQuery.newDoubleRange(field, doubleMinimum, doubleValue, true, true); case LIKE: // This is not allowed ... assert false; return null; } break; case BINARY: case OBJECT: // This is not allowed ... assert false; return null; } return null; } @Override protected Query findNodesWith(SelectorName selectorName, ReferenceValue referenceValue, Operator operator, Object value) { String field = referenceValue.getPropertyName(); if (field == null) { if (referenceValue.includesWeakReferences()) { field = FieldName.ALL_REFERENCES; } else { field = FieldName.STRONG_REFERENCES; } } String stringValue = stringFactory.create(value); CaseOperation caseOperation = CaseOperations.AS_IS; switch (operator) { case EQUAL_TO: return CompareStringQuery.createQueryForNodesWithFieldEqualTo(stringValue, field, factories, caseOperation); case NOT_EQUAL_TO: return not(CompareStringQuery.createQueryForNodesWithFieldEqualTo(stringValue, field, factories, caseOperation)); case GREATER_THAN: return CompareStringQuery.createQueryForNodesWithFieldGreaterThan(stringValue, field, factories, caseOperation); case GREATER_THAN_OR_EQUAL_TO: return CompareStringQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(stringValue, field, factories, caseOperation); case LESS_THAN: return CompareStringQuery.createQueryForNodesWithFieldLessThan(stringValue, field, factories, caseOperation); case LESS_THAN_OR_EQUAL_TO: return CompareStringQuery.createQueryForNodesWithFieldLessThanOrEqualTo(stringValue, field, factories, caseOperation); case LIKE: return findNodesLike(selectorName, field, stringValue, caseOperation); } return null; } @Override protected Query findNodesWithNumericRange(SelectorName selectorName, PropertyValue propertyValue, Object lowerValue, Object upperValue, boolean includesLower, boolean includesUpper) { String field = stringFactory.create(propertyValue.getPropertyName()); return findNodesWithNumericRange(selectorName, field, lowerValue, upperValue, includesLower, includesUpper); } @Override protected Query findNodesWithNumericRange(SelectorName selectorName, NodeDepth depth, Object lowerValue, Object upperValue, boolean includesLower, boolean includesUpper) { return findNodesWithNumericRange(selectorName, FieldName.DEPTH, lowerValue, upperValue, includesLower, includesUpper); } protected Query findNodesWithNumericRange(SelectorName selectorName, String field, Object lowerValue, Object upperValue, boolean includesLower, boolean includesUpper) { Schemata.Column metadata = getMetadataFor(selectorName, field); PropertyType type = metadata.getRequiredType(); switch (type) { case DATE: long lowerDate = factories.getLongFactory().create(lowerValue); long upperDate = factories.getLongFactory().create(upperValue); return NumericRangeQuery.newLongRange(field, lowerDate, upperDate, includesLower, includesUpper); case LONG: long lowerLong = factories.getLongFactory().create(lowerValue); long upperLong = factories.getLongFactory().create(upperValue); return NumericRangeQuery.newLongRange(field, lowerLong, upperLong, includesLower, includesUpper); case DOUBLE: double lowerDouble = factories.getDoubleFactory().create(lowerValue); double upperDouble = factories.getDoubleFactory().create(upperValue); return NumericRangeQuery.newDoubleRange(field, lowerDouble, upperDouble, includesLower, includesUpper); case BOOLEAN: int lowerInt = factories.getBooleanFactory().create(lowerValue).booleanValue() ? 1 : 0; int upperInt = factories.getBooleanFactory().create(upperValue).booleanValue() ? 1 : 0; return NumericRangeQuery.newIntRange(field, lowerInt, upperInt, includesLower, includesUpper); case DECIMAL: BigDecimal lowerDecimal = factories.getDecimalFactory().create(lowerValue); BigDecimal upperDecimal = factories.getDecimalFactory().create(upperValue); CaseOperation caseOp = CaseOperations.AS_IS; // decimals are stored the same way regardless String lsv = FieldUtil.decimalToString(lowerDecimal); String usv = FieldUtil.decimalToString(upperDecimal); Query lower = null; if (includesLower) { lower = CompareStringQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(lsv, field, factories, caseOp); } else { lower = CompareStringQuery.createQueryForNodesWithFieldGreaterThan(lsv, field, factories, caseOp); } Query upper = null; if (includesUpper) { upper = CompareStringQuery.createQueryForNodesWithFieldLessThanOrEqualTo(usv, field, factories, caseOp); } else { upper = CompareStringQuery.createQueryForNodesWithFieldLessThan(usv, field, factories, caseOp); } BooleanQuery query = new BooleanQuery(); query.add(lower, Occur.MUST); query.add(upper, Occur.MUST); return query; case OBJECT: case URI: case UUID: case PATH: case NAME: case STRING: case REFERENCE: case WEAKREFERENCE: case BINARY: assert false; } return new MatchNoneQuery(); } protected String likeExpresionForWildcardPath(String path) { if (path.equals("/") || path.equals("%")) return path; StringBuilder sb = new StringBuilder(); path = path.replaceAll("%+", "%"); if (path.startsWith("%/")) { sb.append("%"); if (path.length() == 2) return sb.toString(); path = path.substring(2); } for (String segment : path.split("/")) { if (segment.length() == 0) continue; sb.append("/"); sb.append(segment); if (segment.equals("%") || segment.equals("_")) continue; if (!segment.endsWith("]") && !segment.endsWith("]%") && !segment.endsWith("]_")) { sb.append("[1]"); } } if (path.endsWith("/")) sb.append("/"); return sb.toString(); } @Override protected Query findNodesWith(SelectorName selectorName, NodePath nodePath, Operator operator, Object value, CaseOperation caseOperation) { if (caseOperation == null) caseOperation = CaseOperations.AS_IS; Path pathValue = operator != Operator.LIKE ? pathFactory.create(value) : null; Query query = null; switch (operator) { case EQUAL_TO: return findNodeAt(pathValue); case NOT_EQUAL_TO: return not(findNodeAt(pathValue)); case LIKE: String likeExpression = stringFactory.create(value); likeExpression = likeExpresionForWildcardPath(likeExpression); if (likeExpression.indexOf("[%]") != -1) { // We can't use '[%]' because we only want to match digits, // so handle this using a regex ... String regex = likeExpression; regex = regex.replace("[%]", "[\\d+]"); regex = regex.replace("[", "\\["); regex = regex.replace("*", ".*").replace("?", "."); regex = regex.replace("%", ".*").replace("_", "."); // Now create a regex query ... RegexQuery regexQuery = new RegexQuery(new Term(FieldName.PATH, regex)); int flags = caseOperation == CaseOperations.AS_IS ? 0 : Pattern.CASE_INSENSITIVE; regexQuery.setRegexImplementation(new JavaUtilRegexCapabilities(flags)); query = regexQuery; } else { query = findNodesLike(selectorName, FieldName.PATH, likeExpression, caseOperation); } break; case GREATER_THAN: query = ComparePathQuery.createQueryForNodesWithPathGreaterThan(pathValue, FieldName.PATH, factories, caseOperation); break; case GREATER_THAN_OR_EQUAL_TO: query = ComparePathQuery.createQueryForNodesWithPathGreaterThanOrEqualTo(pathValue, FieldName.PATH, factories, caseOperation); break; case LESS_THAN: query = ComparePathQuery.createQueryForNodesWithPathLessThan(pathValue, FieldName.PATH, factories, caseOperation); break; case LESS_THAN_OR_EQUAL_TO: query = ComparePathQuery.createQueryForNodesWithPathLessThanOrEqualTo(pathValue, FieldName.PATH, factories, caseOperation); break; } return query; } @Override protected Query findNodesWith(SelectorName selectorName, NodeName nodeName, Operator operator, Object value, CaseOperation caseOperation) { String stringValue = stringFactory.create(value); if (stringValue.startsWith("./") && stringValue.length() > 2) { // Then it is a URI, and per 3.6.4.9 the './' prefix should be removed ... stringValue = stringValue.substring(2); } if (caseOperation == null) caseOperation = CaseOperations.AS_IS; Path.Segment segment = operator != Operator.LIKE ? pathFactory.createSegment(stringValue) : null; // Determine if the string value contained a SNS index ... boolean includeSns = stringValue.indexOf('[') != -1; Query query = null; switch (operator) { case EQUAL_TO: query = CompareNameQuery.createQueryForNodesWithNameEqualTo(segment, FieldName.NODE_NAME, FieldName.SNS_INDEX, factories, caseOperation, includeSns); break; case NOT_EQUAL_TO: query = CompareNameQuery.createQueryForNodesWithNameEqualTo(segment, FieldName.NODE_NAME, FieldName.SNS_INDEX, factories, caseOperation, includeSns); query = not(query); break; case GREATER_THAN: query = CompareNameQuery.createQueryForNodesWithNameGreaterThan(segment, FieldName.NODE_NAME, FieldName.SNS_INDEX, factories, caseOperation, includeSns); break; case GREATER_THAN_OR_EQUAL_TO: query = CompareNameQuery.createQueryForNodesWithNameGreaterThanOrEqualTo(segment, FieldName.NODE_NAME, FieldName.SNS_INDEX, factories, caseOperation, includeSns); break; case LESS_THAN: query = CompareNameQuery.createQueryForNodesWithNameLessThan(segment, FieldName.NODE_NAME, FieldName.SNS_INDEX, factories, caseOperation, includeSns); break; case LESS_THAN_OR_EQUAL_TO: query = CompareNameQuery.createQueryForNodesWithNameLessThanOrEqualTo(segment, FieldName.NODE_NAME, FieldName.SNS_INDEX, factories, caseOperation, includeSns); break; case LIKE: // See whether the like expression has brackets ... String likeExpression = stringValue; int openBracketIndex = likeExpression.indexOf('['); if (openBracketIndex != -1) { String localNameExpression = likeExpression.substring(0, openBracketIndex); String snsIndexExpression = likeExpression.substring(openBracketIndex); Query localNameQuery = CompareStringQuery.createQueryForNodesWithFieldLike(localNameExpression, FieldName.NODE_NAME, factories, caseOperation); Query snsQuery = createSnsIndexQuery(snsIndexExpression); if (localNameQuery == null) { if (snsQuery == null) { query = new MatchNoneQuery(); } else { // There is just an SNS part ... query = snsQuery; } } else { // There is a local name part ... if (snsQuery == null) { query = localNameQuery; } else { // There is both a local name part and a SNS part ... BooleanQuery booleanQuery = new BooleanQuery(); booleanQuery.add(localNameQuery, Occur.MUST); booleanQuery.add(snsQuery, Occur.MUST); query = booleanQuery; } } } else { // There is no SNS expression ... query = CompareStringQuery.createQueryForNodesWithFieldLike(likeExpression, FieldName.NODE_NAME, factories, caseOperation); } assert query != null; break; } return query; } @Override protected Query findNodesWith(SelectorName selectorName, NodeLocalName nodeName, Operator operator, Object value, CaseOperation caseOperation) { String nameValue = stringFactory.create(value); if (caseOperation == null) caseOperation = CaseOperations.AS_IS; Query query = null; switch (operator) { case LIKE: String likeExpression = nameValue; query = findNodesLike(FieldName.LOCAL_NAME, likeExpression, caseOperation); break; case EQUAL_TO: query = CompareStringQuery.createQueryForNodesWithFieldEqualTo(nameValue, FieldName.LOCAL_NAME, factories, caseOperation); break; case NOT_EQUAL_TO: query = CompareStringQuery.createQueryForNodesWithFieldEqualTo(nameValue, FieldName.LOCAL_NAME, factories, caseOperation); query = not(query); break; case GREATER_THAN: query = CompareStringQuery.createQueryForNodesWithFieldGreaterThan(nameValue, FieldName.LOCAL_NAME, factories, caseOperation); break; case GREATER_THAN_OR_EQUAL_TO: query = CompareStringQuery.createQueryForNodesWithFieldGreaterThanOrEqualTo(nameValue, FieldName.LOCAL_NAME, factories, caseOperation); break; case LESS_THAN: query = CompareStringQuery.createQueryForNodesWithFieldLessThan(nameValue, FieldName.LOCAL_NAME, factories, caseOperation); break; case LESS_THAN_OR_EQUAL_TO: query = CompareStringQuery.createQueryForNodesWithFieldLessThanOrEqualTo(nameValue, FieldName.LOCAL_NAME, factories, caseOperation); break; } return query; } @Override protected Query findNodesWith(SelectorName selectorName, NodeDepth depthConstraint, Operator operator, Object value) { int depth = factories.getLongFactory().create(value).intValue(); switch (operator) { case EQUAL_TO: return NumericRangeQuery.newIntRange(FieldName.DEPTH, depth, depth, true, true); case NOT_EQUAL_TO: Query query = NumericRangeQuery.newIntRange(FieldName.DEPTH, depth, depth, true, true); return not(query); case GREATER_THAN: return NumericRangeQuery.newIntRange(FieldName.DEPTH, depth, MAX_DEPTH, false, true); case GREATER_THAN_OR_EQUAL_TO: return NumericRangeQuery.newIntRange(FieldName.DEPTH, depth, MAX_DEPTH, true, true); case LESS_THAN: return NumericRangeQuery.newIntRange(FieldName.DEPTH, MIN_DEPTH, depth, true, false); case LESS_THAN_OR_EQUAL_TO: return NumericRangeQuery.newIntRange(FieldName.DEPTH, MIN_DEPTH, depth, true, true); case LIKE: // This is not allowed ... return null; } return null; } /** * Utility method to generate a query against the SNS indexes. This method attempts to generate a query that works most * efficiently, depending upon the supplied expression. For example, if the supplied expression is just "[3]", then a range * query is used to find all values matching '3'. However, if "[3_]" is used (where '_' matches any single-character, or digit * in this case), then a range query is used to find all values between '30' and '39'. Similarly, if "[3%]" is used, then a * regular expression query is used. * * @param likeExpression the expression that uses the JCR 2.0 LIKE representation, and which includes the leading '[' and * trailing ']' characters * @return the query, or null if the expression cannot be represented as a query */ protected Query createSnsIndexQuery(String likeExpression) { if (likeExpression == null) return null; likeExpression = likeExpression.trim(); if (likeExpression.length() == 0) return null; // Remove the leading '[' ... assert likeExpression.charAt(0) == '['; likeExpression = likeExpression.substring(1); // Remove the trailing ']' if it exists ... int closeBracketIndex = likeExpression.indexOf(']'); if (closeBracketIndex != -1) { likeExpression = likeExpression.substring(0, closeBracketIndex); } if (likeExpression.equals("_")) { // The SNS expression can only be one digit ... return NumericRangeQuery.newIntRange(FieldName.SNS_INDEX, MIN_SNS_INDEX, 9, true, true); } if (likeExpression.equals("%")) { // The SNS expression can be any digits ... return NumericRangeQuery.newIntRange(FieldName.SNS_INDEX, MIN_SNS_INDEX, MAX_SNS_INDEX, true, true); } if (likeExpression.indexOf('_') != -1) { if (likeExpression.indexOf('%') != -1) { // Contains both ... return findNodesLike(FieldName.SNS_INDEX, likeExpression, null); } // It presumably contains some numbers and at least one '_' character ... int firstWildcardChar = likeExpression.indexOf('_'); if (firstWildcardChar + 1 < likeExpression.length()) { // There's at least some characters after the first '_' ... int secondWildcardChar = likeExpression.indexOf('_', firstWildcardChar + 1); if (secondWildcardChar != -1) { // There are multiple '_' characters ... return findNodesLike(FieldName.SNS_INDEX, likeExpression, null); } } // There's only one '_', so parse the lowermost value and uppermost value ... String lowerExpression = likeExpression.replace('_', '0'); String upperExpression = likeExpression.replace('_', '9'); try { // This SNS is just a number ... int lowerSns = Integer.parseInt(lowerExpression); int upperSns = Integer.parseInt(upperExpression); return NumericRangeQuery.newIntRange(FieldName.SNS_INDEX, lowerSns, upperSns, true, true); } catch (NumberFormatException e) { // It's not a number but it's in the SNS field, so there will be no results ... return new MatchNoneQuery(); } } if (likeExpression.indexOf('%') != -1) { // It presumably contains some numbers and at least one '%' character ... return findNodesLike(FieldName.SNS_INDEX, likeExpression, null); } // This is not a LIKE expression but an exact value specification and should be a number ... try { // This SNS is just a number ... int sns = Integer.parseInt(likeExpression); return NumericRangeQuery.newIntRange(FieldName.SNS_INDEX, sns, sns, true, true); } catch (NumberFormatException e) { // It's not a number but it's in the SNS field, so there will be no results ... return new MatchNoneQuery(); } } }