Added: 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/FulltextIndexPlanner.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/FulltextIndexPlanner.java?rev=1832166&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/FulltextIndexPlanner.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/FulltextIndexPlanner.java
 Thu May 24 13:07:54 2018
@@ -0,0 +1,1061 @@
+/*
+ * 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.oak.plugins.index.search.spi.query;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.annotation.CheckForNull;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.oak.api.PropertyValue;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.index.IndexConstants;
+import org.apache.jackrabbit.oak.plugins.index.property.ValuePatternUtil;
+import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition;
+import 
org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition.IndexingRule;
+import org.apache.jackrabbit.oak.plugins.index.search.IndexFormatVersion;
+import org.apache.jackrabbit.oak.plugins.index.search.IndexNode;
+import org.apache.jackrabbit.oak.plugins.index.search.IndexStatistics;
+import org.apache.jackrabbit.oak.plugins.index.search.PropertyDefinition;
+import org.apache.jackrabbit.oak.spi.query.Filter;
+import org.apache.jackrabbit.oak.spi.query.Filter.PropertyRestriction;
+import org.apache.jackrabbit.oak.spi.query.QueryConstants;
+import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextContains;
+import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextExpression;
+import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextTerm;
+import org.apache.jackrabbit.oak.spi.query.fulltext.FullTextVisitor;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static com.google.common.collect.Lists.newArrayListWithCapacity;
+import static com.google.common.collect.Maps.newHashMap;
+import static org.apache.jackrabbit.JcrConstants.JCR_SCORE;
+import static org.apache.jackrabbit.JcrConstants.NT_BASE;
+import static org.apache.jackrabbit.oak.commons.PathUtils.getAncestorPath;
+import static org.apache.jackrabbit.oak.commons.PathUtils.getDepth;
+import static org.apache.jackrabbit.oak.commons.PathUtils.getName;
+import static org.apache.jackrabbit.oak.commons.PathUtils.getParentPath;
+import static org.apache.jackrabbit.oak.spi.query.QueryIndex.IndexPlan;
+import static org.apache.jackrabbit.oak.spi.query.QueryIndex.OrderEntry;
+
+public class FulltextIndexPlanner {
+
+    /**
+     * IndexPaln Attribute name which refers to the name of the fields that 
should be used for facets.
+     */
+    public static final String ATTR_FACET_FIELDS = "oak.facet.fields";
+
+    private static final String FLAG_ENTRY_COUNT = 
"oak.fulltext.useActualEntryCount";
+    private static final Logger log = 
LoggerFactory.getLogger(FulltextIndexPlanner.class);
+    private final IndexDefinition definition;
+    private final Filter filter;
+    private final String indexPath;
+    private final List<OrderEntry> sortOrder;
+    private IndexNode indexNode;
+    private PlanResult result;
+    private static boolean useActualEntryCount = false;
+
+    static {
+        useActualEntryCount = 
Boolean.parseBoolean(System.getProperty(FLAG_ENTRY_COUNT, "true"));
+        if (!useActualEntryCount) {
+            log.info("System property {} found to be false. IndexPlanner would 
use a default entryCount of 1000 instead" +
+                    " of using the actual entry count", FLAG_ENTRY_COUNT);
+        }
+    }
+
+    public FulltextIndexPlanner(IndexNode indexNode,
+                                String indexPath,
+                                Filter filter, List<OrderEntry> sortOrder) {
+        this.indexNode = indexNode;
+        this.indexPath = indexPath;
+        this.definition = indexNode.getDefinition();
+        this.filter = filter;
+        this.sortOrder = sortOrder;
+    }
+
+    IndexPlan getPlan() {
+        IndexPlan.Builder builder = getPlanBuilder();
+
+        if (definition.isTestMode()){
+            if (builder == null) {
+                if (notSupportedFeature()) {
+                    return null;
+                }
+                String msg = String.format("No plan found for filter [%s] " +
+                        "while using definition [%s] and testMode is found to 
be enabled", filter, definition);
+                throw new IllegalStateException(msg);
+            } else {
+                builder.setEstimatedEntryCount(1)
+                        .setCostPerExecution(1e-3)
+                        .setCostPerEntry(1e-3);
+            }
+        }
+
+        return builder != null ? builder.build() : null;
+    }
+
+    @Override
+    public String toString() {
+        return "IndexPlanner{" +
+                "indexPath='" + indexPath + '\'' +
+                ", filter=" + filter +
+                ", sortOrder=" + sortOrder +
+                '}';
+    }
+
+    //For tests
+    static void setUseActualEntryCount(boolean useActualEntryCount) {
+        FulltextIndexPlanner.useActualEntryCount = useActualEntryCount;
+    }
+
+    private IndexPlan.Builder getPlanBuilder() {
+        log.trace("Evaluating plan with index definition {}", definition);
+
+        if (wrongIndex()) {
+            return null;
+        }
+
+        FullTextExpression ft = filter.getFullTextConstraint();
+
+        if (!definition.getVersion().isAtLeast(IndexFormatVersion.V2)){
+            log.trace("Index is old format. Not supported");
+            return null;
+        }
+
+        //Query Fulltext and Index does not support fulltext
+        if (ft != null && !definition.isFullTextEnabled()) {
+            return null;
+        }
+
+        IndexDefinition.IndexingRule indexingRule = getApplicableRule();
+        if (indexingRule == null){
+            return null;
+        }
+
+        //Query Fulltext and indexing rule does not support fulltext
+        if (ft != null && !indexingRule.isFulltextEnabled()){
+            return null;
+        }
+
+        if (!checkForQueryPaths()) {
+            log.trace("Opting out due mismatch between path restriction {} and 
query paths {}",
+                    filter.getPath(), definition.getQueryPaths());
+            return null;
+        }
+
+        result = new PlanResult(indexPath, definition, indexingRule);
+
+        if (definition.hasFunctionDefined()
+                && filter.getPropertyRestriction(definition.getFunctionName()) 
!= null) {
+            return 
getNativeFunctionPlanBuilder(indexingRule.getBaseNodeType());
+        }
+
+        List<String> indexedProps = 
newArrayListWithCapacity(filter.getPropertyRestrictions().size());
+
+        for (PropertyDefinition functionIndex : 
indexingRule.getFunctionRestrictions()) {
+            for (PropertyRestriction pr : filter.getPropertyRestrictions()) {
+                String f = functionIndex.function;
+                if (pr.propertyName.equals(f)) {
+                    indexedProps.add(f);
+                    result.propDefns.put(f, functionIndex);
+                }
+            }
+        }
+        //Optimization - Go further only if any of the property is configured
+        //for property index
+        List<String> facetFields = new LinkedList<String>();
+        boolean ntBaseRule = NT_BASE.equals(indexingRule.getNodeTypeName());
+        Map<String, PropertyDefinition> relativePropDefns = new HashMap<>();
+        if (indexingRule.propertyIndexEnabled) {
+            for (PropertyRestriction pr : filter.getPropertyRestrictions()) {
+                String name = pr.propertyName;
+                if (QueryConstants.RESTRICTION_LOCAL_NAME.equals(name)) {
+                    continue;
+                }
+                if 
(name.startsWith(QueryConstants.FUNCTION_RESTRICTION_PREFIX)) {
+                    // function-based indexes were handled before
+                    continue;
+                }
+                if (QueryConstants.REP_FACET.equals(pr.propertyName)) {
+                    String value = pr.first.getValue(Type.STRING);
+                    facetFields.add(FulltextIndex.parseFacetField(value));
+                }
+
+                PropertyDefinition pd = 
indexingRule.getConfig(pr.propertyName);
+
+                boolean relativeProps = false;
+                if (pd == null && ntBaseRule) {
+                    //Direct match not possible. Check for relative property 
definition
+                    //i.e. if no match found for jcr:content/@keyword then 
check if
+                    //property definition exists for 'keyword'
+                    pd = getSimpleProperty(indexingRule, pr.propertyName);
+                    relativeProps = pd != null;
+                }
+
+                if (pd != null && pd.propertyIndexEnabled()) {
+                    if (pr.isNullRestriction() && !pd.nullCheckEnabled){
+                        continue;
+                    }
+
+                    if (!matchesValuePattern(pr, pd)) {
+                        continue;
+                    }
+
+                    //A property definition with weight == 0 is only meant to 
be used
+                    //with some other definitions
+                    if (pd.weight != 0 && !relativeProps) {
+                        indexedProps.add(name);
+                    }
+
+                    if (relativeProps) {
+                        relativePropDefns.put(name, pd);
+                    } else {
+                        result.propDefns.put(name, pd);
+                    }
+                }
+            }
+        }
+
+        boolean evalNodeTypeRestrictions = 
canEvalNodeTypeRestrictions(indexingRule);
+        boolean evalPathRestrictions = canEvalPathRestrictions(indexingRule);
+        boolean canEvalAlFullText = canEvalAllFullText(indexingRule, ft);
+        boolean canEvalNodeNameRestriction = 
canEvalNodeNameRestriction(indexingRule);
+
+        if (ft != null && !canEvalAlFullText){
+            return null;
+        }
+
+        if (indexedProps.isEmpty() && !relativePropDefns.isEmpty() && 
!canEvalAlFullText) {
+            indexedProps = planForRelativeProperties(relativePropDefns);
+        }
+
+        //Fulltext expression can also be like 
jcr:contains(jcr:content/metadata/@format, 'image')
+
+        List<OrderEntry> sortOrder = createSortOrder(indexingRule);
+        boolean canSort = canSortByProperty(sortOrder);
+        if (!indexedProps.isEmpty() || canSort || ft != null
+                || evalPathRestrictions || evalNodeTypeRestrictions || 
canEvalNodeNameRestriction) {
+            int costPerEntryFactor = 1;
+            costPerEntryFactor += sortOrder.size();
+
+            IndexPlan.Builder plan = defaultPlan();
+            if (!sortOrder.isEmpty()) {
+                plan.setSortOrder(sortOrder);
+            }
+
+            if (facetFields.size() > 0) {
+                plan.setAttribute(ATTR_FACET_FIELDS, facetFields);
+            }
+
+            if (ft == null){
+                result.enableNonFullTextConstraints();
+            }
+
+            if (evalNodeTypeRestrictions){
+                result.enableNodeTypeEvaluation();
+            }
+
+            if (canEvalNodeNameRestriction){
+                result.enableNodeNameRestriction();
+            }
+
+            // Set a index based guess here. Unique would set its own value 
below
+            if (useActualEntryCount && !definition.isEntryCountDefined()) {
+                int maxPossibleNumDocs = 
getMaxPossibleNumDocs(result.propDefns, filter);
+                if (maxPossibleNumDocs >= 0) {
+                    plan.setEstimatedEntryCount(maxPossibleNumDocs);
+                }
+            }
+
+            if (sortOrder.isEmpty() && ft == null) {
+                boolean uniqueIndexFound = planForSyncIndexes(indexingRule);
+                if (uniqueIndexFound) {
+                    //For unique index there would be at max 1 entry
+                    plan.setEstimatedEntryCount(1);
+                }
+            }
+
+            return plan.setCostPerEntry(definition.getCostPerEntry() / 
costPerEntryFactor);
+        }
+
+        //TODO Support for property existence queries
+
+        return null;
+    }
+
+    private boolean matchesValuePattern(PropertyRestriction pr, 
PropertyDefinition pd) {
+        if (!pd.valuePattern.matchesAll()){
+            //So we have a valuePattern defined. So determine if
+            //this index can return a plan based on values
+            Set<String> values = ValuePatternUtil.getAllValues(pr);
+            if (values == null) {
+                // "is not null" condition, but we have a value pattern
+                // that doesn't match everything
+                // case of like search
+                String prefix = ValuePatternUtil.getLongestPrefix(filter, 
pr.propertyName);
+                if (!pd.valuePattern.matchesPrefix(prefix)) {
+                    // region match which is not fully in the pattern
+                    return false;
+                }
+            } else {
+                // we have a value pattern, for example (a|b),
+                // but we search (also) for 'c': can't match
+                if (!pd.valuePattern.matchesAll(values)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    private boolean wrongIndex() {
+        // REMARK: similar code is used in oak-core, PropertyIndex
+        // skip index if "option(index ...)" doesn't match
+        PropertyRestriction indexName = 
filter.getPropertyRestriction(IndexConstants.INDEX_NAME_OPTION);
+        boolean wrong = false;
+        if (indexName != null && indexName.first != null) {
+            String name = indexName.first.getValue(Type.STRING);
+            String thisName = definition.getIndexName();
+            if (thisName != null) {
+                thisName = PathUtils.getName(thisName);
+                if (thisName.equals(name)) {
+                    // index name specified, and matches
+                    return false;
+                }
+            }
+            wrong = true;
+        }
+        PropertyRestriction indexTag = 
filter.getPropertyRestriction(IndexConstants.INDEX_TAG_OPTION);
+        if (indexTag != null && indexTag.first != null) {
+            // index tag specified
+            String[] tags = definition.getIndexTags();
+            if (tags == null) {
+                // no tag
+                return true;
+            }
+            String tag = indexTag.first.getValue(Type.STRING);
+            for(String t : tags) {
+                if (t.equals(tag)) {
+                    // tag matches
+                    return false;
+                }
+            }
+            // no tag matches
+            return true;
+        }
+        // no tag specified
+        return wrong;
+    }
+
+    private IndexPlan.Builder getNativeFunctionPlanBuilder(String 
indexingRuleBaseNodeType) {
+        boolean canHandleNativeFunction = true;
+
+        PropertyValue pv = 
filter.getPropertyRestriction(definition.getFunctionName()).first;
+        String query = pv.getValue(Type.STRING);
+
+        if (query.startsWith("suggest?term=")) {
+            if (definition.isSuggestEnabled()) {
+                canHandleNativeFunction = 
indexingRuleBaseNodeType.equals(filter.getNodeType());
+            } else {
+                canHandleNativeFunction = false;
+            }
+        } else if (query.startsWith("spellcheck?term=")) {
+            if (definition.isSpellcheckEnabled()) {
+                canHandleNativeFunction = 
indexingRuleBaseNodeType.equals(filter.getNodeType());
+            } else {
+                canHandleNativeFunction = false;
+            }
+        }
+
+        //Suggestion and SpellCheck use virtual paths which is same for all 
results
+        if (canHandleNativeFunction) {
+            result.disableUniquePaths();
+        }
+
+        //If native function can be handled by this index then ensure
+        // that lowest cost if returned
+        return canHandleNativeFunction ? 
defaultPlan().setEstimatedEntryCount(1) : null;
+    }
+
+    /**
+     * Check if there is a mismatch between QueryPaths associated with index
+     * and path restriction specified in query
+
+     * @return true if QueryPaths and path restrictions do not have any 
conflict
+     */
+    private boolean checkForQueryPaths() {
+        String[] queryPaths = definition.getQueryPaths();
+        if (queryPaths == null){
+            //No explicit value specified. Assume '/' which results in true
+            return true;
+        }
+
+        String pathRestriction = filter.getPath();
+        for (String queryPath : queryPaths){
+            if (queryPath.equals(pathRestriction) || 
PathUtils.isAncestor(queryPath, pathRestriction)){
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private boolean canEvalNodeNameRestriction(IndexingRule indexingRule) {
+        PropertyRestriction pr = 
filter.getPropertyRestriction(QueryConstants.RESTRICTION_LOCAL_NAME);
+        if (pr == null){
+            return false;
+        }
+        return indexingRule.isNodeNameIndexed();
+    }
+
+    private static boolean canSortByProperty(List<OrderEntry> sortOrder) {
+        if (sortOrder.isEmpty()) {
+            return false;
+        }
+
+        // If jcr:score is the only sort order then opt out
+        if (sortOrder.size() == 1 &&
+                JCR_SCORE.equals(sortOrder.get(0).getPropertyName())) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private boolean canEvalAllFullText(final IndexingRule indexingRule, 
FullTextExpression ft) {
+        if (ft == null){
+            return false;
+        }
+
+        final HashSet<String> relPaths = new HashSet<String>();
+        final HashSet<String> nonIndexedPaths = new HashSet<String>();
+        final AtomicBoolean relativeParentsFound = new AtomicBoolean();
+        final AtomicBoolean nodeScopedCondition = new AtomicBoolean();
+        ft.accept(new FullTextVisitor.FullTextVisitorBase() {
+            @Override
+            public boolean visit(FullTextContains contains) {
+                visitTerm(contains.getPropertyName());
+                return true;
+            }
+
+            @Override
+            public boolean visit(FullTextTerm term) {
+                visitTerm(term.getPropertyName());
+                return true;
+            }
+
+            private void visitTerm(String propertyName) {
+                String p = propertyName;
+                String propertyPath = null;
+                String nodePath = null;
+                if (p == null) {
+                    relPaths.add("");
+                } else if (p.startsWith("../") || p.startsWith("./")) {
+                    relPaths.add(p);
+                    relativeParentsFound.set(true);
+                } else if (getDepth(p) > 1) {
+                    String parent = getParentPath(p);
+                    if (FulltextIndex.isNodePath(p)){
+                        nodePath = parent;
+                    } else {
+                        propertyPath = p;
+                    }
+                    relPaths.add(parent);
+                } else {
+                    propertyPath = p;
+                    relPaths.add("");
+                }
+
+                if (nodePath != null
+                        && !indexingRule.isAggregated(nodePath)){
+                    nonIndexedPaths.add(p);
+                } else if (propertyPath != null) {
+                    PropertyDefinition pd = 
indexingRule.getConfig(propertyPath);
+                    //If given prop is not analyzed then its
+                    //not indexed
+                    if (pd == null){
+                        nonIndexedPaths.add(p);
+                    } else if (!pd.analyzed){
+                        nonIndexedPaths.add(p);
+                    }
+                }
+
+                if (nodeScopedTerm(propertyName)){
+                    nodeScopedCondition.set(true);
+                }
+            }
+        });
+
+        if (nodeScopedCondition.get() && 
!indexingRule.isNodeFullTextIndexed()){
+            return false;
+        }
+
+        if (relativeParentsFound.get()){
+            log.debug("Relative parents found {} which are not supported", 
relPaths);
+            return false;
+        }
+
+        //where contains('jcr:content/bar', 'mountain OR valley') and 
contains('jcr:content/foo', 'mountain OR valley')
+        //above query can be evaluated by index which indexes foo and bar with 
restriction that both belong to same node
+        //by displacing the query path to evaluate on contains('bar', ...) and 
filter out those parents which do not
+        //have jcr:content as parent. So ensure that relPaths size is 1 or 0
+        if (!nonIndexedPaths.isEmpty()){
+            if (relPaths.size() > 1){
+                log.debug("Following relative  property paths are not index", 
relPaths);
+                return false;
+            }
+            result.setParentPath(Iterables.getOnlyElement(relPaths, ""));
+
+            //Such non indexed path can possibly be evaluated via any rule on 
nt:base
+            //which can possibly index everything
+            IndexingRule rule = definition.getApplicableIndexingRule(NT_BASE);
+            if (rule == null){
+                return false;
+            }
+
+            for (String p : nonIndexedPaths){
+                //Index can only evaluate a node search jcr:content/*
+                //if it indexes node scope indexing is enabled
+                if (FulltextIndex.isNodePath(p)){
+                    if (!rule.isNodeFullTextIndexed()) {
+                        return false;
+                    }
+                } else {
+                    //Index can only evaluate a property like jcr:content/type
+                    //if it indexes 'type' and that too analyzed
+                    String propertyName = PathUtils.getName(p);
+                    PropertyDefinition pd = rule.getConfig(propertyName);
+                    if (pd == null){
+                        return false;
+                    }
+                    if (!pd.analyzed){
+                        return false;
+                    }
+                }
+            }
+        } else {
+            result.setParentPath("");
+        }
+
+        return true;
+    }
+
+    /**
+     * Computes the indexedProps which can be part of query by virtue of 
relativizing i.e.
+     * if query is on jcr:content/keyword then perform search on keyword and 
change parent
+     * path to jcr:content
+     * @param relativePropDefns property definitions for such relative 
properties. The key
+     *                          would be actual property name as in query i.e. 
jcr:content/keyword
+     *                          while property definition would be for 
'keyword'
+     * @return list of properties which are included in query issued to Lucene
+     */
+    private List<String> planForRelativeProperties(Map<String, 
PropertyDefinition> relativePropDefns) {
+        Multimap<String, Map.Entry<String, PropertyDefinition>> relpaths = 
ArrayListMultimap.create();
+        int maxSize = 0;
+        String maxCountedParent = null;
+
+        //Collect the relative properties grouped by parent path
+        //and track the parent having maximum properties
+        for (Map.Entry<String, PropertyDefinition> e : 
relativePropDefns.entrySet()) {
+            String relativePropertyPath = e.getKey();
+            String parent = getParentPath(relativePropertyPath);
+
+            relpaths.put(parent, e);
+            int count = relpaths.get(parent).size();
+            if (count > maxSize) {
+                maxSize = count;
+                maxCountedParent = parent;
+            }
+        }
+
+        //Set the parent path to one which is present in most prop. In case of 
tie any one
+        //such path would be picked
+        result.setParentPath(maxCountedParent);
+
+        //Now add only those properties to plan which have the maxCountedParent
+        List<String> indexedProps = new ArrayList<>(maxSize);
+        for (Map.Entry<String, PropertyDefinition> e : 
relpaths.get(maxCountedParent)) {
+            String relativePropertyPath = e.getKey();
+            result.propDefns.put(relativePropertyPath, e.getValue());
+            result.relPropMapping.put(relativePropertyPath, 
PathUtils.getName(relativePropertyPath));
+            if (e.getValue().weight != 0) {
+                indexedProps.add(relativePropertyPath);
+            }
+        }
+
+        return indexedProps;
+    }
+
+    @CheckForNull
+    private static PropertyDefinition getSimpleProperty(IndexingRule 
indexingRule, String relativePropertyName) {
+        String name = PathUtils.getName(relativePropertyName);
+        if (name.equals(relativePropertyName)){
+            //Not a relative property
+            return null;
+        }
+
+        //Properties using ../ or ./ notation not support. The relative 
property path
+        //must be fixed
+        if (relativePropertyName.startsWith("../") || 
relativePropertyName.startsWith("./")) {
+            return null;
+        }
+        return indexingRule.getConfig(name);
+    }
+
+    private boolean planForSyncIndexes(IndexDefinition.IndexingRule 
indexingRule) {
+        //If no sync index involved then return right away
+        if (!definition.hasSyncPropertyDefinitions()) {
+            return false;
+        }
+
+        if (result.propDefns.isEmpty() && 
!result.evaluateNodeTypeRestriction()) {
+            return false;
+        }
+
+        List<PropertyIndexResult> unique = newArrayList();
+        List<PropertyIndexResult> nonUnique = newArrayList();
+
+        for (PropertyRestriction pr : filter.getPropertyRestrictions()) {
+            String propertyName = result.getPropertyName(pr);
+            PropertyDefinition pd = result.propDefns.get(pr.propertyName);
+
+            if (pd != null) {
+                PropertyIndexResult e = new PropertyIndexResult(propertyName, 
pr);
+                if (pd.unique) {
+                    unique.add(e);
+                } else {
+                    nonUnique.add(e);
+                }
+            }
+        }
+
+        //Pick the first index (if multiple). For unique its fine
+        //For non unique we can probably later add support for cost
+        //based selection
+        boolean uniqueIndexFound = false;
+        if (!unique.isEmpty()) {
+            result.propertyIndexResult = unique.get(0);
+            uniqueIndexFound = true;
+        } else if (!nonUnique.isEmpty()) {
+            result.propertyIndexResult = nonUnique.get(0);
+        }
+
+        if (result.propertyIndexResult == null && 
result.evaluateNodeTypeRestriction()) {
+            PropertyDefinition pd = 
indexingRule.getConfig(JcrConstants.JCR_PRIMARYTYPE);
+            if (pd != null && pd.sync) {
+                result.syncNodeTypeRestrictions = true;
+            }
+        }
+
+        return uniqueIndexFound;
+    }
+
+    private boolean canEvalPathRestrictions(IndexingRule rule) {
+        //Opt out if one is looking for all children for '/' as its equivalent 
to
+        //NO_RESTRICTION
+        if (filter.getPathRestriction() == 
Filter.PathRestriction.NO_RESTRICTION
+                || (filter.getPathRestriction() == 
Filter.PathRestriction.ALL_CHILDREN
+                        && PathUtils.denotesRoot(filter.getPath()))
+                ){
+            return false;
+        }
+        //If no other restrictions is provided and query is pure
+        //path restriction based then need to be sure that index definition at 
least
+        //allows indexing all the path for given nodeType
+        return definition.evaluatePathRestrictions() && 
rule.indexesAllNodesOfMatchingType();
+    }
+
+
+    private boolean canEvalNodeTypeRestrictions(IndexingRule rule) {
+        //No need to handle nt:base
+        if (filter.matchesAllTypes()){
+            return false;
+        }
+
+        //Only opt in if rule is not derived from nt:base otherwise it would
+        //get used when there a full text index on all nodes
+        return rule.indexesAllNodesOfMatchingType() && !rule.isBasedOnNtBase();
+    }
+
+    private IndexPlan.Builder defaultPlan() {
+        return new IndexPlan.Builder()
+                .setCostPerExecution(definition.getCostPerExecution())
+                .setCostPerEntry(definition.getCostPerEntry())
+                .setFulltextIndex(definition.isFullTextEnabled())
+                .setIncludesNodeData(false) // we should not include node data
+                .setFilter(filter)
+                .setPathPrefix(getPathPrefix())
+                
.setSupportsPathRestriction(definition.evaluatePathRestrictions())
+                .setDelayed(true) //Lucene is always async
+                .setAttribute(FulltextIndex.ATTR_PLAN_RESULT, result)
+                .setEstimatedEntryCount(estimatedEntryCount())
+                .setPlanName(indexPath);
+    }
+
+    private long estimatedEntryCount() {
+        int numOfDocs = getNumDocs();
+        if (useActualEntryCount) {
+            return definition.isEntryCountDefined() ? 
definition.getEntryCount() : numOfDocs;
+        } else {
+            return estimatedEntryCount_Compat(numOfDocs);
+        }
+    }
+
+    private long estimatedEntryCount_Compat(int numOfDocs) {
+        //Other index only compete in case of property indexes. For fulltext
+        //index return true count so as to allow multiple property indexes
+        //to be compared fairly
+        FullTextExpression ft = filter.getFullTextConstraint();
+        if (ft != null && definition.isFullTextEnabled()){
+            return definition.getFulltextEntryCount(numOfDocs);
+        }
+        return Math.min(definition.getEntryCount(), numOfDocs);
+    }
+
+    private String getPathPrefix() {
+        // 2 = /oak:index/<index name>
+        String parentPath = PathUtils.getAncestorPath(indexPath, 2);
+        return PathUtils.denotesRoot(parentPath) ? "" : parentPath;
+    }
+
+    private int getNumDocs() {
+        return indexNode.getIndexStatistics().numDocs();
+    }
+
+    private int getMaxPossibleNumDocs(Map<String, PropertyDefinition> 
propDefns, Filter filter) {
+        IndexStatistics indexStatistics = indexNode.getIndexStatistics();
+        int minNumDocs = indexStatistics.numDocs();
+        for (Map.Entry<String, PropertyDefinition> propDef : 
propDefns.entrySet()) {
+            String key = propDef.getKey();
+            if (result.relPropMapping.containsKey(key)) {
+                key = getName(key);
+            }
+            int docCntForField = indexStatistics.getDocCountFor(key);
+            if (docCntForField == -1) {
+                continue;
+            }
+
+            int weight = propDef.getValue().weight;
+
+            PropertyRestriction pr = filter.getPropertyRestriction(key);
+            if (pr != null) {
+                if (pr.isNotNullRestriction()) {
+                    // don't use weight for "is not null" restrictions
+                    weight = 1;
+                } else {
+                    if (weight > 1) {
+                        // for non-equality conditions such as
+                        // where x > 1, x < 2, x like y,...:
+                        // use a maximum weight of 3,
+                        // so assume we read at least 30%
+                        if (!isEqualityRestriction(pr)) {
+                            weight = Math.min(3, weight);
+                        }
+                    }
+                }
+            }
+
+            if (weight > 1) {
+                // use it to scale down the doc count - in broad strokes, we 
can think of weight
+                // as number of terms for the field with all terms getting 
equal share of
+                // the documents in this field
+                double scaledDocCnt = Math.ceil((double) docCntForField / 
weight);
+                if (minNumDocs < scaledDocCnt) {
+                    continue;
+                }
+                // since, we've already taken care that scaled cost is lower 
than minCost,
+                // we can safely cast without risking overflow
+                minNumDocs = (int)scaledDocCnt;
+            } else if (docCntForField < minNumDocs) {
+                minNumDocs = docCntForField;
+            }
+        }
+        return minNumDocs;
+    }
+
+    private static boolean isEqualityRestriction(PropertyRestriction pr) {
+        return pr.first != null && pr.first == pr.last;
+    }
+
+    private List<OrderEntry> createSortOrder(IndexDefinition.IndexingRule 
rule) {
+        if (sortOrder == null) {
+            return Collections.emptyList();
+        }
+
+        List<OrderEntry> orderEntries = 
newArrayListWithCapacity(sortOrder.size());
+        for (OrderEntry o : sortOrder) {
+            PropertyDefinition pd = rule.getConfig(o.getPropertyName());
+            if (pd != null
+                    && pd.ordered
+                    && o.getPropertyType() != null
+                    && !o.getPropertyType().isArray()) {
+                orderEntries.add(o); // can manage any order desc/asc
+                result.sortedProperties.add(pd);
+            } else if 
(o.getPropertyName().equals(IndexDefinition.NATIVE_SORT_ORDER.getPropertyName()))
 {
+                // Supports jcr:score descending natively
+                orderEntries.add(IndexDefinition.NATIVE_SORT_ORDER);
+            }
+            for (PropertyDefinition functionIndex : 
rule.getFunctionRestrictions()) {
+                if (o.getPropertyName().equals(functionIndex.function)) {
+                    // can manage any order desc/asc
+                    orderEntries.add(o);
+                    result.sortedProperties.add(functionIndex);
+                }
+            }
+        }
+
+        //TODO Should we return order entries only when all order clauses are 
satisfied
+        return orderEntries;
+    }
+
+    @CheckForNull
+    private IndexDefinition.IndexingRule getApplicableRule() {
+        if (filter.matchesAllTypes()){
+            return definition.getApplicableIndexingRule(JcrConstants.NT_BASE);
+        } else {
+            //TODO May be better if filter.getSuperTypes returned a list which 
maintains
+            //inheritance order and then we iterate over that
+            for (IndexDefinition.IndexingRule rule : 
definition.getDefinedRules()){
+                if (filter.getSupertypes().contains(rule.getNodeTypeName())){
+                    //Theoretically there may be multiple rules for same 
nodeType with
+                    //some condition defined. So again find a rule which 
applies
+                    IndexDefinition.IndexingRule matchingRule = 
definition.getApplicableIndexingRule(rule.getNodeTypeName());
+
+                    if (matchingRule == null && 
rule.getNodeTypeName().equals(filter.getNodeType())){
+                        //In case nodetype registry in IndexDefinition is 
stale then it would not populate
+                        //rules for new nodetype even though at indexing time 
it was able to index (due to
+                        //use of latest nodetype reg nodestate)
+                        //In such a case if the rule name and nodetype name 
for query matches then it is
+                        //considered a match.
+                        //This would though not work for the case where rule 
is related to nodetype as used
+                        //in query matched via some inheritance chain
+                        //TODO Need a way to check if nodetype reg as seen by 
IndexDefinition is old then
+                        //IndexNode is reopened
+                        matchingRule = rule;
+                    }
+                    if (matchingRule != null){
+                        log.debug("Applicable IndexingRule found {}", 
matchingRule);
+                        return rule;
+                    }
+                }
+                //nt:base is applicable for all. This specific condition is
+                //required to support mixin case as filter.getSupertypes() for 
mixin based
+                //query only includes the mixin type and not nt:base
+                if (rule.getNodeTypeName().equals(JcrConstants.NT_BASE)){
+                    return rule;
+                }
+            }
+            log.trace("No applicable IndexingRule found for any of the 
superTypes {}",
+                filter.getSupertypes());
+        }
+        return null;
+    }
+
+    private boolean notSupportedFeature() {
+        if(filter.getPathRestriction() == Filter.PathRestriction.NO_RESTRICTION
+                && filter.matchesAllTypes()
+                && filter.getPropertyRestrictions().isEmpty()) {
+            //This mode includes name(), localname() queries
+            //OrImpl [a/name] = 'Hello' or [b/name] = 'World'
+            //Relative parent properties where [../foo1] is not null
+            return true;
+        }
+        boolean failTestOnMissingFunctionIndex = true;
+        if (failTestOnMissingFunctionIndex) {
+            // this means even just function restrictions fail the test
+            // (for example "where upper(name) = 'X'",
+            // if a matching function-based index is missing
+            return false;
+        }
+        // the following would ensure the test doesn't fail in that case:
+        for (PropertyRestriction r : filter.getPropertyRestrictions()) {
+            if 
(!r.propertyName.startsWith(QueryConstants.FUNCTION_RESTRICTION_PREFIX)) {
+                // not a function restriction
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Determine if the propertyName of a fulltext term indicates current node
+     * @param propertyName property name in the full text term clause
+     */
+    private static boolean nodeScopedTerm(String propertyName) {
+        return propertyName == null || ".".equals(propertyName) || 
"*".equals(propertyName);
+    }
+
+    //~--------------------------------------------------------< PlanResult >
+
+    public static class PlanResult {
+        final String indexPath;
+        final IndexDefinition indexDefinition;
+        final IndexDefinition.IndexingRule indexingRule;
+        private final List<PropertyDefinition> sortedProperties = 
newArrayList();
+
+        //Map of actual property name as present in our property definitions
+        private final Map<String, PropertyDefinition> propDefns = newHashMap();
+
+        //Map of property restriction name -> property definition name
+        //like 'jcr:content/status' -> 'status'
+        private final Map<String, String> relPropMapping = newHashMap();
+
+        private boolean nonFullTextConstraints;
+        private int parentDepth;
+        private String parentPathSegment;
+        private boolean relativize;
+        private boolean nodeTypeRestrictions;
+        private boolean nodeNameRestriction;
+        private boolean uniquePathsRequired = true;
+        private PropertyIndexResult propertyIndexResult;
+        private boolean syncNodeTypeRestrictions;
+
+        public PlanResult(String indexPath, IndexDefinition defn, 
IndexDefinition.IndexingRule indexingRule) {
+            this.indexPath = indexPath;
+            this.indexDefinition = defn;
+            this.indexingRule = indexingRule;
+        }
+
+        public PropertyDefinition getPropDefn(PropertyRestriction pr){
+            return propDefns.get(pr.propertyName);
+        }
+
+        /**
+         * Returns the property name to be used for query for given 
PropertyRestriction
+         * The name can be same as one for property restriction or it can be a 
mapped one
+         */
+        public String getPropertyName(PropertyRestriction pr) {
+            return relPropMapping.getOrDefault(pr.propertyName, 
pr.propertyName);
+        }
+
+        public boolean hasProperty(String propName){
+            return propDefns.containsKey(propName);
+        }
+
+        public PropertyDefinition getOrderedProperty(int index){
+            return sortedProperties.get(index);
+        }
+
+        public boolean isPathTransformed(){
+            return relativize;
+        }
+
+        public boolean isUniquePathsRequired() {
+            return uniquePathsRequired;
+        }
+
+        /**
+         * Transforms the given path if the query involved relative properties 
and index
+         * is not making use of aggregated properties. If the path
+         *
+         * @param path path to transform
+         * @return transformed path. Returns null if the path does not confirm 
to relative
+         * path requirements
+         */
+        @CheckForNull
+        public String transformPath(String path){
+            if (isPathTransformed()){
+                // get the base path
+                // ensure the path ends with the given
+                // relative path
+                if (!path.endsWith(parentPathSegment)) {
+                    return null;
+                }
+                return getAncestorPath(path, parentDepth);
+            }
+            return path;
+        }
+
+        public boolean evaluateNonFullTextConstraints(){
+            return nonFullTextConstraints;
+        }
+
+        public boolean evaluateNodeTypeRestriction() {
+            return nodeTypeRestrictions;
+        }
+
+        public boolean evaluateSyncNodeTypeRestriction() {
+            return syncNodeTypeRestrictions;
+        }
+
+        public boolean evaluateNodeNameRestriction() {return 
nodeNameRestriction;}
+
+        @CheckForNull
+        public PropertyIndexResult getPropertyIndexResult() {
+            return propertyIndexResult;
+        }
+
+        public boolean hasPropertyIndexResult(){
+            return propertyIndexResult != null;
+        }
+
+        private void setParentPath(String relativePath){
+            parentPathSegment = "/" + relativePath;
+            if (relativePath.isEmpty()){
+                // we only restrict non-full-text conditions if there is
+                // no relative property in the full-text constraint
+                enableNonFullTextConstraints();
+            } else {
+                relativize = true;
+                parentDepth = getDepth(relativePath);
+            }
+        }
+
+        private void enableNonFullTextConstraints(){
+            nonFullTextConstraints = true;
+        }
+
+        private void enableNodeTypeEvaluation() {
+            nodeTypeRestrictions = true;
+        }
+
+        private void enableNodeNameRestriction(){
+            nodeNameRestriction = true;
+        }
+
+        private void disableUniquePaths(){
+            uniquePathsRequired = false;
+        }
+    }
+
+    public static class PropertyIndexResult {
+        final String propertyName;
+        final PropertyRestriction pr;
+
+        public PropertyIndexResult(String propertyName, PropertyRestriction 
pr) {
+            this.propertyName = propertyName;
+            this.pr = pr;
+        }
+    }
+}

Propchange: 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/FulltextIndexPlanner.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/FulltextIndexTracker.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/FulltextIndexTracker.java?rev=1832166&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/FulltextIndexTracker.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/FulltextIndexTracker.java
 Thu May 24 13:07:54 2018
@@ -0,0 +1,259 @@
+/*
+ * 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.oak.plugins.index.search.spi.query;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.CheckForNull;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.commons.PerfLogger;
+import org.apache.jackrabbit.oak.plugins.index.AsyncIndexInfoService;
+import org.apache.jackrabbit.oak.plugins.index.search.BadIndexTracker;
+import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition;
+import org.apache.jackrabbit.oak.plugins.index.search.IndexNode;
+import org.apache.jackrabbit.oak.plugins.index.search.util.IndexHelper;
+import org.apache.jackrabbit.oak.spi.commit.CompositeEditor;
+import org.apache.jackrabbit.oak.spi.commit.DefaultEditor;
+import org.apache.jackrabbit.oak.spi.commit.Editor;
+import org.apache.jackrabbit.oak.spi.commit.EditorDiff;
+import org.apache.jackrabbit.oak.spi.commit.SubtreeEditor;
+import org.apache.jackrabbit.oak.spi.state.EqualsDiff;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Predicates.in;
+import static com.google.common.base.Predicates.not;
+import static com.google.common.base.Predicates.notNull;
+import static com.google.common.collect.Lists.newArrayListWithCapacity;
+import static com.google.common.collect.Maps.filterKeys;
+import static com.google.common.collect.Maps.filterValues;
+import static com.google.common.collect.Maps.newHashMap;
+import static java.util.Collections.emptyMap;
+import static 
org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition.INDEX_DEFINITION_NODE;
+import static 
org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition.STATUS_NODE;
+import static 
org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
+
+public abstract class FulltextIndexTracker {
+
+    /** Logger instance. */
+    private static final Logger log = 
LoggerFactory.getLogger(FulltextIndexTracker.class);
+    private static final PerfLogger PERF_LOGGER =
+            new 
PerfLogger(LoggerFactory.getLogger(FulltextIndexTracker.class.getName() + 
".perf"));
+
+    private final BadIndexTracker badIndexTracker = new BadIndexTracker();
+
+    private NodeState root = EMPTY_NODE;
+
+    private AsyncIndexInfoService asyncIndexInfoService;
+
+    private volatile Map<String, IndexNodeManager> indices = emptyMap();
+
+    private volatile boolean refresh;
+
+    protected abstract IndexNodeManager openIndex(String path, NodeState root, 
NodeState node);
+
+    synchronized void close() {
+        Map<String, IndexNodeManager> indices = this.indices;
+        this.indices = emptyMap();
+
+        for (Map.Entry<String, IndexNodeManager> entry : indices.entrySet()) {
+            try {
+                entry.getValue().close();
+            } catch (IOException e) {
+                log.error("Failed to close the Lucene index at " + 
entry.getKey(), e);
+            }
+        }
+    }
+
+    public synchronized void update(final NodeState root) {
+        if (refresh) {
+            this.root = root;
+            close();
+            refresh = false;
+            log.info("Refreshed the opened indexes");
+        } else {
+            diffAndUpdate(root);
+        }
+    }
+
+    public void setAsyncIndexInfoService(AsyncIndexInfoService 
asyncIndexInfoService) {
+        this.asyncIndexInfoService = asyncIndexInfoService;
+    }
+
+    AsyncIndexInfoService getAsyncIndexInfoService() {
+        return asyncIndexInfoService;
+    }
+
+    private synchronized void diffAndUpdate(final NodeState root) {
+        if (asyncIndexInfoService != null && 
!asyncIndexInfoService.hasIndexerUpdatedForAnyLane(this.root, root)) {
+            log.trace("No changed detected in async indexer state. Skipping 
further diff");
+            this.root = root;
+            return;
+        }
+
+        Map<String, IndexNodeManager> original = indices;
+        final Map<String, IndexNodeManager> updates = newHashMap();
+
+        Set<String> indexPaths = Sets.newHashSet();
+        indexPaths.addAll(original.keySet());
+        indexPaths.addAll(badIndexTracker.getIndexPaths());
+
+        List<Editor> editors = newArrayListWithCapacity(indexPaths.size());
+        for (final String path : indexPaths) {
+            editors.add(new SubtreeEditor(new DefaultEditor() {
+                @Override
+                public void leave(NodeState before, NodeState after) {
+                    try {
+                        if (isStatusChanged(before, after) || 
isIndexDefinitionChanged(before, after)) {
+                            long start = PERF_LOGGER.start();
+                            IndexNodeManager index = openIndex(path, root, 
after);
+                            PERF_LOGGER.end(start, -1, "[{}] Index found to be 
updated. Reopening the IndexNode", path);
+                            updates.put(path, index); // index can be null
+                        }
+                    } catch (Exception e) {
+                        badIndexTracker.markBadPersistedIndex(path, e);
+                    }
+                }
+            }, Iterables.toArray(PathUtils.elements(path), String.class)));
+        }
+
+        EditorDiff.process(CompositeEditor.compose(editors), this.root, root);
+        this.root = root;
+
+        if (!updates.isEmpty()) {
+            indices = ImmutableMap.<String, IndexNodeManager>builder()
+                    .putAll(filterKeys(original, not(in(updates.keySet()))))
+                    .putAll(filterValues(updates, notNull()))
+                    .build();
+
+            badIndexTracker.markGoodIndexes(updates.keySet());
+
+            //This might take some time as close need to acquire the
+            //write lock which might be held by current running searches
+            //Given that Tracker is now invoked from a BackgroundObserver
+            //not a high concern
+            for (String path : updates.keySet()) {
+                IndexNodeManager index = original.get(path);
+                try {
+                    if (index != null) {
+                        index.close();
+                    }
+                } catch (IOException e) {
+                    log.error("Failed to close Lucene index at " + path, e);
+                }
+            }
+        }
+    }
+
+    void refresh() {
+        log.info("Marked tracker to refresh upon next cycle");
+        refresh = true;
+    }
+
+    public IndexNode acquireIndexNode(String path, String type) {
+        IndexNodeManager index = indices.get(path);
+        IndexNode indexNode = index != null ? index.acquire() : null;
+        if (indexNode != null) {
+            return indexNode;
+        } else {
+            return findIndexNode(path, type);
+        }
+    }
+
+    @CheckForNull
+    public IndexDefinition getIndexDefinition(String indexPath){
+        IndexNodeManager node = indices.get(indexPath);
+        if (node != null){
+            //Accessing the definition should not require
+            //locking as its immutable state
+            return node.getDefinition();
+        }
+        return null;
+    }
+
+    Set<String> getIndexNodePaths(){
+        return indices.keySet();
+    }
+
+    BadIndexTracker getBadIndexTracker() {
+        return badIndexTracker;
+    }
+
+    NodeState getRoot() {
+        return root;
+    }
+
+    private synchronized IndexNode findIndexNode(String path, String type) {
+        // Retry the lookup from acquireIndexNode now that we're
+        // synchronized. The acquire() call is guaranteed to succeed
+        // since the close() method is also synchronized.
+        IndexNodeManager index = indices.get(path);
+        if (index != null) {
+            IndexNode indexNode = index.acquire();
+            return checkNotNull(indexNode);
+        }
+
+        if (badIndexTracker.isIgnoredBadIndex(path)){
+            return null;
+        }
+
+        NodeState node = root;
+        for (String name : PathUtils.elements(path)) {
+            node = node.getChildNode(name);
+        }
+
+        try {
+            if (IndexHelper.isIndexNodeOfType(node, type)) {
+                index = openIndex(path, root, node);
+                if (index != null) {
+                    IndexNode indexNode = index.acquire();
+                    checkNotNull(indexNode);
+                    indices = ImmutableMap.<String, IndexNodeManager>builder()
+                            .putAll(indices)
+                            .put(path, index)
+                            .build();
+                    badIndexTracker.markGoodIndex(path);
+                    return indexNode;
+                }
+            } else if (node.exists()) {
+                log.warn("Cannot open Index at path {} as the index is not of 
type {}", path, type);
+            }
+        } catch (Throwable e) {
+            badIndexTracker.markBadIndexForRead(path, e);
+        }
+
+        return null;
+    }
+
+
+    private static boolean isStatusChanged(NodeState before, NodeState after) {
+        return !EqualsDiff.equals(before.getChildNode(STATUS_NODE), 
after.getChildNode(STATUS_NODE));
+    }
+
+    private static boolean isIndexDefinitionChanged(NodeState before, 
NodeState after) {
+        return !EqualsDiff.equals(before.getChildNode(INDEX_DEFINITION_NODE), 
after.getChildNode(INDEX_DEFINITION_NODE));
+    }
+}

Propchange: 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/FulltextIndexTracker.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/IndexNodeManager.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/IndexNodeManager.java?rev=1832166&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/IndexNodeManager.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/IndexNodeManager.java
 Thu May 24 13:07:54 2018
@@ -0,0 +1,139 @@
+/*
+ * 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.oak.plugins.index.search.spi.query;
+
+import java.io.IOException;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import javax.annotation.CheckForNull;
+
+import org.apache.jackrabbit.oak.commons.PerfLogger;
+import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition;
+import org.apache.jackrabbit.oak.plugins.index.search.IndexNode;
+import org.apache.jackrabbit.oak.plugins.index.search.ReaderRefreshPolicy;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static com.google.common.base.Preconditions.checkState;
+import static 
org.apache.jackrabbit.oak.plugins.index.IndexConstants.ASYNC_PROPERTY_NAME;
+import static 
org.apache.jackrabbit.oak.plugins.index.IndexUtils.getAsyncLaneName;
+
+public abstract class IndexNodeManager {
+    /**
+     * Name of the hidden node under which information about the checkpoints
+     * seen and indexed by each async indexer is kept.
+     */
+    private static final String ASYNC = ":async";
+
+    private static final AtomicInteger SEARCHER_ID_COUNTER = new 
AtomicInteger();
+
+    private static final PerfLogger PERF_LOGGER =
+            new 
PerfLogger(LoggerFactory.getLogger(IndexNodeManager.class.getName() + ".perf"));
+
+    protected abstract IndexNodeManager open(String indexPath, NodeState root, 
NodeState defnNodeState);
+
+    protected abstract void releaseResources();
+
+    protected abstract IndexNode getIndexNode();
+
+    protected abstract ReaderRefreshPolicy getReaderRefreshPolicy();
+
+    protected abstract void refreshReaders();
+
+    protected abstract String getName();
+
+    protected abstract IndexDefinition getDefinition();
+
+    static boolean hasAsyncIndexerRun(NodeState root, String indexPath, 
NodeState defnNodeState) {
+        boolean hasAsyncNode = root.hasChildNode(ASYNC);
+
+        String asyncLaneName = getAsyncLaneName(defnNodeState, indexPath, 
defnNodeState.getProperty(ASYNC_PROPERTY_NAME));
+
+        if (asyncLaneName != null) {
+            return hasAsyncNode && 
root.getChildNode(ASYNC).hasProperty(asyncLaneName);
+        } else {
+            // useful only for tests - basically non-async index defs which 
don't rely on /:async
+            // hence either readers are there (and this method doesn't come 
into play during open)
+            // OR there is no cycle (where we return false correctly)
+            return  false;
+        }
+    }
+
+    private static final Logger log = 
LoggerFactory.getLogger(IndexNodeManager.class);
+
+    private final ReadWriteLock lock = new ReentrantReadWriteLock();
+
+    private final Semaphore refreshLock = new Semaphore(1);
+
+    private final Runnable refreshCallback = new Runnable() {
+        @Override
+        public void run() {
+            if (refreshLock.tryAcquire()) {
+                try {
+                    refreshReaders();
+                }finally {
+                    refreshLock.release();
+                }
+            }
+        }
+    };
+
+    private boolean closed = false;
+
+
+    @CheckForNull
+    IndexNode acquire() {
+        lock.readLock().lock();
+        if (closed) {
+            lock.readLock().unlock();
+            return null;
+        } else {
+            boolean success = false;
+            try {
+                
getReaderRefreshPolicy().refreshOnReadIfRequired(refreshCallback);
+                IndexNode indexNode = getIndexNode();
+                success = true;
+                return indexNode;
+            } finally {
+                if (!success) {
+                    lock.readLock().unlock();
+                }
+            }
+        }
+    }
+
+    private void release() {
+        lock.readLock().unlock();
+    }
+
+    void close() throws IOException {
+        lock.writeLock().lock();
+        try {
+            checkState(!closed);
+            closed = true;
+        } finally {
+            lock.writeLock().unlock();
+        }
+
+        releaseResources();
+    }
+
+}

Propchange: 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/IndexNodeManager.java
------------------------------------------------------------------------------
    svn:eol-style = native


Reply via email to