You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by to...@apache.org on 2018/04/12 11:46:18 UTC

svn commit: r1828972 [3/5] - in /jackrabbit/oak/trunk/oak-search: ./ src/main/java/org/apache/jackrabbit/oak/plugins/index/search/ src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/ src/test/java/org/apache/jackrabbit/oak/plugins/index/...

Added: jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/PropertyDefinition.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/PropertyDefinition.java?rev=1828972&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/PropertyDefinition.java (added)
+++ jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/PropertyDefinition.java Thu Apr 12 11:46:17 2018
@@ -0,0 +1,299 @@
+/*
+ * 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;
+
+import javax.annotation.CheckForNull;
+import javax.jcr.PropertyType;
+
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition.IndexingRule;
+import org.apache.jackrabbit.oak.plugins.index.search.util.FunctionIndexProcessor;
+import org.apache.jackrabbit.oak.plugins.index.property.ValuePattern;
+import org.apache.jackrabbit.oak.plugins.index.search.util.IndexHelper;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static com.google.common.collect.ImmutableList.copyOf;
+import static com.google.common.collect.Iterables.toArray;
+import static org.apache.jackrabbit.oak.commons.PathUtils.elements;
+import static org.apache.jackrabbit.oak.commons.PathUtils.isAbsolute;
+import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.FIELD_BOOST;
+import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.PROP_IS_REGEX;
+import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.PROP_WEIGHT;
+import static org.apache.jackrabbit.oak.plugins.index.search.util.ConfigUtil.getOptionalValue;
+
+public class PropertyDefinition {
+    private static final Logger log = LoggerFactory.getLogger(PropertyDefinition.class);
+
+    private static final String[] EMPTY_ANCESTORS = new String[0];
+
+    /**
+     * The default boost: 1.0f.
+     */
+    static final float DEFAULT_BOOST = 1.0f;
+
+    /**
+     * Property name. By default derived from the NodeState name which has the
+     * property definition. However in case property name is a pattern, relative
+     * property etc then it should be defined via 'name' property in NodeState.
+     * In such case NodeState name can be set to anything
+     */
+    final String name;
+
+    private final int propertyType;
+    /**
+     * The boost value for a property.
+     */
+    final float boost;
+
+    final boolean isRegexp;
+
+    final boolean index;
+
+    final boolean stored;
+
+    final boolean nodeScopeIndex;
+
+    final boolean propertyIndex;
+
+    final boolean analyzed;
+
+    final boolean ordered;
+
+    final boolean nullCheckEnabled;
+
+    final boolean notNullCheckEnabled;
+
+    final int includedPropertyTypes;
+
+    final boolean relative;
+
+    final boolean useInSuggest;
+
+    final boolean useInSpellcheck;
+
+    final boolean facet;
+
+    final String[] ancestors;
+
+    final boolean excludeFromAggregate;
+
+    final int weight;
+
+    /**
+     * Property name excluding the relativePath. For regular expression based definition
+     * its set to null
+     */
+    @CheckForNull
+    final String nonRelativeName;
+
+    /**
+     * For function-based indexes: the function name, in Polish notation.
+     */
+    final String function;
+
+    /**
+     * For function-based indexes: the function code, as tokens.
+     */
+    final String[] functionCode;
+
+    public final ValuePattern valuePattern;
+
+    public final boolean sync;
+
+    public final boolean unique;
+
+    public PropertyDefinition(IndexingRule idxDefn, String nodeName, NodeState defn) {
+        this.isRegexp = getOptionalValue(defn, PROP_IS_REGEX, false);
+        this.name = getName(defn, nodeName);
+        this.relative = isRelativeProperty(name);
+        this.boost = getOptionalValue(defn, FIELD_BOOST, DEFAULT_BOOST);
+        this.weight = getOptionalValue(defn, PROP_WEIGHT, 5);
+
+        //By default if a property is defined it is indexed
+        this.index = getOptionalValue(defn, FulltextIndexConstants.PROP_INDEX, true);
+        this.stored = getOptionalValueIfIndexed(defn, FulltextIndexConstants.PROP_USE_IN_EXCERPT, false);
+        this.nodeScopeIndex = getOptionalValueIfIndexed(defn, FulltextIndexConstants.PROP_NODE_SCOPE_INDEX, false);
+
+        //If boost is specified then that field MUST be analyzed
+        if (defn.hasProperty(FIELD_BOOST)){
+            this.analyzed = true;
+        } else {
+            this.analyzed = getOptionalValueIfIndexed(defn, FulltextIndexConstants.PROP_ANALYZED, false);
+        }
+
+        this.ordered = getOptionalValueIfIndexed(defn, FulltextIndexConstants.PROP_ORDERED, false);
+        this.includedPropertyTypes = IndexDefinition.getSupportedTypes(defn, FulltextIndexConstants.PROP_INCLUDED_TYPE,
+                IndexDefinition.TYPES_ALLOW_ALL);
+
+        //TODO Add test case for above cases
+
+        this.propertyType = getPropertyType(idxDefn, nodeName, defn);
+        this.useInSuggest = getOptionalValueIfIndexed(defn, FulltextIndexConstants.PROP_USE_IN_SUGGEST, false);
+        this.useInSpellcheck = getOptionalValueIfIndexed(defn, FulltextIndexConstants.PROP_USE_IN_SPELLCHECK, false);
+        this.nullCheckEnabled = getOptionalValueIfIndexed(defn, FulltextIndexConstants.PROP_NULL_CHECK_ENABLED, false);
+        this.notNullCheckEnabled = getOptionalValueIfIndexed(defn, FulltextIndexConstants.PROP_NOT_NULL_CHECK_ENABLED, false);
+        this.excludeFromAggregate = getOptionalValueIfIndexed(defn, FulltextIndexConstants.PROP_EXCLUDE_FROM_AGGREGATE, false);
+        this.nonRelativeName = determineNonRelativeName();
+        this.ancestors = computeAncestors(name);
+        this.facet = getOptionalValueIfIndexed(defn, FulltextIndexConstants.PROP_FACETS, false);
+        this.function = FunctionIndexProcessor.convertToPolishNotation(
+                getOptionalValue(defn, FulltextIndexConstants.PROP_FUNCTION, null));
+        this.functionCode = FunctionIndexProcessor.getFunctionCode(this.function);
+        this.valuePattern = new ValuePattern(defn);
+        this.unique = getOptionalValueIfIndexed(defn, FulltextIndexConstants.PROP_UNIQUE, false);
+        this.sync = unique || getOptionalValueIfIndexed(defn, FulltextIndexConstants.PROP_SYNC, false);
+
+        //If some property is set to sync then propertyIndex mode is always enabled
+        this.propertyIndex = sync || getOptionalValueIfIndexed(defn, FulltextIndexConstants.PROP_PROPERTY_INDEX, false);
+        validate();
+    }
+
+
+    /**
+     * If 'analyzed' is enabled then property value would be used to evaluate the
+     * contains clause related to those properties. In such mode also some properties
+     * would be skipped from analysis
+     *
+     * @param propertyName name of the property to check. As property definition might
+     *                     be regEx based this is required to be passed explicitly
+     * @return true if the property value should be tokenized/analyzed
+     */
+    public boolean skipTokenization(String propertyName) {
+        //For regEx case check against a whitelist
+        if (isRegexp && IndexHelper.skipTokenization(propertyName)){
+            return true;
+        }
+        return !analyzed;
+    }
+
+    public boolean fulltextEnabled(){
+        return index && (analyzed || nodeScopeIndex);
+    }
+
+    public boolean propertyIndexEnabled(){
+        return index && propertyIndex;
+    }
+
+    public boolean isTypeDefined(){
+        return propertyType != PropertyType.UNDEFINED;
+    }
+
+    /**
+     * Returns the property type. If no explicit type is defined the default is assumed
+     * to be {@link PropertyType#STRING}
+     *
+     * @return propertyType as per javax.jcr.PropertyType
+     */
+    public int getType(){
+        //If no explicit type is defined we assume it to be string
+        return isTypeDefined() ? propertyType : PropertyType.STRING;
+    }
+
+    public boolean includePropertyType(int type){
+        return IndexDefinition.includePropertyType(includedPropertyTypes, type);
+    }
+
+    @Override
+    public String toString() {
+        return "PropertyDefinition{" +
+                "name='" + name + '\'' +
+                ", propertyType=" + propertyType +
+                ", boost=" + boost +
+                ", isRegexp=" + isRegexp +
+                ", index=" + index +
+                ", stored=" + stored +
+                ", nodeScopeIndex=" + nodeScopeIndex +
+                ", propertyIndex=" + propertyIndex +
+                ", analyzed=" + analyzed +
+                ", ordered=" + ordered +
+                ", useInSuggest=" + useInSuggest+
+                ", nullCheckEnabled=" + nullCheckEnabled +
+                ", notNullCheckEnabled=" + notNullCheckEnabled +
+                ", function=" + function +
+                '}';
+    }
+
+    static boolean isRelativeProperty(String propertyName){
+        return !isAbsolute(propertyName)
+                && !FulltextIndexConstants.REGEX_ALL_PROPS.equals(propertyName)
+                && PathUtils.getNextSlash(propertyName, 0) > 0;
+    }
+
+    //~---------------------------------------------< internal >
+
+    private boolean getOptionalValueIfIndexed(NodeState definition, String propName, boolean defaultVal){
+        //If property is not to be indexed then all other config would be
+        //set to false ignoring whatever is defined in config for them
+        if (!index){
+            return false;
+        }
+        return getOptionalValue(definition, propName, defaultVal);
+    }
+
+    private void validate() {
+        if (nullCheckEnabled && isRegexp){
+            throw new IllegalStateException(String.format("%s can be set to true for property definition using " +
+                    "regular expression", FulltextIndexConstants.PROP_NULL_CHECK_ENABLED));
+        }
+    }
+
+    private String determineNonRelativeName() {
+        if (isRegexp){
+            return null;
+        }
+
+        if (!relative){
+            return name;
+        }
+
+        return PathUtils.getName(name);
+    }
+
+    private static String[] computeAncestors(String path) {
+        if (FulltextIndexConstants.REGEX_ALL_PROPS.equals(path)) {
+            return EMPTY_ANCESTORS;
+        } else {
+            return toArray(copyOf(elements(PathUtils.getParentPath(path))), String.class);
+        }
+    }
+
+
+    private static String getName(NodeState definition, String defaultName){
+        PropertyState ps = definition.getProperty(FulltextIndexConstants.PROP_NAME);
+        return ps == null ? defaultName : ps.getValue(Type.STRING);
+    }
+
+    private static int getPropertyType(IndexingRule idxDefn, String name, NodeState defn) {
+        int type = PropertyType.UNDEFINED;
+        if (defn.hasProperty(FulltextIndexConstants.PROP_TYPE)) {
+            String typeName = defn.getString(FulltextIndexConstants.PROP_TYPE);
+            try {
+                type = PropertyType.valueFromName(typeName);
+            } catch (IllegalArgumentException e) {
+                log.warn("Invalid property type {} for property {} in Index {}", typeName, name, idxDefn);
+            }
+        }
+        return type;
+    }
+}

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

