/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.core.query.lucene;

import static javax.jcr.PropertyType.DATE;
import static javax.jcr.PropertyType.DECIMAL;
import static javax.jcr.PropertyType.DOUBLE;
import static javax.jcr.PropertyType.LONG;
import static javax.jcr.PropertyType.NAME;
import static javax.jcr.PropertyType.PATH;
import static javax.jcr.PropertyType.STRING;
import static javax.jcr.PropertyType.UNDEFINED;
import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_EQUAL_TO;
import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN;
import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO;
import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN;
import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO;
import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_LIKE;
import static javax.jcr.query.qom.QueryObjectModelConstants.JCR_OPERATOR_NOT_EQUAL_TO;
import static org.apache.jackrabbit.core.query.lucene.FieldNames.LOCAL_NAME;
import static org.apache.jackrabbit.core.query.lucene.FieldNames.MVP;
import static org.apache.jackrabbit.core.query.lucene.FieldNames.NAMESPACE_URI;
import static org.apache.jackrabbit.core.query.lucene.FieldNames.PARENT;
import static org.apache.jackrabbit.core.query.lucene.FieldNames.PROPERTIES;
import static org.apache.jackrabbit.core.query.lucene.FieldNames.UUID;
import static org.apache.jackrabbit.core.query.lucene.TransformConstants.TRANSFORM_LOWER_CASE;
import static org.apache.jackrabbit.core.query.lucene.TransformConstants.TRANSFORM_NONE;
import static org.apache.jackrabbit.core.query.lucene.TransformConstants.TRANSFORM_UPPER_CASE;
import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_MIXINTYPES;
import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_PRIMARYTYPE;
import static org.apache.lucene.search.BooleanClause.Occur.MUST;
import static org.apache.lucene.search.BooleanClause.Occur.MUST_NOT;
import static org.apache.lucene.search.BooleanClause.Occur.SHOULD;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.Value;
import javax.jcr.ValueFormatException;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.NodeTypeIterator;
import javax.jcr.nodetype.NodeTypeManager;
import javax.jcr.nodetype.PropertyDefinition;
import javax.jcr.query.InvalidQueryException;
import javax.jcr.query.Row;
import javax.jcr.query.qom.And;
import javax.jcr.query.qom.ChildNode;
import javax.jcr.query.qom.Comparison;
import javax.jcr.query.qom.Constraint;
import javax.jcr.query.qom.DescendantNode;
import javax.jcr.query.qom.DynamicOperand;
import javax.jcr.query.qom.FullTextSearch;
import javax.jcr.query.qom.FullTextSearchScore;
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.Or;
import javax.jcr.query.qom.PropertyExistence;
import javax.jcr.query.qom.PropertyValue;
import javax.jcr.query.qom.SameNode;
import javax.jcr.query.qom.Selector;
import javax.jcr.query.qom.StaticOperand;
import javax.jcr.query.qom.UpperCase;

import org.apache.jackrabbit.commons.predicate.Predicate;
import org.apache.jackrabbit.commons.predicate.Predicates;
import org.apache.jackrabbit.commons.predicate.RowPredicate;
import org.apache.jackrabbit.commons.query.qom.OperandEvaluator;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.query.lucene.join.SelectorRow;
import org.apache.jackrabbit.core.query.lucene.join.ValueComparator;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException;
import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver;
import org.apache.jackrabbit.spi.commons.query.qom.FullTextSearchImpl;
import org.apache.jackrabbit.spi.commons.query.qom.PropertyExistenceImpl;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;

/**
 * Factory that creates Lucene queries from QOM elements.
 */
public class LuceneQueryFactory {

    /**
     * Session of the user executing this query
     */
    protected final SessionImpl session;

    /**
     * Node type manager
     */
    protected final NodeTypeManager ntManager;

    /** Lucene search index */
    protected final SearchIndex index;

    /**
     * Namespace mappings to internal prefixes
     */
    protected final NamespaceMappings nsMappings;

