Author: mkataria
Date: Mon Feb  3 11:55:04 2020
New Revision: 1873532

URL: http://svn.apache.org/viewvc?rev=1873532&view=rev
Log:
OAK-8788: Add index creation and index completion time to indexes

Added:
    
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/
    
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/IndexVersionOperation.java
   (with props)
    
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/PurgeOldIndexVersion.java
   (with props)
    
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/PurgeOldVersionUtils.java
   (with props)
    
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/PurgeOldIndexVersionCommand.java
   (with props)
    
jackrabbit/oak/trunk/oak-run/src/test/java/org/apache/jackrabbit/oak/indexversion/
    
jackrabbit/oak/trunk/oak-run/src/test/java/org/apache/jackrabbit/oak/indexversion/PurgeOldIndexVersionTest.java
   (with props)
Modified:
    
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/AvailableModes.java
    
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/IndexDefinition.java
    
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/ReindexOperations.java
    
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/editor/FulltextIndexEditorContext.java
    
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/IndexName.java

Added: 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/IndexVersionOperation.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/IndexVersionOperation.java?rev=1873532&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/IndexVersionOperation.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/IndexVersionOperation.java
 Mon Feb  3 11:55:04 2020
@@ -0,0 +1,189 @@
+/*
+ * 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.indexversion;
+
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition;
+import org.apache.jackrabbit.oak.plugins.index.search.spi.query.IndexName;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/*
+    Main operation of this class is to mark IndexName's with operations
+ */
+public class IndexVersionOperation {
+    private static final Logger LOG = 
LoggerFactory.getLogger(IndexVersionOperation.class);
+
+    private IndexName indexName;
+    private Operation operation;
+
+    public IndexVersionOperation(IndexName indexName) {
+        this.indexName = indexName;
+        this.operation = Operation.NOOP;
+    }
+
+    public void setOperation(Operation operation) {
+        this.operation = operation;
+    }
+
+    public Operation getOperation() {
+        return this.operation;
+    }
+
+    public IndexName getIndexName() {
+        return this.indexName;
+    }
+
+    @Override
+    public String toString() {
+        return this.getIndexName() + " operation:" + this.getOperation();
+    }
+
+    /**
+     * @param indexDefParentNode   NodeState of parent of baseIndex
+     * @param indexNameObjectList  This is a list of IndexName Objects with 
same baseIndexName on which operations will be applied.
+     * @param purgeThresholdMillis after which a fully functional index is 
eligible for purge operations
+     * @return This method returns an IndexVersionOperation list i.e 
indexNameObjectList marked with operations
+     */
+    public static List<IndexVersionOperation> 
generateIndexVersionOperationList(NodeState indexDefParentNode,
+                                                                               
 List<IndexName> indexNameObjectList, long purgeThresholdMillis) {
+        int activeProductVersion = -1;
+        List<IndexName> reverseSortedIndexNameList = 
getReverseSortedIndexNameList(indexNameObjectList);
+        removeDisabledCustomIndexesFromList(indexDefParentNode, 
reverseSortedIndexNameList);
+        List<IndexVersionOperation> indexVersionOperationList = new 
LinkedList<>();
+        for (IndexName indexNameObject : reverseSortedIndexNameList) {
+            NodeState indexNode = indexDefParentNode
+                    
.getChildNode(PathUtils.getName(indexNameObject.getNodeName()));
+            /*
+            All Indexes are by default NOOP until we get index which is active 
for more than threshold limit
+             */
+            if (activeProductVersion == -1) {
+                indexVersionOperationList.add(new 
IndexVersionOperation(indexNameObject));
+                if (indexNode.hasChildNode(IndexDefinition.STATUS_NODE)) {
+                    if (indexNode.getChildNode(IndexDefinition.STATUS_NODE)
+                            
.getProperty(IndexDefinition.REINDEX_COMPLETION_TIMESTAMP) != null) {
+                        String reindexCompletionTime = indexDefParentNode
+                                
.getChildNode(PathUtils.getName(indexNameObject.getNodeName()))
+                                .getChildNode(IndexDefinition.STATUS_NODE)
+                                
.getProperty(IndexDefinition.REINDEX_COMPLETION_TIMESTAMP).getValue(Type.DATE);
+                        long reindexCompletionTimeInMillis = 
PurgeOldVersionUtils.getMillisFromString(reindexCompletionTime);
+                        long currentTimeInMillis = System.currentTimeMillis();
+                        if (currentTimeInMillis - 
reindexCompletionTimeInMillis > purgeThresholdMillis) {
+                            activeProductVersion = 
indexNameObject.getProductVersion();
+                        }
+                    } else {
+                        LOG.warn(IndexDefinition.REINDEX_COMPLETION_TIMESTAMP
+                                + " property is not set for index " + 
indexNameObject.getNodeName());
+                    }
+                }
+            } else {
+            /*
+            Once we found active index, we mark all its custom version with 
DELETE
+            and OOTB same product version index with DELETE_HIDDEN_AND_DISABLE
+             */
+                if (indexNameObject.getProductVersion() == activeProductVersion
+                        && indexNameObject.getCustomerVersion() == 0) {
+                    IndexVersionOperation indexVersionOperation = new 
IndexVersionOperation(indexNameObject);
+                    
indexVersionOperation.setOperation(Operation.DELETE_HIDDEN_AND_DISABLE);
+                    indexVersionOperationList.add(indexVersionOperation);
+                } else {
+                    IndexVersionOperation indexVersionOperation = new 
IndexVersionOperation(indexNameObject);
+                    indexVersionOperation.setOperation(Operation.DELETE);
+                    indexVersionOperationList.add(indexVersionOperation);
+                }
+            }
+        }
+        if (!isValidIndexVersionOperationList(indexVersionOperationList)) {
+            indexVersionOperationList = Collections.emptyList();
+        }
+        return indexVersionOperationList;
+    }
+
+    /*
+        returns IndexNameObjects in descending order of version i.e from 
newest version to oldest
+    */
+    private static List<IndexName> 
getReverseSortedIndexNameList(List<IndexName> indexNameObjectList) {
+        List<IndexName> reverseSortedIndexNameObjectList = new 
ArrayList<>(indexNameObjectList);
+        Collections.sort(reverseSortedIndexNameObjectList, 
Collections.reverseOrder());
+        return reverseSortedIndexNameObjectList;
+    }
+
+    /**
+     * @param indexVersionOperations
+     * @return true if the IndexVersionOperation list passes following 
criteria.
+     * For merging indexes we need baseIndex and latest custom index.
+     * So we first validate that if there are custom indexes than OOTB index 
with same product must be marked with DELETE_HIDDEN_AND_DISABLE
+     */
+    private static boolean 
isValidIndexVersionOperationList(List<IndexVersionOperation> 
indexVersionOperations) {
+        boolean isValid = false;
+        IndexVersionOperation lastNoopOperationIndexVersion = null;
+        IndexVersionOperation indexWithDeleteHiddenOp = null;
+        for (IndexVersionOperation indexVersionOperation : 
indexVersionOperations) {
+            if (indexVersionOperation.getOperation() == Operation.NOOP) {
+                lastNoopOperationIndexVersion = indexVersionOperation;
+            }
+            if (indexVersionOperation.getOperation() == 
Operation.DELETE_HIDDEN_AND_DISABLE) {
+                indexWithDeleteHiddenOp = indexVersionOperation;
+            }
+        }
+        if (lastNoopOperationIndexVersion.getIndexName().getCustomerVersion() 
== 0) {
+            isValid = true;
+        } else if 
(lastNoopOperationIndexVersion.getIndexName().getCustomerVersion() != 0) {
+            if (indexWithDeleteHiddenOp != null
+                    && 
lastNoopOperationIndexVersion.getIndexName().getProductVersion() == 
indexWithDeleteHiddenOp.getIndexName().getProductVersion()) {
+                isValid = true;
+            }
+        }
+        if (!isValid) {
+            LOG.info("IndexVersionOperation List is not valid for index {}", 
lastNoopOperationIndexVersion.getIndexName().getNodeName());
+        }
+        return isValid;
+    }
+
+    private static void removeDisabledCustomIndexesFromList(NodeState 
indexDefParentNode,
+                                                            List<IndexName> 
indexNameObjectList) {
+        for (int i = 0; i < indexNameObjectList.size(); i++) {
+            NodeState indexNode = indexDefParentNode
+                    
.getChildNode(PathUtils.getName(indexNameObjectList.get(i).getNodeName()));
+            if (indexNode.getProperty("type") != null && 
"disabled".equals(indexNode.getProperty("type").getValue(Type.STRING))) {
+                indexNameObjectList.remove(i);
+            }
+        }
+    }
+
+    /*
+        NOOP means : No operation to be performed index Node
+        DELETE_HIDDEN_AND_DISABLE: This operation means that we should disable 
this indexNode and delete all hidden nodes under it
+        DELETE: Delete this index altogether
+     */
+    enum Operation {
+        NOOP,
+        DELETE_HIDDEN_AND_DISABLE,
+        DELETE
+    }
+}
+
+