Added: jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/PropertyUpdateCallback.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/PropertyUpdateCallback.java?rev=1828972&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/PropertyUpdateCallback.java (added)
+++ jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/PropertyUpdateCallback.java Thu Apr 12 11:46:17 2018
@@ -0,0 +1,53 @@
+/*
+ * 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;
+
+import javax.annotation.Nullable;
+
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.PropertyState;
+
+/**
+ * Callback to be invoked for each indexable property change
+ */
+public interface PropertyUpdateCallback {
+
+    /**
+     * Invoked upon any change in property either added, updated or removed.
+     * Implementation can determine if property is added, updated or removed based
+     * on whether before or after is null
+     *
+     * @param nodePath path of node for which is to be indexed for this property change
+     * @param propertyRelativePath relative path of the property wrt the indexed node
+     * @param pd property definition associated with the property to be indexed
+     * @param before before state. Is null when property is added. For other cases its not null
+     * @param after after state of the property. Is null when property is removed. For other cases its not null
+     */
+    void propertyUpdated(String nodePath, String propertyRelativePath, PropertyDefinition pd,
+                         @Nullable PropertyState before, @Nullable PropertyState after);
+
+    /**
+     * Invoked after editor has traversed all the changes
+     *
+     * @throws CommitFailedException in case some validation fails
+     */
+    void done() throws CommitFailedException;
+
+}

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

Added: jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/SizeEstimator.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/SizeEstimator.java?rev=1828972&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/SizeEstimator.java (added)
+++ jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/SizeEstimator.java Thu Apr 12 11:46:17 2018
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+public interface SizeEstimator {
+
+    /**
+     * Get the estimated size, or -1 if not known.
+     * 
+     * @return the size
+     */
+    long getSize();
+    
+}

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