    /**
     * NamePathResolver to map namespace mappings to internal prefixes
     */
    protected final NamePathResolver npResolver;

    /** Operand evaluator */
    protected final OperandEvaluator evaluator;

    protected final String mixinTypesField;

    protected final String primaryTypeField;

    private final PerQueryCache cache = new PerQueryCache();

    /**
     * Creates a new lucene query factory.
     *
     * @param session         the session that executes the query.
     * @param index           the search index
     * @param bindVariables   the bind variable values of the query
     */
    public LuceneQueryFactory(
            SessionImpl session, SearchIndex index,
            Map<String, Value> bindVariables) throws RepositoryException {
        this.session = session;
        this.ntManager = session.getWorkspace().getNodeTypeManager();
        this.index = index;
        this.nsMappings = index.getNamespaceMappings();
        this.npResolver = NamePathResolverImpl.create(nsMappings);
        this.evaluator =
            new OperandEvaluator(session.getValueFactory(), bindVariables);
        this.mixinTypesField = nsMappings.translateName(JCR_MIXINTYPES);
        this.primaryTypeField = nsMappings.translateName(JCR_PRIMARYTYPE);
    }

    /**
     * @param columns
     * @param selector
     * @param constraint
     * @param externalSort
     *            if <code>true</code> it means that the lqf should just let the
     *            QueryEngine take care of sorting and applying applying offset
     *            and limit constraints
     * @param offsetIn
     *            used in pagination
     * @param limitIn
     *            used in pagination
     * @return a list of rows
     * @throws RepositoryException
     * @throws IOException
     */
    public List<Row> execute(Map<String, PropertyValue> columns,
            Selector selector, Constraint constraint, Sort sort,
            boolean externalSort, long offsetIn, long limitIn)
            throws RepositoryException, IOException {
        final IndexReader reader = index.getIndexReader(true);
        final int offset = offsetIn < 0 ? 0 : (int) offsetIn;
        final int limit = limitIn < 0 ? Integer.MAX_VALUE : (int) limitIn;

        QueryHits hits = null;
        try {
            JackrabbitIndexSearcher searcher = new JackrabbitIndexSearcher(
                    session, reader, index.getContext().getItemStateManager());
            searcher.setSimilarity(index.getSimilarity());

            Predicate filter = Predicate.TRUE;
            BooleanQuery query = new BooleanQuery();

            QueryPair qp = new QueryPair(query);

            query.add(create(selector), MUST);
            if (constraint != null) {
                String name = selector.getSelectorName();
                NodeType type =
                    ntManager.getNodeType(selector.getNodeTypeName());
                filter = mapConstraintToQueryAndFilter(qp,
                        constraint, Collections.singletonMap(name, type),
                        searcher, reader);
            }

            List<Row> rows = new ArrayList<Row>();

            // TODO depending on the filters, we could push the offset info
            // into the searcher
            hits = searcher.evaluate(qp.mainQuery, sort, offset + limit);
            int currentNode = 0;
            int addedNodes = 0;

            ScoreNode node = hits.nextScoreNode();
            while (node != null) {
                Row row = null;
                try {
                    row = new SelectorRow(columns, evaluator,
                            selector.getSelectorName(),
                            session.getNodeById(node.getNodeId()),
                            node.getScore());
                } catch (ItemNotFoundException e) {
                    // skip the node
                }
                if (row != null && filter.evaluate(row)) {
                    if (externalSort) {
                        // return everything and not worry about sort
                        rows.add(row);
                    } else {
                        // apply limit and offset rules locally
                        if (currentNode >= offset
                                && currentNode - offset < limit) {
                            rows.add(row);
                            addedNodes++;
                        }
                        currentNode++;
                        // end the loop when going over the limit
                        if (addedNodes == limit) {
                            break;
                        }
                    }
                }
                node = hits.nextScoreNode();
            }
            return rows;
        } finally {
            if (hits != null) {
                hits.close();
            }
            Util.closeOrRelease(reader);
        }
    }