Propchange: 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/IndexVersionOperation.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/PurgeOldIndexVersion.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/PurgeOldIndexVersion.java?rev=1873532&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/PurgeOldIndexVersion.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/PurgeOldIndexVersion.java
 Mon Feb  3 11:55:04 2020
@@ -0,0 +1,170 @@
+/*
+ * 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.indexversion;
+
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.index.IndexPathService;
+import org.apache.jackrabbit.oak.plugins.index.IndexPathServiceImpl;
+import org.apache.jackrabbit.oak.plugins.index.IndexUpdateProvider;
+import 
org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider;
+import org.apache.jackrabbit.oak.plugins.index.search.spi.query.IndexName;
+import org.apache.jackrabbit.oak.run.cli.NodeStoreFixture;
+import org.apache.jackrabbit.oak.run.cli.NodeStoreFixtureProvider;
+import org.apache.jackrabbit.oak.run.cli.Options;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EditorHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStateUtils;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class PurgeOldIndexVersion {
+    private static final Logger LOG = 
LoggerFactory.getLogger(PurgeOldIndexVersion.class);
+
+    public void execute(Options opts, long purgeThresholdMillis, List<String> 
indexPaths) throws Exception {
+        boolean isReadWriteRepository = opts.getCommonOpts().isReadWrite();
+        if (!isReadWriteRepository) {
+            LOG.info("Repository connected in read-only mode. Use 
'--read-write' for write operations");
+        }
+        try (NodeStoreFixture fixture = NodeStoreFixtureProvider.create(opts)) 
{
+            NodeStore nodeStore = fixture.getStore();
+            List<String> sanitisedIndexPaths = 
sanitiseUserIndexPaths(indexPaths);
+            Set<String> indexPathSet = 
filterIndexPaths(getRepositoryIndexPaths(nodeStore), sanitisedIndexPaths);
+            Map<String, Set<String>> segregateIndexes = 
segregateIndexes(indexPathSet);
+            for (Map.Entry<String, Set<String>> entry : 
segregateIndexes.entrySet()) {
+                String baseIndexPath = entry.getKey();
+                String parentPath = PathUtils.getParentPath(entry.getKey());
+                List<IndexName> indexNameObjectList = 
getIndexNameObjectList(entry.getValue());
+                NodeState indexDefParentNode = 
NodeStateUtils.getNode(nodeStore.getRoot(),
+                        parentPath);
+                List<IndexVersionOperation> toDeleteIndexNameObjectList =
+                        
IndexVersionOperation.generateIndexVersionOperationList(indexDefParentNode, 
indexNameObjectList, purgeThresholdMillis);
+                if (isReadWriteRepository && 
!toDeleteIndexNameObjectList.isEmpty()) {
+                    purgeOldIndexVersion(nodeStore, 
toDeleteIndexNameObjectList);
+                } else {
+                    LOG.info("Repository is opened in read-only mode: 
IndexOperations" +
+                            " for index at path {} are : {}", baseIndexPath, 
toDeleteIndexNameObjectList);
+                }
+            }
+        }
+    }
+
+    /**
+     * @param userIndexPaths indexpaths provided by user
+     * @return a list of Indexpaths having baseIndexpaths or path till 
oak:index
+     * @throws IllegalArgumentException if the paths provided are not till 
oak:index or till index
+     */
+    private List<String> sanitiseUserIndexPaths(List<String> userIndexPaths) {
+        List<String> sanitisedUserIndexPaths = new ArrayList<>();
+        for (String userIndexPath : userIndexPaths) {
+            if 
(PathUtils.getName(userIndexPath).equals(PurgeOldVersionUtils.OAK_INDEX)) {
+                sanitisedUserIndexPaths.add(userIndexPath);
+            } else if 
(PathUtils.getName(PathUtils.getParentPath(userIndexPath)).equals(PurgeOldVersionUtils.OAK_INDEX))
 {
+                
sanitisedUserIndexPaths.add(IndexName.parse(userIndexPath).getBaseName());
+            } else {
+                throw new IllegalArgumentException(userIndexPath + " indexpath 
is not valid");
+            }
+        }
+        return sanitisedUserIndexPaths;
+    }
+
+    /**
+     * @param indexPathSet
+     * @return a map with baseIndexName as key and a set of indexpaths having 
same baseIndexName
+     */
+    private Map<String, Set<String>> segregateIndexes(Set<String> 
indexPathSet) {
+        Map<String, Set<String>> segregatedIndexes = new HashMap<>();
+        for (String path : indexPathSet) {
+            String baseIndexPath = IndexName.parse(path).getBaseName();
+            Set<String> indexPaths = segregatedIndexes.get(baseIndexPath);
+            if (indexPaths == null) {
+                indexPaths = new HashSet<>();
+            }
+            indexPaths.add(path);
+            segregatedIndexes.put(baseIndexPath, indexPaths);
+        }
+        return segregatedIndexes;
+    }
+
+    private Iterable<String> getRepositoryIndexPaths(NodeStore store) throws 
CommitFailedException, IOException {
+        IndexPathService indexPathService = new IndexPathServiceImpl(store);
+        return indexPathService.getIndexPaths();
+    }
+
+
+    /**
+     * @param repositoryIndexPaths: list of indexpaths retrieved from  index 
service
+     * @param commandlineIndexPaths indexpaths provided by user
+     * @return returns set of indexpaths which are to be considered for purging
+     */
+    private Set<String> filterIndexPaths(Iterable<String> 
repositoryIndexPaths, List<String> commandlineIndexPaths) {
+        Set<String> filteredIndexPaths = new HashSet<>();
+        for (String commandlineIndexPath : commandlineIndexPaths) {
+            for (String repositoryIndexPath : repositoryIndexPaths) {
+                if 
(PurgeOldVersionUtils.isIndexChildNode(commandlineIndexPath, 
repositoryIndexPath)
+                        || 
PurgeOldVersionUtils.isBaseIndexEqual(commandlineIndexPath, 
repositoryIndexPath)) {
+                    filteredIndexPaths.add(repositoryIndexPath);
+                }
+            }
+        }
+        return filteredIndexPaths;
+    }
+
+    private List<IndexName> getIndexNameObjectList(Set<String> 
versionedIndexPaths) {
+        List<IndexName> indexNameObjectList = new ArrayList<>();
+        for (String indexNameString : versionedIndexPaths) {
+            indexNameObjectList.add(IndexName.parse(indexNameString));
+        }
+        return indexNameObjectList;
+    }
+
+    private void purgeOldIndexVersion(NodeStore store,
+                                      List<IndexVersionOperation> 
toDeleteIndexNameObjectList) throws CommitFailedException {
+        for (IndexVersionOperation toDeleteIndexNameObject : 
toDeleteIndexNameObjectList) {
+            NodeState root = store.getRoot();
+            NodeBuilder rootBuilder = root.builder();
+            NodeBuilder nodeBuilder = 
PurgeOldVersionUtils.getNode(rootBuilder, 
toDeleteIndexNameObject.getIndexName().getNodeName());
+            if (nodeBuilder.exists()) {
+                if (toDeleteIndexNameObject.getOperation() == 
IndexVersionOperation.Operation.DELETE_HIDDEN_AND_DISABLE) {
+                    nodeBuilder.setProperty("type", "disabled", Type.STRING);
+                    
PurgeOldVersionUtils.recursiveDeleteHiddenChildNodes(store, 
toDeleteIndexNameObject.getIndexName().getNodeName());
+                } else if (toDeleteIndexNameObject.getOperation() == 
IndexVersionOperation.Operation.DELETE) {
+                    nodeBuilder.remove();
+                }
+                EditorHook hook = new EditorHook(
+                        new IndexUpdateProvider(new 
PropertyIndexEditorProvider()));
+                store.merge(rootBuilder, hook, CommitInfo.EMPTY);
+            } else {
+                LOG.error("nodebuilder null for path " + 
toDeleteIndexNameObject.getIndexName().getNodeName());
+            }
+        }
+    }
+}

