This is an automated email from the ASF dual-hosted git repository.

baedke pushed a commit to branch OAK-11617-2
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git


The following commit(s) were added to refs/heads/OAK-11617-2 by this push:
     new 5a5a2bf7d3 OAK-11617: Provide oak-run commands to analyze and fix 
inconsistencies in the namespace registry
5a5a2bf7d3 is described below

commit 5a5a2bf7d3416e23ab5df87f7c53f0ba788f6179
Author: Manfred Baedke <[email protected]>
AuthorDate: Wed Jun 11 17:23:49 2025 +0200

    OAK-11617: Provide oak-run commands to analyze and fix inconsistencies in 
the namespace registry
    
    First working version.
---
 .../oak/plugins/name/NamespaceRegistryModel.java   | 398 +++++++++++++++++++++
 .../plugins/name/ReadOnlyNamespaceRegistry.java    |  63 +---
 .../name/ReadWriteNamespaceRegistryTest.java       | 179 +++++++++
 .../apache/jackrabbit/oak/run/AvailableModes.java  |   1 +
 .../oak/run/NamespaceRegistryCommand.java          | 155 ++++++++
 .../oak/run/NamespaceRegistryOptions.java          | 111 ++++++
 6 files changed, 852 insertions(+), 55 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..c9eae34f2f
--- /dev/null
+++ 
b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/name/NamespaceRegistryModel.java
@@ -0,0 +1,398 @@
+/*
+ * 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.Collections;
+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 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;
+
+public final class NamespaceRegistryModel {
+
+    //private static final Logger LOG = 
LoggerFactory.getLogger(NamespaceRegistryModel.class);
+
+    private final Map<String, String> prefixToNamespaceMap;
+    private final Map<String, String> namespaceToPrefixMap;
+
+    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> namespaceToPrefixMap) {
+        // 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.namespaceToPrefixMap = new HashMap<>(namespaceToPrefixMap);
+        mappedPrefixes = this.prefixToNamespaceMap.keySet();
+        mappedNamespacesEncoded = this.namespaceToPrefixMap.keySet();
+        mappedToPrefixes = new HashSet<>(namespaceToPrefixMap.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));;
+        refresh();
+    }
+
+    private void refresh() {
+        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 = 
namespaceToPrefixMap.get(Namespaces.encodeUri(prefixToNamespaceMap.get(prefix)));
+                if (revMapped == null || !revMapped.equals(prefix)) {
+                    doesRoundtrip = false;
+                    break;
+                }
+            }
+            if (doesRoundtrip) {
+                for (String ns : mappedNamespacesEncoded) {
+                    String revMapped = 
Namespaces.encodeUri(prefixToNamespaceMap.get(namespaceToPrefixMap.get(ns)));
+                    if (revMapped == null || !revMapped.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();
+        }
+    }
+
+    public static @NotNull NamespaceRegistryModel create(@NotNull Root root) {
+        Tree rootTree = root.getTree("/");
+        Tree namespaces = rootTree.getChild( JcrConstants.JCR_SYSTEM 
).getChild(REP_NAMESPACES);
+        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);
+    }
+
+    public NamespaceRegistryModel tryRegistryRepair() {
+        return tryRegistryRepair(Collections.emptyMap());
+    }
+
+    //TODO handle additionalPrefixToUrisMappings
+    public NamespaceRegistryModel tryRegistryRepair(@NotNull Map<String, 
String> additionalPrefixToUrisMappings) {
+        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 : 
namespaceToPrefixMap.entrySet()) {
+                        if (entry.getValue().equals(prefix)) {
+                            fixedRegisteredPrefixesList.add(prefix);
+                            fixedPrefixToNamespaceMap.put(prefix, 
Text.unescape(entry.getKey()));
+                            break;
+                        }
+                    }
+                }
+            }
+            for (String prefix : additionalPrefixToUrisMappings.keySet()) {
+                fixedRegisteredPrefixesList.add(prefix);
+                fixedPrefixToNamespaceMap.put(prefix, 
additionalPrefixToUrisMappings.get(prefix));
+            }
+            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, 
namespaceToPrefixMap.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;
+                        }
+                    }
+                }
+            }
+            for (Map.Entry<String, String> entry : 
additionalPrefixToUrisMappings.entrySet()) {
+                String prefix = entry.getKey();
+                String uri = entry.getValue();
+                String encodedUri = Namespaces.encodeUri(uri);
+                if (!fixedRegisteredPrefixesList.contains(prefix)) {
+                    fixedRegisteredPrefixesList.add(prefix);
+                }
+                if 
(!fixedRegisteredNamespacesEncodedList.contains(encodedUri)) {
+                    fixedRegisteredNamespacesEncodedList.add(encodedUri);
+                }
+                fixedPrefixToNamespaceMap.put(prefix, uri);
+                fixedNamespaceToPrefixMap.put(encodedUri, prefix);
+            }
+            return new NamespaceRegistryModel(fixedRegisteredPrefixesList, 
fixedRegisteredNamespacesEncodedList,
+                    fixedPrefixToNamespaceMap, fixedNamespaceToPrefixMap);
+        }
+        return this;
+    }
+
+    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 : 
namespaceToPrefixMap.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);
+        refresh();
+        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.
+    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.
+    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 : 
namespaceToPrefixMap.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());
+    }
+
+    public void dump() throws IOException {
+        dump(System.out);
+    }
+
+    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..4eed0daf2e 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
@@ -60,7 +57,7 @@ public class ReadOnlyNamespaceRegistry
         this.namespaces = root.getTree(NAMESPACES_PATH);
         this.nsdata = namespaces.getChild(REP_NSDATA);
         if (!CONSISTENCY_CHECKED) {
-            checkConsistency();
+            checkConsistency(root);
         }
     }
 
@@ -130,56 +127,12 @@ 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());
-        }
-        CONSISTENCY_CHECKED = true;
+    public boolean checkConsistency(Root root) throws IllegalStateException {
+        return createNamespaceRegistryModel(root).isConsistent();
+    }
+
+    public NamespaceRegistryModel createNamespaceRegistryModel(Root root) {
+        return NamespaceRegistryModel.create(root);
     }
+
 }
diff --git 
a/oak-it/src/test/java/org/apache/jackrabbit/oak/plugins/name/ReadWriteNamespaceRegistryTest.java
 
b/oak-it/src/test/java/org/apache/jackrabbit/oak/plugins/name/ReadWriteNamespaceRegistryTest.java
index 30d70c9e46..36f161539c 100644
--- 
a/oak-it/src/test/java/org/apache/jackrabbit/oak/plugins/name/ReadWriteNamespaceRegistryTest.java
+++ 
b/oak-it/src/test/java/org/apache/jackrabbit/oak/plugins/name/ReadWriteNamespaceRegistryTest.java
@@ -16,10 +16,16 @@
 */
 package org.apache.jackrabbit.oak.plugins.name;
 