    /**
     * Creates a lucene query for the given QOM selector.
     *
     * @param selector the selector.
     * @return a lucene query for the given selector.
     * @throws RepositoryException if an error occurs while creating the query.
     */
    public Query create(Selector selector) throws RepositoryException {
        List<Term> terms = new ArrayList<Term>();

        String name = selector.getNodeTypeName();
        NodeTypeIterator allTypes = ntManager.getAllNodeTypes();
        while (allTypes.hasNext()) {
            NodeType nt = allTypes.nextNodeType();
            if (nt.isNodeType(name)) {
                terms.add(createNodeTypeTerm(nt));
            }
        }

        if (terms.size() == 1) {
            return new JackrabbitTermQuery(terms.get(0));
        } else {
            BooleanQuery b = new BooleanQuery();
            for (Term term : terms) {
                b.add(new JackrabbitTermQuery(term), SHOULD);
            }
            return b;
        }
    }

    protected Term createNodeTypeTerm(NodeType type) throws RepositoryException {
        String field;
        if (type.isMixin()) {
            // search for nodes where jcr:mixinTypes is set to this mixin
            field = mixinTypesField;
        } else {
            // search for nodes where jcr:primaryType is set to this type
            field = primaryTypeField;
        }
        String name = nsMappings.translateName(session.getQName(type.getName()));
        return new Term(PROPERTIES, FieldNames.createNamedValue(field, name));
    }

    /**
     * Creates a lucene query for the given QOM full text search.
     *
     * @param constraint the full text search constraint.
     * @return the lucene query for the given constraint.
     * @throws RepositoryException if an error occurs while creating the query.
     */
    public Query create(FullTextSearchImpl fts) throws RepositoryException {
        String fieldname;
        if (fts.getPropertyName() == null) {
            // fulltext on node
            fieldname = FieldNames.FULLTEXT;
        } else {
            // final path element is a property name
            Name propName = fts.getPropertyQName();
            StringBuffer tmp = new StringBuffer();
            tmp.append(nsMappings.getPrefix(propName.getNamespaceURI()));
            tmp.append(":").append(FieldNames.FULLTEXT_PREFIX);
            tmp.append(propName.getLocalName());
            fieldname = tmp.toString();
        }
        QueryParser parser = new JackrabbitQueryParser(
                fieldname, index.getTextAnalyzer(),
                index.getSynonymProvider(), cache);
        try {
            StaticOperand expr = fts.getFullTextSearchExpression();
            return parser.parse(evaluator.getValue(expr).getString());
        } catch (ParseException e) {
            throw new RepositoryException(e);
        }
    }

    /**
     * Creates a lucene query for the given QOM property existence constraint.
     *
     * @param constraint the QOM constraint.
     * @return the lucene query for the given constraint.
     * @throws RepositoryException if an error occurs while creating the query.
     */
    public Query create(PropertyExistenceImpl prop) throws RepositoryException {
        String propName = npResolver.getJCRName(prop.getPropertyQName());
        return Util.createMatchAllQuery(
                propName, index.getIndexFormatVersion(), cache);
    }

