org.apache.jackrabbit.core.query.lucene.JahiaLuceneQueryFactoryImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.jackrabbit.core.query.lucene.JahiaLuceneQueryFactoryImpl.java

Source

/**
 * ==========================================================================================
 * =                   JAHIA'S DUAL LICENSING - IMPORTANT INFORMATION                       =
 * ==========================================================================================
 *
 *                                 http://www.jahia.com
 *
 *     Copyright (C) 2002-2017 Jahia Solutions Group SA. All rights reserved.
 *
 *     THIS FILE IS AVAILABLE UNDER TWO DIFFERENT LICENSES:
 *     1/GPL OR 2/JSEL
 *
 *     1/ GPL
 *     ==================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE GPL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program 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 General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 *
 *     2/ JSEL - Commercial and Supported Versions of the program
 *     ===================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE JSEL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     Alternatively, commercial and supported versions of the program - also known as
 *     Enterprise Distributions - must be used in accordance with the terms and conditions
 *     contained in a separate written agreement between you and Jahia Solutions Group SA.
 *
 *     If you are unsure which license is appropriate for your use,
 *     please contact the sales department at sales@jahia.com.
 */
package org.apache.jackrabbit.core.query.lucene;

import com.google.common.collect.Sets;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.math.util.MathUtils;
import org.apache.jackrabbit.commons.predicate.Predicate;
import org.apache.jackrabbit.commons.query.qom.OperandEvaluator;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.query.lucene.join.SelectorRow;
import org.apache.jackrabbit.core.security.JahiaAccessManager;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.KeywordAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldSelector;
import org.apache.lucene.document.FieldSelectorResult;
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.*;
import org.apache.lucene.util.DocIdBitSet;
import org.apache.lucene.util.OpenBitSet;
import org.apache.lucene.util.OpenBitSetDISI;
import org.apache.solr.schema.SchemaField;
import org.jahia.api.Constants;
import org.jahia.services.content.JCRSessionFactory;
import org.jahia.services.content.JCRSessionWrapper;
import org.jahia.services.search.facets.JahiaQueryParser;
import org.jahia.services.visibility.VisibilityService;
import org.jahia.utils.LanguageCodeConverters;
import org.jahia.utils.Patterns;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.*;
import javax.jcr.nodetype.NodeType;
import javax.jcr.query.Row;
import javax.jcr.query.qom.*;
import javax.jcr.security.Privilege;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.*;

import static org.apache.jackrabbit.core.query.lucene.FieldNames.UUID;
import static org.apache.lucene.search.BooleanClause.Occur.MUST;

/**
 * Override LuceneQueryFactory
 *
 * - optimize descendantNode constraint (use index)
 * - optimize childNode constraint (use index)
 * - handles rep:facet when executing (todo : might handle that in a constraint, as rep:filter ?)
 * - handles rep:filter in fulltext constraint  
 */
public class JahiaLuceneQueryFactoryImpl extends LuceneQueryFactory {
    private static Logger logger = LoggerFactory.getLogger(JahiaLuceneQueryFactoryImpl.class); // <-- Added by jahia

    private Locale locale;
    private String queryLanguage;

    @SuppressWarnings("serial")
    public static final FieldSelector ONLY_MAIN_NODE_UUID = new FieldSelector() {
        public FieldSelectorResult accept(String fieldName) {
            if (JahiaNodeIndexer.TRANSLATED_NODE_PARENT == fieldName) {
                return FieldSelectorResult.LOAD;
            } else if (FieldNames.PARENT == fieldName) {
                return FieldSelectorResult.LOAD;
            } else {
                return FieldSelectorResult.NO_LOAD;
            }
        }
    };
    @SuppressWarnings("serial")
    public static final FieldSelector OPTIMIZATION_FIELDS = new FieldSelector() {
        public FieldSelectorResult accept(String fieldName) {
            if (JahiaNodeIndexer.TRANSLATED_NODE_PARENT == fieldName) {
                return FieldSelectorResult.LOAD;
            } else if (FieldNames.PARENT == fieldName) {
                return FieldSelectorResult.LOAD;
            } else if (JahiaNodeIndexer.ACL_UUID == fieldName) {
                return FieldSelectorResult.LOAD;
            } else if (JahiaNodeIndexer.CHECK_VISIBILITY == fieldName) {
                return FieldSelectorResult.LOAD;
            } else if (JahiaNodeIndexer.PUBLISHED == fieldName) {
                return FieldSelectorResult.LOAD;
            } else if (JahiaNodeIndexer.INVALID_LANGUAGES == fieldName) {
                return FieldSelectorResult.LOAD;
            } else {
                return FieldSelectorResult.NO_LOAD;
            }
        }
    };

