This is an automated email from the ASF dual-hosted git repository.
baedke pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git
The following commit(s) were added to refs/heads/trunk by this push:
new c103ff7093 OAK-11617: Provide oak-run commands to analyze and fix
inconsistencies in the namespace registry (#2362)
c103ff7093 is described below
commit c103ff7093b2dd8455d32a52f74048a8c47abd75
Author: mbaedke <[email protected]>
AuthorDate: Fri Jul 4 13:29:20 2025 +0200
OAK-11617: Provide oak-run commands to analyze and fix inconsistencies in
the namespace registry (#2362)
Fixed performance issue caused by redundant multiple integrity checks.
---------
Co-authored-by: Julian Reschke <[email protected]>
Co-authored-by: Joerg Hoh<[email protected]>
---
.../oak/plugins/name/NamespaceRegistryModel.java | 456 +++++++++++++++++++++
.../plugins/name/ReadOnlyNamespaceRegistry.java | 67 +--
.../oak/plugins/name/NamespaceRegistryTest.java | 293 +++++++++++++
.../apache/jackrabbit/oak/run/AvailableModes.java | 1 +
.../oak/run/NamespaceRegistryCommand.java | 169 ++++++++
.../oak/run/NamespaceRegistryOptions.java | 104 +++++
.../oak/run/NamespaceRegistryCommandTest.java | 107 +++++
7 files changed, 1143 insertions(+), 54 deletions(-)
diff --git
a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NamespaceRegistryModel.java
b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NamespaceRegistryModel.java
new file mode 100755
index 0000000000..99868effdb
--- /dev/null
+++
b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NamespaceRegistryModel.java
@@ -0,0 +1,456 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.oak.plugins.name;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.util.Text;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.commons.collections.IterableUtils;
+import org.apache.jackrabbit.oak.commons.collections.SetUtils;
+import org.apache.jackrabbit.oak.commons.collections.StreamUtils;
+import org.apache.jackrabbit.oak.spi.namespace.NamespaceConstants;
+import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
+import static org.apache.jackrabbit.oak.api.Type.STRING;
+import static org.apache.jackrabbit.oak.api.Type.STRINGS;
+import static
org.apache.jackrabbit.oak.spi.namespace.NamespaceConstants.REP_NAMESPACES;
+
+/**
+ * A model of the namespace registry, containing the mappings from prefixes to
+ * namespace URIs and vice versa.
+ * <p>
+ * The model is created from the namespace registry stored in the repository.
+ * It can be used to check the consistency of the registry, repair it if
+ * possible, and apply the changes back to the repository.
+ */
+public final class NamespaceRegistryModel {
+
+ private final Map<String, String> prefixToNamespaceMap;
+ private final Map<String, String> encodedNamespaceToPrefixMap;
+
+ private final Set<String> registeredPrefixes;
+ private final Set<String> registeredNamespacesEncoded;
+ private final Set<String> mappedPrefixes;
+ private final Set<String> mappedNamespacesEncoded;
+ private final Set<String> mappedToPrefixes;
+ private final Set<String> mappedToNamespacesEncoded;
+ private final Set<String> allPrefixes;
+ private final Set<String> allNamespacesEncoded;
+ private final Set<String> consistentPrefixes;
+ private final Set<String> consistentNamespacesEncoded;
+ private final int registrySize;
+
+ private final Set<String> duplicatePrefixes;
+ private final Set<String> duplicateNamespacesEncoded;
+
+ private final Set<String> danglingPrefixes;
+ private final Set<String> danglingNamespacesEncoded;
+
+ private volatile boolean consistent = false;
+ private volatile boolean fixable = false;
+
+ private NamespaceRegistryModel(
+ List<String> registeredPrefixesList, List<String>
registeredNamespacesEncodedList,
+ // prefixes to URIs
+ Map<String, String> prefixToNamespaceMap,
+ // encoded URIs to prefixes
+ Map<String, String> encodedNamespaceToPrefixMap) {
+ // ignore the empty namespace which is not mapped
+ registeredPrefixes = registeredPrefixesList.stream().filter(s ->
!(Objects.isNull(s) || s.isEmpty())).collect(Collectors.toSet());
+ duplicatePrefixes = findDuplicates(registeredPrefixesList);
+ registeredNamespacesEncoded =
registeredNamespacesEncodedList.stream().filter(s -> !(Objects.isNull(s) ||
s.isEmpty())).collect(Collectors.toSet());
+ duplicateNamespacesEncoded =
findDuplicates(registeredNamespacesEncodedList);
+ this.prefixToNamespaceMap = new HashMap<>(prefixToNamespaceMap);
+ this.encodedNamespaceToPrefixMap = new
HashMap<>(encodedNamespaceToPrefixMap);
+ mappedPrefixes = this.prefixToNamespaceMap.keySet();
+ mappedNamespacesEncoded = this.encodedNamespaceToPrefixMap.keySet();
+ mappedToPrefixes = new HashSet<>(encodedNamespaceToPrefixMap.values());
+ mappedToNamespacesEncoded =
this.prefixToNamespaceMap.values().stream().map(Namespaces::encodeUri).collect(Collectors.toSet());
+ allPrefixes = SetUtils.union(SetUtils.union(registeredPrefixes,
mappedPrefixes), mappedToPrefixes);
+ allNamespacesEncoded =
SetUtils.union(SetUtils.union(registeredNamespacesEncoded,
mappedNamespacesEncoded), mappedToNamespacesEncoded);
+ registrySize = Math.max(allPrefixes.size(),
allNamespacesEncoded.size());
+ consistentPrefixes =
SetUtils.intersection(SetUtils.intersection(registeredPrefixes,
mappedPrefixes), mappedToPrefixes);
+ consistentNamespacesEncoded =
SetUtils.intersection(SetUtils.intersection(registeredNamespacesEncoded,
mappedNamespacesEncoded), mappedToNamespacesEncoded);
+ danglingPrefixes = SetUtils.difference(registeredPrefixes,
SetUtils.union(mappedPrefixes, mappedToPrefixes));
+ danglingNamespacesEncoded =
SetUtils.difference(registeredNamespacesEncoded,
SetUtils.union(mappedNamespacesEncoded, mappedToNamespacesEncoded));
+
+ boolean sizeMatches = duplicatePrefixes.isEmpty()
+ && duplicateNamespacesEncoded.isEmpty()
+ && consistentNamespacesEncoded.size() ==
allNamespacesEncoded.size()
+ && consistentPrefixes.size() == allPrefixes.size();
+ boolean doesRoundtrip = true;
+ if (sizeMatches) {
+ for (String prefix : mappedPrefixes) {
+ String revMapped =
encodedNamespaceToPrefixMap.get(Namespaces.encodeUri(prefixToNamespaceMap.get(prefix)));
+ if (revMapped == null || !revMapped.equals(prefix)) {
+ doesRoundtrip = false;
+ break;
+ }
+ }
+ if (doesRoundtrip) {
+ for (String ns : mappedNamespacesEncoded) {
+ String uri =
prefixToNamespaceMap.get(encodedNamespaceToPrefixMap.get(ns));
+ if (uri == null || !Namespaces.encodeUri(uri).equals(ns)) {
+ doesRoundtrip = false;
+ break;
+ }
+ }
+ }
+ }
+ consistent = sizeMatches && doesRoundtrip;
+ fixable = consistent;
+ if (!consistent && doesRoundtrip) {
+ fixable = registrySize == SetUtils.union(mappedPrefixes,
mappedToPrefixes).size()
+ && registrySize == SetUtils.union(mappedNamespacesEncoded,
mappedToNamespacesEncoded).size();
+ }
+ }
+
+ /**
+ * Creates a new {@link NamespaceRegistryModel} from the namespace registry
+ * stored in the system tree under the given repository {@link Root}.
+ *
+ * @param root the root of the repository
+ * @return a new {@link NamespaceRegistryModel} or {@code null} if the
+ * namespace registry does not exist
+ */
+ public static @Nullable NamespaceRegistryModel create(@NotNull Root root) {
+ Tree rootTree = root.getTree("/");
+ Tree namespaces = rootTree.getChild( JcrConstants.JCR_SYSTEM
).getChild(REP_NAMESPACES);
+ if (namespaces.exists()) {
+ Tree nsdata = namespaces.getChild(NamespaceConstants.REP_NSDATA);
+ Map<String, String> prefixToNamespaceMap = new HashMap<>();
+ Map<String, String> namespaceToPrefixMap = new HashMap<>();
+ for (PropertyState propertyState : namespaces.getProperties()) {
+ String prefix = propertyState.getName();
+ if (!prefix.equals(NodeTypeConstants.JCR_PRIMARYTYPE)) {
+ prefixToNamespaceMap.put(prefix,
propertyState.getValue(STRING));
+ }
+ }
+ for (PropertyState propertyState : nsdata.getProperties()) {
+ String encodedUri = propertyState.getName();
+ switch (encodedUri) {
+ case NamespaceConstants.REP_PREFIXES:
+ case NamespaceConstants.REP_URIS:
+ case NodeTypeConstants.JCR_PRIMARYTYPE:
+ break;
+ default:
+ namespaceToPrefixMap.put(encodedUri,
propertyState.getValue(STRING));
+ }
+ }
+ Iterable<String> uris =
Objects.requireNonNull(nsdata.getProperty(NamespaceConstants.REP_URIS))
+ .getValue(STRINGS);
+ return new NamespaceRegistryModel(
+
Arrays.asList(IterableUtils.toArray(Objects.requireNonNull(nsdata.getProperty(NamespaceConstants.REP_PREFIXES)).getValue(STRINGS),
String.class)),
+
StreamUtils.toStream(uris).map(Namespaces::encodeUri).collect(Collectors.toList()),
+ prefixToNamespaceMap, namespaceToPrefixMap);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Creates a new {@link NamespaceRegistryModel} with the given mappings.
Used by {@see NamespaceRegistryCommand} to
+ * repair a namespace registry that cannot be fixed automatically because
mapping information is missing.
+ *
+ * @param additionalPrefixToUrisMappings a map from prefixes to namespace
URIs
+ * @return a new {@link NamespaceRegistryModel}
+ */
+ public NamespaceRegistryModel setMappings(@NotNull Map<String, String>
additionalPrefixToUrisMappings) {
+ List<String> newRegisteredPrefixesList = new
ArrayList<>(registeredPrefixes);
+ HashMap<String, String> newPrefixToNamespaceMap = new
HashMap<>(prefixToNamespaceMap);
+ List<String> newRegisteredNamespacesEncodedList = new
ArrayList<>(registeredNamespacesEncoded);
+ HashMap<String, String> newEncodedNamespaceToPrefixMap = new
HashMap<>(encodedNamespaceToPrefixMap);
+ for (Map.Entry<String, String> entry :
additionalPrefixToUrisMappings.entrySet()) {
+ String prefix = entry.getKey();
+ String uri = entry.getValue();
+ String encodedUri = Namespaces.encodeUri(uri);
+
+ if (!newRegisteredPrefixesList.contains(prefix)) {
+ newRegisteredPrefixesList.add(prefix);
+ }
+ if (!newRegisteredNamespacesEncodedList.contains(encodedUri)) {
+ newRegisteredNamespacesEncodedList.add(encodedUri);
+ }
+ String previousUri = newPrefixToNamespaceMap.get(prefix);
+ newPrefixToNamespaceMap.put(prefix, uri);
+ if (previousUri != null) {
+ String previousEncodedUri = Namespaces.encodeUri(previousUri);
+ newRegisteredNamespacesEncodedList.remove(previousEncodedUri);
+ newEncodedNamespaceToPrefixMap.remove(previousEncodedUri);
+ }
+ newEncodedNamespaceToPrefixMap.put(encodedUri, prefix);
+ }
+ return new NamespaceRegistryModel(newRegisteredPrefixesList,
newRegisteredNamespacesEncodedList,
+ newPrefixToNamespaceMap, newEncodedNamespaceToPrefixMap);
+ }
+
+ /** Tries to repair the namespace registry model by fixing the mappings
+ * from prefixes to namespace URIs and vice versa. If the model is not
+ * fixable, it returns the original model.
+ *
+ * @return a new {@link NamespaceRegistryModel} with fixed mappings or the
+ * original model if it cannot be fixed
+ */
+ public NamespaceRegistryModel tryRegistryRepair() {
+ if (fixable) {
+ List<String> fixedRegisteredPrefixesList = new ArrayList<>();
+ HashMap<String, String> fixedPrefixToNamespaceMap = new
HashMap<>();
+ for (String prefix : allPrefixes) {
+ if (mappedPrefixes.contains(prefix)) {
+ fixedRegisteredPrefixesList.add(prefix);
+ fixedPrefixToNamespaceMap.put(prefix,
prefixToNamespaceMap.get(prefix));
+ } else {
+ for (Map.Entry<String, String> entry :
encodedNamespaceToPrefixMap.entrySet()) {
+ if (entry.getValue().equals(prefix)) {
+ fixedRegisteredPrefixesList.add(prefix);
+ fixedPrefixToNamespaceMap.put(prefix,
Text.unescape(entry.getKey()));
+ break;
+ }
+ }
+ }
+ }
+ List<String> fixedRegisteredNamespacesEncodedList = new
ArrayList<>();
+ HashMap<String, String> fixedNamespaceToPrefixMap = new
HashMap<>();
+ for (String encodedNamespace : allNamespacesEncoded) {
+ if (mappedNamespacesEncoded.contains(encodedNamespace)) {
+ fixedRegisteredNamespacesEncodedList.add(encodedNamespace);
+ fixedNamespaceToPrefixMap.put(encodedNamespace,
encodedNamespaceToPrefixMap.get(encodedNamespace));
+ } else {
+ for (Map.Entry<String, String> entry :
prefixToNamespaceMap.entrySet()) {
+ if
(Namespaces.encodeUri(entry.getValue()).equals(encodedNamespace)) {
+
fixedRegisteredNamespacesEncodedList.add(encodedNamespace);
+ fixedNamespaceToPrefixMap.put(encodedNamespace,
entry.getKey());
+ break;
+ }
+ }
+ }
+ }
+ return new NamespaceRegistryModel(fixedRegisteredPrefixesList,
fixedRegisteredNamespacesEncodedList,
+ fixedPrefixToNamespaceMap, fixedNamespaceToPrefixMap);
+ }
+ return this;
+ }
+
+ /**
+ * Applies this namespace registry model to the given repository {@link
Root}.
+ *
+ * @param root the root of the repository
+ * @throws RepositoryException if an error occurs while applying the
changes
+ * @throws CommitFailedException if the commit fails
+ */
+ public void apply(Root root) throws RepositoryException,
CommitFailedException {
+ Tree rootTree = root.getTree("/");
+ Tree namespaces = rootTree.getChild( JcrConstants.JCR_SYSTEM
).getChild(REP_NAMESPACES);
+ Tree nsdata = namespaces.getChild(NamespaceConstants.REP_NSDATA);
+ for (PropertyState propertyState : namespaces.getProperties()) {
+ String name = propertyState.getName();
+ if (!JCR_PRIMARYTYPE.equals(name)) {
+ namespaces.removeProperty(name);
+ }
+ }
+ for (PropertyState propertyState : nsdata.getProperties()) {
+ String name = propertyState.getName();
+ if (!JCR_PRIMARYTYPE.equals(name)) {
+ nsdata.removeProperty(name);
+ }
+ }
+ nsdata.removeProperty(NamespaceConstants.REP_PREFIXES);
+ nsdata.removeProperty(NamespaceConstants.REP_URIS);
+ for (Map.Entry<String, String> entry :
prefixToNamespaceMap.entrySet()) {
+ String prefix = entry.getKey();
+ String uri = entry.getValue();
+ namespaces.setProperty(prefix, uri);
+ }
+ for (Map.Entry<String, String> entry :
encodedNamespaceToPrefixMap.entrySet()) {
+ String encodedUri = entry.getKey();
+ String prefix = entry.getValue();
+ nsdata.setProperty(encodedUri, prefix);
+ }
+ nsdata.setProperty(NamespaceConstants.REP_PREFIXES, mappedPrefixes,
STRINGS);
+ nsdata.setProperty(NamespaceConstants.REP_URIS,
prefixToNamespaceMap.values(), STRINGS);
+ if (!consistent) {
+ throw new IllegalStateException("Final registry consistency check
failed.");
+ }
+ }
+
+ public boolean isConsistent() {
+ return consistent;
+ }
+
+ public boolean isFixable() {
+ return fixable;
+ }
+
+ /** Prefixes that are registered, but not mapped to or from a namespace
uri.
+ * This kind of inconsistency cannot be fixed automatically, because the
namespace uri
+ * corresponding to the prefix is unknown.
+ * Apply the {@link #setMappings(Map)} method to create a new model with
the missing mappings.
+ */
+ public Set<String> getDanglingPrefixes() {
+ return danglingPrefixes;
+ }
+
+ /** Namespace uris that are registered, but not mapped to or from a prefix.
+ * This kind of inconsistency cannot be fixed automatically, because the
prefix
+ * corresponding to the namespace uri is unknown.
+ * Apply the {@link #setMappings(Map)} method to create a new model with
the missing mappings.
+ */
+ public Set<String> getDanglingEncodedNamespaceUris() {
+ return danglingNamespacesEncoded;
+ }
+
+ /**
+ * Broken mappings completed with the missing prefix or namespace uri.
+ */
+ public Map<String, String> getRepairedMappings() {
+ Map<String, String> map = new HashMap<>();
+ Set<String> repairablePrefixes =
SetUtils.difference(SetUtils.difference(allPrefixes, consistentPrefixes),
danglingPrefixes);
+ Set<String> repairableUrisEncoded =
SetUtils.difference(SetUtils.difference(allNamespacesEncoded,
consistentNamespacesEncoded), danglingNamespacesEncoded);
+ for (Map.Entry<String, String> entry :
prefixToNamespaceMap.entrySet()) {
+ String prefix = entry.getKey();
+ String uri = entry.getValue();
+ if (repairablePrefixes.contains(prefix) ||
repairableUrisEncoded.contains(uri)) {
+ map.put(prefix, uri);
+ }
+ }
+ for (Map.Entry<String, String> entry :
encodedNamespaceToPrefixMap.entrySet()) {
+ String prefix = entry.getValue();
+ String uri = entry.getKey();
+ if (repairablePrefixes.contains(prefix) ||
repairableUrisEncoded.contains(uri)) {
+ map.put(prefix, uri);
+ }
+ }
+ return map;
+ }
+
+ private <T> Set<T> findDuplicates(Collection<T> c) {
+ HashSet<T> uniques = new HashSet<>();
+ return c.stream().filter(t ->
!uniques.add(t)).collect(Collectors.toSet());
+ }
+
+ /**
+ * Write a human-readable analysis of the namespace registry model to
System.out.
+ */
+ public void dump() throws IOException {
+ dump(System.out);
+ }
+
+ /**
+ * Write a human-readable analysis of the namespace registry model to the
+ * given {@link OutputStream}.
+ *
+ * @param out the output stream to write to
+ * @throws IOException if an error occurs while writing to the output
stream
+ */
+ public void dump(OutputStream out) throws IOException {
+ dump(new OutputStreamWriter(out, StandardCharsets.UTF_8));
+ out.flush();
+ }
+
+ public void dump(Writer out) throws IOException {
+ BufferedWriter writer = new BufferedWriter(out);
+ if (consistent) {
+ writer.write("This namespace registry model is consistent,
containing the following mappings from prefixes to namespace uris:");
+ writer.newLine();
+ writer.newLine();
+ for (Map.Entry<String, String> entry :
prefixToNamespaceMap.entrySet()) {
+ writer.write(entry.getKey() + " -> " + entry.getValue());
+ writer.newLine();
+ }
+ } else {
+ writer.write("This namespace registry model is inconsistent.
The inconsistency can " + (isFixable()? "" : "NOT ") + "be fixed.");
+ writer.newLine();
+ writer.newLine();
+ writer.write("Registered prefixes without any namespace
mapping: " + danglingPrefixes);
+ writer.newLine();
+ writer.write("Registered namespace URIs without any prefix
mapping: " + danglingNamespacesEncoded);
+ writer.newLine();
+ writer.write("Duplicate prefixes: " + duplicatePrefixes);
+ writer.newLine();
+ writer.write("Duplicate namespace URIs: " +
duplicateNamespacesEncoded);
+ writer.newLine();
+ writer.write("Mapped unregistered prefixes: " +
SetUtils.difference(SetUtils.union(mappedPrefixes, mappedToPrefixes),
registeredPrefixes));
+ writer.newLine();
+ writer.write("Mapped unregistered namespace URIs: " +
SetUtils.difference(SetUtils.union(mappedNamespacesEncoded,
mappedToNamespacesEncoded), registeredNamespacesEncoded));
+ writer.newLine();
+ writer.write("Mapped prefixes without a reverse mapping: " +
SetUtils.difference(mappedPrefixes, mappedToPrefixes));
+ writer.newLine();
+ writer.write("Mapped namespace URIs without a reverse mapping:
" + SetUtils.difference(mappedNamespacesEncoded, mappedToNamespacesEncoded));
+ writer.newLine();
+ writer.newLine();
+ if (isFixable()) {
+ NamespaceRegistryModel repaired = tryRegistryRepair();
+ writer.newLine();
+ writer.write("The following mappings could be repaired:");
+ writer.newLine();
+ writer.newLine();
+ for (Map.Entry<String, String> entry :
getRepairedMappings().entrySet()) {
+ writer.write(entry.getKey() + " -> " +
entry.getValue());
+ writer.newLine();
+ }
+ writer.newLine();
+ writer.newLine();
+ writer.write("The repaired registry would contain the
following mappings:");
+ writer.newLine();
+ writer.newLine();
+ for (Map.Entry<String, String> entry :
repaired.prefixToNamespaceMap.entrySet()) {
+ writer.write(entry.getKey() + " -> " +
entry.getValue());
+ writer.newLine();
+ }
+ } else {
+ writer.write("The following mappings could be repaired:");
+ writer.newLine();
+ writer.newLine();
+ for (Map.Entry<String, String> entry :
getRepairedMappings().entrySet()) {
+ writer.write(entry.getKey() + " -> " +
entry.getValue());
+ writer.newLine();
+ }
+ writer.newLine();
+ writer.newLine();
+ writer.write("To create a fixed model, use
#tryRegistryRepair(Map<String, String>) and supply missing prefix to namespace
mappings as parameters");
+ writer.newLine();
+ }
+ }
+ writer.flush();
+ }
+}
diff --git
a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/ReadOnlyNamespaceRegistry.java
b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/ReadOnlyNamespaceRegistry.java
index ddf6509ce0..8bf13a7848 100644
---
a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/ReadOnlyNamespaceRegistry.java
+++
b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/ReadOnlyNamespaceRegistry.java
@@ -25,7 +25,6 @@ import javax.jcr.NamespaceRegistry;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;
-import org.apache.jackrabbit.util.Text;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.api.Tree;
@@ -35,9 +34,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
-import java.util.stream.Collectors;
/**
* Read-only namespace registry. Used mostly internally when access to the
@@ -53,14 +50,17 @@ public class ReadOnlyNamespaceRegistry
private static volatile boolean CONSISTENCY_CHECKED;
+ protected final Root root;
protected final Tree namespaces;
protected final Tree nsdata;
public ReadOnlyNamespaceRegistry(Root root) {
+ this.root = root;
this.namespaces = root.getTree(NAMESPACES_PATH);
this.nsdata = namespaces.getChild(REP_NSDATA);
if (!CONSISTENCY_CHECKED) {
- checkConsistency();
+ checkConsistency(root);
+ CONSISTENCY_CHECKED = true;
}
}
@@ -130,56 +130,15 @@ public class ReadOnlyNamespaceRegistry
"No namespace prefix registered for URI " + uri);
}
- protected void checkConsistency() {
- final String jcrPrimaryType = "jcr:primaryType";
- List<String> prefixes = Arrays.asList(getPrefixes());
- List<String> encodedUris =
Arrays.stream(getURIs()).map(Namespaces::encodeUri).collect(Collectors.toList());
- if (prefixes.size() != encodedUris.size()) {
- LOG.error("The namespace registry is inconsistent: found {}
registered namespace prefixes and {} registered namespace URIs. The numbers
have to be equal.", prefixes.size(), encodedUris.size());
- }
- int mappedPrefixCount = 0;
- for (PropertyState propertyState : namespaces.getProperties()) {
- String prefix = propertyState.getName();
- if (!prefix.equals(jcrPrimaryType)) {
- mappedPrefixCount++;
- if (!prefixes.contains(prefix)) {
- LOG.error("The namespace registry is inconsistent:
namespace prefix {} is mapped to a namespace URI, but not contained in the list
of registered namespace prefixes.", prefix);
- }
- try {
- getURI(prefix);
- } catch (NamespaceException e) {
- LOG.error("The namespace registry is inconsistent:
namespace prefix {} is not mapped to a namespace URI.", prefix);
- }
- }
- }
- //prefixes contains the unmapped empty prefix
- if (mappedPrefixCount + 1 != prefixes.size()) {
- LOG.error("The namespace registry is inconsistent: found {} mapped
namespace prefixes and {} registered namespace prefixes. The numbers have to be
equal.", mappedPrefixCount, prefixes.size());
- }
- int mappedUriCount = 0;
- for (PropertyState propertyState : nsdata.getProperties()) {
- String encodedUri = propertyState.getName();
- switch (encodedUri) {
- case REP_PREFIXES:
- case REP_URIS:
- case jcrPrimaryType:
- break;
- default:
- mappedUriCount++;
- if (!encodedUris.contains(encodedUri)) {
- LOG.error("The namespace registry is inconsistent:
encoded namespace URI {} is mapped to a namespace prefix, but not contained in
the list of registered namespace URIs.", encodedUri);
- }
- try {
- getPrefix(Text.unescapeIllegalJcrChars(encodedUri));
- } catch (NamespaceException e) {
- LOG.error("The namespace registry is inconsistent:
namespace URI {} is not mapped to a namespace prefix.", encodedUri);
- }
- }
- }
- //encodedUris contains the unmapped empty namespace URI
- if (mappedUriCount + 1 != encodedUris.size()) {
- LOG.error("The namespace registry is inconsistent: found {} mapped
namespace URIs and {} registered namespace URIs. The numbers have to be
equal.", mappedUriCount, encodedUris.size());
+ public boolean checkConsistency() throws IllegalStateException {
+ return checkConsistency(root);
+ }
+
+ public boolean checkConsistency(Root root) throws IllegalStateException {
+ NamespaceRegistryModel model = NamespaceRegistryModel.create(root);
+ if (model == null) {
+ LOG.warn("Consistency check skipped because there is no namespace
registry.");
}
- CONSISTENCY_CHECKED = true;
+ return model == null || model.isConsistent();
}
}
diff --git
a/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/name/NamespaceRegistryTest.java
b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/name/NamespaceRegistryTest.java
new file mode 100755
index 0000000000..5fd2590ed4
--- /dev/null
+++
b/oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/name/NamespaceRegistryTest.java
@@ -0,0 +1,293 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.jackrabbit.oak.plugins.name;
+
+import org.apache.jackrabbit.oak.InitialContent;
+import org.apache.jackrabbit.oak.Oak;
+import org.apache.jackrabbit.oak.api.ContentSession;
+import org.apache.jackrabbit.oak.api.PropertyState;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.plugins.memory.PropertyBuilder;
+import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider;
+
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Objects;
+
+import static
org.apache.jackrabbit.oak.spi.namespace.NamespaceConstants.REP_NSDATA;
+import static
org.apache.jackrabbit.oak.spi.namespace.NamespaceConstants.REP_PREFIXES;
+import static
org.apache.jackrabbit.oak.spi.namespace.NamespaceConstants.REP_URIS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class NamespaceRegistryTest {
+
+ /**
+ * Artificially apply inconsistencies to the namespace registry and test
if the NamespaceRegistryModel
+ * handles them correctly.
+ * @throws Exception
+ */
+ @Test
+ public void testNamespaceRegistryModel() throws Exception {
+ try (ContentSession session = new Oak()
+ .with(new OpenSecurityProvider())
+ .with(new InitialContent())
+ .with(new NamespaceEditorProvider())
+ .createContentSession()) {
+ Root root = session.getLatestRoot();
+ ReadWriteNamespaceRegistry registry = new
TestNamespaceRegistry(root);
+ Tree namespaces = root.getTree("/jcr:system/rep:namespaces");
+ Tree nsdata = namespaces.getChild(REP_NSDATA);
+ PropertyState prefixProp = nsdata.getProperty(REP_PREFIXES);
+ PropertyState namespaceProp = nsdata.getProperty(REP_URIS);
+
+ // Check the initial state of the namespace registry
+ assertTrue(registry.checkConsistency());
+ NamespaceRegistryModel model = NamespaceRegistryModel.create(root);
+ assertNotNull(model);
+ assertTrue(model.isConsistent());
+ assertTrue(model.isFixable());
+
+ assertEquals(0, model.getDanglingPrefixes().size());
+ assertEquals(0, model.getDanglingEncodedNamespaceUris().size());
+ assertEquals(0, model.getRepairedMappings().size());
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ model.dump(out);
+ String dump = out.toString(StandardCharsets.UTF_8);
+ assertTrue(dump.contains("This namespace registry model is
consistent"));
+
+ // Add a registered prefix without any mapping
+ PropertyBuilder<String> builder =
PropertyBuilder.copy(Type.STRING, prefixProp);
+ builder.addValue("foo");
+ nsdata.setProperty(builder.getPropertyState());
+
+ // Now it cannot be fixed automatically
+ assertFalse(registry.checkConsistency(root));
+ model = NamespaceRegistryModel.create(root);
+ assertNotNull(model);
+ assertFalse(model.isConsistent());
+ assertFalse(model.isFixable());
+
+ assertEquals(1, model.getDanglingPrefixes().size());
+ assertEquals(0, model.getDanglingEncodedNamespaceUris().size());
+ assertEquals(0, model.getRepairedMappings().size());
+
+ assertFalse(model.isConsistent());
+ out = new ByteArrayOutputStream();
+ model.dump(out);
+ assertFalse(model.isConsistent());
+ dump = out.toString(StandardCharsets.UTF_8);
+ assertFalse(model.isConsistent());
+ assertTrue(dump.contains("This namespace registry model is
inconsistent. The inconsistency can NOT be fixed."));
+ assertFalse(model.isConsistent());
+
+ model = model.tryRegistryRepair();
+ assertFalse(model.isConsistent());
+ assertFalse(model.isFixable());
+
+ out = new ByteArrayOutputStream();
+ model.dump(out);
+ dump = out.toString(StandardCharsets.UTF_8);
+ assertTrue(dump.contains("This namespace registry model is
inconsistent. The inconsistency can NOT be fixed."));
+
+ // Now add a mapping to a namespace uri, but not the reverse
mapping
+ namespaces.setProperty("foo", "urn:foo", Type.STRING);
+
+ // This is inconsistent, but can be fixed automatically
+ assertFalse(registry.checkConsistency(root));
+ model = NamespaceRegistryModel.create(root);
+ assertNotNull(model);
+ assertFalse(model.isConsistent());
+ assertTrue(model.isFixable());
+
+ assertEquals(0, model.getDanglingPrefixes().size());
+ assertEquals(0, model.getDanglingEncodedNamespaceUris().size());
+ assertEquals(1, model.getRepairedMappings().size());
+
+ out = new ByteArrayOutputStream();
+ model.dump(out);
+ dump = out.toString(StandardCharsets.UTF_8);
+ assertTrue(dump.contains("This namespace registry model is
inconsistent. The inconsistency can be fixed."));
+
+ model = model.tryRegistryRepair();
+ assertTrue(model.isConsistent());
+ assertTrue(model.isFixable());
+
+ out = new ByteArrayOutputStream();
+ model.dump(out);
+ dump = out.toString(StandardCharsets.UTF_8);
+ assertTrue(dump.contains("This namespace registry model is
consistent"));
+
+ // Add a registered namespace uri without any mapping
+ builder = PropertyBuilder.copy(Type.STRING, namespaceProp);
+ builder.addValue("urn:bar");
+ nsdata.setProperty(builder.getPropertyState());
+
+ // Now it again cannot be fixed automatically
+ assertFalse(registry.checkConsistency(root));
+ model = NamespaceRegistryModel.create(root);
+ assertNotNull(model);
+ assertFalse(model.isConsistent());
+ assertFalse(model.isFixable());
+
+ assertEquals(0, model.getDanglingPrefixes().size());
+ assertEquals(1, model.getDanglingEncodedNamespaceUris().size());
+ assertEquals(1, model.getRepairedMappings().size());
+
+ model = model.tryRegistryRepair();
+ assertFalse(model.isConsistent());
+ assertFalse(model.isFixable());
+
+ // Now add a reverse mapping to a prefix, but not the forward
mapping
+ nsdata.setProperty("urn%3Abar", "bar", Type.STRING);
+
+ // Now it can be fixed automatically again
+ assertFalse(registry.checkConsistency(root));
+ model = NamespaceRegistryModel.create(root);
+ assertFalse(model.isConsistent());
+ assertTrue(model.isFixable());
+
+ assertEquals(0, model.getDanglingPrefixes().size());
+ assertEquals(0, model.getDanglingEncodedNamespaceUris().size());
+ assertEquals(2, model.getRepairedMappings().size());
+
+ model = model.tryRegistryRepair();
+ assertTrue(model.isConsistent());
+ assertTrue(model.isFixable());
+
+ // Double a registered prefix
+ builder = PropertyBuilder.copy(Type.STRING, prefixProp);
+ builder.addValue("foo");
+ nsdata.setProperty(builder.getPropertyState());
+
+ // Can still be fixed automatically
+ assertFalse(registry.checkConsistency(root));
+ model = NamespaceRegistryModel.create(root);
+ assertNotNull(model);
+ assertFalse(model.isConsistent());
+ assertTrue(model.isFixable());
+
+ assertEquals(0, model.getDanglingPrefixes().size());
+ assertEquals(0, model.getDanglingEncodedNamespaceUris().size());
+ assertEquals(2, model.getRepairedMappings().size());
+
+ model = model.tryRegistryRepair();
+ assertTrue(model.isConsistent());
+ assertTrue(model.isFixable());
+
+ // Double a registered namespace uri
+ builder = PropertyBuilder.copy(Type.STRING, namespaceProp);
+ builder.addValue("urn:bar");
+ nsdata.setProperty(builder.getPropertyState());
+
+ // Can still be fixed automatically
+ assertFalse(registry.checkConsistency(root));
+ model = NamespaceRegistryModel.create(root);
+ assertFalse(model.isConsistent());
+ assertTrue(model.isFixable());
+
+ assertEquals(0, model.getDanglingPrefixes().size());
+ assertEquals(0, model.getDanglingEncodedNamespaceUris().size());
+ assertEquals(2, model.getRepairedMappings().size());
+
+ // remap a prefix
+ model = model.setMappings(Collections.singletonMap("foo",
"urn:foo2"));
+ assertFalse(model.isConsistent());
+ assertTrue(model.isFixable());
+
+ // Add a registered namespace uri without any mapping
+ builder = PropertyBuilder.copy(Type.STRING, namespaceProp);
+ builder.addValue("urn:bar2");
+ nsdata.setProperty(builder.getPropertyState());
+
+ // Cannot be fixed automatically
+ assertFalse(registry.checkConsistency(root));
+ model = NamespaceRegistryModel.create(root);
+ assertNotNull(model);
+ assertFalse(model.isConsistent());
+ assertFalse(model.isFixable());
+
+ // remap a prefix and map the new URI to make it fixable
+ HashMap<String, String> mappings = new HashMap<>();
+ mappings.put("foo", "urn:foo2");
+ mappings.put("bar2", "urn:bar2");
+ assertFalse(registry.checkConsistency(root));
+ model = model.setMappings(mappings);
+ assertFalse(model.isConsistent());
+ assertTrue(model.isFixable());
+
+ // Apply the fixed model
+ model = model.tryRegistryRepair();
+ assertTrue(model.isConsistent());
+ assertTrue(model.isFixable());
+ assertFalse(registry.checkConsistency(root));
+ model.apply(root);
+ assertTrue(registry.checkConsistency(root));
+
assertTrue(Objects.requireNonNull(NamespaceRegistryModel.create(root)).isConsistent());
+
+ assertEquals(0, model.getDanglingPrefixes().size());
+ assertEquals(0, model.getDanglingEncodedNamespaceUris().size());
+ assertEquals(0, model.getRepairedMappings().size());
+
+ // Check the extra mappings
+ assertEquals("urn:foo2", registry.getURI("foo"));
+ assertEquals("foo", registry.getPrefix("urn:foo2"));
+ assertEquals("urn:bar2", registry.getURI("bar2"));
+ assertEquals("bar2", registry.getPrefix("urn:bar2"));
+ }
+ }
+
+ @Test
+ public void testConsistencyCheckInvocationCount() throws Exception {
+ Oak oak = new Oak()
+ .with(new OpenSecurityProvider())
+ .with(new InitialContent())
+ .with(new NamespaceEditorProvider());
+ try (ContentSession session = oak.createContentSession()) {
+ Root root = session.getLatestRoot();
+ ReadWriteNamespaceRegistry registry = new
TestNamespaceRegistry(root);
+ ReadWriteNamespaceRegistry spy = spy(registry);
+ verify(spy, times(0)).checkConsistency(any(Root.class));
+ new TestNamespaceRegistry(root);
+ verify(spy, times(0)).checkConsistency(any(Root.class));
+ }
+ }
+
+ static class TestNamespaceRegistry extends ReadWriteNamespaceRegistry {
+ public TestNamespaceRegistry(Root root) {
+ super(root);
+ }
+
+ @Override
+ protected Root getWriteRoot() {
+ return root;
+ }
+ }
+}
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 87a37a46a8..9ea6adf590 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
@@ -82,6 +82,7 @@ public final class AvailableModes {
builder.put("server", new ServerCommand());
builder.put("purge-index-versions", new
LucenePurgeOldIndexVersionCommand());
builder.put("create-test-garbage", new CreateGarbageCommand());
+ builder.put("namespace-registry", new NamespaceRegistryCommand());
return Collections.unmodifiableMap(builder);
}
diff --git
a/oak-run/src/main/java/org/apache/jackrabbit/oak/run/NamespaceRegistryCommand.java
b/oak-run/src/main/java/org/apache/jackrabbit/oak/run/NamespaceRegistryCommand.java
new file mode 100755
index 0000000000..605c7e0cb9
--- /dev/null
+++
b/oak-run/src/main/java/org/apache/jackrabbit/oak/run/NamespaceRegistryCommand.java
@@ -0,0 +1,169 @@
+/*
+ * 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 org.apache.jackrabbit.oak.Oak;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.ContentSession;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.commons.pio.Closer;
+import org.apache.jackrabbit.oak.plugins.name.NamespaceRegistryModel;
+import org.apache.jackrabbit.oak.plugins.name.ReadWriteNamespaceRegistry;
+import org.apache.jackrabbit.oak.run.cli.CommonOptions;
+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.run.commons.Command;
+import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.jcr.RepositoryException;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Command to analyze and repair the namespace registry in an Oak repository
({@link NamespaceRegistryModel}).
+ * Possible options are: --analyse, --fix, and --mappings, which will execute
corresponding operations on
+ * the namespace registry.
+ * <p>
+ * --analyse executes an operation that will print the current consistency
state of the namespace registry to
+ * the console. If the namespace registry is inconsistent and fixable, it will
also perform a dry run of the
+ * --fix operation and print the result to the console.
+ * <p>
+ * --fix executes an operation that will attempt to repair an inconsistent the
namespace registry.
+ * <p>
+ * --mappings is an option for both operations, allowing to specify additional
namespace mappings in
+ * the format "prefix=uri", which will be applied during the operation.
+ */
+public class NamespaceRegistryCommand implements Command {
+
+ public static final String NAME = "namespace-registry";
+
+ private static final Logger LOG =
LoggerFactory.getLogger(NamespaceRegistryCommand.class);
+ private static final String SUMMARY = "Provides commands to analyse the
integrity of the namespace registry and repair it if necessary.";
+
+ private final OptionParser parser = new OptionParser();
+
+ @Override
+ public void execute(String... args) throws Exception {
+ Options opts = getOptions(args);
+ NamespaceRegistryOptions namespaceRegistryOpts =
opts.getOptionBean(NamespaceRegistryOptions.class);
+ try (Closer closer = Utils.createCloserWithShutdownHook()) {
+
+ NodeStoreFixture fixture = NodeStoreFixtureProvider.create(opts);
+ closer.register(fixture);
+
+ if (!checkParameters(namespaceRegistryOpts, fixture)) {
+ return;
+ }
+ doExecute(fixture, namespaceRegistryOpts);
+ } catch (Exception e) {
+ LOG.error("Error occurred while performing namespace registry
operation", e);
+ e.printStackTrace(System.err);
+ throw e;
+ }
+ }
+
+ Options getOptions(String... args) throws IOException {
+ Options opts = new Options();
+ opts.setCommandName(NAME);
+ opts.setSummary(SUMMARY);
+ opts.setConnectionString(CommonOptions.DEFAULT_CONNECTION_STRING);
+ opts.registerOptionsFactory(NamespaceRegistryOptions.FACTORY);
+ opts.parseAndConfigure(parser, args);
+ return opts;
+ }
+
+ private boolean checkParameters(NamespaceRegistryOptions
namespaceRegistryOptions, NodeStoreFixture fixture)
+ throws IOException {
+ if (!namespaceRegistryOptions.anyActionSelected()) {
+ LOG.info("No actions specified");
+ parser.printHelpOn(System.out);
+ return false;
+ } else if (fixture.getStore() == null) {
+ LOG.info("No NodeStore specified");
+ parser.printHelpOn(System.out);
+ return false;
+ }
+ return true;
+ }
+
+ private void doExecute(NodeStoreFixture fixture, NamespaceRegistryOptions
namespaceRegistryOptions)
+ throws IOException, RepositoryException, CommitFailedException {
+ boolean analyse = namespaceRegistryOptions.analyse();
+ boolean fix = namespaceRegistryOptions.fix();
+ List<String> mappings = namespaceRegistryOptions.mappings();
+ Oak oak = new Oak(fixture.getStore()).with(new OpenSecurityProvider());
+ try (ContentSession contentSession = oak.createContentSession()) {
+ Root root = contentSession.getLatestRoot();
+ ReadWriteNamespaceRegistry namespaceRegistry = new
ReadWriteNamespaceRegistry(root) {
+ @Override
+ protected Root getWriteRoot() {
+ return root;
+ }
+ };
+ if (analyse || fix) {
+ NamespaceRegistryModel registryModel =
NamespaceRegistryModel.create(root);
+ if (fix) {
+ Map<String, String> additionalMappings = new HashMap<>();
+ if (mappings != null) {
+ for (String mapping : mappings) {
+ String[] parts = mapping.split("=");
+ if (parts.length != 2) {
+ System.err.println("Invalid mapping: " +
mapping);
+ return;
+ }
+ additionalMappings.put(parts[0].trim(),
parts[1].trim());
+ }
+ }
+ registryModel =
registryModel.setMappings(additionalMappings);
+ if (registryModel.isConsistent() &&
additionalMappings.isEmpty()) {
+ System.out.println("The namespace registry is already
consistent. No action is required.");
+ } else if (registryModel.isFixable()) {
+ registryModel.dump(System.out);
+ System.out.println();
+ System.out.println("Now fixing the registry.");
+ System.out.println();
+ System.out.flush();
+ NamespaceRegistryModel repaired =
registryModel.tryRegistryRepair();
+ if (repaired == null) {
+ System.out.println("An unknown error has occurred.
No changes have been made to the namespace registry.");
+ return;
+ }
+ repaired.apply(root);
+ root.commit();
+ repaired.dump();
+ } else {
+ registryModel.dump();
+ }
+ } else {
+ if (registryModel == null) {
+ System.out.println("There is no namespace registry in
the repository.");
+ } else {
+ registryModel.dump();
+ }
+ }
+ } else {
+ System.err.println("No action specified. Use --analyse to
check the integrity of the namespace registry. Use --fix to repair it if
necessary and possible.");
+ }
+ }
+ }
+}
diff --git
a/oak-run/src/main/java/org/apache/jackrabbit/oak/run/NamespaceRegistryOptions.java
b/oak-run/src/main/java/org/apache/jackrabbit/oak/run/NamespaceRegistryOptions.java
new file mode 100755
index 0000000000..49b5d8f357
--- /dev/null
+++
b/oak-run/src/main/java/org/apache/jackrabbit/oak/run/NamespaceRegistryOptions.java
@@ -0,0 +1,104 @@
+/*
+ * 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 java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import joptsimple.OptionParser;
+import joptsimple.OptionSet;
+import joptsimple.OptionSpec;
+import org.apache.jackrabbit.oak.run.cli.OptionsBean;
+import org.apache.jackrabbit.oak.run.cli.OptionsBeanFactory;
+
+public class NamespaceRegistryOptions implements OptionsBean {
+
+ public static final OptionsBeanFactory FACTORY =
NamespaceRegistryOptions::new;
+
+ private OptionSet options;
+ private final Set<OptionSpec<Void>> actionOpts;
+ private final Set<String> operationNames;
+
+ private final OptionSpec<Void> analyseOpt;
+ private final OptionSpec<Void> fixOpt;
+ private final OptionSpec<String> mappingsOpt;
+
+ public NamespaceRegistryOptions(OptionParser parser) {
+ analyseOpt = parser.accepts("analyse", "List the prefix to namespace
map and check for consistency.");
+ fixOpt = parser.accepts("fix", "List the prefix to namespace map,
check for consistency and fix any inconsistencies, if possible.");
+ mappingsOpt = parser.accepts("mappings", "Optionally specify explicit
prefix to namespace mappings ad a list of prefix=uri
expressions").withRequiredArg();
+ actionOpts = Set.of(analyseOpt, fixOpt);
+ operationNames = collectionOperationNames(actionOpts);
+ }
+
+ @Override
+ public void configure(OptionSet options) {
+ this.options = options;
+ }
+
+ @Override
+ public String title() {
+ return "";
+ }
+
+ @Override
+ public String description() {
+ return "The namespace-registry command supports the following
operations.";
+ }
+
+ @Override
+ public int order() {
+ return Integer.MAX_VALUE;
+ }
+
+ @Override
+ public Set<String> operationNames() {
+ return operationNames;
+ }
+
+ public boolean anyActionSelected() {
+ for (OptionSpec<Void> spec : actionOpts) {
+ if (options.has(spec)){
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean analyse() {
+ return options.has(analyseOpt);
+ }
+
+ public boolean fix() {
+ return options.has(fixOpt);
+ }
+
+ public List<String> mappings() {
+ return options.valuesOf(mappingsOpt);
+ }
+
+ private static Set<String> collectionOperationNames(Set<OptionSpec<Void>>
actionOpts) {
+ Set<String> result = new HashSet<>();
+ for (OptionSpec<Void> spec : actionOpts){
+ result.addAll(spec.options());
+ }
+ return result;
+ }
+}
diff --git
a/oak-run/src/test/java/org/apache/jackrabbit/oak/run/NamespaceRegistryCommandTest.java
b/oak-run/src/test/java/org/apache/jackrabbit/oak/run/NamespaceRegistryCommandTest.java
new file mode 100755
index 0000000000..89d0ed2d73
--- /dev/null
+++
b/oak-run/src/test/java/org/apache/jackrabbit/oak/run/NamespaceRegistryCommandTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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 org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.oak.InitialContent;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.plugins.document.DocumentNodeStore;
+import org.apache.jackrabbit.oak.plugins.document.MongoUtils;
+import org.apache.jackrabbit.oak.plugins.name.Namespaces;
+import org.apache.jackrabbit.oak.run.cli.NodeStoreFixture;
+import org.apache.jackrabbit.oak.run.cli.NodeStoreFixtureProvider;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.namespace.NamespaceConstants;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+/**
+ * Tests for the {@link NamespaceRegistryCommand}.
+ */
+public class NamespaceRegistryCommandTest {
+
+ private final NamespaceRegistryCommand cmd = new
NamespaceRegistryCommand();
+ private DocumentNodeStore store;
+
+ @Before
+ public void before() throws CommitFailedException {
+ assumeTrue(MongoUtils.isAvailable());
+ try {
+ NodeStoreFixture fixture =
NodeStoreFixtureProvider.create(cmd.getOptions(MongoUtils.URL, "--fix",
"--read-write"));
+ store = (DocumentNodeStore) fixture.getStore();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ NodeBuilder rootBuilder = store.getRoot().builder();
+ new InitialContent().initialize(rootBuilder);
+ NodeBuilder system = rootBuilder.getChildNode(JcrConstants.JCR_SYSTEM);
+ NodeBuilder namespaces =
system.getChildNode(NamespaceConstants.REP_NAMESPACES);
+ namespaces.remove();
+
Namespaces.setupNamespaces(rootBuilder.getChildNode(JcrConstants.JCR_SYSTEM));
+ store.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+ store.runBackgroundOperations();
+ }
+
+ @Test
+ public void analyse() throws Exception {
+ testCmd(new String[] { MongoUtils.URL, "--analyse" }, new String[] {
"This namespace registry model is consistent, containing the following mappings
from prefixes to namespace uris:" });
+ }
+
+ @Test
+ public void fix() throws Exception {
+ testCmd(new String[] { MongoUtils.URL, "--fix" }, new String[] { "The
namespace registry is already consistent. No action is required." });
+ }
+
+ @Test
+ public void breakAndFix() throws Exception {
+ NodeBuilder rootBuilder = store.getRoot().builder();
+ NodeBuilder namespaces =
rootBuilder.getChildNode(JcrConstants.JCR_SYSTEM).getChildNode(NamespaceConstants.REP_NAMESPACES);
+ namespaces.setProperty("foo", "urn:foo");
+ store.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+ store.runBackgroundOperations();
+ testCmd(new String[] { MongoUtils.URL, "--analyse" }, new String[] {
"This namespace registry model is inconsistent. The inconsistency can be
fixed.", "The repaired registry would contain the following mappings:", "foo ->
urn:foo" });
+ testCmd(new String[] { MongoUtils.URL, "--fix", "--read-write" }, new
String[] { "This namespace registry model is consistent, containing the
following mappings from prefixes to namespace uris:", "foo -> urn:foo" });
+ }
+
+ @Test
+ public void mappings() throws Exception {
+ testCmd(new String[] { MongoUtils.URL, "--analyse" }, new String[] {
"This namespace registry model is consistent"});
+ testCmd(new String[] { MongoUtils.URL, "--fix", "--mappings",
"foo=urn:foo", "--read-write" }, new String[] { "This namespace registry model
is consistent, containing the following mappings from prefixes to namespace
uris:", "foo -> urn:foo" });
+ }
+
+ private void testCmd(String[] opts, String[] output) throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try(PrintStream printStream = new PrintStream(out)) {
+ System.setOut(printStream);
+ cmd.execute(opts);
+ printStream.flush();
+ for (String expected : output) {
+ String s = out.toString(StandardCharsets.UTF_8);
+ assertTrue(s.contains(expected));
+ }
+ }
+ }
+}