org.modeshape.jcr.query.lucene.basic.BasicLuceneQueryFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.modeshape.jcr.query.lucene.basic.BasicLuceneQueryFactory.java

Source

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

}