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 e158656bce OAK-11617: Provide oak-run commands to analyze and fix 
inconsistencie… (#2615)
e158656bce is described below

commit e158656bceede498436331e9be8ae76c3f918127
Author: mbaedke <[email protected]>
AuthorDate: Tue Nov 25 14:07:25 2025 +0100

    OAK-11617: Provide oak-run commands to analyze and fix inconsistencie… 
(#2615)
    
    * OAK-11617: Provide oak-run commands to analyze and fix inconsistencies in 
the namespace registry
    
    Introduced new option --prune, added documentation and tests.
---
 .../oak/plugins/name/NamespaceRegistryModel.java   |  54 +++++-
 .../oak/plugins/name/NamespaceRegistryTest.java    | 184 ++++++++++++++++++++-
 oak-run/README.md                                  |  34 ++++
 .../oak/run/NamespaceRegistryCommand.java          |  44 ++---
 .../oak/run/NamespaceRegistryOptions.java          |   4 +
 .../oak/run/NamespaceRegistryCommandTest.java      |  88 +++++++++-
 6 files changed, 377 insertions(+), 31 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
index 99868effdb..4817fbe726 100644
--- 
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
@@ -30,6 +30,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -188,6 +189,22 @@ public final class NamespaceRegistryModel {
         }
     }
 
+    /**
+     * Remove any unmapped prefixes and URIs, so that they don't have to be 
mapped to dummy values in order to
+     * create a consistent registry.
+     * @return new NamespaceRegistryModel without unmapped prefixes or URIs.
+     */
+    public NamespaceRegistryModel prune() {
+        List<String> newRegisteredPrefixesList = new 
ArrayList<>(registeredPrefixes);
+        newRegisteredPrefixesList.removeAll(danglingPrefixes);
+        List<String> newRegisteredNamespacesEncodedList = new 
ArrayList<>(registeredNamespacesEncoded);
+        
newRegisteredNamespacesEncodedList.removeAll(danglingNamespacesEncoded);
+        Map<String, String> newPrefixToNamespaceMap = new 
HashMap<>(prefixToNamespaceMap);
+        Map<String, String> newEncodedNamespaceToPrefixMap = new 
HashMap<>(encodedNamespaceToPrefixMap);
+        return new NamespaceRegistryModel(newRegisteredPrefixesList, 
newRegisteredNamespacesEncodedList,
+                newPrefixToNamespaceMap, newEncodedNamespaceToPrefixMap);
+    }
+
     /**
      * 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.
@@ -205,19 +222,44 @@ public final class NamespaceRegistryModel {
             String uri = entry.getValue();
             String encodedUri = Namespaces.encodeUri(uri);
 
+            String previousMappedUri = newPrefixToNamespaceMap.get(prefix);
+            //if the prefix of the new mapping is already mapped to an 
existing URI, remove this existing mapping so
+            //that it doesn't need to be explicitly overridden (which still 
may be done, if needed).
+            if (newPrefixToNamespaceMap.containsValue(uri)) {
+                Optional<String> s = 
newPrefixToNamespaceMap.entrySet().stream()
+                        .filter(mapEntry -> uri.equals(mapEntry.getValue()))
+                        .map(Map.Entry::getKey).findFirst();
+                if (s.isPresent() && !prefix.equals(s.get())) {
+                    newPrefixToNamespaceMap.remove(s.get());
+                }
+            }
+            //if the URI of the new mapping is already mapped to an existing 
prefix, remove this existing mapping so
+            //that it doesn't need to be explicitly overridden (which still 
may be done, if needed).
+            String previousMappedPrefix = 
newEncodedNamespaceToPrefixMap.get(encodedUri);
+            if (newEncodedNamespaceToPrefixMap.containsValue(prefix)) {
+                Optional<String> s = 
newEncodedNamespaceToPrefixMap.entrySet().stream()
+                        .filter(mapEntry -> prefix.equals(mapEntry.getValue()))
+                        .map(Map.Entry::getKey).findFirst();
+                if (s.isPresent() && !encodedUri.equals(s.get())) {
+                    newEncodedNamespaceToPrefixMap.remove(s.get());
+                }
+            }
+            if (previousMappedPrefix != null) {
+                newRegisteredPrefixesList.remove(previousMappedPrefix);
+                newPrefixToNamespaceMap.remove(previousMappedPrefix);
+            }
             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);
+            if (previousMappedUri != null) {
+                String previousEncodedUri = 
Namespaces.encodeUri(previousMappedUri);
                 newRegisteredNamespacesEncodedList.remove(previousEncodedUri);
                 newEncodedNamespaceToPrefixMap.remove(previousEncodedUri);
             }
+            if (!newRegisteredNamespacesEncodedList.contains(encodedUri)) {
+                newRegisteredNamespacesEncodedList.add(encodedUri);
+            }
             newEncodedNamespaceToPrefixMap.put(encodedUri, prefix);
         }
         return new NamespaceRegistryModel(newRegisteredPrefixesList, 
newRegisteredNamespacesEncodedList,
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
index 5fd2590ed4..dde91baef5 100644
--- 
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
@@ -28,6 +28,7 @@ import 
org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider;
 
 import org.junit.Test;
 
+import javax.jcr.NamespaceException;
 import java.io.ByteArrayOutputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.Collections;
@@ -40,6 +41,7 @@ import static 
org.apache.jackrabbit.oak.spi.namespace.NamespaceConstants.REP_URI
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.spy;
@@ -222,9 +224,10 @@ public class NamespaceRegistryTest {
             assertFalse(model.isConsistent());
             assertTrue(model.isFixable());
 
-            // Add a registered namespace uri without any mapping
+            // Add two registered namespace uris without any mapping
             builder = PropertyBuilder.copy(Type.STRING, namespaceProp);
             builder.addValue("urn:bar2");
+            builder.addValue("urn:bar3");
             nsdata.setProperty(builder.getPropertyState());
 
             // Cannot be fixed automatically
@@ -234,12 +237,13 @@ public class NamespaceRegistryTest {
             assertFalse(model.isConsistent());
             assertFalse(model.isFixable());
 
-            // remap a prefix and map the new URI to make it fixable
+            // remap a prefix, map the first new URI and remove the dangling 
second 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);
+            model = model.prune();
             assertFalse(model.isConsistent());
             assertTrue(model.isFixable());
 
@@ -280,6 +284,182 @@ public class NamespaceRegistryTest {
         }
     }
 
+    @Test
+    public void testNamespaceRegistryModelRemappedPrefix() 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);
+
+            // Add a mapping
+            NamespaceRegistryModel model = NamespaceRegistryModel.create(root);
+            String foo = "foo";
+            String fooUri = "urn:foo";
+            model = model.setMappings(Collections.singletonMap(foo, fooUri));
+            assertTrue(model.isConsistent());
+            model.apply(root);
+            assertTrue(registry.checkConsistency());
+            assertEquals(foo, registry.getPrefix(fooUri));
+
+            // re-map
+            String barUri = "urn:bar";
+            model = model.setMappings(Collections.singletonMap(foo, barUri));
+            assertTrue(model.isConsistent());
+            model.apply(root);
+            assertTrue(registry.checkConsistency());
+            assertEquals(foo, registry.getPrefix(barUri));
+            assertThrows(NamespaceException.class, () -> 
registry.getPrefix(fooUri));
+        }
+    }
+
+    @Test
+    public void testNamespaceRegistryModelRemappedNamespace() 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);
+
+            // Add a mapping
+            NamespaceRegistryModel model = NamespaceRegistryModel.create(root);
+            String foo = "foo";
+            String fooUri = "urn:foo";
+            model = model.setMappings(Collections.singletonMap(foo, fooUri));
+            assertTrue(model.isConsistent());
+            model.apply(root);
+            assertTrue(registry.checkConsistency());
+            assertEquals(foo, registry.getPrefix(fooUri));
+
+            // re-map
+            String bar = "bar";
+            model = model.setMappings(Collections.singletonMap(bar, fooUri));
+            assertTrue(model.isConsistent());
+            model.apply(root);
+            assertTrue(registry.checkConsistency());
+            assertEquals(bar, registry.getPrefix(fooUri));
+            assertThrows(NamespaceException.class, () -> registry.getURI(foo));
+        }
+    }
+
+    @Test
+    public void testNamespaceRegistryModelAmbiguousUri() 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);
+
+            // Add a mapping and an incompatible reverse mapping
+            String foo = "foo";
+            String bar = "bar";
+            String fooUri = "urn:foo";
+            String barUri = "urn:bar";
+            namespaces.setProperty(foo, fooUri);
+            nsdata.setProperty(Namespaces.encodeUri(barUri), foo);
+
+            NamespaceRegistryModel model = NamespaceRegistryModel.create(root);
+            assertNotNull(model);
+            assertFalse(registry.checkConsistency(root));
+            assertFalse(model.isConsistent());
+            assertFalse(model.isFixable());
+            model = model.setMappings(Collections.singletonMap(foo, fooUri));
+            assertTrue(model.isConsistent());
+            model.apply(root);
+            assertTrue(registry.checkConsistency());
+
+            assertEquals(fooUri, registry.getURI(foo));
+            assertThrows(NamespaceException.class, () -> registry.getURI(bar));
+            assertEquals(foo, registry.getPrefix(fooUri));
+            assertThrows(NamespaceException.class, () -> 
registry.getPrefix(barUri));
+        }
+    }
+
+    @Test
+    public void testNamespaceRegistryModelAmbiguousPrefix() 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);
+
+            // Add a mapping and an incompatible reverse mapping
+            String foo = "foo";
+            String bar = "bar";
+            String fooUri = "urn:foo";
+            String barUri = "urn:bar";
+            namespaces.setProperty(foo, fooUri);
+            nsdata.setProperty(Namespaces.encodeUri(fooUri), bar);
+
+            NamespaceRegistryModel model = NamespaceRegistryModel.create(root);
+            assertNotNull(model);
+            assertFalse(registry.checkConsistency(root));
+            assertFalse(model.isConsistent());
+            assertFalse(model.isFixable());
+            model = model.setMappings(Collections.singletonMap(foo, fooUri));
+            assertTrue(model.isConsistent());
+            model.apply(root);
+            assertTrue(registry.checkConsistency());
+
+            assertEquals(fooUri, registry.getURI(foo));
+            assertThrows(NamespaceException.class, () -> registry.getURI(bar));
+            assertEquals(foo, registry.getPrefix(fooUri));
+            assertThrows(NamespaceException.class, () -> 
registry.getPrefix(barUri));
+        }
+    }
+
+    @Test
+    public void testNamespaceRegistryModelPruneUnmappedData() 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);
+
+            // Add a prefix and an URI without mappings
+            String foo = "foo";
+            String barUri = "urn:bar";
+
+            PropertyBuilder<String> builder = 
PropertyBuilder.copy(Type.STRING, prefixProp);
+            builder.addValue(foo);
+            nsdata.setProperty(builder.getPropertyState());
+            builder = PropertyBuilder.copy(Type.STRING, namespaceProp);
+            builder.addValue(barUri);
+            nsdata.setProperty(builder.getPropertyState());
+
+            NamespaceRegistryModel model = NamespaceRegistryModel.create(root);
+            assertNotNull(model);
+            assertFalse(registry.checkConsistency(root));
+            assertFalse(model.isConsistent());
+            assertFalse(model.isFixable());
+            model = model.prune();
+            assertTrue(model.isConsistent());
+            model.apply(root);
+            assertTrue(registry.checkConsistency());
+
+            assertThrows(NamespaceException.class, () -> registry.getURI(foo));
+            assertThrows(NamespaceException.class, () -> 
registry.getPrefix(barUri));
+        }
+    }
+
     static class TestNamespaceRegistry extends ReadWriteNamespaceRegistry {
         public TestNamespaceRegistry(Root root) {
             super(root);
diff --git a/oak-run/README.md b/oak-run/README.md
index 049961d3e7..00ec262fcb 100644
--- a/oak-run/README.md
+++ b/oak-run/README.md
@@ -24,6 +24,7 @@ The following runmodes are currently available:
     * help            : Print a list of available runmodes
     * history         : Trace the history of a node
     * iotrace         : Collect a trace of segment store read accesses 
+    * namespace-registry : Analyze and optionally repair the namespace 
registry in an Oak repository
     * recovery        : Run a _lastRev recovery on a DocumentMK repository
     * resetclusterid  : Resets the cluster id
     * restore         : Restore a backup of an Oak repository
@@ -778,6 +779,39 @@ Upgrades the JR2 DataStore cache by moving files to the 
Upload staging and the d
         --moveCache <true|false> \
         --deleteMapFile <true|false>
 
+Namespace Registry
+-------
+
+The 'namespace-registry' mode analyses and optionally repairs the 
namespace-registry of an existing Oak repository.
+
+It will write a human-readable summary to stdout. If inconsistencies are 
detected, they will be reported and
+if they can be repaired automatically, a dry run of the fix operation will be 
performed and the results will be
+written to stdout. If automatic repair is not possible, the missing 
information may be added using the --mappings
+option.
+
+    $ java -jar oak-run-*.jar namespace-registry \
+            { /path/to/oak/repository | mongodb://host:port/database | 
jdbc:...} \
+            [--analyse] \
+            [--fix --read-write] \
+            [--prune] \
+            [--mappings prefix=uri]...
+
+The following options are available:
+
+    --analyse               - An analysis of the namespace registry will be 
performed. The result will be written
+                                to stdout in human-readable form. If 
inconsistencies are detected, they will be
+                                reported and if they can be repaired 
automatically, a dry run of the --fix operation
+                                will be performed and the results will be 
written to stdout.
+                                If --mappings are supplied, they will be 
considered part of the registry.
+    --fix                   - This will try an automatic repair of an 
inconsistent namespace registry.
+                                If --mappings are supplied, they will be added 
to the registry, overriding existing
+                                mappings containing the same prefix or URI.
+    --mappings              - Sometimes an inconsistent namespace registry 
cannot be repaired without additional
+                                information. The missing information can be 
supplied using this option if the form
+                                of prefix to URI mappings which will override 
any (incomplete) mappings in the
+                                registry during --analyse or --fix operations.
+    --prune                 - Remove any unmapped prefixes and URIs before 
--analyse or --fix operations.
+
 Unlock DocumentMK upgrade
 -------------------------
 
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
index 605c7e0cb9..6e8849f966 100644
--- 
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
@@ -23,7 +23,6 @@ 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;
@@ -41,7 +40,7 @@ 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
+ * Possible options are: --analyse, --fix, --prune 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
@@ -51,7 +50,9 @@ import java.util.Map;
  * --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.
+ * the format "prefix=uri", which will be applied during the operation. It may 
be used multiple times.
+ * <p>
+ * --prune is an option for both operations. All unmapped data will be removed.
  */
 public class NamespaceRegistryCommand implements Command {
 
@@ -110,32 +111,31 @@ public class NamespaceRegistryCommand implements Command {
             throws IOException, RepositoryException, CommitFailedException {
         boolean analyse = namespaceRegistryOptions.analyse();
         boolean fix = namespaceRegistryOptions.fix();
+        boolean prune = namespaceRegistryOptions.prune();
         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());
+                NamespaceRegistryModel originalModel = 
NamespaceRegistryModel.create(root);
+                NamespaceRegistryModel registryModel = originalModel;
+                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()) {
+                }
+                registryModel = registryModel.setMappings(additionalMappings);
+                if (prune) {
+                    registryModel = registryModel.prune();
+                }
+                if (fix) {
+                    if (originalModel.isConsistent() && 
additionalMappings.isEmpty()) {
                         System.out.println("The namespace registry is already 
consistent. No action is required.");
                     } else if (registryModel.isFixable()) {
                         registryModel.dump(System.out);
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
index 49b5d8f357..85d156a0cb 100644
--- 
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
@@ -38,11 +38,13 @@ public class NamespaceRegistryOptions implements 
OptionsBean {
 
     private final OptionSpec<Void> analyseOpt;
     private final OptionSpec<Void> fixOpt;
+    private final OptionSpec<Void> pruneOpt;
     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.");
+        pruneOpt = parser.accepts("prune", "Remove any unmapped uris and 
prefixes");
         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);
@@ -90,6 +92,8 @@ public class NamespaceRegistryOptions implements OptionsBean {
         return  options.has(fixOpt);
     }
 
+    public boolean prune() { return  options.has(pruneOpt); }
+
     public List<String> mappings() {
         return  options.valuesOf(mappingsOpt);
     }
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
index 89d0ed2d73..86aa1079f5 100644
--- 
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
@@ -34,7 +34,11 @@ import org.junit.Test;
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
 
+import static org.apache.jackrabbit.oak.api.Type.STRINGS;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
@@ -76,16 +80,98 @@ public class NamespaceRegistryCommandTest {
     }
 
     @Test
-    public void breakAndFix() throws Exception {
+    public void breakAndFixNoReverseMapping() throws Exception {
         NodeBuilder rootBuilder = store.getRoot().builder();
         NodeBuilder namespaces = 
rootBuilder.getChildNode(JcrConstants.JCR_SYSTEM).getChildNode(NamespaceConstants.REP_NAMESPACES);
+        //inconsistent mapping: no reverse mapping
         namespaces.setProperty("foo", "urn:foo");
         store.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
         store.runBackgroundOperations();
+        //complete information: automatic fix
         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 breakAndFixPrefixAmbiguity() throws Exception {
+        NodeBuilder rootBuilder = store.getRoot().builder();
+        NodeBuilder namespaces = 
rootBuilder.getChildNode(JcrConstants.JCR_SYSTEM).getChildNode(NamespaceConstants.REP_NAMESPACES);
+        NodeBuilder nsdata = 
namespaces.getChildNode(NamespaceConstants.REP_NSDATA);
+        //inconsistent mapping: one URI, two prefixes
+        namespaces.setProperty("foo", "urn:foo");
+        nsdata.setProperty(Namespaces.encodeUri("urn:foo"), "bar");
+        store.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        store.runBackgroundOperations();
+        //ambiguous information: no automatic fix
+        testCmd(new String[] { MongoUtils.URL, "--analyse" }, new String[] { 
"This namespace registry model is inconsistent. The inconsistency can NOT be 
fixed." });
+        //consistent with supplied specific mapping
+        testCmd(new String[] { MongoUtils.URL, "--analyse", "--mappings", 
"foo=urn:foo" }, new String[] { "This namespace registry model is consistent, 
containing the following mappings from prefixes to namespace uris:", "foo -> 
urn:foo" });
+        testCmd(new String[] { MongoUtils.URL, "--fix", "--read-write", 
"--mappings", "foo=urn:foo" }, new String[] { "This namespace registry model is 
consistent, containing the following mappings from prefixes to namespace 
uris:", "foo -> urn:foo" });
+    }
+
+    @Test
+    public void breakAndFixUriAmbiguity() throws Exception {
+        NodeBuilder rootBuilder = store.getRoot().builder();
+        NodeBuilder namespaces = 
rootBuilder.getChildNode(JcrConstants.JCR_SYSTEM).getChildNode(NamespaceConstants.REP_NAMESPACES);
+        NodeBuilder nsdata = 
namespaces.getChildNode(NamespaceConstants.REP_NSDATA);
+        //inconsistent mapping: one prefix, two URIs
+        namespaces.setProperty("foo", "urn:foo");
+        nsdata.setProperty(Namespaces.encodeUri("urn:bar"), "foo");
+        store.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        store.runBackgroundOperations();
+        //ambiguous information: no automatic fix
+        testCmd(new String[] { MongoUtils.URL, "--analyse" }, new String[] { 
"This namespace registry model is inconsistent. The inconsistency can NOT be 
fixed." });
+        //consistent with supplied specific mapping
+        testCmd(new String[] { MongoUtils.URL, "--analyse", "--mappings", 
"foo=urn:foo" }, new String[] { "This namespace registry model is consistent, 
containing the following mappings from prefixes to namespace uris:", "foo -> 
urn:foo" });
+        testCmd(new String[] { MongoUtils.URL, "--fix", "--read-write", 
"--mappings", "foo=urn:foo" }, new String[] { "This namespace registry model is 
consistent, containing the following mappings from prefixes to namespace 
uris:", "foo -> urn:foo" });
+    }
+
+    @Test
+    public void breakAndFixDanglingPrefix() throws Exception {
+        NodeBuilder rootBuilder = store.getRoot().builder();
+        NodeBuilder namespaces = 
rootBuilder.getChildNode(JcrConstants.JCR_SYSTEM).getChildNode(NamespaceConstants.REP_NAMESPACES);
+        NodeBuilder nsdata = namespaces.child(NamespaceConstants.REP_NSDATA);
+        //adding a prefix without any mapping to an URI
+        Iterable<String> prefixes = 
Objects.requireNonNull(nsdata.getProperty(NamespaceConstants.REP_PREFIXES)).getValue(STRINGS);
+        List<String> newValue = new ArrayList<>();
+        prefixes.forEach(newValue::add);
+        newValue.add("foo");
+        nsdata.setProperty(NamespaceConstants.REP_PREFIXES, newValue, STRINGS);
+        store.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        store.runBackgroundOperations();
+        //missing information: no automatic fix
+        testCmd(new String[] { MongoUtils.URL, "--analyse" }, new String[] { 
"This namespace registry model is inconsistent. The inconsistency can NOT be 
fixed." });
+        //consistent after removal of unmapped data.
+        testCmd(new String[] { MongoUtils.URL, "--analyse", "--prune" }, new 
String[] { "This namespace registry model is consistent" });
+        //consistent with supplied complete information.
+        testCmd(new String[] { MongoUtils.URL, "--analyse", "--mappings",  
"foo=urn:foo" }, new String[] { "This namespace registry model is consistent, 
containing the following mappings from prefixes to namespace uris:", "foo -> 
urn:foo" });
+        testCmd(new String[] { MongoUtils.URL, "--fix", "--read-write" }, new 
String[] { "This namespace registry model is inconsistent. The inconsistency 
can NOT be fixed." });
+        testCmd(new String[] { MongoUtils.URL, "--fix", "--read-write", 
"--prune" }, new String[] { "This namespace registry model is consistent" });
+    }
+
+    @Test
+    public void breakAndFixDanglingUri() throws Exception {
+        NodeBuilder rootBuilder = store.getRoot().builder();
+        NodeBuilder namespaces = 
rootBuilder.getChildNode(JcrConstants.JCR_SYSTEM).getChildNode(NamespaceConstants.REP_NAMESPACES);
+        NodeBuilder nsdata = namespaces.child(NamespaceConstants.REP_NSDATA);
+        //adding an URI without any mapping to a prefix
+        Iterable<String> prefixes = 
Objects.requireNonNull(nsdata.getProperty(NamespaceConstants.REP_URIS)).getValue(STRINGS);
+        List<String> newValue = new ArrayList<>();
+        prefixes.forEach(newValue::add);
+        newValue.add("urn:foo");
+        nsdata.setProperty(NamespaceConstants.REP_URIS, newValue, STRINGS);
+        store.merge(rootBuilder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        store.runBackgroundOperations();
+        //missing information: no automatic fix
+        testCmd(new String[] { MongoUtils.URL, "--analyse" }, new String[] { 
"This namespace registry model is inconsistent. The inconsistency can NOT be 
fixed." });
+        //consistent after removal of unmapped data.
+        testCmd(new String[] { MongoUtils.URL, "--analyse", "--prune" }, new 
String[] { "This namespace registry model is consistent" });
+        //consistent with supplied complete information.
+        testCmd(new String[] { MongoUtils.URL, "--analyse", "--mappings",  
"foo=urn:foo" }, new String[] { "This namespace registry model is consistent, 
containing the following mappings from prefixes to namespace uris:", "foo -> 
urn:foo" });
+        testCmd(new String[] { MongoUtils.URL, "--fix", "--read-write" }, new 
String[] { "This namespace registry model is inconsistent. The inconsistency 
can NOT be fixed." });
+        testCmd(new String[] { MongoUtils.URL, "--fix", "--read-write", 
"--prune" }, new String[] { "This namespace registry model is consistent" });
+    }
+
     @Test
     public void mappings() throws Exception {
         testCmd(new String[] { MongoUtils.URL, "--analyse" }, new String[] { 
"This namespace registry model is consistent"});


Reply via email to