Added: jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/ConfigUtil.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/ConfigUtil.java?rev=1828972&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/ConfigUtil.java (added)
+++ jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/ConfigUtil.java Thu Apr 12 11:46:17 2018
@@ -0,0 +1,86 @@
+/*
+ * 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.util;
+
+import java.util.Collections;
+
+import javax.annotation.CheckForNull;
+
+import com.google.common.primitives.Ints;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.oak.api.Blob;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Utility class to retrieve configuration values for index definitions
+ */
+public class ConfigUtil {
+
+    public static boolean getOptionalValue(NodeState definition, String propName, boolean defaultVal){
+        PropertyState ps = definition.getProperty(propName);
+        return ps == null ? defaultVal : ps.getValue(Type.BOOLEAN);
+    }
+
+    public static int getOptionalValue(NodeState definition, String propName, int defaultVal){
+        PropertyState ps = definition.getProperty(propName);
+        return ps == null ? defaultVal : Ints.checkedCast(ps.getValue(Type.LONG));
+    }
+
+    public static String getOptionalValue(NodeState definition, String propName, String defaultVal){
+        PropertyState ps = definition.getProperty(propName);
+        return ps == null ? defaultVal : ps.getValue(Type.STRING);
+    }
+
+    public static float getOptionalValue(NodeState definition, String propName, float defaultVal){
+        PropertyState ps = definition.getProperty(propName);
+        return ps == null ? defaultVal : ps.getValue(Type.DOUBLE).floatValue();
+    }
+
+    public static double getOptionalValue(NodeState definition, String propName, double defaultVal){
+        PropertyState ps = definition.getProperty(propName);
+        return ps == null ? defaultVal : ps.getValue(Type.DOUBLE);
+    }
+
+    public static String getPrimaryTypeName(NodeState nodeState) {
+        PropertyState ps = nodeState.getProperty(JcrConstants.JCR_PRIMARYTYPE);
+        return (ps == null) ? JcrConstants.NT_BASE : ps.getValue(Type.NAME);
+    }
+
+    public static Iterable<String> getMixinNames(NodeState nodeState) {
+        PropertyState ps = nodeState.getProperty(JcrConstants.JCR_MIXINTYPES);
+        return (ps == null) ? Collections.<String>emptyList() : ps.getValue(Type.NAMES);
+    }
+
+    /**
+     * Assumes that given state is of type nt:file and then reads
+     * the jcr:content/@jcr:data property to get the binary content
+     */
+    @CheckForNull
+    public static Blob getBlob(NodeState state, String resourceName){
+        NodeState contentNode = state.getChildNode(JcrConstants.JCR_CONTENT);
+        checkArgument(contentNode.exists(), "Was expecting to find jcr:content node to read resource %s", resourceName);
+        PropertyState property = contentNode.getProperty(JcrConstants.JCR_DATA);
+        return property != null ? property.getValue(Type.BINARY) : null;
+    }
+}

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

Added: jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/FunctionIndexProcessor.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/FunctionIndexProcessor.java?rev=1828972&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/FunctionIndexProcessor.java (added)
+++ jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/FunctionIndexProcessor.java Thu Apr 12 11:46:17 2018
@@ -0,0 +1,279 @@
+/*
+ * 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.util;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.memory.EmptyPropertyState;
+import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
+import org.apache.jackrabbit.oak.spi.query.QueryConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A parser for function-based indexes. It converts the human-readable function
+ * definition (XPath) to the internal Polish notation.
+ */
+public class FunctionIndexProcessor {
+    
+    private static final Logger LOG =
+            LoggerFactory.getLogger(FunctionIndexProcessor.class);
+    
+    private String remaining;
+
+    private static final PropertyState EMPTY_PROPERTY_STATE = EmptyPropertyState.emptyProperty("empty", Type.STRINGS);
+
+    private FunctionIndexProcessor(String function) {
+        this.remaining = function;
+    }
+    
+    /**
+     * Get the list of properties used in the given function code.
+     * 
+     * @param functionCode the tokens, for example ["function", "lower", "@name"]
+     * @return the list of properties, for example ["name"]
+     */
+    public static String[] getProperties(String[] functionCode) {
+        ArrayList<String> properties = new ArrayList<String>();
+        for(String token : functionCode) {
+            if (token.startsWith("@")) {
+                String propertyName = token.substring(1);
+                properties.add(propertyName);
+            }
+        }
+        return properties.toArray(new String[0]);
+    }
+
+    /**
+     * Try to calculate the value for the given function code.
+     * 
+     * @param path the path of the node
+     * @param state the node state
+     * @param functionCode the tokens, for example ["function", "lower", "@name"]
+     * @return null, or the calculated value
+     */
+    public static PropertyState tryCalculateValue(String path, NodeState state, String[] functionCode) {
+        Deque<PropertyState> stack = new ArrayDeque<PropertyState>();
+        for (int i = functionCode.length - 1; i > 0; i--) {
+            String token = functionCode[i];
+            PropertyState ps;
+            if (token.startsWith("@")) {
+                String propertyName = token.substring(1);
+                ps = getProperty(path, state, propertyName);
+            } else {
+                ps = calculateFunction(token, stack);
+            }
+            if (ps == null) {
+                ps = EMPTY_PROPERTY_STATE;
+            }
+            stack.push(ps);
+        }
+
+        PropertyState ret = stack.pop();
+        return ret==EMPTY_PROPERTY_STATE ? null : ret;
+    }
+    
+    /**
+     * Split the polish notation into a tokens that can more easily be processed.
+     *  
+     *  @param functionDescription in polish notation, for example "function*lower*{@literal @}name"
+     *  @return tokens, for example ["function", "lower", "{@literal @}name"]
+     */
+    public static String[] getFunctionCode(String functionDescription) {
+        if (functionDescription == null) {
+            return null;
+        }
+        return functionDescription.split("\\*");
+    }
+    
+    private static PropertyState calculateFunction(String functionName, 
+            Deque<PropertyState> stack) {
+        PropertyState ps = stack.pop();
+        if ("coalesce".equals(functionName)) {
+            // coalesce (a, b) => (a != null ? a : b)
+            // we pop stack again to consume the second parameter
+            // also, if ps is EMPTY_PROPERTY_STATE, then newly popped value is to be used
+            PropertyState ps2 = stack.pop();
+            if (ps == EMPTY_PROPERTY_STATE) {
+                ps = ps2;
+            }
+        }
+        if (ps == EMPTY_PROPERTY_STATE) {
+            return ps;
+        }
+        Type<?> type = null;
+        ArrayList<Object> values = new ArrayList<Object>(ps.count());
+        for (int i = 0; i < ps.count(); i++) {
+            String s = ps.getValue(Type.STRING, i);
+            Object x;
+            if ("lower".equals(functionName)) {
+                x = s.toLowerCase();
+                type = Type.STRING;
+            } else if ("coalesce".equals(functionName)) {
+                x = s;
+                type = Type.STRING;
+            } else if ("upper".equals(functionName)) {
+                x = s.toUpperCase();
+                type = Type.STRING;
+            } else if ("length".equals(functionName)) {
+                x = (long) s.length();
+                type = Type.LONG;
+            } else {
+                LOG.debug("Unknown function {}", functionName);
+                return null;
+            }
+            values.add(x);
+        }
+        PropertyState result;
+        if (values.size() == 1) {
+            result = PropertyStates.createProperty("value", values.get(0), type);
+        } else {
+            type = type.getArrayType();
+            result = PropertyStates.createProperty("value", values, type);
+        }
+        return result;
+    }
+    
+    private static PropertyState getProperty(String path, NodeState state, 
+            String propertyName) {
+        if (PathUtils.getDepth(propertyName) != 1) {
+            for(String n : PathUtils.elements(PathUtils.getParentPath(propertyName))) {
+                state = state.getChildNode(n);
+                if (!state.exists()) {
+                    return null;
+                }
+            }
+            propertyName = PathUtils.getName(propertyName);
+        }
+        PropertyState ps;
+        if (":localname".equals(propertyName)) {
+            ps = PropertyStates.createProperty("value", 
+                    getLocalName(PathUtils.getName(path)), Type.STRING);
+        } else if (":name".equals(propertyName)) {
+            ps = PropertyStates.createProperty("value", 
+                    PathUtils.getName(path), Type.STRING);
+        } else {
+            ps = state.getProperty(propertyName);
+        }
+        if (ps == null || ps.count() == 0) {
+            return null;
+        }
+        return ps;
+    }
+    
+    private static String getLocalName(String name) {
+        int colon = name.indexOf(':');
+        // TODO LOCALNAME: evaluation of local name might not be correct
+        return colon < 0 ? name : name.substring(colon + 1);
+    }
+    
+    /**
+     * Convert a function (in human-readable form) to the polish notation.
+     * 
+     * @param function the function, for example "lower([name])"
+     * @return the polish notation, for example "function*lower*{@literal @}name"
+     */
+    public static String convertToPolishNotation(String function) {
+        if (function == null) {
+            return null;
+        }
+        FunctionIndexProcessor p = new FunctionIndexProcessor(function);
+        return QueryConstants.FUNCTION_RESTRICTION_PREFIX + p.parse();
+    }
+    
+    String parse() {
+        if (match("fn:local-name()") || match("localname()")) {
+            return "@:localname";
+        }
+        if (match("fn:name()") || match("name()")) {
+            return "@:name";
+        }
+        if (match("fn:upper-case(") || match("upper(")) {
+            return "upper*" + parse() + read(")");
+        }
+        if (match("fn:lower-case(") || match("lower(")) {
+            return "lower*" + parse() + read(")");
+        }
+        if (match("fn:coalesce(") || match("coalesce(")) {
+            return "coalesce*" + parse() + readCommaAndWhitespace() + parse() + read(")");
+        }
+        if (match("fn:string-length(") || match("length(")) {
+            return "length*" + parse() + read(")");
+        }
+
+        // property name
+        if (match("[")) {
+            String prop = remaining;
+            int indexOfComma = remaining.indexOf(",");
+            if (indexOfComma > 0) {
+                prop = remaining.substring(0, indexOfComma);
+            }
+            prop = prop.substring(0, prop.lastIndexOf(']'));
+            remaining = remaining.substring(prop.length() + 1);
+            return property(prop.replaceAll("]]", "]"));
+        } else {
+            String prop = remaining;
+            int paren = remaining.indexOf(')');
+            int comma = remaining.indexOf(',');
+            int end = comma;
+            if (paren >=0) {
+                end = (end < 0) ? paren : Math.min(end, paren);
+            }
+            if (end >= 0) {
+                prop = remaining.substring(0, end);
+            }
+            remaining = remaining.substring(prop.length());
+            return property(prop.replaceAll("@", ""));
+        }
+    }
+    
+    String property(String p) {
+        return "@" + p;
+    }
+    
+    private String read(String string) {
+        match(string);
+        return "";
+    }
+
+    private String
+    readCommaAndWhitespace() {
+        while (match(" ")) {
+        }
+        match(",");
+        while (match(" ")) {
+        }
+        return "*";
+    }
+    
+    private boolean match(String string) {
+        if (remaining.startsWith(string)) {
+            remaining = remaining.substring(string.length());
+            return true;
+        }
+        return false;
+    }
+
+}

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