    protected Predicate mapConstraintToQueryAndFilter(
            QueryPair query, Constraint constraint,
            Map<String, NodeType> selectorMap,
            JackrabbitIndexSearcher searcher, IndexReader reader)
            throws RepositoryException, IOException {
        Predicate filter = Predicate.TRUE;
        if (constraint instanceof And) {
            And and = (And) constraint;
            filter = mapConstraintToQueryAndFilter(
                    query, and.getConstraint1(), selectorMap, searcher, reader);
            Predicate other = mapConstraintToQueryAndFilter(
                    query, and.getConstraint2(), selectorMap, searcher, reader);
            if (filter == Predicate.TRUE) {
                filter = other;
            } else if (other != Predicate.TRUE) {
                filter = Predicates.and(filter, other);
            }
        } else if (constraint instanceof Comparison) {
            Comparison c = (Comparison) constraint;
            Transform transform = new Transform(c.getOperand1());
            DynamicOperand left = transform.operand;
            final String operator = c.getOperator();
            StaticOperand right = c.getOperand2();
            if (left instanceof Length
                    || left instanceof FullTextSearchScore
                    || (((!JCR_OPERATOR_EQUAL_TO.equals(operator) && !JCR_OPERATOR_LIKE
                            .equals(operator)) || transform.transform != TRANSFORM_NONE) && (left instanceof NodeName || left instanceof NodeLocalName))) {
                try {
                    int type = PropertyType.UNDEFINED;
                    if (left instanceof Length) {
                        type = PropertyType.LONG;
                    } else if (left instanceof FullTextSearchScore) {
                        type = PropertyType.DOUBLE;
                    }
                    final DynamicOperand operand = c.getOperand1();
                    final Value value = evaluator.getValue(right, type);
                    filter = new RowPredicate() {
                        @Override
                        protected boolean evaluate(Row row)
                                throws RepositoryException {
                            return new ValueComparator().evaluate(
                                    operator,
                                    evaluator.getValue(operand, row), value);
                        }
                    };
                } catch (ValueFormatException e) {
                    throw new InvalidQueryException(e);
                }
            } else {
                Query cq = getComparisonQuery(
                        left, transform.transform, operator, right, selectorMap);
                query.subQuery.add(cq, MUST);
            }
        } else if (constraint instanceof DescendantNode) {
            final DescendantNode descendantNode = (DescendantNode) constraint;
            Query context = getNodeIdQuery(UUID, descendantNode.getAncestorPath());
            query.mainQuery = new DescendantSelfAxisQuery(context, query.subQuery, false);
        } else {
            query.subQuery.add(create(constraint, selectorMap, searcher), MUST);
        }
        return filter;
    }


    protected Query create(
            Constraint constraint, Map<String, NodeType> selectorMap,
            JackrabbitIndexSearcher searcher)
            throws RepositoryException, IOException {
        if (constraint instanceof And) {
            return getAndQuery((And) constraint, selectorMap, searcher);
        } else if (constraint instanceof Or) {
            return getOrQuery((Or) constraint, selectorMap, searcher);
        } else if (constraint instanceof Not) {
            return getNotQuery((Not) constraint, selectorMap, searcher);
        } else if (constraint instanceof PropertyExistence) {
            return getPropertyExistenceQuery((PropertyExistence) constraint);
        } else if (constraint instanceof Comparison) {
            Comparison c = (Comparison) constraint;
            Transform left = new Transform(c.getOperand1());
            return getComparisonQuery(
                    left.operand, left.transform, c.getOperator(),
                    c.getOperand2(), selectorMap);
        } else if (constraint instanceof FullTextSearch) {
            return getFullTextSearchQuery((FullTextSearch) constraint);
        } else if (constraint instanceof SameNode) {
            SameNode sn = (SameNode) constraint;
            return getNodeIdQuery(UUID, sn.getPath());
        } else if (constraint instanceof ChildNode) {
            ChildNode cn = (ChildNode) constraint;
            return getNodeIdQuery(PARENT, cn.getParentPath());
        } else if (constraint instanceof DescendantNode) {
            DescendantNode dn = (DescendantNode) constraint;
            return getDescendantNodeQuery(dn, searcher);
        } else {
            throw new UnsupportedRepositoryOperationException(
                    "Unknown constraint type: " + constraint);
        }
    }