Propchange: 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/PurgeOldIndexVersion.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/PurgeOldVersionUtils.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/PurgeOldVersionUtils.java?rev=1873532&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/PurgeOldVersionUtils.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/PurgeOldVersionUtils.java
 Mon Feb  3 11:55:04 2020
@@ -0,0 +1,102 @@
+/*
+ * 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.indexversion;
+
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.plugins.index.lucene.property.RecursiveDelete;
+import org.apache.jackrabbit.oak.plugins.index.search.spi.query.IndexName;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStateUtils;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.apache.jackrabbit.util.ISO8601;
+import org.jetbrains.annotations.NotNull;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+public class PurgeOldVersionUtils {
+
+    public static final String OAK_INDEX = "oak:index";
+
+    public static long getMillisFromString(String strDate) {
+        return ISO8601.parse(strDate).getTimeInMillis();
+    }
+
+    /**
+     * @param nodeBuilder
+     * @param path        Path of node whose nodeBuilder object should be 
returned.
+     * @return nodeBuilder object of node at @param{path}
+     */
+    public static NodeBuilder getNode(@NotNull NodeBuilder nodeBuilder, 
@NotNull String path) {
+        NodeBuilder resultNodeBuilder = nodeBuilder;
+        for (String name : PathUtils.elements(checkNotNull(path))) {
+            resultNodeBuilder = 
resultNodeBuilder.getChildNode(checkNotNull(name));
+        }
+        return resultNodeBuilder;
+    }
+
+    /**
+     * @param store
+     * @param path
+     * @throws CommitFailedException recursively deletes child nodes under path
+     */
+    public static void recursiveDeleteHiddenChildNodes(NodeStore store, String 
path) throws CommitFailedException {
+        NodeState nodeState = NodeStateUtils.getNode(store.getRoot(), path);
+        Iterable<String> childNodeNames = nodeState.getChildNodeNames();
+        for (String childNodeName : childNodeNames) {
+            if (NodeStateUtils.isHidden(childNodeName)) {
+                RecursiveDelete recursiveDelete = new RecursiveDelete(store, 
EmptyHook.INSTANCE, () -> CommitInfo.EMPTY);
+                recursiveDelete.run(path + "/" + childNodeName);
+            }
+        }
+    }
+
+    /**
+     * @param commandlineIndexPath
+     * @param repositoryIndexPath
+     * @return true if baseIndexName at  commandlineIndexPath and 
repositoryIndexPath are equal
+     */
+    public static boolean isBaseIndexEqual(String commandlineIndexPath, String 
repositoryIndexPath) {
+        if 
(PathUtils.getName(PathUtils.getParentPath(commandlineIndexPath)).equals(OAK_INDEX))
 {
+            String commandlineIndexBaseName = 
IndexName.parse(commandlineIndexPath).getBaseName();
+            String repositoryIndexBaseName = 
IndexName.parse(repositoryIndexPath).getBaseName();
+            if (commandlineIndexBaseName.equals(repositoryIndexBaseName)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @param commandlineIndexPath
+     * @param repositoryIndexPath
+     * @return true if repositoryIndexPath is child of commandlineIndexPath
+     */
+    public static boolean isIndexChildNode(String commandlineIndexPath, String 
repositoryIndexPath) {
+        if (PathUtils.getName(commandlineIndexPath).equals(OAK_INDEX)) {
+            if (repositoryIndexPath.startsWith(commandlineIndexPath)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}

Propchange: 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/indexversion/PurgeOldVersionUtils.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/AvailableModes.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/AvailableModes.java?rev=1873532&r1=1873531&r2=1873532&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/AvailableModes.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/AvailableModes.java
 Mon Feb  3 11:55:04 2020
@@ -64,5 +64,6 @@ public final class AvailableModes {
             .put("search-nodes", new SearchNodesCommand())
             .put("segment-copy", new SegmentCopyCommand())
             .put("server", new ServerCommand())
+            .put("purge-index-versions", new PurgeOldIndexVersionCommand())
             .build());
 }

Added: 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/PurgeOldIndexVersionCommand.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/PurgeOldIndexVersionCommand.java?rev=1873532&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/PurgeOldIndexVersionCommand.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/PurgeOldIndexVersionCommand.java
 Mon Feb  3 11:55:04 2020
@@ -0,0 +1,55 @@
+/*
+ * 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.run;
+
+import joptsimple.OptionParser;
+import joptsimple.OptionSet;
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.oak.indexversion.PurgeOldIndexVersion;
+import org.apache.jackrabbit.oak.run.cli.Options;
+import org.apache.jackrabbit.oak.run.commons.Command;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class PurgeOldIndexVersionCommand implements Command {
+    private long threshold;
+    private List<String> indexPaths;
+    private long DEFAULT_PURGE_THRESHOLD = TimeUnit.DAYS.toMillis(5); // 5 
days in millis
+    private final static String DEFAULT_INDEX_PATH = "/oak:index";
+
+    @Override
+    public void execute(String... args) throws Exception {
+        Options opts = parseCommandLineParams(args);
+        new PurgeOldIndexVersion().execute(opts, threshold, indexPaths);
+    }
+
+    private Options parseCommandLineParams(String... args) throws Exception {
+        OptionParser parser = new OptionParser();
+        OptionSpec<Long> thresholdOption = parser.accepts("threshold")
+                
.withOptionalArg().ofType(Long.class).defaultsTo(DEFAULT_PURGE_THRESHOLD);
+        OptionSpec<String> indexPathsOption = parser.accepts("index-paths", 
"Comma separated list of index paths for which the " +
+                "selected operations need to be performed")
+                
.withOptionalArg().ofType(String.class).withValuesSeparatedBy(",").defaultsTo(DEFAULT_INDEX_PATH);
+        Options opts = new Options();
+        OptionSet optionSet = opts.parseAndConfigure(parser, args);
+        this.threshold = optionSet.valueOf(thresholdOption);
+        this.indexPaths = optionSet.valuesOf(indexPathsOption);
+        return opts;
+    }
+}

Propchange: 
jackrabbit/oak/trunk/oak-run/src/main/java/org/apache/jackrabbit/oak/run/PurgeOldIndexVersionCommand.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: 
jackrabbit/oak/trunk/oak-run/src/test/java/org/apache/jackrabbit/oak/indexversion/PurgeOldIndexVersionTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-run/src/test/java/org/apache/jackrabbit/oak/indexversion/PurgeOldIndexVersionTest.java?rev=1873532&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-run/src/test/java/org/apache/jackrabbit/oak/indexversion/PurgeOldIndexVersionTest.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-run/src/test/java/org/apache/jackrabbit/oak/indexversion/PurgeOldIndexVersionTest.java
 Mon Feb  3 11:55:04 2020
@@ -0,0 +1,323 @@
+/*
+ * 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.indexversion;
+
+import ch.qos.logback.classic.Level;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.commons.junit.LogCustomizer;
+import org.apache.jackrabbit.oak.index.AbstractIndexCommandTest;
+import org.apache.jackrabbit.oak.index.RepositoryFixture;
+import 
org.apache.jackrabbit.oak.plugins.index.lucene.util.IndexDefinitionBuilder;
+import org.apache.jackrabbit.oak.run.PurgeOldIndexVersionCommand;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.junit.Assert;
+import org.junit.Test;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import static org.apache.jackrabbit.commons.JcrUtils.getOrCreateByPath;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+public class PurgeOldIndexVersionTest extends AbstractIndexCommandTest {
+    private final static String FOO1_INDEX_PATH = "/oak:index/fooIndex1";
+
+    private void createCustomIndex(String path, int ootbVersion, int 
customVersion, boolean asyncIndex) throws IOException, RepositoryException {
+        IndexDefinitionBuilder idxBuilder = new IndexDefinitionBuilder();
+        if (!asyncIndex) {
+            idxBuilder.noAsync();
+        }
+        idxBuilder.indexRule("nt:base").property("foo").propertyIndex();
+
+        Session session = fixture.getAdminSession();
+        String indexName = customVersion != 0
+                ? path + "-" + ootbVersion + "-custom-" + customVersion
+                : path + "-" + ootbVersion;
+        Node fooIndex = getOrCreateByPath(indexName,
+                "oak:QueryIndexDefinition", session);
+
+        idxBuilder.build(fooIndex);
+        session.save();
+        session.logout();
+    }
+
+    /*
+        All indexes have reindexCompletionTime present (Reindexing is done 
after creating all indexes)
+     */
+    @Test
+    public void deleteOldIndexCompletely() throws Exception {
+        createTestData(false);
+        createCustomIndex(TEST_INDEX_PATH, 2, 1, false);
+        createCustomIndex(TEST_INDEX_PATH, 3, 0, false);
+        createCustomIndex(TEST_INDEX_PATH, 3, 1, false);
+        createCustomIndex(TEST_INDEX_PATH, 3, 2, false);
+        createCustomIndex(TEST_INDEX_PATH, 4, 0, false);
+        createCustomIndex(TEST_INDEX_PATH, 4, 1, false);
+        createCustomIndex(TEST_INDEX_PATH, 4, 2, false);
+        fixture.getAsyncIndexUpdate("async").run();
+        fixture.close();
+        PurgeOldIndexVersionCommand command = new 
PurgeOldIndexVersionCommand();
+        File storeDir = fixture.getDir();
+        String[] args = {
+                storeDir.getAbsolutePath(),
+                "--read-write",
+                "--threshold=1"
+        };
+
+        command.execute(args);
+        fixture = new RepositoryFixture(storeDir);
+        fixture.close();
+
+        Assert.assertFalse("Index:" + "fooIndex-2" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-2").exists());
+        Assert.assertFalse("Index:" + "fooIndex-2-custom-1" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-2-custom-1").exists());
+        Assert.assertFalse("Index:" + "fooIndex-3" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-3").exists());
+        Assert.assertFalse("Index:" + "fooIndex-3-custom-1" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-3-custom-1").exists());
+        Assert.assertFalse("Index:" + "fooIndex-3-custom-2" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-3-custom-2").exists());
+        Assert.assertFalse("Index:" + "fooIndex" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex").exists());
+        
Assert.assertEquals(fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-4").getProperty("type").getValue(Type.STRING),
 "disabled");
+        
Assert.assertFalse(isHiddenChildNodePresent(fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-4")));
+        Assert.assertFalse("Index:" + "fooIndex-4-custom-1" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-4-custom-1").exists());
+        Assert.assertTrue("Index:" + "fooIndex-4-custom-2" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-4-custom-2").exists());
+    }
+
+    @Test
+    public void invalidIndexOperationVersion() throws Exception {
+
+        LogCustomizer custom = LogCustomizer
+                .forLogger(
+                        
"org.apache.jackrabbit.oak.indexversion.IndexVersionOperation")
+                .enable(Level.INFO).create();
+        try {
+            custom.starting();
+            createTestData(false);
+            createCustomIndex(TEST_INDEX_PATH, 2, 1, false);
+            createCustomIndex(TEST_INDEX_PATH, 3, 0, false);
+            createCustomIndex(TEST_INDEX_PATH, 3, 1, false);
+            createCustomIndex(TEST_INDEX_PATH, 3, 2, false);
+            createCustomIndex(TEST_INDEX_PATH, 4, 1, false);
+            createCustomIndex(TEST_INDEX_PATH, 4, 2, false);
+            fixture.getAsyncIndexUpdate("async").run();
+            fixture.close();
+            PurgeOldIndexVersionCommand command = new 
PurgeOldIndexVersionCommand();
+
+            // File outDir = temporaryFolder.newFolder();
+            File storeDir = fixture.getDir();
+            String[] args = {
+                    storeDir.getAbsolutePath(),
+                    "--read-write",
+                    "--threshold=1"
+            };
+            command.execute(args);
+            fixture = new RepositoryFixture(storeDir);
+            fixture.close();
+            List<String> logs = custom.getLogs();
+            assertTrue(logs.size() == 1);
+            assertThat("custom fooIndex don't have product version ", 
logs.toString(),
+                    containsString("IndexVersionOperation List is not valid 
for index"));
+        } finally {
+            custom.finished();
+        }
+    }
+
+/*
+    Not all indexes have reIndexCompletionTime present (Reindexing is done 
before creating all indexes)
+    We are deleting hidden nodes for indexes fooIndex-4-custom-2 and 
fooIndex-3-custom-2
+    Now according to logic because   fooIndex-4-custom-1 will have 
reindexCompletionTimestamp all index
+    versions previous to this will get be marked for deletion or deleting 
hidden node operation.
+ */
+
+    @Test
+    public void deleteOldIndexPartially() throws Exception {
+        createTestData(false);
+        createCustomIndex(TEST_INDEX_PATH, 2, 1, false);
+        createCustomIndex(TEST_INDEX_PATH, 3, 0, false);
+        createCustomIndex(TEST_INDEX_PATH, 3, 1, false);
+        createCustomIndex(TEST_INDEX_PATH, 3, 2, false);
+        createCustomIndex(TEST_INDEX_PATH, 4, 0, false);
+        createCustomIndex(TEST_INDEX_PATH, 4, 1, false);
+        fixture.getAsyncIndexUpdate("async").run();
+        createCustomIndex(TEST_INDEX_PATH, 4, 2, false);
+        PurgeOldIndexVersionCommand command = new 
PurgeOldIndexVersionCommand();
+        
PurgeOldVersionUtils.recursiveDeleteHiddenChildNodes(fixture.getNodeStore(), 
"/oak:index/fooIndex-4-custom-2");
+        
PurgeOldVersionUtils.recursiveDeleteHiddenChildNodes(fixture.getNodeStore(), 
"/oak:index/fooIndex-3-custom-2");
+        fixture.close();
+
+        File storeDir = fixture.getDir();
+        String[] args = {
+                storeDir.getAbsolutePath(),
+                "--read-write",
+                "--threshold=1"
+        };
+
+        command.execute(args);
+        fixture = new RepositoryFixture(storeDir);
+        fixture.close();
+        Assert.assertFalse("Index:" + "fooIndex-2" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-2").exists());
+        Assert.assertFalse("Index:" + "fooIndex-2-custom-1" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-2-custom-1").exists());
+        Assert.assertFalse("Index:" + "fooIndex-3" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-3").exists());
+        Assert.assertFalse("Index:" + "fooIndex-3-custom-1" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-3-custom-1").exists());
+        Assert.assertFalse("Index:" + "fooIndex-3-custom-2" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-3-custom-2").exists());
+        Assert.assertFalse("Index:" + "fooIndex" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex").exists());
+        
Assert.assertEquals(fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-4").getProperty("type").getValue(Type.STRING),
 "disabled");
+        
Assert.assertFalse(isHiddenChildNodePresent(fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-4")));
+        Assert.assertTrue("Index:" + "fooIndex-4-custom-1" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-4-custom-1").exists());
+        Assert.assertTrue("Index:" + "fooIndex-4-custom-2" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-4-custom-2").exists());
+    }
+
+    @Test
+    public void donotDeleteDisabledIndexes() throws Exception {
+        createTestData(false);
+        createCustomIndex(TEST_INDEX_PATH, 2, 1, false);
+        createCustomIndex(TEST_INDEX_PATH, 3, 0, false);
+        createCustomIndex(TEST_INDEX_PATH, 3, 1, false);
+        createCustomIndex(TEST_INDEX_PATH, 3, 2, false);
+        createCustomIndex(TEST_INDEX_PATH, 4, 0, false);
+        createCustomIndex(TEST_INDEX_PATH, 4, 1, false);
+        createCustomIndex(TEST_INDEX_PATH, 4, 2, false);
+        NodeStore store = fixture.getNodeStore();
+        NodeBuilder rootBuilder = store.getRoot().builder();
+        NodeBuilder nodeBuilder = rootBuilder.getChildNode("oak:index")
+                .getChildNode("fooIndex-4-custom-1").setProperty("type", 
"disabled");
+        store.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        fixture.close();
+        PurgeOldIndexVersionCommand command = new 
PurgeOldIndexVersionCommand();
+        File storeDir = fixture.getDir();
+        String[] args = {
+                storeDir.getAbsolutePath(),
+                "--read-write",
+                "--threshold=1",
+                "--index-paths=/oak:index/fooIndex,/oak:index"
+        };
+        command.execute(args);
+        fixture = new RepositoryFixture(storeDir);
+        fixture.close();
+
+        Assert.assertFalse("Index:" + "fooIndex-2" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-2").exists());
+        Assert.assertFalse("Index:" + "fooIndex-2-custom-1" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-2-custom-1").exists());
+        Assert.assertFalse("Index:" + "fooIndex-3" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-3").exists());
+        Assert.assertFalse("Index:" + "fooIndex-3-custom-1" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-3-custom-1").exists());
+        Assert.assertFalse("Index:" + "fooIndex-3-custom-2" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-3-custom-2").exists());
+        Assert.assertFalse("Index:" + "fooIndex" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex").exists());
+        
Assert.assertEquals(fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-4").getProperty("type").getValue(Type.STRING),
 "disabled");
+        
Assert.assertFalse(isHiddenChildNodePresent(fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-4")));
+        Assert.assertTrue("Index:" + "fooIndex-4-custom-1" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-4-custom-1").exists());
+        Assert.assertTrue("Index:" + "fooIndex-4-custom-2" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-4-custom-2").exists());
+    }
+
+    @Test
+    public void onlyDeleteVersionIndexesMentionedUnderIndexPaths() throws 
Exception {
+        createTestData(false);
+        createCustomIndex(TEST_INDEX_PATH, 2, 1, false);
+        createCustomIndex(TEST_INDEX_PATH, 3, 0, false);
+        createCustomIndex(TEST_INDEX_PATH, 3, 1, false);
+        createCustomIndex(TEST_INDEX_PATH, 3, 2, false);
+        createCustomIndex(TEST_INDEX_PATH, 4, 0, false);
+        createCustomIndex(TEST_INDEX_PATH, 4, 1, false);
+        createCustomIndex(TEST_INDEX_PATH, 4, 2, false);
+        createCustomIndex(FOO1_INDEX_PATH, 2, 1, false);
+        createCustomIndex(FOO1_INDEX_PATH, 2, 2, false);
+        createCustomIndex(FOO1_INDEX_PATH, 3, 0, false);
+        createCustomIndex(FOO1_INDEX_PATH, 3, 1, false);
+        createCustomIndex(FOO1_INDEX_PATH, 3, 2, false);
+
+        fixture.getAsyncIndexUpdate("async").run();
+        fixture.close();
+        PurgeOldIndexVersionCommand command = new 
PurgeOldIndexVersionCommand();
+        File storeDir = fixture.getDir();
+        String[] args = {
+                storeDir.getAbsolutePath(),
+                "--read-write",
+                "--threshold=1",
+                "--index-paths=/oak:index/fooIndex"
+        };
+        command.execute(args);
+        fixture = new RepositoryFixture(storeDir);
+        fixture.close();
+
+        Assert.assertFalse("Index:" + "fooIndex-2" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-2").exists());
+        Assert.assertFalse("Index:" + "fooIndex-2-custom-1" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-2-custom-1").exists());
+        Assert.assertFalse("Index:" + "fooIndex-3" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-3").exists());
+        Assert.assertFalse("Index:" + "fooIndex-3-custom-1" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-3-custom-1").exists());
+        Assert.assertFalse("Index:" + "fooIndex-3-custom-2" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-3-custom-2").exists());
+        Assert.assertFalse("Index:" + "fooIndex" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex").exists());
+        
Assert.assertEquals(fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-4").getProperty("type").getValue(Type.STRING),
 "disabled");
+        
Assert.assertFalse(isHiddenChildNodePresent(fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-4")));
+        Assert.assertFalse("Index:" + "fooIndex-4-custom-1" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-4-custom-1").exists());
+        Assert.assertTrue("Index:" + "fooIndex-4-custom-2" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex-4-custom-2").exists());
+
+        Assert.assertTrue("Index:" + "fooIndex1-3-custom-1" + " deleted", 
fixture.getNodeStore().getRoot().getChildNode("oak:index").getChildNode("fooIndex1-3-custom-1").exists());
+    }
+
+    @Test
+    public void donotDeleteNonReadWriteMode() throws Exception {
+        LogCustomizer custom = LogCustomizer
+                .forLogger(
+                        
"org.apache.jackrabbit.oak.indexversion.PurgeOldIndexVersion")
+                .enable(Level.INFO).create();
+        try {
+            custom.starting();
+            createTestData(false);
+            createCustomIndex(TEST_INDEX_PATH, 2, 1, false);
+            createCustomIndex(TEST_INDEX_PATH, 3, 0, false);
+            createCustomIndex(TEST_INDEX_PATH, 3, 1, false);
+            createCustomIndex(TEST_INDEX_PATH, 3, 2, false);
+            createCustomIndex(TEST_INDEX_PATH, 4, 0, false);
+            createCustomIndex(TEST_INDEX_PATH, 4, 1, false);
+            createCustomIndex(TEST_INDEX_PATH, 4, 2, false);
+            fixture.getAsyncIndexUpdate("async").run();
+            fixture.close();
+            PurgeOldIndexVersionCommand command = new 
PurgeOldIndexVersionCommand();
+
+            File storeDir = fixture.getDir();
+            String[] args = {
+                    storeDir.getAbsolutePath()
+            };
+
+            command.execute(args);
+            fixture = new RepositoryFixture(storeDir);
+            fixture.close();
+            List<String> logs = custom.getLogs();
+            assertThat("repository is opened in read only mode ", 
logs.toString(),
+                    containsString("Repository is opened in read-only mode"));
+        } finally {
+            custom.finished();
+        }
+    }
+
+    private boolean isHiddenChildNodePresent(NodeState nodeState) {
+        boolean isHiddenChildNodePresent = false;
+        for (String childNodeName : nodeState.getChildNodeNames()) {
+            if (childNodeName.charAt(0) == ':') {
+                isHiddenChildNodePresent = true;
+            }
+        }
+        return isHiddenChildNodePresent;
+    }
+
+}

Propchange: 
jackrabbit/oak/trunk/oak-run/src/test/java/org/apache/jackrabbit/oak/indexversion/PurgeOldIndexVersionTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/IndexDefinition.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/IndexDefinition.java?rev=1873532&r1=1873531&r2=1873532&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/IndexDefinition.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/IndexDefinition.java
 Mon Feb  3 11:55:04 2020
@@ -143,6 +143,9 @@ public class IndexDefinition implements
      */
     public static final String STATUS_LAST_UPDATED = "lastUpdated";
 
+    public static final String CREATION_TIMESTAMP = "creationTimestamp";
+    public static final String REINDEX_COMPLETION_TIMESTAMP = 
"reindexCompletionTimestamp";
+
     /**
      * Meta property which provides the unique id
      */

Modified: 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/ReindexOperations.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/ReindexOperations.java?rev=1873532&r1=1873531&r2=1873532&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/ReindexOperations.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/ReindexOperations.java
 Mon Feb  3 11:55:04 2020
@@ -22,14 +22,19 @@ package org.apache.jackrabbit.oak.plugin
 import org.apache.jackrabbit.oak.plugins.index.search.util.NodeStateCloner;
 import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
 import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import static 
org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition.INDEX_DEFINITION_NODE;
+import static 
org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition.REINDEX_COMPLETION_TIMESTAMP;
+import static 
org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition.STATUS_NODE;
 import static 
org.apache.jackrabbit.oak.plugins.index.search.spi.editor.FulltextIndexEditorContext.configureUniqueId;
 
 /**
  * Reindexing operations
  */
 public class ReindexOperations {
+    private static final Logger LOG = 
LoggerFactory.getLogger(ReindexOperations.class);
     private final NodeState root;
     private final NodeBuilder definitionBuilder;
     private final String indexPath;
@@ -58,9 +63,12 @@ public class ReindexOperations {
         NodeState defnState = useStateFromBuilder ? 
definitionBuilder.getNodeState() : definitionBuilder.getBaseState();
         if (!IndexDefinition.isDisableStoredIndexDefinition()) {
             definitionBuilder.setChildNode(INDEX_DEFINITION_NODE, 
NodeStateCloner.cloneVisibleState(defnState));
+            if (definitionBuilder.getChildNode(STATUS_NODE).exists()) {
+                
definitionBuilder.getChildNode(STATUS_NODE).removeProperty(REINDEX_COMPLETION_TIMESTAMP);
+                LOG.info("{} property removed for index at {}", 
REINDEX_COMPLETION_TIMESTAMP, this.indexPath);
+            }
         }
         String uid = configureUniqueId(definitionBuilder);
-
         //Refresh the index definition based on update builder state
         return indexDefBuilder
                 .root(root)

Modified: 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/editor/FulltextIndexEditorContext.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/editor/FulltextIndexEditorContext.java?rev=1873532&r1=1873531&r2=1873532&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/editor/FulltextIndexEditorContext.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/editor/FulltextIndexEditorContext.java
 Mon Feb  3 11:55:04 2020
@@ -48,7 +48,10 @@ import org.slf4j.LoggerFactory;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static 
org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.PROP_RANDOM_SEED;
 import static 
org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.PROP_REFRESH_DEFN;
+import static 
org.apache.jackrabbit.oak.plugins.index.search.FulltextIndexConstants.REGEX_ALL_PROPS;
 import static 
org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition.INDEX_DEFINITION_NODE;
+import static 
org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition.REINDEX_COMPLETION_TIMESTAMP;
+import static 
org.apache.jackrabbit.oak.plugins.index.search.IndexDefinition.STATUS_NODE;
 
 /**
  *
@@ -164,6 +167,11 @@ public abstract class FulltextIndexEdito
       NodeBuilder status = 
definitionBuilder.child(IndexDefinition.STATUS_NODE);
       status.setProperty(IndexDefinition.STATUS_LAST_UPDATED, 
getUpdatedTime(currentTime), Type.DATE);
       status.setProperty("indexedNodes", indexedNodes);
+      if (!IndexDefinition.isDisableStoredIndexDefinition() && reindex) {
+        NodeBuilder indexDefinition = definitionBuilder.child(STATUS_NODE);
+        
indexDefinition.setProperty(IndexDefinition.REINDEX_COMPLETION_TIMESTAMP, 
ISO8601.format(currentTime), Type.DATE);
+        log.info(IndexDefinition.REINDEX_COMPLETION_TIMESTAMP + " set to 
current time for index:" + definition.getIndexPath());
+      }
 
       PERF_LOGGER.end(start, -1, "Overall Closed IndexWriter for directory 
{}", definition);
 
@@ -267,14 +275,20 @@ public abstract class FulltextIndexEdito
           definition.removeProperty(PROP_REFRESH_DEFN);
           NodeState clonedState = NodeStateCloner.cloneVisibleState(defnState);
           definition.setChildNode(INDEX_DEFINITION_NODE, clonedState);
+          definition.getChildNode(INDEX_DEFINITION_NODE)
+                  .setProperty(IndexDefinition.CREATION_TIMESTAMP, 
ISO8601.format(Calendar.getInstance()), Type.DATE);
           log.info("Refreshed the index definition for [{}]", 
indexingContext.getIndexPath());
+          log.info("IndexDefinition creation timestamp updated for [{}]", 
indexingContext.getIndexPath());
           if (log.isDebugEnabled()) {
             log.debug("Updated index definition is {}", 
NodeStateUtils.toString(clonedState));
           }
         } else if (!definition.hasChildNode(INDEX_DEFINITION_NODE)) {
           definition.setChildNode(INDEX_DEFINITION_NODE, 
NodeStateCloner.cloneVisibleState(defnState));
+          definition.getChildNode(INDEX_DEFINITION_NODE)
+                  .setProperty(IndexDefinition.CREATION_TIMESTAMP, 
ISO8601.format(Calendar.getInstance()), Type.DATE);
           log.info("Stored the cloned index definition for [{}]. Changes in 
index definition would now only be " +
                   "effective post reindexing", indexingContext.getIndexPath());
+          log.info("IndexDefinition creation timestamp added for [{}]", 
indexingContext.getIndexPath());
         } else {
           // This is neither reindex nor refresh. So, let's update cloned def 
with random seed
           // if it doesn't match what's there in main definition

Modified: 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/IndexName.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/IndexName.java?rev=1873532&r1=1873531&r2=1873532&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/IndexName.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-search/src/main/java/org/apache/jackrabbit/oak/plugins/index/search/spi/query/IndexName.java
 Mon Feb  3 11:55:04 2020
@@ -40,7 +40,7 @@ import org.slf4j.LoggerFactory;
  * If the node name doesn't contain version numbers / dashes, then version 0 is
  * assumed (for both the product version number and customer version number).
  */
-class IndexName implements Comparable<IndexName> {
+public class IndexName implements Comparable<IndexName> {
 
     private final static Logger LOG = LoggerFactory.getLogger(IndexName.class);
     
@@ -193,5 +193,29 @@ class IndexName implements Comparable<In
         }
         return false;
     }
+
+    public String getNodeName() {
+        return nodeName;
+    }
+
+    public int getCustomerVersion() {
+        return customerVersion;
+    }
+
+    public int getProductVersion() {
+        return productVersion;
+    }
+
+    public String getBaseName() {
+        return baseName;
+    }
+
+    public boolean isVersioned() {
+        return isVersioned;
+    }
+
+    public boolean isLegal() {
+        return isLegal;
+    }
     
 }
\ No newline at end of file


Reply via email to