Added: jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/IndexDefinitionBuilder.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/IndexDefinitionBuilder.java?rev=1828972&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/IndexDefinitionBuilder.java (added)
+++ jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/IndexDefinitionBuilder.java Thu Apr 12 11:46:17 2018
@@ -0,0 +1,561 @@
+/*
+ * 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.util;
+
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.Node;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants;
+import org.apache.jackrabbit.oak.plugins.index.IndexConstants;
+import org.apache.jackrabbit.oak.plugins.tree.factories.TreeFactory;
+import org.apache.jackrabbit.oak.spi.filter.PathFilter;
+import org.apache.jackrabbit.oak.spi.state.EqualsDiff;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+
+import static java.util.Arrays.asList;
+import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
+import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED;
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRINGS;
+import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
+
+public final class IndexDefinitionBuilder {
+    private final NodeBuilder builder;
+    private final Tree tree;
+    private final Map<String, IndexRule> rules = Maps.newHashMap();
+    private final Map<String, AggregateRule> aggRules = Maps.newHashMap();
+    private final Tree indexRule;
+    private final boolean autoManageReindexFlag;
+    private Tree aggregatesTree;
+    private final NodeState initial;
+    private boolean reindexRequired;
+
+
+    public IndexDefinitionBuilder(){
+        this(EMPTY_NODE.builder());
+    }
+
+    public IndexDefinitionBuilder(NodeBuilder nodeBuilder){
+        this(nodeBuilder, true);
+    }
+
+    public IndexDefinitionBuilder(NodeBuilder nodeBuilder, boolean autoManageReindexFlag){
+        this.autoManageReindexFlag = autoManageReindexFlag;
+        this.builder = nodeBuilder;
+        this.initial = nodeBuilder.getNodeState();
+        this.tree = TreeFactory.createTree(builder);
+        tree.setProperty("async", "async");
+        setType();
+        tree.setProperty(JCR_PRIMARYTYPE, "oak:QueryIndexDefinition", NAME);
+        indexRule = getOrCreateChild(tree, FulltextIndexConstants.INDEX_RULES);
+    }
+
+    public IndexDefinitionBuilder evaluatePathRestrictions(){
+        tree.setProperty(FulltextIndexConstants.EVALUATE_PATH_RESTRICTION, true);
+        return this;
+    }
+
+    public IndexDefinitionBuilder includedPaths(String ... paths){
+        tree.setProperty(PathFilter.PROP_INCLUDED_PATHS, asList(paths), STRINGS);
+        return this;
+    }
+
+    public IndexDefinitionBuilder excludedPaths(String ... paths){
+        tree.setProperty(PathFilter.PROP_EXCLUDED_PATHS, asList(paths), STRINGS);
+        return this;
+    }
+
+    public IndexDefinitionBuilder queryPaths(String ... paths){
+        tree.setProperty(IndexConstants.QUERY_PATHS, asList(paths), STRINGS);
+        return this;
+    }
+
+    public IndexDefinitionBuilder supersedes(String ... paths){
+        tree.setProperty(IndexConstants.SUPERSEDED_INDEX_PATHS, asList(paths), STRINGS);
+        return this;
+    }
+
+    public IndexDefinitionBuilder noAsync(){
+        tree.removeProperty("async");
+        return this;
+    }
+
+    public IndexDefinitionBuilder async(String ... asyncVals){
+        tree.removeProperty("async");
+        tree.setProperty("async", asList(asyncVals), STRINGS);
+        return this;
+    }
+
+    public IndexDefinitionBuilder nodeTypeIndex() {
+        tree.setProperty(FulltextIndexConstants.PROP_INDEX_NODE_TYPE, true);
+        return this;
+    }
+
+    public Tree getBuilderTree(){
+        return tree;
+    }
+
+    public NodeState build(){
+        setReindexFlagIfRequired();
+        return builder.getNodeState();
+    }
+
+    public Tree build(Tree tree){
+        NodeStateCopyUtils.copyToTree(build(), tree);
+        return tree;
+    }
+
+    public Node build(Node node) throws RepositoryException {
+        NodeStateCopyUtils.copyToNode(build(), node);
+        return node;
+    }
+
+    public boolean isReindexRequired() {
+        if (reindexRequired){
+            return true;
+        }
+        return !SelectiveEqualsDiff.equals(initial, builder.getNodeState());
+    }
+
+    private void setReindexFlagIfRequired(){
+        if (!reindexRequired && !SelectiveEqualsDiff.equals(initial, builder.getNodeState()) && autoManageReindexFlag){
+            tree.setProperty("reindex", true);
+            reindexRequired = true;
+        }
+    }
+
+    private void setType() {
+        PropertyState type = tree.getProperty(IndexConstants.TYPE_PROPERTY_NAME);
+        if (type == null || !"disabled".equals(type.getValue(Type.STRING))) {
+            tree.setProperty(IndexConstants.TYPE_PROPERTY_NAME, "fulltext");
+        }
+    }
+
+    //~--------------------------------------< IndexRule >
+
+    public IndexRule indexRule(String type){
+        IndexRule rule = rules.get(type);
+        if (rule == null){
+            rule = new IndexRule(getOrCreateChild(indexRule, type), type);
+            rules.put(type, rule);
+        }
+        return rule;
+    }
+
+    public boolean hasIndexRule(String type){
+        return indexRule.hasChild(type);
+    }
+
+    public static class IndexRule {
+        private final Tree indexRule;
+        private final String ruleName;
+        private final Map<String, PropertyRule> props = Maps.newHashMap();
+        private final Set<String> propNodeNames = Sets.newHashSet();
+
+        private IndexRule(Tree indexRule, String type) {
+            this.indexRule = indexRule;
+            this.ruleName = type;
+            loadExisting();
+        }
+
+        public IndexRule indexNodeName(){
+            indexRule.setProperty(FulltextIndexConstants.INDEX_NODE_NAME, true);
+            return this;
+        }
+
+        public IndexRule includePropertyTypes(String ... types){
+            indexRule.setProperty(FulltextIndexConstants.INCLUDE_PROPERTY_TYPES, asList(types), STRINGS);
+            return this;
+        }
+
+        public IndexRule sync() {
+            indexRule.setProperty(FulltextIndexConstants.PROP_SYNC, true);
+            return this;
+        }
+
+        public PropertyRule property(String name){
+            return property(name, false);
+        }
+
+        public PropertyRule property(String name, boolean regex) {
+            return property(null, name, regex);
+        }
+
+        public PropertyRule property(String propDefnNodeName, String name) {
+            return property(propDefnNodeName, name, false);
+        }
+
+        public PropertyRule property(String propDefnNodeName, String name, boolean regex){
+            PropertyRule propRule = props.get(name);
+            if (propRule == null){
+                Tree propTree = findExisting(name);
+                if (propTree == null){
+                    if (propDefnNodeName == null){
+                        propDefnNodeName = createPropNodeName(name, regex);
+                    }
+                    propTree = getOrCreateChild(getPropsTree(), propDefnNodeName);
+                }
+                propRule = new PropertyRule(this, propTree, name, regex);
+                props.put(name != null ? name : propDefnNodeName, propRule);
+            }
+            return propRule;
+        }
+
+        private void loadExisting() {
+            if (!indexRule.hasChild(FulltextIndexConstants.PROP_NAME)) {
+                return;
+            }
+
+            for (Tree tree : getPropsTree().getChildren()){
+                if (!tree.hasProperty(FulltextIndexConstants.PROP_NAME)){
+                    continue;
+                }
+                String name = tree.getProperty(FulltextIndexConstants.PROP_NAME).getValue(Type.STRING);
+                boolean regex = false;
+                if (tree.hasProperty(FulltextIndexConstants.PROP_IS_REGEX)) {
+                    regex = tree.getProperty(FulltextIndexConstants.PROP_IS_REGEX).getValue(Type.BOOLEAN);
+                }
+                PropertyRule pr = new PropertyRule(this, tree, name, regex);
+                props.put(name, pr);
+            }
+        }
+
+        private Tree findExisting(String name) {
+            for (Tree tree : getPropsTree().getChildren()){
+                if (name.equals(tree.getProperty(FulltextIndexConstants.PROP_NAME).getValue(Type.STRING))){
+                    return tree;
+                }
+            }
+            return null;
+        }
+
+        private String createPropNodeName(String name, boolean regex) {
+            name = regex ? "prop" : getSafePropName(name);
+            if (name.isEmpty()){
+                name = "prop";
+            }
+            if (propNodeNames.contains(name)){
+                name = name + "_" + propNodeNames.size();
+            }
+            propNodeNames.add(name);
+            return name;
+        }
+
+        public String getRuleName() {
+            return ruleName;
+        }
+
+        public boolean hasPropertyRule(String propName){
+            return findExisting(propName) != null;
+        }
+
+        private Tree getPropsTree() {
+            return getOrCreateChild(indexRule, FulltextIndexConstants.PROP_NODE);
+        }
+    }
+
+    public static class PropertyRule {
+        private final IndexRule indexRule;
+        private final Tree propTree;
+
+        private PropertyRule(IndexRule indexRule, Tree propTree, String name, boolean regex) {
+            this.indexRule = indexRule;
+            this.propTree = propTree;
+            if (name != null) {
+                propTree.setProperty(FulltextIndexConstants.PROP_NAME, name);
+            }
+            if (regex) {
+                propTree.setProperty(FulltextIndexConstants.PROP_IS_REGEX, true);
+            }
+        }
+
+        public PropertyRule useInExcerpt(){
+            propTree.setProperty(FulltextIndexConstants.PROP_USE_IN_EXCERPT, true);
+            return this;
+        }
+
+        public PropertyRule useInSpellcheck(){
+            propTree.setProperty(FulltextIndexConstants.PROP_USE_IN_SPELLCHECK, true);
+            return this;
+        }
+
+        public PropertyRule type(String type){
+            //This would throw an IAE if type is invalid
+            PropertyType.valueFromName(type);
+            propTree.setProperty(FulltextIndexConstants.PROP_TYPE, type);
+            return this;
+        }
+
+        public PropertyRule useInSuggest(){
+            propTree.setProperty(FulltextIndexConstants.PROP_USE_IN_SUGGEST, true);
+            return this;
+        }
+
+        public PropertyRule analyzed(){
+            propTree.setProperty(FulltextIndexConstants.PROP_ANALYZED, true);
+            return this;
+        }
+
+        public PropertyRule nodeScopeIndex(){
+            propTree.setProperty(FulltextIndexConstants.PROP_NODE_SCOPE_INDEX, true);
+            return this;
+        }
+
+        public PropertyRule ordered(){
+            propTree.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+            return this;
+        }
+
+        public PropertyRule ordered(String type){
+            type(type);
+            propTree.setProperty(FulltextIndexConstants.PROP_ORDERED, true);
+            return this;
+        }
+
+        public PropertyRule disable() {
+            propTree.setProperty(FulltextIndexConstants.PROP_INDEX, false);
+            return this;
+        }
+
+        public PropertyRule propertyIndex(){
+            propTree.setProperty(FulltextIndexConstants.PROP_PROPERTY_INDEX, true);
+            return this;
+        }
+
+        public PropertyRule nullCheckEnabled(){
+            propTree.setProperty(FulltextIndexConstants.PROP_NULL_CHECK_ENABLED, true);
+            return this;
+        }
+
+        public PropertyRule excludeFromAggregation(){
+            propTree.setProperty(FulltextIndexConstants.PROP_EXCLUDE_FROM_AGGREGATE, true);
+            return this;
+        }
+
+        public PropertyRule notNullCheckEnabled(){
+            propTree.setProperty(FulltextIndexConstants.PROP_NOT_NULL_CHECK_ENABLED, true);
+            return this;
+        }
+
+        public PropertyRule weight(int weight){
+            propTree.setProperty(FulltextIndexConstants.PROP_WEIGHT, weight);
+            return this;
+        }
+
+        public PropertyRule valuePattern(String valuePattern){
+            propTree.setProperty(IndexConstants.VALUE_PATTERN, valuePattern);
+            return this;
+        }
+
+        public PropertyRule valueExcludedPrefixes(String... values){
+            propTree.setProperty(IndexConstants.VALUE_EXCLUDED_PREFIXES, asList(values), STRINGS);
+            return this;
+        }
+
+        public PropertyRule valueIncludedPrefixes(String... values){
+            propTree.setProperty(IndexConstants.VALUE_INCLUDED_PREFIXES, asList(values), STRINGS);
+            return this;
+        }
+
+        public PropertyRule sync(){
+            propTree.setProperty(FulltextIndexConstants.PROP_SYNC, true);
+            return this;
+        }
+
+        public PropertyRule unique(){
+            propTree.setProperty(FulltextIndexConstants.PROP_UNIQUE, true);
+            return this;
+        }
+
+        public PropertyRule function(String fn) {
+            propTree.setProperty(FulltextIndexConstants.PROP_FUNCTION, fn);
+            return this;
+        }
+
+        public IndexRule enclosingRule(){
+            return indexRule;
+        }
+
+        public Tree getBuilderTree(){
+            return propTree;
+        }
+
+        public PropertyRule property(String name){
+            return indexRule.property(name, false);
+        }
+
+        public PropertyRule property(String name, boolean regex) {
+            return indexRule.property(null, name, regex);
+        }
+
+        public PropertyRule property(String propDefnNodeName, String name) {
+            return indexRule.property(propDefnNodeName, name, false);
+        }
+    }
+
+    //~--------------------------------------< Aggregates >
+
+    public AggregateRule aggregateRule(String type){
+        if (aggregatesTree == null){
+            aggregatesTree = getOrCreateChild(tree, FulltextIndexConstants.AGGREGATES);
+        }
+        AggregateRule rule = aggRules.get(type);
+        if (rule == null){
+            rule = new AggregateRule(getOrCreateChild(aggregatesTree, type));
+            aggRules.put(type, rule);
+        }
+        return rule;
+    }
+
+    public AggregateRule aggregateRule(String primaryType, String ... includes){
+        AggregateRule rule = aggregateRule(primaryType);
+        for (String include : includes){
+            rule.include(include);
+        }
+        return rule;
+    }
+
+    public static class AggregateRule {
+        private final Tree aggregate;
+        private final Map<String, Include> includes = Maps.newHashMap();
+
+        private AggregateRule(Tree aggregate) {
+            this.aggregate = aggregate;
+            loadExisting(aggregate);
+        }
+
+        public Include include(String includePath) {
+            Include include = includes.get(includePath);
+            if (include == null){
+                Tree includeTree = findExisting(includePath);
+                if (includeTree == null){
+                    includeTree = getOrCreateChild(aggregate, "include" + includes.size());
+                }
+                include = new Include(this, includeTree);
+                includes.put(includePath, include);
+            }
+            include.path(includePath);
+            return include;
+        }
+
+        private Tree findExisting(String includePath) {
+            for (Tree tree : aggregate.getChildren()){
+                if (includePath.equals(tree.getProperty(FulltextIndexConstants.AGG_PATH).getValue(Type.STRING))){
+                    return tree;
+                }
+            }
+            return null;
+        }
+
+        private void loadExisting(Tree aggregate) {
+            for (Tree tree : aggregate.getChildren()){
+                if (tree.hasProperty(FulltextIndexConstants.AGG_PATH)) {
+                    Include include = new Include(this, tree);
+                    includes.put(include.getPath(), include);
+                }
+            }
+        }
+
+        public static class Include {
+            private final AggregateRule aggregateRule;
+            private final Tree include;
+
+            private Include(AggregateRule aggregateRule, Tree include) {
+                this.aggregateRule = aggregateRule;
+                this.include = include;
+            }
+
+            public Include path(String includePath) {
+                include.setProperty(FulltextIndexConstants.AGG_PATH, includePath);
+                return this;
+            }
+
+            public Include relativeNode(){
+                include.setProperty(FulltextIndexConstants.AGG_RELATIVE_NODE, true);
+                return this;
+            }
+
+            public Include include(String path){
+                return aggregateRule.include(path);
+            }
+
+            public String getPath(){
+                return include.getProperty(FulltextIndexConstants.AGG_PATH).getValue(Type.STRING);
+            }
+        }
+    }
+
+    private static Tree getOrCreateChild(Tree tree, String name){
+        if (tree.hasChild(name)){
+            return tree.getChild(name);
+        }
+        Tree child = tree.addChild(name);
+        child.setOrderableChildren(true);
+        child.setProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, NAME);
+        return child;
+    }
+
+    static class SelectiveEqualsDiff extends EqualsDiff {
+        public static boolean equals(NodeState before, NodeState after) {
+            return before.exists() == after.exists()
+                    && after.compareAgainstBaseState(before, new SelectiveEqualsDiff());
+        }
+
+        @Override
+        public boolean propertyChanged(PropertyState before, PropertyState after) {
+            if (IndexConstants.ASYNC_PROPERTY_NAME.equals(before.getName())){
+                Set<String> asyncBefore = getAsyncValuesWithoutNRT(before);
+                Set<String> asyncAfter = getAsyncValuesWithoutNRT(after);
+                return asyncBefore.equals(asyncAfter);
+            }
+            return false;
+        }
+
+        private Set<String> getAsyncValuesWithoutNRT(PropertyState state){
+            Set<String> async = Sets.newHashSet(state.getValue(Type.STRINGS));
+            async.remove(IndexConstants.INDEXING_MODE_NRT);
+            async.remove(IndexConstants.INDEXING_MODE_SYNC);
+            return async;
+        }
+    }
+
+    static String getSafePropName(String relativePropName) {
+        String propName = PathUtils.getName(relativePropName);
+        int indexOfColon = propName.indexOf(':');
+        if (indexOfColon > 0){
+            propName = propName.substring(indexOfColon + 1);
+        }
+
+        //Just keep ascii chars
+        propName = propName.replaceAll("\\W", "");
+        return propName;
+    }
+}

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

Added: jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/IndexHelper.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/IndexHelper.java?rev=1828972&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/IndexHelper.java (added)
+++ jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/IndexHelper.java Thu Apr 12 11:46:17 2018
@@ -0,0 +1,181 @@
+/*
+ * 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.util;
+
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableSet.of;
+import static com.google.common.collect.Sets.newHashSet;
+import static javax.jcr.PropertyType.TYPENAME_BINARY;
+import static javax.jcr.PropertyType.TYPENAME_STRING;
+import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
+import static org.apache.jackrabbit.JcrConstants.JCR_UUID;
+import static org.apache.jackrabbit.oak.api.Type.NAME;
+import static org.apache.jackrabbit.oak.api.Type.STRINGS;
+import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.ASYNC_PROPERTY_NAME;
+import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NODE_TYPE;
+import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.REINDEX_PROPERTY_NAME;
+import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.TYPE_PROPERTY_NAME;
+import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.EXCLUDE_PROPERTY_NAMES;
+import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.EXPERIMENTAL_STORAGE;
+import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.INCLUDE_PROPERTY_NAMES;
+import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.INCLUDE_PROPERTY_TYPES;
+import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.PERSISTENCE_FILE;
+import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.PERSISTENCE_NAME;
+import static org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.PERSISTENCE_PATH;
+import static org.apache.jackrabbit.oak.plugins.memory.PropertyStates.createProperty;
+import static org.apache.jackrabbit.oak.spi.security.user.UserConstants.GROUP_PROPERTY_NAMES;
+import static org.apache.jackrabbit.oak.spi.security.user.UserConstants.USER_PROPERTY_NAMES;
+
+public class IndexHelper {
+
+    public static final Set<String> JR_PROPERTY_INCLUDES = of(TYPENAME_STRING,
+            TYPENAME_BINARY);
+
+    /**
+     * Nodes that represent content that shold not be tokenized (like UUIDs,
+     * etc)
+     * 
+     */
+    private final static Set<String> NOT_TOKENIZED = newHashSet(JCR_UUID);
+
+    static {
+        NOT_TOKENIZED.addAll(USER_PROPERTY_NAMES);
+        NOT_TOKENIZED.addAll(GROUP_PROPERTY_NAMES);
+    }
+
+    private IndexHelper() {
+    }
+
+    public static NodeBuilder newFTIndexDefinition(
+            @Nonnull NodeBuilder index, @Nonnull String name, String type,
+            @Nullable Set<String> propertyTypes) {
+        return newFTIndexDefinition(index, name, type, propertyTypes, null, null, null);
+    }
+
+    public static NodeBuilder newFTIndexDefinition(
+            @Nonnull NodeBuilder index, @Nonnull String name, String type,
+            @Nullable Set<String> propertyTypes,
+            @Nullable Set<String> excludes, @Nullable String async) {
+        return newFTIndexDefinition(index, type, name, propertyTypes, excludes,
+                async, null);
+    }
+
+    public static NodeBuilder newFTIndexDefinition(
+            @Nonnull NodeBuilder index, @Nonnull String name, String type,
+            @Nullable Set<String> propertyTypes,
+            @Nullable Set<String> excludes, @Nullable String async,
+            @Nullable Boolean stored) {
+        if (index.hasChildNode(name)) {
+            return index.child(name);
+        }
+        index = index.child(name);
+        index.setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, NAME)
+                .setProperty(TYPE_PROPERTY_NAME, type)
+                .setProperty(REINDEX_PROPERTY_NAME, true);
+        if (async != null) {
+            index.setProperty(ASYNC_PROPERTY_NAME, async);
+        }
+        if (propertyTypes != null && !propertyTypes.isEmpty()) {
+            index.setProperty(createProperty(INCLUDE_PROPERTY_TYPES,
+                    propertyTypes, STRINGS));
+        }
+        if (excludes != null && !excludes.isEmpty()) {
+            index.setProperty(createProperty(EXCLUDE_PROPERTY_NAMES, excludes,
+                    STRINGS));
+        }
+        if (stored != null) {
+            index.setProperty(createProperty(EXPERIMENTAL_STORAGE, stored));
+        }
+        return index;
+    }
+
+    public static NodeBuilder newFTFileIndexDefinition(
+            @Nonnull NodeBuilder index, @Nonnull String name, String type,
+            @Nullable Set<String> propertyTypes, @Nonnull String path) {
+        return newFTFileIndexDefinition(index, type, name, propertyTypes, null,
+                path, null);
+    }
+
+    public static NodeBuilder newFTFileIndexDefinition(
+            @Nonnull NodeBuilder index, @Nonnull String name, String type,
+            @Nullable Set<String> propertyTypes,
+            @Nullable Set<String> excludes, @Nonnull String path,
+            @Nullable String async) {
+        if (index.hasChildNode(name)) {
+            return index.child(name);
+        }
+        index = index.child(name);
+        index.setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, NAME)
+                .setProperty(TYPE_PROPERTY_NAME, type)
+                .setProperty(PERSISTENCE_NAME, PERSISTENCE_FILE)
+                .setProperty(PERSISTENCE_PATH, path)
+                .setProperty(REINDEX_PROPERTY_NAME, true);
+        if (async != null) {
+            index.setProperty(ASYNC_PROPERTY_NAME, async);
+        }
+        if (propertyTypes != null && !propertyTypes.isEmpty()) {
+            index.setProperty(createProperty(INCLUDE_PROPERTY_TYPES,
+                    propertyTypes, STRINGS));
+        }
+        if (excludes != null && !excludes.isEmpty()) {
+            index.setProperty(createProperty(EXCLUDE_PROPERTY_NAMES, excludes,
+                    STRINGS));
+        }
+        return index;
+    }
+
+    public static NodeBuilder newFTPropertyIndexDefinition(
+            @Nonnull NodeBuilder index, @Nonnull String name, String type,
+            @Nonnull Set<String> includes,
+            @Nonnull String async) {
+        checkArgument(!includes.isEmpty(), "Lucene property index " +
+                "requires explicit list of property names to be indexed");
+
+        index = index.child(name);
+        index.setProperty(JCR_PRIMARYTYPE, INDEX_DEFINITIONS_NODE_TYPE, NAME)
+                .setProperty(TYPE_PROPERTY_NAME, type)
+                .setProperty(REINDEX_PROPERTY_NAME, true);
+        index.setProperty(FulltextIndexConstants.FULL_TEXT_ENABLED, false);
+        index.setProperty(createProperty(INCLUDE_PROPERTY_NAMES, includes, STRINGS));
+
+        if (async != null) {
+            index.setProperty(ASYNC_PROPERTY_NAME, async);
+        }
+        return index;
+    }
+
+    /**
+     * Nodes that represent UUIDs and shold not be tokenized
+     * 
+     */
+    public static boolean skipTokenization(String name) {
+        return NOT_TOKENIZED.contains(name);
+    }
+
+    public static boolean isIndexNodeOfType(NodeState node, String type){
+        return type.equals(node.getString(TYPE_PROPERTY_NAME));
+    }
+}

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