    protected Query getDescendantNodeQuery(
            DescendantNode dn, JackrabbitIndexSearcher searcher)
            throws RepositoryException, IOException {
        BooleanQuery query = new BooleanQuery();
        int clauses = 0;

        try {
            LinkedList<String> ids = new LinkedList<String>();
            Node ancestor = session.getNode(dn.getAncestorPath());
            ids.add(ancestor.getIdentifier());
            while (!ids.isEmpty()) {
                String id = ids.removeFirst();
                Query q = new JackrabbitTermQuery(new Term(FieldNames.PARENT, id));
                QueryHits hits = searcher.evaluate(q);
                ScoreNode sn = hits.nextScoreNode();
                if (sn != null) {
                    // reset query so it does not overflow because of the max
                    // clause count condition,
                    // see JCR-3108
                    clauses++;
                    if (clauses == BooleanQuery.getMaxClauseCount()) {
                        BooleanQuery wrapQ = new BooleanQuery();
                        wrapQ.add(query, SHOULD);
                        query = wrapQ;
                        clauses = 1;
                    }
                    query.add(q, SHOULD);
                    do {
                        ids.add(sn.getNodeId().toString());
                        sn = hits.nextScoreNode();
                    } while (sn != null);
                }
            }
        } catch (PathNotFoundException e) {
            query.add(new JackrabbitTermQuery(new Term(
                    FieldNames.UUID, "invalid-node-id")), // never matches
                    SHOULD);
        }

        return query;
    }

    protected Query getFullTextSearchQuery(FullTextSearch fts)
            throws RepositoryException {
        String field = FieldNames.FULLTEXT;
        String property = fts.getPropertyName();
        if (property != null) {
            Name name = session.getQName(property);
            field = nsMappings.getPrefix(name.getNamespaceURI()) + ":"
                + FieldNames.FULLTEXT_PREFIX + name.getLocalName();
        }

        StaticOperand expression = fts.getFullTextSearchExpression();
        String query = evaluator.getValue(expression).getString();
        try {
            QueryParser parser = new JackrabbitQueryParser(
                    field, index.getTextAnalyzer(),
                    index.getSynonymProvider(), cache);
            return parser.parse(query);
        } catch (ParseException e) {
            throw new RepositoryException(
                    "Invalid full text search expression: " + query, e);
        }
    }

    protected BooleanQuery getAndQuery(
            And and, Map<String, NodeType> selectorMap,
            JackrabbitIndexSearcher searcher)
            throws RepositoryException, IOException {
        BooleanQuery query = new BooleanQuery();
        addBooleanConstraint(
                query, and.getConstraint1(), MUST, selectorMap, searcher);
        addBooleanConstraint(
                query, and.getConstraint2(), MUST, selectorMap, searcher);
        return query;
    }

    protected BooleanQuery getOrQuery(
            Or or, Map<String, NodeType> selectorMap,
            JackrabbitIndexSearcher searcher)
            throws RepositoryException, IOException {
        BooleanQuery query = new BooleanQuery();
        addBooleanConstraint(
                query, or.getConstraint1(), SHOULD, selectorMap, searcher);
        addBooleanConstraint(
                query, or.getConstraint2(), SHOULD, selectorMap, searcher);
        return query;
    }

    protected void addBooleanConstraint(
            BooleanQuery query, Constraint constraint, Occur occur,
            Map<String, NodeType> selectorMap, JackrabbitIndexSearcher searcher)
            throws RepositoryException, IOException {
        if (occur == MUST && constraint instanceof And) {
            And and = (And) constraint;
            addBooleanConstraint(
                    query, and.getConstraint1(), occur, selectorMap, searcher);
            addBooleanConstraint(
                    query, and.getConstraint2(), occur, selectorMap, searcher);
        } else if (occur == SHOULD && constraint instanceof Or) {
            Or or = (Or) constraint;
            addBooleanConstraint(
                    query, or.getConstraint1(), occur, selectorMap, searcher);
            addBooleanConstraint(
                    query, or.getConstraint2(), occur, selectorMap, searcher);
        } else {
            query.add(create(constraint, selectorMap, searcher), occur);
        }
    }

    protected NotQuery getNotQuery(
            Not not, Map<String, NodeType> selectorMap,
            JackrabbitIndexSearcher searcher)
            throws RepositoryException, IOException {
        return new NotQuery(create(not.getConstraint(), selectorMap, searcher));
    }