+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.assertTrue;
 import static org.junit.Assert.fail;
 
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
 import java.util.List;
 
 import javax.jcr.NamespaceException;
@@ -29,11 +35,16 @@ import org.apache.jackrabbit.oak.InitialContent;
 import org.apache.jackrabbit.oak.Oak;
 import org.apache.jackrabbit.oak.OakBaseTest;
 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.commons.collections.SetUtils;
 import org.apache.jackrabbit.oak.commons.junit.LogCustomizer;
 import org.apache.jackrabbit.oak.fixture.NodeStoreFixture;
+import org.apache.jackrabbit.oak.plugins.memory.PropertyBuilder;
 import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider;
+
 import org.junit.Test;
 import org.slf4j.event.Level;
 
@@ -106,6 +117,174 @@ public class ReadWriteNamespaceRegistryTest extends 
OakBaseTest {
         }
     }
 
+    @Test
+    public void testNamespaceRegistryModel() throws Exception {
+        ContentSession session = createContentSession();
+        Root root = session.getLatestRoot();
+        ReadWriteNamespaceRegistry registry = (ReadWriteNamespaceRegistry) 
getNamespaceRegistry(session, 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);
+
+        assertTrue(registry.checkConsistency(root));
+        NamespaceRegistryModel model = 
registry.createNamespaceRegistryModel(root);
+        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 = registry.createNamespaceRegistryModel(root);
+        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());
+
+        NamespaceRegistryModel fixedModel = model.tryRegistryRepair();
+        assertFalse(fixedModel.isConsistent());
+        assertFalse(fixedModel.isFixable());
+
+        out = new ByteArrayOutputStream();
+        fixedModel.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 = registry.createNamespaceRegistryModel(root);
+        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."));
+
+        fixedModel = model.tryRegistryRepair();
+        assertTrue(fixedModel.isConsistent());
+        assertTrue(fixedModel.isFixable());
+
+        out = new ByteArrayOutputStream();
+        fixedModel.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 = registry.createNamespaceRegistryModel(root);
+        assertFalse(model.isConsistent());
+        assertFalse(model.isFixable());
+
+        assertEquals(0, model.getDanglingPrefixes().size());
+        assertEquals(1, model.getDanglingEncodedNamespaceUris().size());
+        assertEquals(1, model.getRepairedMappings().size());
+
+        fixedModel = model.tryRegistryRepair();
+        assertFalse(fixedModel.isConsistent());
+        assertFalse(fixedModel.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 = registry.createNamespaceRegistryModel(root);
+        assertFalse(model.isConsistent());
+        assertTrue(model.isFixable());
+
+        assertEquals(0, model.getDanglingPrefixes().size());
+        assertEquals(0, model.getDanglingEncodedNamespaceUris().size());
+        assertEquals(2, model.getRepairedMappings().size());
+
+        fixedModel = model.tryRegistryRepair();
+        assertTrue(fixedModel.isConsistent());
+        assertTrue(fixedModel.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 = registry.createNamespaceRegistryModel(root);
+        assertFalse(model.isConsistent());
+        assertTrue(model.isFixable());
+
+        assertEquals(0, model.getDanglingPrefixes().size());
+        assertEquals(0, model.getDanglingEncodedNamespaceUris().size());
+        assertEquals(2, model.getRepairedMappings().size());
+
+        fixedModel = model.tryRegistryRepair();
+        assertTrue(fixedModel.isConsistent());
+        assertTrue(fixedModel.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 = registry.createNamespaceRegistryModel(root);
+        assertFalse(model.isConsistent());
+        assertTrue(model.isFixable());
+
+        assertEquals(0, model.getDanglingPrefixes().size());
+        assertEquals(0, model.getDanglingEncodedNamespaceUris().size());
+        assertEquals(2, model.getRepairedMappings().size());
+
+        fixedModel = model.tryRegistryRepair();
+        assertTrue(fixedModel.isConsistent());
+        assertTrue(fixedModel.isFixable());
+
+        // Apply the fixed model
+        fixedModel.apply(root);
+        assertTrue(registry.createNamespaceRegistryModel(root).isConsistent());
+        assertTrue(registry.checkConsistency(root));
+
+        assertEquals(0, fixedModel.getDanglingPrefixes().size());
+        assertEquals(0, fixedModel.getDanglingEncodedNamespaceUris().size());
+        assertEquals(0, fixedModel.getRepairedMappings().size());
+    }
+
     private static NamespaceRegistry getNamespaceRegistry(ContentSession 
session, Root root) {
         return new ReadWriteNamespaceRegistry(root) {
             @Override
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..4ae38a55ea
--- /dev/null
+++ 
b/oak-run/src/main/java/org/apache/jackrabbit/oak/run/NamespaceRegistryCommand.java
@@ -0,0 +1,155 @@
+/*
+ * 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.JcrConstants;
+import org.apache.jackrabbit.oak.Oak;
+import org.apache.jackrabbit.oak.api.ContentRepository;
+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.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.security.internal.SecurityProviderBuilder;
+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.security.OpenSecurityProvider;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.jcr.SimpleCredentials;
+import java.io.IOException;
+
+import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
+import static 
org.apache.jackrabbit.oak.spi.namespace.NamespaceConstants.REP_NAMESPACES;
+
+public class NamespaceRegistryCommand implements Command {
+
+    public static final String NAME = "namespace-registry";
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(NamespaceRegistryCommand.class);
+    private final String SUMMARY = "Provides commands to analyse the integrity 
of the namespace registry and repair it if necessary.";
+
+    private Options opts;
+    private NamespaceRegistryOptions namespaceRegistryOpts;
+
+    @Override
+    public void execute(String... args) throws Exception {
+        OptionParser parser = new OptionParser();
+
+        opts = new Options();
+        opts.setCommandName(NAME);
+        opts.setSummary(SUMMARY);
+        opts.setConnectionString(CommonOptions.DEFAULT_CONNECTION_STRING);
+        opts.registerOptionsFactory(NamespaceRegistryOptions.FACTORY);
+        opts.parseAndConfigure(parser, args);
+
+        namespaceRegistryOpts = 
opts.getOptionBean(NamespaceRegistryOptions.class);
+
+        try (Closer closer = Utils.createCloserWithShutdownHook()) {
+
+            NodeStoreFixture fixture = NodeStoreFixtureProvider.create(opts);
+            closer.register(fixture);
+
+            if (!checkParameters(namespaceRegistryOpts, opts, fixture, 
parser)) {
+                return;
+            }
+            doExecute(fixture, namespaceRegistryOpts, opts, closer);
+        } catch (Throwable e) {
+            LOG.error("Error occurred while performing namespace registry 
operation", e);
+            e.printStackTrace(System.err);
+        }
+    }
+
+    private static boolean checkParameters(NamespaceRegistryOptions 
namespaceRegistryOptions,
+                                           Options opts,
+                                           NodeStoreFixture fixture,
+                                           OptionParser parser) 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, Options opts, Closer closer)
+            throws Exception {
+
+        boolean analyse = namespaceRegistryOptions.analyse();
+        boolean fix = namespaceRegistryOptions.fix();
+        //TODO decide whether admin credentials should be required for this 
command
+        NodeStore store = fixture.getStore();
+        NodeState rootState = store.getRoot();
+        Oak oak = new 
Oak(store).with(SecurityProviderBuilder.newBuilder().build());
+        //Oak oak = new Oak(fixture.getStore()).with(new 
OpenSecurityProvider());
+        ContentRepository cr = oak.createContentRepository();
+        ContentSession contentSession = cr.login(new 
SimpleCredentials("admin", "admin".toCharArray()), null);
+        Root root = contentSession.getLatestRoot();
+        ReadWriteNamespaceRegistry namespaceRegistry = new 
ReadWriteNamespaceRegistry(root) {
+            @Override
+            protected Root getWriteRoot() {
+                return root;
+            }
+       };
+       if (analyse || fix) {
+            NamespaceRegistryModel registryModel = 
namespaceRegistry.createNamespaceRegistryModel(root);
+            if (fix) {
+                if (registryModel.isConsistent()) {
+                    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();
+                    store.merge(rootState.builder(), EmptyHook.INSTANCE, 
CommitInfo.EMPTY);
+                    repaired.dump();
+                } else {
+                    registryModel.dump();
+                }
+            } 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..a2b870e3b5
--- /dev/null
+++ 
b/oak-run/src/main/java/org/apache/jackrabbit/oak/run/NamespaceRegistryOptions.java
@@ -0,0 +1,111 @@
+/*
+ * 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.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;
+
+    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.");
+        actionOpts = Set.of(analyseOpt, fixOpt);
+        operationNames = collectionOperationNames(actionOpts);
+//        userOpt = parser
+//                .accepts("user", "User name").withOptionalArg()
+//                .defaultsTo("admin");
+//        passwordOpt = parser
+//                .accepts("password", "Password").withOptionalArg()
+//                .defaultsTo("admin");
+    }
+
+    @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 String getUser() {
+//        return userOpt.value(options);
+//    }
+//
+//    public String getPassword() {
+//        return passwordOpt.value(options);
+//    }
+
+    public boolean anyActionSelected() {
+        for (OptionSpec spec : actionOpts) {
+            if (options.has(spec)){
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public boolean analyse() {
+        return  options.has(analyseOpt);
+    }
+
+    public boolean fix() {
+        return  options.has(fixOpt);
+    }
+
+    private static Set<String> collectionOperationNames(Set<OptionSpec<Void>> 
actionOpts) {
+        Set<String> result = new HashSet<>();
+        for (OptionSpec spec : actionOpts){
+            result.addAll(spec.options());
+        }
+        return result;
+    }
+}


Reply via email to