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));
+            }
+        }
+    }
+}

Reply via email to