    protected Query getPropertyExistenceQuery(PropertyExistence property)
            throws RepositoryException {
        String name = npResolver.getJCRName(session.getQName(
                property.getPropertyName()));
        return Util.createMatchAllQuery(
                name, index.getIndexFormatVersion(), cache);
    }

    protected static class Transform {

        private final DynamicOperand operand;

        private final int transform;

        public Transform(DynamicOperand operand) {
            // Check the transformation type
            if (operand instanceof UpperCase) {
                this.transform = TRANSFORM_UPPER_CASE;
            } else if (operand instanceof LowerCase) {
                this.transform = TRANSFORM_LOWER_CASE;
            } else {
                this.transform = TRANSFORM_NONE;
            }

            // Unwrap any nested transformations
            while (true) {
                if (operand instanceof UpperCase) {
                    operand = ((UpperCase) operand).getOperand();
                } else if (operand instanceof LowerCase) {
                    operand = ((LowerCase) operand).getOperand();
                } else {
                    break;
                }
            }
            this.operand = operand;
        }
    }

    protected Query getComparisonQuery(
            DynamicOperand left, int transform, String operator,
            StaticOperand rigth, Map<String, NodeType> selectorMap)
            throws RepositoryException {
        if (left instanceof PropertyValue) {
            PropertyValue pv = (PropertyValue) left;
            String field = npResolver.getJCRName(session.getQName(
                    pv.getPropertyName()));
            int type = PropertyType.UNDEFINED;
            NodeType nt = selectorMap.get(pv.getSelectorName());
            if (nt != null) {
                for (PropertyDefinition pd : nt.getPropertyDefinitions()) {
                    if (pd.getName().equals(pv.getPropertyName())) {
                        type = pd.getRequiredType();
                    }
                }
            }
            return getPropertyValueQuery(
                    field, operator, evaluator.getValue(rigth), type, transform);
        } else if (left instanceof NodeName) {
            return getNodeNameQuery(transform, operator, rigth);
        } else if (left instanceof NodeLocalName) {
            return getNodeLocalNameQuery(transform, operator, rigth);
        } else {
            throw new UnsupportedRepositoryOperationException(
                    "Unknown operand type: " + left); // FIXME
        }
    }

    protected Query getNodeNameQuery(
            int transform, String operator, StaticOperand right)
            throws RepositoryException {
        if (transform != TRANSFORM_NONE
                || !JCR_OPERATOR_EQUAL_TO.equals(operator)) {
            throw new UnsupportedRepositoryOperationException();
        }

        Value value = evaluator.getValue(right);
        int type = value.getType();
        String string = value.getString();
        if (type == PropertyType.URI && string.startsWith("./")) {
            string = string.substring("./".length());
        } else if (type == PropertyType.DOUBLE
                || type == PropertyType.DECIMAL
                || type == PropertyType.LONG
                || type == PropertyType.BOOLEAN
                || type == PropertyType.REFERENCE
                || type == PropertyType.WEAKREFERENCE) {
            throw new InvalidQueryException("Invalid name value: " + string);
        }

        try {
            Name name = session.getQName(string);
            Term uri = new Term(NAMESPACE_URI, name.getNamespaceURI());
            Term local = new Term(LOCAL_NAME, name.getLocalName());

            BooleanQuery query = new BooleanQuery();
            query.add(new JackrabbitTermQuery(uri), MUST);
            query.add(new JackrabbitTermQuery(local), MUST);
            return query;
        } catch (IllegalNameException e) {
            throw new InvalidQueryException("Illegal name: " + string, e);
        }
    }

    protected Query getNodeLocalNameQuery(int transform, String operator,
            StaticOperand right) throws RepositoryException {
        if (!JCR_OPERATOR_EQUAL_TO.equals(operator) && !JCR_OPERATOR_LIKE.equals(operator)) {
            throw new UnsupportedRepositoryOperationException();
        }
        String name = evaluator.getValue(right).getString();

        if (JCR_OPERATOR_LIKE.equals(operator)) {
            return new WildcardQuery(LOCAL_NAME, null, name, transform, cache);
        }
        return new JackrabbitTermQuery(new Term(LOCAL_NAME, name));
    }

