OAK-9325 Tool to compare and manually merge index definitions
git-svn-id: https://svn.apache.org/repos/asf/jackrabbit/oak/trunk@1886381 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/oak-run/src/main/java/org/apache/jackrabbit/oak/index/IndexCommand.java b/oak-run/src/main/java/org/apache/jackrabbit/oak/index/IndexCommand.java
index 04881e9..8689223 100644
--- a/oak-run/src/main/java/org/apache/jackrabbit/oak/index/IndexCommand.java
+++ b/oak-run/src/main/java/org/apache/jackrabbit/oak/index/IndexCommand.java
@@ -22,10 +22,8 @@
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
-import java.lang.management.RuntimeMXBean;
import java.nio.file.Path;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Calendar;
import java.util.LinkedHashSet;
import java.util.List;
diff --git a/oak-run/src/main/java/org/apache/jackrabbit/oak/index/IndexConsistencyCheckPrinter.java b/oak-run/src/main/java/org/apache/jackrabbit/oak/index/IndexConsistencyCheckPrinter.java
index 4e91236..9e87d37 100644
--- a/oak-run/src/main/java/org/apache/jackrabbit/oak/index/IndexConsistencyCheckPrinter.java
+++ b/oak-run/src/main/java/org/apache/jackrabbit/oak/index/IndexConsistencyCheckPrinter.java
@@ -19,7 +19,6 @@
package org.apache.jackrabbit.oak.index;
-import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.ArrayList;
diff --git a/oak-run/src/main/java/org/apache/jackrabbit/oak/index/IndexDefinitionUpdater.java b/oak-run/src/main/java/org/apache/jackrabbit/oak/index/IndexDefinitionUpdater.java
index bf380cd..c4d8e70 100644
--- a/oak-run/src/main/java/org/apache/jackrabbit/oak/index/IndexDefinitionUpdater.java
+++ b/oak-run/src/main/java/org/apache/jackrabbit/oak/index/IndexDefinitionUpdater.java
@@ -77,7 +77,7 @@
Preconditions.checkArgument(optionSet.has("initializer"), "initializer class must be provided");
String initializerClassName = optionSet.valueOf("initializer").toString();
- Class repoInitClazz = Class.forName(initializerClassName);
+ Class<?> repoInitClazz = Class.forName(initializerClassName);
Object obj = repoInitClazz.newInstance();
Preconditions.checkArgument(obj instanceof RepositoryInitializer, repoInitClazz + " is not a RepositoryInitializer");
diff --git a/oak-run/src/main/java/org/apache/jackrabbit/oak/index/OutOfBandIndexer.java b/oak-run/src/main/java/org/apache/jackrabbit/oak/index/OutOfBandIndexer.java
index 708bf45..fbae7ba 100644
--- a/oak-run/src/main/java/org/apache/jackrabbit/oak/index/OutOfBandIndexer.java
+++ b/oak-run/src/main/java/org/apache/jackrabbit/oak/index/OutOfBandIndexer.java
@@ -37,7 +37,6 @@
import org.apache.jackrabbit.oak.plugins.index.lucene.directory.FSDirectoryFactory;
import org.apache.jackrabbit.oak.plugins.index.progress.MetricRateEstimator;
import org.apache.jackrabbit.oak.plugins.index.progress.NodeCounterMBeanEstimator;
-import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider;
import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
import org.apache.jackrabbit.oak.plugins.metric.MetricStatisticsProvider;
import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
@@ -55,7 +54,7 @@
import static java.util.Arrays.asList;
public class OutOfBandIndexer implements Closeable, IndexUpdateCallback, NodeTraversalCallback {
- private final Logger log = LoggerFactory.getLogger(getClass());
+ private final Logger LOG = LoggerFactory.getLogger(getClass());
/**
* Index lane name which is used for indexing
*/
diff --git a/oak-run/src/main/java/org/apache/jackrabbit/oak/index/merge/IndexDefMergerUtils.java b/oak-run/src/main/java/org/apache/jackrabbit/oak/index/merge/IndexDefMergerUtils.java
index b530357..71d7719 100644
--- a/oak-run/src/main/java/org/apache/jackrabbit/oak/index/merge/IndexDefMergerUtils.java
+++ b/oak-run/src/main/java/org/apache/jackrabbit/oak/index/merge/IndexDefMergerUtils.java
@@ -30,6 +30,7 @@
import java.util.stream.Collectors;
import org.apache.jackrabbit.oak.commons.json.JsonObject;
+import org.apache.jackrabbit.oak.commons.json.JsopBuilder;
import org.apache.jackrabbit.oak.plugins.index.search.spi.query.IndexName;
/**
@@ -39,21 +40,30 @@
private static HashSet<String> IGNORE_LEVEL_0 = new HashSet<>(Arrays.asList(
"reindex", "refresh", "seed", "reindexCount"));
+ private static HashSet<String> USE_PRODUCT_PROPERTY = new HashSet<>(Arrays.asList(
+ "jcr:created", "jcr:lastModified", "jcr:uuid"));
+ private static HashSet<String> USE_PRODUCT_CHILD_LEVEL_0 = new HashSet<>(Arrays.asList(
+ "tika"));
/**
* Merge index definition changes.
*
+ * @param ancestorName the name of the node of the ancestor index (e.g. /oak:index/lucene-1)
* @param ancestor the common ancestor (the old product index, e.g. lucene)
- * @param custom the latest customized version (e.g. lucene-custom-1)
+ * @param customName the name of the node of the customized index (e.g. /oak:index/lucene-1-custom-1)
+ * @param custom the latest customized version (e.g. lucene-1-custom-1)
* @param product the latest product index (e.g. lucene-2)
* @return the merged index definition (e.g. lucene-2-custom-1)
*/
- public static JsonObject merge(JsonObject ancestor, JsonObject custom, JsonObject product) {
+ public static JsonObject merge(String ancestorName, JsonObject ancestor, String customName, JsonObject custom, JsonObject product) {
ArrayList<String> conflicts = new ArrayList<>();
JsonObject merged = merge(0, ancestor, custom, product, conflicts);
if (!conflicts.isEmpty()) {
throw new UnsupportedOperationException("Conflicts detected: " + conflicts);
}
+ merged.getProperties().put("merges", "[" +
+ JsopBuilder.encode(ancestorName) + ", " +
+ JsopBuilder.encode(customName) + "]");
return merged;
}
@@ -95,9 +105,10 @@
}
}
LinkedHashMap<String, Boolean> children = new LinkedHashMap<>();
+ // first the (new) product index - to ensure the order of children matches the new product index
+ addAllChildren(product, children);
addAllChildren(ancestor, children);
addAllChildren(custom, children);
- addAllChildren(product, children);
for (String k : children.keySet()) {
if (k.startsWith(":")) {
// ignore hidden nodes
@@ -126,6 +137,9 @@
private static String mergeProperty(String property, JsonObject ancestor, JsonObject custom, JsonObject product,
ArrayList<String> conflicts) {
+ if (USE_PRODUCT_PROPERTY.contains(property)) {
+ return product.getProperties().get(property);
+ }
String ap = ancestor.getProperties().get(property);
String cp = custom.getProperties().get(property);
String pp = product.getProperties().get(property);
@@ -145,6 +159,9 @@
JsonObject a = ancestor.getChildren().get(child);
JsonObject c = custom.getChildren().get(child);
JsonObject p = product.getChildren().get(child);
+ if (level == 0 && USE_PRODUCT_CHILD_LEVEL_0.contains(child)) {
+ return p;
+ }
if (isSameJson(a, p) || isSameJson(c, p)) {
return c;
} else if (isSameJson(a, c)) {
@@ -200,8 +217,15 @@
JsonObject latestCustomized = allIndexes.getChildren().get(latest.getNodeName());
JsonObject latestAncestor = allIndexes.getChildren().get(ancestor.getNodeName());
JsonObject newProduct = newIndexes.getChildren().get(n.getNodeName());
- JsonObject merged = merge(latestAncestor, latestCustomized, newProduct);
- mergedMap.put(n.nextCustomizedName(), merged);
+ try {
+ JsonObject merged = merge(
+ ancestor.getNodeName(), latestAncestor,
+ latest.getNodeName(), latestCustomized,
+ newProduct);
+ mergedMap.put(n.nextCustomizedName(), merged);
+ } catch (UnsupportedOperationException e) {
+ throw new UnsupportedOperationException("Index: " + n.getNodeName() + ": " + e.getMessage(), e);
+ }
}
}
}
diff --git a/oak-run/src/main/java/org/apache/jackrabbit/oak/index/merge/IndexDiff.java b/oak-run/src/main/java/org/apache/jackrabbit/oak/index/merge/IndexDiff.java
new file mode 100644
index 0000000..7b4a0b3
--- /dev/null
+++ b/oak-run/src/main/java/org/apache/jackrabbit/oak/index/merge/IndexDiff.java
@@ -0,0 +1,532 @@
+/*
+ * 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.index.merge;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.commons.json.JsonObject;
+import org.apache.jackrabbit.oak.commons.json.JsopBuilder;
+import org.apache.jackrabbit.oak.plugins.index.search.spi.query.IndexName;
+
+/**
+ * The index diff tools allows to compare and merge indexes
+ */
+public class IndexDiff {
+
+ private static final String OAK_INDEX = "/oak:index/";
+
+ static JsonObject extract(String extractFile, String indexName) {
+ JsonObject indexDefs = parseIndexDefinitions(extractFile);
+ JsonObject index = indexDefs.getChildren().get(indexName);
+ removeUninterestingIndexProperties(indexDefs);
+ simplify(index);
+ return index;
+ }
+
+ static void extractAll(String extractFile, String extractTargetDirectory) {
+ new File(extractTargetDirectory).mkdirs();
+ JsonObject indexDefs = parseIndexDefinitions(extractFile);
+ removeUninterestingIndexProperties(indexDefs);
+ sortPropertiesByName(indexDefs);
+ for (String child : indexDefs.getChildren().keySet()) {
+ JsonObject index = indexDefs.getChildren().get(child);
+ simplify(index);
+ String fileName = child.replaceAll(OAK_INDEX, "");
+ fileName = fileName.replace(':', '-');
+ Path p = Paths.get(extractTargetDirectory, fileName + ".json");
+ writeFile(p, index);
+ }
+ }
+
+ private static void writeFile(Path p, JsonObject json) {
+ try {
+ Files.write(p, json.toString().getBytes());
+ } catch (IOException e) {
+ throw new IllegalStateException("Error writing file: " + p, e);
+ }
+ }
+
+ static JsonObject collectCustomizations(String directory) {
+ Path indexPath = Paths.get(directory);
+ JsonObject target = new JsonObject(true);
+ collectCustomizationsInDirectory(indexPath, target);
+ return target;
+ }
+
+ static JsonObject mergeIndexes(String directory, String newIndexFile) {
+ JsonObject newIndex = null;
+ if (newIndexFile != null && !newIndexFile.isEmpty()) {
+ newIndex = parseIndexDefinitions(newIndexFile);
+ }
+ Path indexPath = Paths.get(directory);
+ JsonObject target = new JsonObject(true);
+ mergeIndexesInDirectory(indexPath, newIndex, target);
+ for(String key : target.getChildren().keySet()) {
+ JsonObject c = target.getChildren().get(key);
+ removeUninterestingIndexProperties(c);
+ sortPropertiesByName(c);
+ simplify(c);
+ target.getChildren().put(key, c);
+ }
+ return target;
+ }
+
+ static void mergeIndex(String oldIndexFile, String newIndexFile, String targetDirectory) {
+ JsonObject oldIndexes = parseIndexDefinitions(oldIndexFile);
+ removeUninterestingIndexProperties(oldIndexes);
+ sortPropertiesByName(oldIndexes);
+ simplify(oldIndexes);
+
+ JsonObject newIndexes = parseIndexDefinitions(newIndexFile);
+ removeUninterestingIndexProperties(newIndexes);
+ sortPropertiesByName(newIndexes);
+ simplify(newIndexes);
+
+ List<IndexName> newNames = newIndexes.getChildren().keySet().stream().map(s -> IndexName.parse(s))
+ .collect(Collectors.toList());
+ List<IndexName> allNames = oldIndexes.getChildren().keySet().stream().map(s -> IndexName.parse(s))
+ .collect(Collectors.toList());
+
+ for (IndexName n : newNames) {
+ if (n.getCustomerVersion() == 0) {
+ IndexName latest = n.getLatestCustomized(allNames);
+ IndexName ancestor = n.getLatestProduct(allNames);
+ if (latest != null && ancestor != null) {
+ if (n.compareTo(latest) <= 0 || n.compareTo(ancestor) <= 0) {
+ // ignore older versions of indexes
+ continue;
+ }
+ JsonObject latestCustomized = oldIndexes.getChildren().get(latest.getNodeName());
+ String fileName = PathUtils.getName(latest.getNodeName());
+ writeFile(Paths.get(targetDirectory, fileName + ".json"),
+ addParent(latest.getNodeName(), latestCustomized));
+
+ JsonObject latestAncestor = oldIndexes.getChildren().get(ancestor.getNodeName());
+ fileName = PathUtils.getName(ancestor.getNodeName());
+ writeFile(Paths.get(targetDirectory, fileName + ".json"),
+ addParent(ancestor.getNodeName(), latestAncestor));
+
+ JsonObject newProduct = newIndexes.getChildren().get(n.getNodeName());
+ fileName = PathUtils.getName(n.getNodeName());
+ writeFile(Paths.get(targetDirectory, fileName + ".json"),
+ addParent(n.getNodeName(), newProduct));
+
+ JsonObject oldCustomizations = new JsonObject(true);
+ compareIndexes("", latestAncestor, latestCustomized, oldCustomizations);
+ // the old product index might be disabled
+ oldCustomizations.getChildren().remove("type");
+ writeFile(Paths.get(targetDirectory, "oldCustomizations.json"),
+ oldCustomizations);
+
+ JsonObject productChanges = new JsonObject(true);
+ compareIndexes("", latestAncestor, newProduct, productChanges);
+ writeFile(Paths.get(targetDirectory, "productChanges.json"),
+ productChanges);
+
+ try {
+ JsonObject merged = IndexDefMergerUtils.merge(
+ ancestor.getNodeName(), latestAncestor,
+ latest.getNodeName(), latestCustomized,
+ newProduct);
+ fileName = PathUtils.getName(n.nextCustomizedName());
+ writeFile(Paths.get(targetDirectory, fileName + ".json"),
+ addParent(n.nextCustomizedName(), merged));
+
+ JsonObject newCustomizations = new JsonObject(true);
+ compareIndexes("", newProduct, merged, newCustomizations);
+ writeFile(Paths.get(targetDirectory, "newCustomizations.json"),
+ newCustomizations);
+
+ JsonObject changes = new JsonObject(true);
+ compareIndexes("", oldCustomizations, newCustomizations, changes);
+ writeFile(Paths.get(targetDirectory, "changes.json"),
+ changes);
+
+ } catch (UnsupportedOperationException e) {
+ throw new UnsupportedOperationException("Index: " + n.getNodeName() + ": " + e.getMessage(), e);
+ }
+ }
+ }
+ }
+ }
+
+ private static JsonObject addParent(String key, JsonObject obj) {
+ JsonObject result = new JsonObject(true);
+ result.getChildren().put(key, obj);
+ return result;
+ }
+
+ static JsonObject compareIndexes(String directory, String index1, String index2) {
+ Path indexPath = Paths.get(directory);
+ JsonObject target = new JsonObject(true);
+ compareIndexesIndexesInDirectory(indexPath, index1, index2, target);
+ return target;
+ }
+
+ private static Stream<Path> indexFiles(Path indexPath) {
+ try {
+ return Files.walk(indexPath).
+ filter(path -> Files.isRegularFile(path)).
+ filter(path -> path.toString().endsWith(".json")).
+ filter(path -> !path.toString().endsWith("allnamespaces.json")).
+ filter(path -> !path.toString().endsWith("-info.json")).
+ filter(path -> !path.toString().endsWith("-stats.json"));
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Error reading from " + indexPath, e);
+ }
+ }
+
+ private static void sortPropertiesByName(JsonObject obj) {
+ ArrayList<String> props = new ArrayList<>(obj.getProperties().keySet());
+ if (!props.isEmpty()) {
+ props.sort(null);
+ for(String key : props) {
+ String value = obj.getProperties().remove(key);
+ obj.getProperties().put(key, value);
+ }
+ }
+ for(String child : obj.getChildren().keySet()) {
+ JsonObject c = obj.getChildren().get(child);
+ sortPropertiesByName(c);
+ }
+ }
+
+ private static void compareIndexesIndexesInDirectory(Path indexPath, String index1, String index2,
+ JsonObject target) {
+ if (Files.isExecutable(indexPath)) {
+ indexFiles(indexPath).forEach(path -> {
+ JsonObject indexDefinitions = IndexDiff.parseIndexDefinitions(path.toString());
+ compareIndexes(indexDefinitions, indexPath.toString(), path.toString(), index1, index2, target);
+ });
+ } else {
+ JsonObject allIndexDefinitions = IndexDiff.parseIndexDefinitions(indexPath.toString());
+ for(String key : allIndexDefinitions.getChildren().keySet()) {
+ JsonObject indexDefinitions = allIndexDefinitions.getChildren().get(key);
+ compareIndexes(indexDefinitions, "", key, index1, index2, target);
+ }
+ }
+ }
+
+ private static void collectCustomizationsInDirectory(Path indexPath, JsonObject target) {
+ indexFiles(indexPath).forEach(path -> {
+ JsonObject indexDefinitions = IndexDiff.parseIndexDefinitions(path.toString());
+ showCustomIndexes(indexDefinitions, indexPath.toString(), path.toString(), target);
+ });
+ }
+
+ private static void mergeIndexesInDirectory(Path indexPath, JsonObject newIndex, JsonObject target) {
+ indexFiles(indexPath).forEach(path -> {
+ JsonObject indexDefinitions = IndexDiff.parseIndexDefinitions(path.toString());
+ mergeIndexes(indexDefinitions, indexPath.toString(), path.toString(), newIndex, target);
+ });
+ }
+
+ private static void mergeIndexes(JsonObject indexeDefinitions, String basePath, String fileName, JsonObject newIndexes, JsonObject target) {
+ JsonObject targetFile = new JsonObject(true);
+ if (newIndexes != null) {
+ for (String newIndexKey : newIndexes.getChildren().keySet()) {
+ if (indexeDefinitions.getChildren().containsKey(newIndexKey)) {
+ targetFile.getProperties().put(newIndexKey, JsopBuilder.encode("WARNING: already exists"));
+ }
+ }
+ } else {
+ newIndexes = new JsonObject(true);
+ }
+ // the superseded indexes of the old repository
+ List<String> supersededKeys = new ArrayList<>(IndexMerge.getSupersededIndexDefs(indexeDefinitions));
+ Collections.sort(supersededKeys);
+
+ // keep only new indexes that are not superseded
+ Map<String, JsonObject> indexMap = indexeDefinitions.getChildren();
+ for (String superseded : supersededKeys) {
+ indexMap.remove(superseded);
+ }
+ Set<String> indexKeys = indexeDefinitions.getChildren().keySet();
+ try {
+ IndexDefMergerUtils.merge(newIndexes, indexeDefinitions);
+ Set<String> newIndexKeys = new HashSet<>(newIndexes.getChildren().keySet());
+ newIndexKeys.removeAll(indexKeys);
+ for (String newIndexKey : newIndexKeys) {
+ JsonObject merged = newIndexes.getChildren().get(newIndexKey);
+ if (merged != null) {
+ targetFile.getChildren().put(newIndexKey, merged);
+ }
+ }
+ } catch (UnsupportedOperationException e) {
+ e.printStackTrace();
+ targetFile.getProperties().put("failed", JsopBuilder.encode(e.toString()));
+ }
+ addIfNotEmpty(basePath, fileName, targetFile, target);
+ }
+
+ private static void addIfNotEmpty(String basePath, String fileName, JsonObject targetFile, JsonObject target) {
+ if (!targetFile.getProperties().isEmpty() || !targetFile.getChildren().isEmpty()) {
+ String f = fileName;
+ if (f.startsWith(basePath)) {
+ f = f.substring(basePath.length());
+ }
+ target.getChildren().put(f, targetFile);
+ }
+ }
+
+ private static void showCustomIndexes(JsonObject indexDefinitions, String basePath, String fileName, JsonObject target) {
+ JsonObject targetFile = new JsonObject(true);
+ processAndRemoveIllegalIndexNames(indexDefinitions, targetFile);
+ removeUninterestingIndex(indexDefinitions);
+ removeUninterestingIndexProperties(indexDefinitions);
+ removeUnusedIndexes(indexDefinitions);
+ for(String k : indexDefinitions.getChildren().keySet()) {
+ if (!k.startsWith(OAK_INDEX)) {
+ targetFile.getProperties().put(k, JsopBuilder.encode("WARNING: Index not under " + OAK_INDEX));
+ continue;
+ }
+ if (!k.contains("-custom-")) {
+ continue;
+ }
+ listNewAndCustomizedIndexes(indexDefinitions, k, targetFile);
+ }
+ addIfNotEmpty(basePath, fileName, targetFile, target);
+ }
+
+ private static void compareIndexes(JsonObject indexDefinitions, String basePath, String fileName, String index1, String index2, JsonObject target) {
+ JsonObject targetFile = new JsonObject(true);
+ JsonObject i1 = indexDefinitions.getChildren().get(index1);
+ JsonObject i2 = indexDefinitions.getChildren().get(index2);
+ if (i1 != null && i2 != null) {
+ compareIndexes("", i1, i2, targetFile);
+ }
+ addIfNotEmpty(basePath, fileName, targetFile, target);
+ }
+
+ private static void listNewAndCustomizedIndexes(JsonObject indexDefinitions, String indexNodeName, JsonObject target) {
+ JsonObject index = indexDefinitions.getChildren().get(indexNodeName);
+ String nodeName = indexNodeName.substring(OAK_INDEX.length());
+ IndexName indexName = IndexName.parse(nodeName);
+ String ootb = indexName.getBaseName();
+ if (indexName.getProductVersion() > 1) {
+ ootb += "-" + indexName.getProductVersion();
+ }
+ simplify(indexDefinitions);
+ JsonObject ootbIndex = indexDefinitions.getChildren().get(OAK_INDEX + ootb);
+ if (ootbIndex != null) {
+ JsonObject targetCustom = new JsonObject(true);
+ targetCustom.getProperties().put("customizes", JsopBuilder.encode(OAK_INDEX + ootb));
+ target.getChildren().put(indexNodeName, targetCustom);
+ compareIndexes("", ootbIndex, index, targetCustom);
+ } else {
+ target.getProperties().put(indexNodeName, JsopBuilder.encode("new"));
+ }
+ }
+
+ private static void processAndRemoveIllegalIndexNames(JsonObject indexDefinitions, JsonObject target) {
+ Set<String> indexes = new HashSet<>(indexDefinitions.getChildren().keySet());
+ for(String k : indexes) {
+ if (!k.startsWith("/oak:index/")) {
+ continue;
+ }
+ String nodeName = k.substring("/oak:index/".length());
+ IndexName indexName = IndexName.parse(nodeName);
+ if (!indexName.isLegal()) {
+ target.getProperties().put(k, JsopBuilder.encode("WARNING: Invalid name"));
+ indexDefinitions.getChildren().remove(k);
+ }
+ }
+ }
+
+ private static void removeUninterestingIndex(JsonObject indexDefinitions) {
+ }
+
+ private static void compareIndexes(String path, JsonObject ootb, JsonObject custom, JsonObject target) {
+ LinkedHashMap<String, Boolean> properties = new LinkedHashMap<>();
+ addAllProperties(ootb, properties);
+ addAllProperties(custom, properties);
+ for (String k : properties.keySet()) {
+ String op = ootb.getProperties().get(k);
+ String cp = custom.getProperties().get(k);
+ if (!Objects.equals(op, cp)) {
+ JsonObject change = new JsonObject(true);
+ if (op != null) {
+ change.getProperties().put("old", op);
+ }
+ if (cp != null) {
+ change.getProperties().put("new", cp);
+ }
+ target.getChildren().put(path + k, change);
+ }
+ }
+ LinkedHashMap<String, Boolean> children = new LinkedHashMap<>();
+ addAllChildren(ootb, children);
+ addAllChildren(custom, children);
+ for (String k : children.keySet()) {
+ JsonObject oc = ootb.getChildren().get(k);
+ JsonObject cc = custom.getChildren().get(k);
+ if (!isSameJson(oc, cc)) {
+ if (oc == null) {
+ target.getProperties().put(path + k, JsopBuilder.encode("added"));
+ } else if (cc == null) {
+ target.getProperties().put(path + k, JsopBuilder.encode("removed"));
+ } else {
+ compareIndexes(path + k + "/", oc, cc, target);
+ }
+ }
+ }
+ compareOrder(path, ootb, custom, target);
+ }
+
+ private static void addAllChildren(JsonObject source, LinkedHashMap<String, Boolean> target) {
+ for(String k : source.getChildren().keySet()) {
+ target.put(k, true);
+ }
+ }
+
+ private static void compareOrder(String path, JsonObject ootb, JsonObject custom, JsonObject target) {
+ // list of entries, sorted by how they appear in the ootb case
+ ArrayList<String> bothSortedByOotb = new ArrayList<>();
+ for(String k : ootb.getChildren().keySet()) {
+ if (custom.getChildren().containsKey(k)) {
+ bothSortedByOotb.add(k);
+ }
+ }
+ // list of entries, sorted by how they appear in the custom case
+ ArrayList<String> bothSortedByCustom = new ArrayList<>();
+ for(String k : custom.getChildren().keySet()) {
+ if (ootb.getChildren().containsKey(k)) {
+ bothSortedByCustom.add(k);
+ }
+ }
+ if (!bothSortedByOotb.toString().equals(bothSortedByCustom.toString())) {
+ JsonObject change = new JsonObject(true);
+ change.getProperties().put("warning", JsopBuilder.encode("WARNING: Order is different"));
+ change.getProperties().put("ootb", JsopBuilder.encode(bothSortedByOotb.toString()));
+ change.getProperties().put("custom", JsopBuilder.encode(bothSortedByCustom.toString()));
+ target.getChildren().put(path + "<order>", change);
+ }
+ }
+
+ private static boolean isSameJson(JsonObject a, JsonObject b) {
+ if (a == null || b == null) {
+ return a == null && b == null;
+ }
+ return a.toString().equals(b.toString());
+ }
+
+ private static void addAllProperties(JsonObject source, LinkedHashMap<String, Boolean> target) {
+ for(String k : source.getProperties().keySet()) {
+ target.put(k, true);
+ }
+ }
+
+ private static void removeUnusedIndexes(JsonObject indexDefinitions) {
+ HashMap<String, IndexName> latest = new HashMap<>();
+ Set<String> indexes = new HashSet<>(indexDefinitions.getChildren().keySet());
+ for(String k : indexes) {
+ if (!k.startsWith("/oak:index/")) {
+ continue;
+ }
+ String nodeName = k.substring("/oak:index/".length());
+ IndexName indexName = IndexName.parse(nodeName);
+ String baseName = indexName.getBaseName();
+ IndexName old = latest.get(baseName);
+ if (old == null) {
+ latest.put(baseName, indexName);
+ } else {
+ if (old.compareTo(indexName) < 0) {
+ if (old.getCustomerVersion() > 0) {
+ indexDefinitions.getChildren().remove("/oak:index/" + old.getNodeName());
+ }
+ latest.put(baseName, indexName);
+ } else {
+ indexDefinitions.getChildren().remove("/oak:index/" + nodeName);
+ }
+ }
+ }
+ }
+
+ private static void removeUninterestingIndexProperties(JsonObject indexDefinitions) {
+ for(String k : indexDefinitions.getChildren().keySet()) {
+ JsonObject indexDef = indexDefinitions.getChildren().get(k);
+ indexDef.getProperties().remove("reindexCount");
+ indexDef.getProperties().remove("reindex");
+ indexDef.getProperties().remove("seed");
+ indexDef.getProperties().remove(":version");
+ }
+ }
+
+ private static void simplify(JsonObject json) {
+ for(String k : json.getChildren().keySet()) {
+ JsonObject child = json.getChildren().get(k);
+ simplify(child);
+
+ // the following properties are not strictly needed for display,
+ // but we keep them to avoid issues with validation
+ // child.getProperties().remove("jcr:created");
+ // child.getProperties().remove("jcr:createdBy");
+ // child.getProperties().remove("jcr:lastModified");
+ // child.getProperties().remove("jcr:lastModifiedBy");
+
+ // the UUID we remove, because duplicate UUIDs are not allowed
+ child.getProperties().remove("jcr:uuid");
+ for(String p : child.getProperties().keySet()) {
+ String v = child.getProperties().get(p);
+ if (v.startsWith("\"str:") || v.startsWith("\"nam:")) {
+ v = "\"" + v.substring(5);
+ child.getProperties().put(p, v);
+ } else if (v.startsWith("[") && v.contains("\"nam:")) {
+ v = v.replaceAll("\\[\"nam:", "\\[\"");
+ v = v.replaceAll(", \"nam:", ", \"");
+ child.getProperties().put(p, v);
+ } else if (v.startsWith("\":blobId:")) {
+ String base64 = v.substring(9, v.length() - 1);
+ String clear = new String(java.util.Base64.getDecoder().decode(base64), StandardCharsets.UTF_8);
+ v = JsopBuilder.encode(clear);
+ // we don't update the property, otherwise importing the index
+ // would change the type
+ // child.getProperties().put(p, v);
+ }
+ }
+ }
+ }
+
+ private static JsonObject parseIndexDefinitions(String jsonFileName) {
+ try {
+ String json = new String(Files.readAllBytes(Paths.get(jsonFileName)));
+ return JsonObject.fromJson(json, true);
+ } catch (Exception e) {
+ throw new IllegalStateException("Error parsing file: " + jsonFileName, e);
+ }
+ }
+
+}
diff --git a/oak-run/src/main/java/org/apache/jackrabbit/oak/index/merge/IndexDiffCommand.java b/oak-run/src/main/java/org/apache/jackrabbit/oak/index/merge/IndexDiffCommand.java
new file mode 100644
index 0000000..cc2b326
--- /dev/null
+++ b/oak-run/src/main/java/org/apache/jackrabbit/oak/index/merge/IndexDiffCommand.java
@@ -0,0 +1,114 @@
+package org.apache.jackrabbit.oak.index.merge;
+
+import static java.util.Arrays.asList;
+
+import org.apache.jackrabbit.oak.run.commons.Command;
+
+import joptsimple.OptionParser;
+import joptsimple.OptionSet;
+import joptsimple.OptionSpec;
+
+public class IndexDiffCommand implements Command {
+
+ @Override
+ public void execute(String... args) throws Exception {
+ OptionParser parser = new OptionParser();
+ OptionSpec<String> customDirOption = parser
+ .accepts("custom", "Path to index definition files (.json). " +
+ "List differences between out-of-the-box and customized indexes").withOptionalArg()
+ .defaultsTo("");
+ OptionSpec<String> mergeDirectoryOption = parser
+ .accepts("merge", "Path to index definition files (.json). " +
+ "This is the existing indexes when merging").withOptionalArg()
+ .defaultsTo("");
+ OptionSpec<String> mergeAddFileOption = parser
+ .accepts("merge-add", "Path to index definition file (.json) to merge. " +
+ "Adds the new out-of-the-box index to each index definition.").withOptionalArg()
+ .defaultsTo("");
+ OptionSpec<String> compareDirectoryOption = parser
+ .accepts("compare", "Path to index definition files (.json). " +
+ "Compare index1 and index2").withOptionalArg()
+ .defaultsTo("");
+ OptionSpec<String> index1Option = parser
+ .accepts("index1", "Index 1").withOptionalArg()
+ .defaultsTo("");
+ OptionSpec<String> index2Option = parser
+ .accepts("index2", "Index 2").withOptionalArg()
+ .defaultsTo("");
+ OptionSpec<String> extractFileOption = parser
+ .accepts("extract", "File from where to extract (an) index definition(s) (.json). " +
+ "This will extract index1, or all indexes").withOptionalArg()
+ .defaultsTo("");
+ OptionSpec<String> targetDirectoryOption = parser
+ .accepts("target", "Target directory where to store results.")
+ .withOptionalArg()
+ .defaultsTo("");
+
+ OptionSpec<?> helpSpec = parser.acceptsAll(
+ asList("h", "?", "help"), "show help").forHelp();
+
+ OptionSet options = parser.parse(args);
+ String customDir = customDirOption.value(options);
+ String mergeDirectory = mergeDirectoryOption.value(options);
+ String compareDirectory = compareDirectoryOption.value(options);
+ String extractFile = extractFileOption.value(options);
+ String targetDirectory = targetDirectoryOption.value(options);
+ if (options.has(helpSpec) || (customDir.isEmpty() &&
+ mergeDirectory.isEmpty() &&
+ compareDirectory.isEmpty() &&
+ extractFile.isEmpty())) {
+ parser.printHelpOn(System.out);
+ return;
+ }
+ if (!customDir.isEmpty()) {
+ System.out.println("Listing differences between out-of-the-box and customized indexes " +
+ "for directory \"" + customDir + "\"");
+ System.out.println(IndexDiff.collectCustomizations(customDir));
+ }
+ if (!mergeDirectory.isEmpty()) {
+ String mergeAdd = mergeAddFileOption.value(options);
+ if (mergeAdd.isEmpty()) {
+ parser.printHelpOn(System.out);
+ return;
+ }
+ if (targetDirectory.isEmpty()) {
+ System.out.println("Merging indexes " +
+ "for directory \"" +
+ mergeDirectory +
+ "\" with \"" + mergeAdd + "\"");
+ System.out.println(IndexDiff.mergeIndexes(mergeDirectory, mergeAdd));
+ } else {
+ System.out.println("Merging index " +
+ "for \"" +
+ mergeDirectory +
+ "\" with \"" + mergeAdd + "\"");
+ IndexDiff.mergeIndex(mergeDirectory, mergeAdd, targetDirectory);
+ }
+ }
+ if (!compareDirectory.isEmpty()) {
+ String index1 = index1Option.value(options);
+ String index2 = index2Option.value(options);
+ if (index1.isEmpty() || index2.isEmpty()) {
+ parser.printHelpOn(System.out);
+ return;
+ }
+ System.out.println("Comparing indexes " + index1 + " and " + index2 +
+ " for directory \"" +
+ compareDirectory + "\"");
+ System.out.println(IndexDiff.compareIndexes(compareDirectory, index1, index2));
+ }
+ if (!extractFile.isEmpty()) {
+ String index1 = index1Option.value(options);
+ if (!index1.isEmpty()) {
+ System.out.println("Extracting index " + index1 + " from \"" +
+ extractFile + "\"");
+ System.out.println(IndexDiff.extract(extractFile, index1));
+ } else if (!targetDirectory.isEmpty()) {
+ System.out.println("Extracting indexes to \"" + targetDirectory + "\" from \"" +
+ extractFile + "\"");
+ IndexDiff.extractAll(extractFile, targetDirectory);
+ }
+ }
+ }
+
+}
diff --git a/oak-run/src/main/java/org/apache/jackrabbit/oak/run/AvailableModes.java b/oak-run/src/main/java/org/apache/jackrabbit/oak/run/AvailableModes.java
index 76d608e..936774c 100644
--- a/oak-run/src/main/java/org/apache/jackrabbit/oak/run/AvailableModes.java
+++ b/oak-run/src/main/java/org/apache/jackrabbit/oak/run/AvailableModes.java
@@ -22,6 +22,7 @@
import com.google.common.collect.ImmutableMap;
import org.apache.jackrabbit.oak.exporter.NodeStateExportCommand;
import org.apache.jackrabbit.oak.index.IndexCommand;
+import org.apache.jackrabbit.oak.index.merge.IndexDiffCommand;
import org.apache.jackrabbit.oak.run.commons.Command;
import org.apache.jackrabbit.oak.run.commons.Modes;
@@ -48,6 +49,7 @@
.put("help", new HelpCommand())
.put("history", new HistoryCommand())
.put("index-merge", new IndexMergeCommand())
+ .put("index-diff", new IndexDiffCommand())
.put(IndexCommand.NAME, new IndexCommand())
.put(IOTraceCommand.NAME, new IOTraceCommand())
.put(JsonIndexCommand.INDEX, new JsonIndexCommand())
diff --git a/oak-run/src/test/java/org/apache/jackrabbit/oak/index/merge/IndexDefMergerTest.java b/oak-run/src/test/java/org/apache/jackrabbit/oak/index/merge/IndexDefMergerTest.java
index b3009e1..fcf74bc 100644
--- a/oak-run/src/test/java/org/apache/jackrabbit/oak/index/merge/IndexDefMergerTest.java
+++ b/oak-run/src/test/java/org/apache/jackrabbit/oak/index/merge/IndexDefMergerTest.java
@@ -68,7 +68,10 @@
JsonObject custom = e.getChildren().get("custom");
JsonObject product = e.getChildren().get("product");
try {
- JsonObject got = IndexDefMergerUtils.merge(ancestor, custom, product);
+ JsonObject got = IndexDefMergerUtils.merge(
+ "/oak:index/test-1", ancestor,
+ "/oak:index/test-1-custom-1", custom,
+ product);
JsonObject expected = e.getChildren().get("expected");
assertEquals(expected.toString(), got.toString());
} catch (UnsupportedOperationException e2) {
diff --git a/oak-run/src/test/resources/org/apache/jackrabbit/oak/index/merge/merge.txt b/oak-run/src/test/resources/org/apache/jackrabbit/oak/index/merge/merge.txt
index 489d515..b8e3618 100644
--- a/oak-run/src/test/resources/org/apache/jackrabbit/oak/index/merge/merge.txt
+++ b/oak-run/src/test/resources/org/apache/jackrabbit/oak/index/merge/merge.txt
@@ -4,13 +4,14 @@
"ancestor": {"value": 1, "a-old": 0},
"custom": {"value": 2, "b-new": 3},
"product": {"value": 1, "c-new": 4},
- "expected": {"value": 2, "b-new": 3, "c-new": 4}
+ "expected": {"value": 2, "b-new": 3, "c-new": 4, "merges": ["/oak:index/test-1", "/oak:index/test-1-custom-1"]
+ }
},
{
"ancestor": {"o": {"a": 1}},
"custom": {"o": {"a": 2, "c": 10}},
"product": {"o": {"a": 1, "p": 20}},
- "expected": {"o": {"a": 2, "c": 10, "p": 20}}
+ "expected": {"merges": ["/oak:index/test-1", "/oak:index/test-1-custom-1"], "o": {"a": 2, "c": 10, "p": 20 }}
},
{
"ancestor": {"o": {"a": 1}},
@@ -70,7 +71,7 @@
, "custom": {
-// changed: facetc
+// changed: facets
"jcr:primaryType": "nam:oak:QueryIndexDefinition",
"compatVersion": 2,
"includedPaths": ["/content/dam"],
@@ -195,7 +196,8 @@
}
}
}
- }
+ },
+ "merges": ["/oak:index/test-1", "/oak:index/test-1-custom-1"]
}
diff --git a/oak-run/src/test/resources/org/apache/jackrabbit/oak/index/merge/mergeIndexes.txt b/oak-run/src/test/resources/org/apache/jackrabbit/oak/index/merge/mergeIndexes.txt
index 94fe60c..20ed5f0 100644
--- a/oak-run/src/test/resources/org/apache/jackrabbit/oak/index/merge/mergeIndexes.txt
+++ b/oak-run/src/test/resources/org/apache/jackrabbit/oak/index/merge/mergeIndexes.txt
@@ -10,7 +10,7 @@
},
"expectedNew": {
"/oak:index/lucene-2": { "a": 1, "b": 11, "d": 100, "z": 2 },
- "/oak:index/lucene-2-custom-1": { "a": 2, "b": 11, "c": 1, "d": 100 }
+ "/oak:index/lucene-2-custom-1": { "a": 2, "b": 11, "c": 1, "d": 100, "merges": ["/oak:index/lucene", "/oak:index/lucene-custom-1"] }
}
}
]