    public JahiaLuceneQueryFactoryImpl(SessionImpl session, SearchIndex index, Map<String, Value> bindVariables)
            throws RepositoryException {
        super(session, index, bindVariables);
    }

    /**
     * Override LuceneQueryFactory.execute()
     */
    @Override
    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);
            }

            // Added by jahia
            Set<String> foundIds = new HashSet<String>();
            int hasFacets = FacetHandler.hasFacetFunctions(columns, session);
            CountHandler.CountType countType = CountHandler.hasCountFunction(columns, session);
            boolean isCount = countType != null;
            BitSet bitset = (hasFacets & FacetHandler.FACET_COLUMNS) == 0 ? null : new BitSet();
            // End

            List<Row> rowList = externalSort ? new LinkedList<Row>() : null;
            Map<String, Row> rows = externalSort ? null : new LinkedHashMap<String, Row>();
            hits = searcher.evaluate(qp.mainQuery, sort, offset + limit);
            int currentNode = 0;
            int addedNodes = 0;
            int resultCount = 0;
            int hitsSize = 0;

            ScoreNode node = hits.nextScoreNode();
            Map<String, Boolean> checkedAcls = new HashMap<String, Boolean>();

            while (node != null) {
                if (isCount && countType.isApproxCount()) {
                    hitsSize++;
                    if (hitsSize > countType.getApproxCountLimit()) {
                        if (hits.getSize() > 0) {
                            hitsSize = hits.getSize();
                            break;
                        } else {
                            node = hits.nextScoreNode();
                            continue;
                        }
                    }
                }
                IndexedNodeInfo infos = getIndexedNodeInfo(node, reader, isCount && countType.isSkipChecks());
                if (foundIds.add(infos.getMainNodeUuid())) { // <-- Added by jahia
                    if (isCount && countType.isSkipChecks()) {
                        resultCount++;
                    } else {
                        try {
                            boolean canRead = true;
                            if (isAclUuidInIndex()) {
                                canRead = checkIndexedAcl(checkedAcls, infos);
                            }
                            boolean checkVisibility = "1".equals(infos.getCheckVisibility())
                                    && Constants.LIVE_WORKSPACE.equals(session.getWorkspace().getName());

                            if (canRead && (!Constants.LIVE_WORKSPACE.equals(session.getWorkspace().getName())
                                    || ((infos.getPublished() == null || "true".equals(infos.getPublished()))
                                            && (infos.getCheckInvalidLanguages() == null || getLocale() == null
                                                    || !infos.getCheckInvalidLanguages()
                                                            .contains(getLocale().toString()))))) {
                                if (filter == Predicate.TRUE) { // <-- Added by jahia
                                    if ((hasFacets & FacetHandler.ONLY_FACET_COLUMNS) == 0) {
                                        Row row = null;

                                        if (checkVisibility || !isAclUuidInIndex()) {
                                            NodeImpl objectNode = getNodeWithAclAndVisibilityCheck(node,
                                                    checkVisibility);
                                            if (isCount) {
                                                resultCount++;
                                            } else {
                                                row = new LazySelectorRow(columns, evaluator,
                                                        selector.getSelectorName(), objectNode, node.getScore());
                                            }
                                        } else {
                                            if (isCount) {
                                                resultCount++;
                                            } else {
                                                row = new LazySelectorRow(columns, evaluator,
                                                        selector.getSelectorName(), node.getNodeId(),
                                                        node.getScore());
                                            }
                                        }

                                        if (row == null) {
                                            continue;
                                        }

                                        if (externalSort) {
                                            rowList.add(row);
                                        } else {
                                            // apply limit and offset rules locally
                                            if (currentNode >= offset && currentNode - offset < limit) {
                                                rows.put(node.getNodeId().toString(), row);
                                                addedNodes++;
                                            }
                                            currentNode++;
                                            // end the loop when going over the limit
                                            if (addedNodes == limit) {
                                                break;
                                            }
                                        }
                                    }
                                    if ((hasFacets & FacetHandler.FACET_COLUMNS) == FacetHandler.FACET_COLUMNS) {
                                        //Added by Jahia
                                        //can be added to bitset when ACL checked and not in live mode or no visibility rule to check
                                        if (isAclUuidInIndex() && !checkVisibility) {
                                            bitset.set(infos.getDocNumber());
                                        } else { //try to load nodeWrapper to check the visibility rules
                                            NodeImpl objectNode = getNodeWithAclAndVisibilityCheck(node,
                                                    checkVisibility);
                                            bitset.set(infos.getDocNumber());
                                        }
                                        //!Added by Jahia
                                    }
                                } else {
                                    NodeImpl objectNode = session.getNodeById(node.getNodeId());
                                    if (objectNode.isNodeType("jnt:translation")) {
                                        objectNode = (NodeImpl) objectNode.getParent();
                                    }
                                    if (isCount) {
                                        resultCount++;
                                    } else {
                                        Row row = new SelectorRow(columns, evaluator, selector.getSelectorName(),
                                                objectNode, node.getScore());
                                        if (filter.evaluate(row)) {
                                            if ((hasFacets & FacetHandler.ONLY_FACET_COLUMNS) == 0) {
                                                if (externalSort) {
                                                    rowList.add(row);
                                                } else {
                                                    // apply limit and offset rules locally
                                                    if (currentNode >= offset && currentNode - offset < limit) {
                                                        rows.put(node.getNodeId().toString(), row);
                                                        addedNodes++;
                                                    }
                                                    currentNode++;
                                                    // end the loop when going over the limit
                                                    if (addedNodes == limit) {
                                                        break;
                                                    }
                                                }
                                            }
                                            if ((hasFacets
                                                    & FacetHandler.FACET_COLUMNS) == FacetHandler.FACET_COLUMNS) {
                                                bitset.set(infos.getDocNumber()); // <-- Added by jahia
                                            }
                                        }
                                    }
                                }
                            }
                        } catch (PathNotFoundException e) {
                        } catch (ItemNotFoundException e) {
                            // skip the node
                        }
                    }
                } else {
                    if (((hasFacets & FacetHandler.ONLY_FACET_COLUMNS) == 0) && !isCount && !externalSort
                            && !infos.getMainNodeUuid().equals(node.getNodeId().toString())
                            && rows.containsKey(infos.getMainNodeUuid())) {
                        // we've got the translation node -> adjusting the position of the original node in the result list
                        rows.put(infos.getMainNodeUuid(), rows.remove(infos.getMainNodeUuid()));
                    }
                } // <-- Added by jahia
                node = hits.nextScoreNode();
            }

            if (rowList == null) {
                if (rows != null) {
                    rowList = new LinkedList<Row>(rows.values());
                } else {
                    rowList = new LinkedList<Row>();
                }
            }
            // Added by jahia
            if ((hasFacets & FacetHandler.FACET_COLUMNS) == FacetHandler.FACET_COLUMNS) {
                OpenBitSet docIdSet = new OpenBitSetDISI(new DocIdBitSet(bitset).iterator(), bitset.size());

                FacetHandler h = new FacetHandler(columns, selector, docIdSet, index, session, nsMappings);
                h.handleFacets(reader);
                rowList.add(0, h.getFacetsRow());
            } else if (isCount) {
                boolean wasApproxLimitReached = false;
                if (countType.isApproxCount() && hitsSize > countType.getApproxCountLimit()) {
                    resultCount = hitsSize * resultCount / countType.getApproxCountLimit();
                    resultCount = (int) Math.ceil(MathUtils.round(resultCount,
                            resultCount < 1000 ? -1 : (resultCount < 10000 ? -2 : -3), BigDecimal.ROUND_UP));
                    wasApproxLimitReached = true;
                }
                rowList.add(0, CountHandler.createCountRow(resultCount, wasApproxLimitReached));
            }
            // End

            return rowList;
        } finally {
            if (hits != null) {
                hits.close();
            }
            Util.closeOrRelease(reader);
        }
    }

    private NodeImpl getNodeWithAclAndVisibilityCheck(ScoreNode node, boolean checkVisibility)
            throws RepositoryException {
        NodeImpl objectNode = session.getNodeById(node.getNodeId());
        if (objectNode.isNodeType("jnt:translation")) {
            objectNode = (NodeImpl) objectNode.getParent();
        }

        if (checkVisibility) {
            JCRSessionWrapper currentUserSession = JCRSessionFactory.getInstance().getCurrentUserSession("live");
            if (currentUserSession.itemExists(objectNode.getPath()) && !VisibilityService.getInstance()
                    .matchesConditions(currentUserSession.getNode(objectNode.getPath()))) {
                throw new ItemNotFoundException(node.getNodeId().toString());
            }
        }
        return objectNode;
    }

    private boolean checkIndexedAcl(Map<String, Boolean> checkedAcls, IndexedNodeInfo infos)
            throws RepositoryException {
        boolean canRead = true;

        String[] acls = infos.getAclUuid() != null ? Patterns.SPACE.split(infos.getAclUuid())
                : ArrayUtils.EMPTY_STRING_ARRAY;
        ArrayUtils.reverse(acls);

        for (String acl : acls) {
            if (acl.contains("/")) {
                // ACL indexed contains a single user ACE, get the username
                String singleUser = StringUtils.substringAfter(acl, "/");
                acl = StringUtils.substringBefore(acl, "/");
                if (singleUser.contains("/")) {
                    // Granted roles are specified in the indexed entry
                    String roles = StringUtils.substringBeforeLast(singleUser, "/");
                    singleUser = StringUtils.substringAfterLast(singleUser, "/");
                    if (!singleUser.equals(session.getUserID())) {
                        // If user does not match, skip this ACL
                        continue;
                    } else {
                        // If user matches, check if one the roles gives the read permission
                        for (String role : StringUtils.split(roles, '/')) {
                            if (((JahiaAccessManager) session.getAccessControlManager()).matchPermission(
                                    Sets.newHashSet(Privilege.JCR_READ + "_" + session.getWorkspace().getName()),
                                    role)) {
                                // User and role matches, read is granted
                                return true;
                            }
                        }
                    }
                } else {
                    if (!singleUser.equals(session.getUserID())) {
                        // If user does not match, skip this ACL
                        continue;
                    }
                    // Otherwise, do normal ACL check.
                }
            }
            // Verify first if this acl has already been checked
            Boolean aclChecked = checkedAcls.get(acl);
            if (aclChecked == null) {
                try {
                    canRead = session.getAccessManager().canRead(null, new NodeId(acl));
                    checkedAcls.put(acl, canRead);
                } catch (RepositoryException e) {
                }
            } else {
                canRead = aclChecked;
            }
            break;
        }
        return canRead;
    }

    /**
     * Returns <code>true</code> if ACL-UUID should be resolved and stored in index.
     * This can have a negative effect on performance, when setting rights on a node,
     * which has a large subtree using the same rights, as all these nodes will need
     * to be reindexed. On the other side the advantage is that the queries are faster,
     * as the user rights are resolved faster.
     *
     * @return Returns <code>true</code> if ACL-UUID should be resolved and stored in index.
     */
    public boolean isAclUuidInIndex() {
        return index instanceof JahiaSearchIndex && ((JahiaSearchIndex) index).isAddAclUuidInIndex();
    }

    /**
     * Get a String array of indexed fields for running quick checks
     * [0] the uuid of the language independent node
     * [1] the acl-id
     * [2] "1" if visibility rule is set for node
     * [3] "true" node is published / "false" node is not published
     */
    private IndexedNodeInfo getIndexedNodeInfo(ScoreNode sn, IndexReader reader, final boolean onlyMainNodeUuid)
            throws IOException {
        IndexedNodeInfo info = new IndexedNodeInfo(sn.getDoc(reader));

        Document doc = reader.document(info.getDocNumber(),
                onlyMainNodeUuid ? ONLY_MAIN_NODE_UUID : OPTIMIZATION_FIELDS);

        if (doc.getField(JahiaNodeIndexer.TRANSLATED_NODE_PARENT) != null) {
            info.setMainNodeUuid(doc.getField(FieldNames.PARENT).stringValue());
        } else {
            info.setMainNodeUuid(sn.getNodeId().toString());
        }
        if (!onlyMainNodeUuid) {
            if (isAclUuidInIndex()) {
                Field aclUuidField = doc.getField(JahiaNodeIndexer.ACL_UUID);
                if (aclUuidField != null) {
                    info.setAclUuid(aclUuidField.stringValue());
                }
            }
            Field checkVisibilityField = doc.getField(JahiaNodeIndexer.CHECK_VISIBILITY);
            if (checkVisibilityField != null) {
                info.setCheckVisibility(checkVisibilityField.stringValue());
            }
            Field publishedField = doc.getField(JahiaNodeIndexer.PUBLISHED);
            if (publishedField != null) {
                info.setPublished(publishedField.stringValue());
            }
            Field[] checkInvalidLanguagesField = doc.getFields(JahiaNodeIndexer.INVALID_LANGUAGES);
            if (checkInvalidLanguagesField != null && checkInvalidLanguagesField.length > 0) {
                for (Field field : checkInvalidLanguagesField) {
                    info.addInvalidLanguages(field.stringValue());
                }
            }
        }
        return info;
    }

    protected Query getNodeIdQuery(String field, String path) throws RepositoryException {
        BooleanQuery or = null;
        try {
            if (field.equals(FieldNames.PARENT)) {
                String identifier = session.getNode(path).getIdentifier();
                Query q1 = new JackrabbitTermQuery(new Term(FieldNames.PARENT, identifier));
                Query q2 = new JackrabbitTermQuery(new Term(JahiaNodeIndexer.TRANSLATED_NODE_PARENT, identifier));
                or = new BooleanQuery();
                or.add(q1, BooleanClause.Occur.SHOULD);
                or.add(q2, BooleanClause.Occur.SHOULD);
            } else {
                return super.getNodeIdQuery(field, path);
            }
        } catch (AccessDeniedException e) {
            return new JackrabbitTermQuery(new Term(FieldNames.UUID, "invalid-node-id")); // never matches
        } catch (PathNotFoundException e) {
            return new JackrabbitTermQuery(new Term(FieldNames.UUID, "invalid-node-id")); // never matches
        }
        return or;
    }

    @Override
    protected Query create(Constraint constraint, Map<String, NodeType> selectorMap,
            JackrabbitIndexSearcher searcher) throws RepositoryException, IOException {
        if (constraint instanceof SameNode) {
            SameNode sn = (SameNode) constraint;
            if (locale != null) {
                String identifier = session.getNode(sn.getPath()).getIdentifier();
                Query q1 = new JackrabbitTermQuery(new Term(FieldNames.UUID, identifier));
                Query q2 = new JackrabbitTermQuery(new Term(FieldNames.PARENT, identifier));
                Query q3 = new JackrabbitTermQuery(
                        new Term(JahiaNodeIndexer.TRANSLATION_LANGUAGE, locale.toString()));
                BooleanQuery and = new BooleanQuery();
                and.add(q2, BooleanClause.Occur.MUST);
                and.add(q3, BooleanClause.Occur.MUST);
                BooleanQuery or = new BooleanQuery();
                or.add(q1, BooleanClause.Occur.SHOULD);
                or.add(and, BooleanClause.Occur.SHOULD);
                return or;
            } else {
                return getNodeIdQuery(UUID, sn.getPath());
            }
        }
        return super.create(constraint, selectorMap, searcher);
    }
    //    protected Query getDescendantNodeQuery(
    //            DescendantNode dn, JackrabbitIndexSearcher searcher)
    //            throws RepositoryException, IOException {
    //
    ////        new DescendantSelfAxisQuery()
    //        Query query = null;
    //        try {
    //            query = new JackrabbitTermQuery(
    //                    new Term(JahiaNodeIndexer.ANCESTOR, session.getNode(dn.getAncestorPath()).getIdentifier()));
    //        } catch (PathNotFoundException e) {
    //            query = new JackrabbitTermQuery(new Term(FieldNames.UUID, "invalid-node-id")); // never matches
    //        }
    //        return query;
    ////        return super.getDescendantNodeQuery(dn, searcher);
    //    }

    protected Query getFullTextSearchQuery(FullTextSearch fts) throws RepositoryException {
        Query qobj = null;

        if (StringUtils.startsWith(fts.getPropertyName(), "rep:filter(")) {
            try {
                StaticOperand expr = fts.getFullTextSearchExpression();
                if (expr instanceof Literal) {
                    String expression = ((Literal) expr).getLiteralValue().getString();
                    // check if query is a single range query with mixed inclusive/exclusive endpoints, then
                    // directly create range query as the Lucene parser fails with ParseException (LUCENE-996)
                    qobj = resolveSingleMixedInclusiveExclusiveRangeQuery(expression);

                    if (qobj == null) {
                        QueryParser qp = new JahiaQueryParser(FieldNames.FULLTEXT, new KeywordAnalyzer());
                        qp.setLowercaseExpandedTerms(false);
                        qobj = qp.parse(expression);
                    }
                } else {
                    throw new RepositoryException("Unknown static operand type: " + expr);
                }
            } catch (ParseException e) {
                throw new RepositoryException(e);
            }
        } else {
            qobj = super.getFullTextSearchQuery(fts);
        }
        return qobj;
    }

    private Query resolveSingleMixedInclusiveExclusiveRangeQuery(String expression) {
        Query qobj = null;
        boolean inclusiveEndRange = expression.endsWith("]");
        boolean exclusiveEndRange = expression.endsWith("}");
        int inclusiveBeginRangeCount = StringUtils.countMatches(expression, "[");
        int exclusiveBeginRangeCount = StringUtils.countMatches(expression, "{");
        if (((inclusiveEndRange && exclusiveBeginRangeCount == 1 && inclusiveBeginRangeCount == 0)
                || (exclusiveEndRange && inclusiveBeginRangeCount == 1 && exclusiveBeginRangeCount == 0))) {
            String fieldName = (inclusiveEndRange || exclusiveEndRange)
                    ? StringUtils.substringBefore(expression, inclusiveEndRange ? ":{" : ":[")
                    : "";
            if (fieldName.indexOf(' ') == -1) {
                fieldName = fieldName.replace("\\:", ":");
                String rangeExpression = StringUtils.substringBetween(expression, inclusiveEndRange ? "{" : "[",
                        inclusiveEndRange ? "]" : "}");
                String part1 = StringUtils.substringBefore(rangeExpression, " TO");
                String part2 = StringUtils.substringAfter(rangeExpression, "TO ");
                SchemaField sf = new SchemaField(fieldName, JahiaQueryParser.STRING_TYPE);
                qobj = JahiaQueryParser.STRING_TYPE.getRangeQuery(null, sf, part1.equals("*") ? null : part1,
                        part2.equals("*") ? null : part2, !inclusiveEndRange, inclusiveEndRange);
            }
        }
        return qobj;
    }

    protected Predicate mapConstraintToQueryAndFilter(QueryPair query, Constraint constraint,
            Map<String, NodeType> selectorMap, JackrabbitIndexSearcher searcher, IndexReader reader)
            throws RepositoryException, IOException {
        try {
            if (constraint instanceof DescendantNode
                    && !((DescendantNode) constraint).getAncestorPath().equals("/")) {
                query.subQuery.add(new TermQuery(new Term(JahiaNodeIndexer.TRANSLATED_NODE_PARENT, session
                        .getNode(((DescendantNode) constraint).getAncestorPath()).getParent().getIdentifier())),
                        BooleanClause.Occur.MUST_NOT);
            } else if (constraint instanceof ChildNode && !((ChildNode) constraint).getParentPath().equals("/")) {
                query.subQuery.add(
                        new TermQuery(new Term(JahiaNodeIndexer.TRANSLATED_NODE_PARENT, session
                                .getNode(((ChildNode) constraint).getParentPath()).getParent().getIdentifier())),
                        BooleanClause.Occur.MUST_NOT);
            } else if (constraint instanceof Or) {
                final BooleanQuery context = new BooleanQuery();
                if (mapOrConstraintWithDescendantNodesOnly(context, constraint)) {
                    query.mainQuery = new DescendantSelfAxisQuery(context, query.subQuery, false);
                    return Predicate.TRUE;
                }
            }
        } catch (AccessDeniedException e) {
            // denied
            // todo : should find another way to test that we are not in a translation sub node
        } catch (PathNotFoundException e) {
            // not found
            query.subQuery.add(new JackrabbitTermQuery(new Term(FieldNames.UUID, "invalid-node-id")), // never matches
                    MUST);
        }

        return super.mapConstraintToQueryAndFilter(query, constraint, selectorMap, searcher, reader);
    }

    public boolean mapOrConstraintWithDescendantNodesOnly(BooleanQuery context, Constraint constraint)
            throws RepositoryException {
        if (constraint instanceof Or) {
            Or or = (Or) constraint;
            return mapOrConstraintWithDescendantNodesOnly(context, or.getConstraint1())
                    && mapOrConstraintWithDescendantNodesOnly(context, or.getConstraint2());
        } else if (constraint instanceof DescendantNode) {
            DescendantNode descendantNode = (DescendantNode) constraint;
            context.add(getNodeIdQuery(UUID, descendantNode.getAncestorPath()), BooleanClause.Occur.SHOULD);
            return true;
        }
        return false;
    }

    public Locale getLocale() {
        // if the query set a specific language, we should probably be using this one
        if (queryLanguage != null) {
            return LanguageCodeConverters.languageCodeToLocale(queryLanguage);
        }

        return locale;
    }

    public void setQueryLanguageAndLocale(String queryLanguage, Locale locale) {
        this.queryLanguage = queryLanguage;
        this.locale = locale;
    }

    @Override
    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;
            if (pv.getPropertyName().equals("_PARENT")) {
                return new JackrabbitTermQuery(new Term(FieldNames.PARENT,
                        getValueString(evaluator.getValue(rigth), PropertyType.REFERENCE)));
            }
        }
        return super.getComparisonQuery(left, transform, operator, rigth, selectorMap);
    }

    @Override
    protected Analyzer getTextAnalyzer() {
        if (locale != null || queryLanguage != null) {
            // if we have a locale or the query specified a language, use it to retrieve a potential language-specific Analyzer
            final AnalyzerRegistry analyzerRegistry = index.getAnalyzerRegistry();
            final String lang = getLocale().toString();
            if (analyzerRegistry.acceptKey(lang)) {
                final Analyzer analyzer = analyzerRegistry.getAnalyzer(lang);
                if (analyzer != null) {
                    return analyzer;
                }
            }
        }

        // if we didn't find a language-specific analyzer, just return the default one
        return super.getTextAnalyzer();
    }

    public NamespaceMappings getNamespaceMappings() {
        return this.nsMappings;
    }

    class IndexedNodeInfo {
        private int docNumber;
        private String mainNodeUuid;
        private String aclUuid;
        private String checkVisibility;
        private String published;
        private List<String> checkInvalidLanguages = null;

        public IndexedNodeInfo(int docNumber) {
            super();
            this.docNumber = docNumber;
        }

        public String getMainNodeUuid() {
            return mainNodeUuid;
        }

        public void setMainNodeUuid(String mainNodeUuid) {
            this.mainNodeUuid = mainNodeUuid;
        }

        public String getAclUuid() {
            return aclUuid;
        }

        public void setAclUuid(String aclUuid) {
            this.aclUuid = aclUuid;
        }

        public String getCheckVisibility() {
            return checkVisibility;
        }

        public void setCheckVisibility(String checkVisibility) {
            this.checkVisibility = checkVisibility;
        }

        public String getPublished() {
            return published;
        }

        public void setPublished(String published) {
            this.published = published;
        }

        public int getDocNumber() {
            return docNumber;
        }

        public List<String> getCheckInvalidLanguages() {
            return checkInvalidLanguages;
        }

        public void addInvalidLanguages(String invalidLanguage) {
            if (checkInvalidLanguages == null) {
                checkInvalidLanguages = new ArrayList<String>();
            }
            checkInvalidLanguages.add(invalidLanguage);
        }
    }

    class LazySelectorRow extends SelectorRow { // <-- Added by jahia
        private Node node;
        private NodeId nodeId;

        LazySelectorRow(Map<String, PropertyValue> columns, OperandEvaluator evaluator, String selector,
                NodeId nodeId, double score) {
            super(columns, evaluator, selector, null, score);
            this.nodeId = nodeId;
        }

        LazySelectorRow(Map<String, PropertyValue> columns, OperandEvaluator evaluator, String selector, Node node,
                double score) {
            super(columns, evaluator, selector, node, score);
            this.node = node;
        }

        @Override
        public Node getNode() {
            try {
                if (node == null) {
                    Node originalNode = session.getNodeById(nodeId);
                    if (originalNode.isNodeType("jnt:translation")) {
                        originalNode = originalNode.getParent();
                    }
                    if (originalNode != null) {
                        node = originalNode;
                    }
                }
            } catch (ItemNotFoundException e) {
            } catch (PathNotFoundException e) {
            } catch (RepositoryException e) {
                logger.error("Cannot get node " + nodeId, e);
            }
            return node;
        }

        @Override
        public Node getNode(String selectorName) throws RepositoryException {
            super.getNode(selectorName);
            return getNode();
        }
    }

}