    protected Query getNodeIdQuery(String field, String path)
            throws RepositoryException {
        String value;
        try {
            value = session.getNode(path).getIdentifier();
        } catch (PathNotFoundException e) {
            value = "invalid-node-id"; // can never match a node
        }
        return new JackrabbitTermQuery(new Term(field, value));
    }

    protected Query getPropertyValueQuery(
            String field, String operator, Value value,
            int type, int transform) throws RepositoryException {
        String string = getValueString(value, type);
        if (JCR_OPERATOR_LIKE.equals(operator)) {
            return new WildcardQuery(PROPERTIES, field, string, transform, cache);
        }

        Term term = getTerm(field, string);
        if (JCR_OPERATOR_EQUAL_TO.equals(operator)) {
            switch (transform) {
            case TRANSFORM_UPPER_CASE:
                return new CaseTermQuery.Upper(term);
            case TRANSFORM_LOWER_CASE:
                return new CaseTermQuery.Lower(term);
            default:
                return new JackrabbitTermQuery(term);
            }
        } else if (JCR_OPERATOR_GREATER_THAN.equals(operator)) {
            return new RangeQuery(term, getTerm(field, "\uFFFF"), false, transform, cache);
        } else if (JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO.equals(operator)) {
            return new RangeQuery(term, getTerm(field, "\uFFFF"), true, transform, cache);
        } else if (JCR_OPERATOR_LESS_THAN.equals(operator)) {
            return new RangeQuery(getTerm(field, ""), term, false, transform, cache);
        } else if (JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO.equals(operator)) {
            return new RangeQuery(getTerm(field, ""), term, true, transform, cache);
        } else if (JCR_OPERATOR_NOT_EQUAL_TO.equals(operator)) {
            BooleanQuery query = new BooleanQuery();
            query.add(Util.createMatchAllQuery(
                    field, index.getIndexFormatVersion(), cache), SHOULD);
            if (transform == TRANSFORM_UPPER_CASE) {
                query.add(new CaseTermQuery.Upper(term), MUST_NOT);
            } else if (transform == TRANSFORM_LOWER_CASE) {
                query.add(new CaseTermQuery.Lower(term), MUST_NOT);
            } else {
                query.add(new JackrabbitTermQuery(term), MUST_NOT);
            }
            // and exclude all nodes where 'field' is multi valued
            query.add(new JackrabbitTermQuery(new Term(MVP, field)), MUST_NOT);
            return query;
        } else {
            throw new UnsupportedRepositoryOperationException(); // FIXME
        }
    }

    protected Term getTerm(String field, String value) {
        return new Term(PROPERTIES, FieldNames.createNamedValue(field, value));
    }

    protected String getValueString(Value value, int type)
            throws RepositoryException {
        switch (value.getType()) {
        case DATE:
            return DateField.dateToString(value.getDate().getTime());
        case DOUBLE:
            return DoubleField.doubleToString(value.getDouble());
        case LONG:
            return LongField.longToString(value.getLong());
        case DECIMAL:
            return DecimalField.decimalToString(value.getDecimal());
        case NAME:
            return npResolver.getJCRName(session.getQName(value.getString()));
        case PATH:
            return npResolver.getJCRPath(session.getQPath(value.getString()));
        default:
            String string = value.getString();
            if (type != UNDEFINED && type != STRING) {
                return getValueString(
                        session.getValueFactory().createValue(string, type),
                        UNDEFINED);
            } else {
                return string;
            }
        }
    }

    protected static class QueryPair {
        Query mainQuery;
        BooleanQuery subQuery;

        QueryPair(BooleanQuery mainQuery) {
            this.mainQuery = mainQuery;
            this.subQuery = mainQuery;
        }
    }

}