Added: jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/NodeStateCopyUtils.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/NodeStateCopyUtils.java?rev=1828972&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/NodeStateCopyUtils.java (added)
+++ jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/util/NodeStateCopyUtils.java Thu Apr 12 11:46:17 2018
@@ -0,0 +1,154 @@
+/*
+ * 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.util;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import javax.jcr.ValueFactory;
+
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.commons.JcrUtils;
+import org.apache.jackrabbit.oak.api.Blob;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.plugins.tree.factories.TreeFactory;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStateUtils;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.apache.jackrabbit.oak.api.Type.NAMES;
+
+final class NodeStateCopyUtils {
+    private static final String OAK_CHILD_ORDER = ":childOrder";
+
+    public static void copyToTree(NodeState state, Tree tree){
+        tree.setOrderableChildren(state.hasProperty(OAK_CHILD_ORDER));
+        copyProps(state, tree);
+
+        Tree src = TreeFactory.createReadOnlyTree(state);
+        for (Tree srcChild : src.getChildren()){
+            String childName = srcChild.getName();
+            Tree child = tree.addChild(childName);
+            copyToTree(state.getChildNode(childName), child);
+        }
+    }
+
+    public static void copyToNode(NodeState state, Node node) throws RepositoryException {
+        copyProps(state, node);
+
+        Tree src = TreeFactory.createReadOnlyTree(state);
+        for (Tree srcChild : src.getChildren()){
+            String childName = srcChild.getName();
+
+            if (NodeStateUtils.isHidden(childName)){
+                continue;
+            }
+
+            NodeState childState = state.getChildNode(childName);
+            Node child = JcrUtils.getOrAddNode(node, childName, primaryType(childState));
+            copyToNode(childState, child);
+        }
+    }
+
+    private static void copyProps(NodeState state, Tree tree) {
+        for (PropertyState ps : state.getProperties()){
+            if (!ps.getName().equals(OAK_CHILD_ORDER)){
+                tree.setProperty(ps);
+            }
+        }
+    }
+
+    private static void copyProps(NodeState state, Node node) throws RepositoryException {
+        ValueFactory vf = node.getSession().getValueFactory();
+        for (PropertyState ps : state.getProperties()){
+            String name = ps.getName();
+            if (name.equals(JcrConstants.JCR_PRIMARYTYPE)
+                    || name.equals(OAK_CHILD_ORDER)){
+                continue;
+            }
+
+            if (name.equals(JcrConstants.JCR_MIXINTYPES)){
+                for (String n : ps.getValue(NAMES)) {
+                    node.addMixin(n);
+                }
+                continue;
+            }
+
+            if (NodeStateUtils.isHidden(name)){
+                continue;
+            }
+
+            if (ps.isArray()){
+                Value[] values = new Value[ps.count()];
+                for (int i = 0; i < ps.count(); i++) {
+                    values[i] = createValue(vf, ps, i);
+                }
+                node.setProperty(name, values, ps.getType().tag());
+            } else {
+                node.setProperty(name, createValue(vf, ps, -1), ps.getType().tag());
+            }
+        }
+    }
+
+    private static Value createValue(ValueFactory vf, PropertyState ps, int index) throws RepositoryException {
+        switch(ps.getType().tag()) {
+            case PropertyType.STRING :
+                return vf.createValue(getValue(ps, Type.STRING, index));
+            case PropertyType.BINARY:
+                Blob blob = getValue(ps, Type.BINARY, index);
+                Binary bin = vf.createBinary(blob.getNewStream());
+                return vf.createValue(bin);
+            case PropertyType.LONG:
+                return vf.createValue(getValue(ps, Type.LONG, index));
+            case PropertyType.DOUBLE:
+                return vf.createValue(getValue(ps, Type.DOUBLE, index));
+            case PropertyType.DATE:
+                return vf.createValue(getValue(ps, Type.DATE, index));
+            case PropertyType.BOOLEAN:
+                return vf.createValue(getValue(ps, Type.BOOLEAN, index));
+            case PropertyType.NAME:
+                return vf.createValue(getValue(ps, Type.NAME, index));
+            case PropertyType.PATH:
+                return vf.createValue(getValue(ps, Type.PATH, index));
+            case PropertyType.REFERENCE:
+                return vf.createValue(getValue(ps, Type.REFERENCE, index));
+            case PropertyType.WEAKREFERENCE:
+                return vf.createValue(getValue(ps, Type.WEAKREFERENCE, index));
+            case PropertyType.URI:
+                return vf.createValue(getValue(ps, Type.URI, index));
+            case PropertyType.DECIMAL:
+                return vf.createValue(getValue(ps, Type.DECIMAL, index));
+            default:
+                throw new IllegalStateException("Unsupported type " + ps.getType());
+        }
+    }
+
+    private static <T> T getValue(PropertyState ps, Type<T> type, int index){
+        return index < 0 ? ps.getValue(type) : ps.getValue(type, index);
+    }
+
+    private static String primaryType(NodeState state){
+        return checkNotNull(state.getName(JcrConstants.JCR_PRIMARYTYPE), "jcr:primaryType not defined for %s", state);
+    }
+}

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