Eliminate use of SchemaNode.getPath() in YII deserializer
[netconf.git] / restconf / restconf-nb-rfc8040 / src / main / java / org / opendaylight / restconf / nb / rfc8040 / utils / parser / YangInstanceIdentifierDeserializer.java
index 553f86da8c0b9fb7b26d5607039352ed3988ccb6..de0755e827691c95650fb77f105970c2c423e5f3 100644 (file)
@@ -7,44 +7,45 @@
  */
 package org.opendaylight.restconf.nb.rfc8040.utils.parser;
 
+import static com.google.common.base.Verify.verifyNotNull;
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.base.CharMatcher;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.text.ParseException;
 import java.util.ArrayList;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
+import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.util.RestUtil;
-import org.opendaylight.restconf.common.util.RestconfSchemaUtil;
+import org.opendaylight.restconf.nb.rfc8040.ApiPath;
+import org.opendaylight.restconf.nb.rfc8040.ApiPath.ListInstance;
+import org.opendaylight.restconf.nb.rfc8040.ApiPath.Step;
 import org.opendaylight.restconf.nb.rfc8040.codecs.RestCodec;
 import org.opendaylight.yangtools.yang.common.ErrorTag;
 import org.opendaylight.yangtools.yang.common.ErrorType;
 import org.opendaylight.yangtools.yang.common.QName;
-import org.opendaylight.yangtools.yang.common.YangNames;
+import org.opendaylight.yangtools.yang.common.QNameModule;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
-import org.opendaylight.yangtools.yang.model.api.ContainerLike;
-import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
-import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
-import org.opendaylight.yangtools.yang.model.api.Module;
 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
-import org.opendaylight.yangtools.yang.model.api.SchemaNode;
 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
+import org.opendaylight.yangtools.yang.model.api.stmt.IdentityEffectiveStatement;
+import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
@@ -53,120 +54,113 @@ import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
  * Deserializer for {@link String} to {@link YangInstanceIdentifier} for restconf.
  */
 public final class YangInstanceIdentifierDeserializer {
-    private static final CharMatcher IDENTIFIER_PREDICATE =
-            CharMatcher.noneOf(ParserConstants.RFC3986_RESERVED_CHARACTERS).precomputed();
-    private static final CharMatcher PERCENT_ENCODING = CharMatcher.is('%');
-    // position of the first encoded char after percent sign in percent encoded string
-    private static final int FIRST_ENCODED_CHAR = 1;
-    // position of the last encoded char after percent sign in percent encoded string
-    private static final int LAST_ENCODED_CHAR = 3;
-    // percent encoded radix for parsing integers
-    private static final int PERCENT_ENCODED_RADIX = 16;
+    private final @NonNull EffectiveModelContext schemaContext;
+    private final @NonNull ApiPath apiPath;
 
-    private final EffectiveModelContext schemaContext;
-    private final String data;
-
-    private DataSchemaContextNode<?> current;
-    private int offset;
-
-    private YangInstanceIdentifierDeserializer(final EffectiveModelContext schemaContext, final String data) {
+    private YangInstanceIdentifierDeserializer(final EffectiveModelContext schemaContext, final ApiPath apiPath) {
         this.schemaContext = requireNonNull(schemaContext);
-        this.data = requireNonNull(data);
-        current = DataSchemaContextTree.from(schemaContext).getRoot();
+        this.apiPath = requireNonNull(apiPath);
     }
 
     /**
-     * Method to create {@link Iterable} from {@link PathArgument} which are parsing from data by {@link SchemaContext}.
+     * Method to create {@link List} from {@link PathArgument} which are parsing from data by {@link SchemaContext}.
      *
      * @param schemaContext for validate of parsing path arguments
      * @param data path to data, in URL string form
      * @return {@link Iterable} of {@link PathArgument}
+     * @throws RestconfDocumentedException the path is not valid
      */
     public static List<PathArgument> create(final EffectiveModelContext schemaContext, final String data) {
-        return new YangInstanceIdentifierDeserializer(schemaContext, data).parse();
+        final ApiPath path;
+        try {
+            path = ApiPath.parse(requireNonNull(data));
+        } catch (ParseException e) {
+            throw new RestconfDocumentedException("Invalid path '" + data + "' at offset " + e.getErrorOffset(),
+                ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e);
+        }
+        return create(schemaContext, path);
     }
 
-    private List<PathArgument> parse() {
-        final List<PathArgument> path = new ArrayList<>();
+    public static List<PathArgument> create(final EffectiveModelContext schemaContext, final ApiPath path) {
+        return new YangInstanceIdentifierDeserializer(schemaContext, path).parse();
+    }
 
-        while (!allCharsConsumed()) {
-            validArg();
-            final QName qname = prepareQName();
+    private ImmutableList<PathArgument> parse() {
+        final var steps = apiPath.steps();
+        if (steps.isEmpty()) {
+            return ImmutableList.of();
+        }
 
-            // this is the last identifier (input is consumed) or end of identifier (slash)
-            if (allCharsConsumed() || currentChar() == '/') {
-                prepareIdentifier(qname, path);
-                path.add(current == null ? NodeIdentifier.create(qname) : current.getIdentifier());
-            } else if (currentChar() == '=') {
-                if (nextContextNode(qname, path).getDataSchemaNode() instanceof ListSchemaNode) {
-                    prepareNodeWithPredicates(qname, path, (ListSchemaNode) current.getDataSchemaNode());
-                } else {
-                    prepareNodeWithValue(qname, path);
+        final List<PathArgument> path = new ArrayList<>();
+        DataSchemaContextNode<?> parentNode = DataSchemaContextTree.from(schemaContext).getRoot();
+        QNameModule parentNs = null;
+        for (Step step : steps) {
+            final var module = step.module();
+            final QNameModule ns;
+            if (module == null) {
+                if (parentNs == null) {
+                    throw new RestconfDocumentedException(
+                        "First member must use namespace-qualified form, '" + step.identifier() + "' does not",
+                        ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
                 }
+                ns = parentNs;
             } else {
-                throw getParsingCharFailedException();
+                ns = resolveNamespace(module);
             }
-        }
-
-        return ImmutableList.copyOf(path);
-    }
-
-    private void prepareNodeWithPredicates(final QName qname, final List<PathArgument> path,
-            final ListSchemaNode listSchemaNode) {
-        checkValid(listSchemaNode != null, ErrorTag.MALFORMED_MESSAGE, "Data schema node is null");
 
-        final Iterator<QName> keys = listSchemaNode.getKeyDefinition().iterator();
-        final ImmutableMap.Builder<QName, Object> values = ImmutableMap.builder();
-
-        // skip already expected equal sign
-        skipCurrentChar();
-
-        // read key value separated by comma
-        while (keys.hasNext() && !allCharsConsumed() && currentChar() != '/') {
-
-            // empty key value
-            if (currentChar() == ',') {
-                values.put(keys.next(), "");
-                skipCurrentChar();
-                continue;
+            final QName qname = step.identifier().bindTo(ns);
+            final DataSchemaContextNode<?> childNode = nextContextNode(parentNode, qname, path);
+            final PathArgument pathArg;
+            if (step instanceof ListInstance) {
+                final var values = ((ListInstance) step).keyValues();
+                final var schema = childNode.getDataSchemaNode();
+                final var absolute = Absolute.of(parentNode.getIdentifier().getNodeType(), qname);
+                pathArg = schema instanceof ListSchemaNode
+                    ? prepareNodeWithPredicates(qname, (ListSchemaNode) schema, absolute, values)
+                        : prepareNodeWithValue(qname, schema, absolute, values);
+            } else if (childNode != null) {
+                RestconfDocumentedException.throwIf(childNode.isKeyedEntry(),
+                    ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE,
+                    "Entry '%s' requires key or value predicate to be present.", qname);
+                pathArg = childNode.getIdentifier();
+            } else {
+                // FIXME: this should be a hard error here, as we cannot resolve the node correctly!
+                pathArg = NodeIdentifier.create(qname);
             }
 
-            // check if next value is parsable
-            checkValid(IDENTIFIER_PREDICATE.matches(currentChar()), ErrorTag.MALFORMED_MESSAGE,
-                    "Value that starts with character %c is not parsable.", currentChar());
-
-            // parse value
-            final QName key = keys.next();
-            Optional<DataSchemaNode> leafSchemaNode = listSchemaNode.findDataChildByName(key);
-            RestconfDocumentedException.throwIf(leafSchemaNode.isEmpty(), ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT,
-                    "Schema not found for %s", key);
+            path.add(pathArg);
+            parentNode = childNode;
+            parentNs = ns;
+        }
 
-            final String value = findAndParsePercentEncoded(nextIdentifierFromNextSequence(IDENTIFIER_PREDICATE));
-            final Object valueByType = prepareValueByType(leafSchemaNode.get(), value);
-            values.put(key, valueByType);
+        return ImmutableList.copyOf(path);
+    }
 
-            // skip comma
-            if (keys.hasNext() && !allCharsConsumed() && currentChar() == ',') {
-                skipCurrentChar();
-            }
+    private NodeIdentifierWithPredicates prepareNodeWithPredicates(final QName qname,
+            final @NonNull ListSchemaNode schema, final Absolute absolute, final List<@NonNull String> keyValues) {
+        final var keyDef = schema.getKeyDefinition();
+        final var keySize = keyDef.size();
+        final var varSize = keyValues.size();
+        if (keySize != varSize) {
+            throw new RestconfDocumentedException(
+                "Schema for " + qname + " requires " + keySize + " key values, " + varSize + " supplied",
+                ErrorType.PROTOCOL, keySize > varSize ? ErrorTag.MISSING_ATTRIBUTE : ErrorTag.UNKNOWN_ATTRIBUTE);
         }
 
-        // the last key is considered to be empty
-        if (keys.hasNext()) {
-            // at this point, it must be true that current char is '/' or all chars have already been consumed
-            values.put(keys.next(), "");
-
-            // there should be no more missing keys
-            RestconfDocumentedException.throwIf(keys.hasNext(), ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE,
-                    "Cannot parse input identifier '%s'. Key value is missing for QName: %s", data, qname);
+        final var values = ImmutableMap.<QName, Object>builderWithExpectedSize(keySize);
+        for (int i = 0; i < keySize; ++i) {
+            final QName keyName = keyDef.get(i);
+            final List<QName> qNames = new ArrayList<>(absolute.getNodeIdentifiers());
+            qNames.add(keyName);
+            final Absolute path = Absolute.of(qNames);
+            values.put(keyName, prepareValueByType(schema.getDataChildByName(keyName), path, keyValues.get(i)));
         }
 
-        path.add(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(qname, values.build()));
+        return NodeIdentifierWithPredicates.of(qname, values.build());
     }
 
-    private Object prepareValueByType(final DataSchemaNode schemaNode, final String value) {
-        Object decoded;
-
+    private Object prepareValueByType(final DataSchemaNode schemaNode, final Absolute absolute,
+            final @NonNull String value) {
         TypeDefinition<? extends TypeDefinition<?>> typedef;
         if (schemaNode instanceof LeafListSchemaNode) {
             typedef = ((LeafListSchemaNode) schemaNode).getType();
@@ -175,262 +169,96 @@ public final class YangInstanceIdentifierDeserializer {
         }
         final TypeDefinition<?> baseType = RestUtil.resolveBaseTypeFrom(typedef);
         if (baseType instanceof LeafrefTypeDefinition) {
-            typedef = SchemaInferenceStack.ofInstantiatedPath(schemaContext, schemaNode.getPath())
-                .resolveLeafref((LeafrefTypeDefinition) baseType);
-        }
-        decoded = RestCodec.from(typedef, null, schemaContext).deserialize(value);
-        if (decoded == null && typedef instanceof IdentityrefTypeDefinition) {
-            decoded = toIdentityrefQName(value, schemaNode);
+            typedef = SchemaInferenceStack.of(schemaContext, absolute).resolveLeafref((LeafrefTypeDefinition) baseType);
         }
-        return decoded;
-    }
 
-    private QName prepareQName() {
-        checkValidIdentifierStart();
-        final String preparedPrefix = nextIdentifierFromNextSequence(ParserConstants.YANG_IDENTIFIER_PART);
-        final String prefix;
-        final String localName;
-
-        if (allCharsConsumed()) {
-            return getQNameOfDataSchemaNode(preparedPrefix);
+        if (typedef instanceof IdentityrefTypeDefinition) {
+            return toIdentityrefQName(value, schemaNode);
         }
-
-        switch (currentChar()) {
-            case '/':
-            case '=':
-                prefix = preparedPrefix;
-                return getQNameOfDataSchemaNode(prefix);
-            case ':':
-                prefix = preparedPrefix;
-                skipCurrentChar();
-                checkValidIdentifierStart();
-                localName = nextIdentifierFromNextSequence(ParserConstants.YANG_IDENTIFIER_PART);
-
-                if (!allCharsConsumed() && currentChar() == '=') {
-                    return getQNameOfDataSchemaNode(localName);
-                } else {
-                    final Module module = moduleForPrefix(prefix);
-                    RestconfDocumentedException.throwIf(module == null, ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT,
-                            "Failed to lookup for module with name '%s'.", prefix);
-                    return QName.create(module.getQNameModule(), localName);
-                }
-            default:
-                throw getParsingCharFailedException();
+        try {
+            return RestCodec.from(typedef, null, schemaContext).deserialize(value);
+        } catch (IllegalArgumentException e) {
+            throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
+                ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
         }
     }
 
-    private void prepareNodeWithValue(final QName qname, final List<PathArgument> path) {
-        skipCurrentChar();
-        final String value = nextIdentifierFromNextSequence(IDENTIFIER_PREDICATE);
-
-        // exception if value attribute is missing
-        RestconfDocumentedException.throwIf(value.isEmpty(), ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE,
-                "Cannot parse input identifier '%s' - value is missing for QName: %s.", data, qname);
-        final DataSchemaNode dataSchemaNode = current.getDataSchemaNode();
-        final Object valueByType = prepareValueByType(dataSchemaNode, findAndParsePercentEncoded(value));
-        path.add(new YangInstanceIdentifier.NodeWithValue<>(qname, valueByType));
+    private NodeWithValue<?> prepareNodeWithValue(final QName qname, final DataSchemaNode schema,
+            final Absolute absolute, final List<String> keyValues) {
+        // TODO: qname should be always equal to schema.getQName(), right?
+        return new NodeWithValue<>(qname, prepareValueByType(schema, absolute,
+            // FIXME: ahem: we probably want to do something differently here
+            keyValues.get(0)));
     }
 
-    private void prepareIdentifier(final QName qname, final List<PathArgument> path) {
-        final DataSchemaContextNode<?> currentNode = nextContextNode(qname, path);
-        if (currentNode != null) {
-            checkValid(!currentNode.isKeyedEntry(), ErrorTag.MISSING_ATTRIBUTE,
-                    "Entry '%s' requires key or value predicate to be present.", qname);
-        }
-    }
-
-    @SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH",
-            justification = "code does check for null 'current' but FB doesn't recognize it")
-    private DataSchemaContextNode<?> nextContextNode(final QName qname, final List<PathArgument> path) {
-        final DataSchemaContextNode<?> initialContext = current;
-        final DataSchemaNode initialDataSchema = initialContext.getDataSchemaNode();
-
-        current = initialContext.getChild(qname);
-
-        if (current == null) {
-            final Optional<Module> module = schemaContext.findModule(qname.getModule());
+    private DataSchemaContextNode<?> nextContextNode(final DataSchemaContextNode<?> parent, final QName qname,
+            final List<PathArgument> path) {
+        final var found = parent.getChild(qname);
+        if (found == null) {
+            // FIXME: why are we making this special case here, especially with ...
+            final var module = schemaContext.findModule(qname.getModule());
             if (module.isPresent()) {
                 for (final RpcDefinition rpcDefinition : module.get().getRpcs()) {
+                    // ... this comparison?
                     if (rpcDefinition.getQName().getLocalName().equals(qname.getLocalName())) {
                         return null;
                     }
                 }
             }
-            if (findActionDefinition(initialDataSchema, qname.getLocalName()).isPresent()) {
+            if (findActionDefinition(parent.getDataSchemaNode(), qname.getLocalName()).isPresent()) {
                 return null;
             }
-        }
-        checkValid(current != null, ErrorTag.MALFORMED_MESSAGE, "'%s' is not correct schema node identifier.", qname);
-        while (current.isMixin()) {
-            path.add(current.getIdentifier());
-            current = current.getChild(qname);
-        }
-        return current;
-    }
-
-    private Module moduleForPrefix(final String prefix) {
-        return schemaContext.findModules(prefix).stream().findFirst().orElse(null);
-    }
-
-    private boolean allCharsConsumed() {
-        return offset == data.length();
-    }
-
-    private void checkValid(final boolean condition, final ErrorTag errorTag, final String errorMsg) {
-        if (!condition) {
-            throw createParsingException(errorTag, errorMsg);
-        }
-    }
-
-    private void checkValid(final boolean condition, final ErrorTag errorTag, final String fmt, final Object arg) {
-        if (!condition) {
-            throw createParsingException(errorTag, String.format(fmt, arg));
-        }
-    }
-
-    private void checkValidIdentifierStart() {
-        checkValid(YangNames.IDENTIFIER_START.matches(currentChar()), ErrorTag.MALFORMED_MESSAGE,
-            "Identifier must start with character from set 'a-zA-Z_'");
-    }
-
-    private RestconfDocumentedException getParsingCharFailedException() {
-        return createParsingException(ErrorTag.MALFORMED_MESSAGE,
-            "Bad char '" + currentChar() + "' on the current position.");
-    }
-
-    private RestconfDocumentedException createParsingException(final ErrorTag errorTag, final String reason) {
-        return new RestconfDocumentedException(
-            "Could not parse Instance Identifier '" + data + "'. Offset: '" + offset + "' : Reason: " + reason,
-            ErrorType.PROTOCOL, errorTag);
-    }
-
-    private char currentChar() {
-        return data.charAt(offset);
-    }
-
-    private void skipCurrentChar() {
-        offset++;
-    }
-
-    private String nextIdentifierFromNextSequence(final CharMatcher matcher) {
-        final int start = offset;
-        while (!allCharsConsumed() && matcher.matches(currentChar())) {
-            skipCurrentChar();
-        }
-        return data.substring(start, offset);
-    }
-
-    private void validArg() {
-        // every identifier except of the first MUST start with slash
-        if (offset != 0) {
-            checkValid('/' == currentChar(), ErrorTag.MALFORMED_MESSAGE, "Identifier must start with '/'.");
-
-            // skip consecutive slashes, users often assume restconf URLs behave just as HTTP does by squashing
-            // multiple slashes into a single one
-            while (!allCharsConsumed() && '/' == currentChar()) {
-                skipCurrentChar();
-            }
-
-            // check if slash is not also the last char in identifier
-            checkValid(!allCharsConsumed(), ErrorTag.MALFORMED_MESSAGE, "Identifier cannot end with '/'.");
-        }
-    }
 
-    private QName getQNameOfDataSchemaNode(final String nodeName) {
-        final DataSchemaNode dataSchemaNode = current.getDataSchemaNode();
-        if (dataSchemaNode instanceof ContainerLike) {
-            return getQNameOfDataSchemaNode((ContainerLike) dataSchemaNode, nodeName);
-        } else if (dataSchemaNode instanceof ListSchemaNode) {
-            return getQNameOfDataSchemaNode((ListSchemaNode) dataSchemaNode, nodeName);
+            throw new RestconfDocumentedException("Schema for '" + qname + "' not found",
+                ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
         }
 
-        throw new UnsupportedOperationException("Unsupported schema node " + dataSchemaNode);
-    }
-
-    private static <T extends DataNodeContainer & SchemaNode & ActionNodeContainer> QName getQNameOfDataSchemaNode(
-            final T parent, final String nodeName) {
-        final Optional<? extends ActionDefinition> actionDef = findActionDefinition(parent, nodeName);
-        final SchemaNode node;
-        if (actionDef.isPresent()) {
-            node = actionDef.get();
-        } else {
-            node = RestconfSchemaUtil.findSchemaNodeInCollection(parent.getChildNodes(), nodeName);
+        var result = found;
+        while (result.isMixin()) {
+            path.add(result.getIdentifier());
+            result = verifyNotNull(result.getChild(qname), "Mixin %s is missing child for %s while resolving %s",
+                result, qname, found);
         }
-        return node.getQName();
+        return result;
     }
 
-    private static Optional<? extends ActionDefinition> findActionDefinition(final SchemaNode dataSchemaNode,
+    private static Optional<? extends ActionDefinition> findActionDefinition(final DataSchemaNode dataSchemaNode,
+            // FIXME: this should be using a namespace
             final String nodeName) {
-        requireNonNull(dataSchemaNode, "DataSchema Node must not be null.");
         if (dataSchemaNode instanceof ActionNodeContainer) {
             return ((ActionNodeContainer) dataSchemaNode).getActions().stream()
-                    .filter(actionDef -> actionDef.getQName().getLocalName().equals(nodeName)).findFirst();
+                    .filter(actionDef -> actionDef.getQName().getLocalName().equals(nodeName))
+                    .findFirst();
         }
         return Optional.empty();
     }
 
-    private static String findAndParsePercentEncoded(final String preparedPrefix) {
-        if (preparedPrefix.indexOf('%') == -1) {
-            return preparedPrefix;
-        }
-
-        // FIXME: this is extremely inefficient: we should be converting ranges of characters, not driven by
-        //        CharMatcher, but by String.indexOf()
-        final StringBuilder parsedPrefix = new StringBuilder(preparedPrefix);
-        while (PERCENT_ENCODING.matchesAnyOf(parsedPrefix)) {
-            final int percentCharPosition = PERCENT_ENCODING.indexIn(parsedPrefix);
-            parsedPrefix.replace(percentCharPosition, percentCharPosition + LAST_ENCODED_CHAR,
-                    String.valueOf((char) Integer.parseInt(parsedPrefix.substring(
-                            percentCharPosition + FIRST_ENCODED_CHAR, percentCharPosition + LAST_ENCODED_CHAR),
-                            PERCENT_ENCODED_RADIX)));
-        }
-
-        return parsedPrefix.toString();
-    }
-
     private QName toIdentityrefQName(final String value, final DataSchemaNode schemaNode) {
-        final String moduleName = toModuleName(value);
-        final String nodeName = toNodeName(value);
-        final Iterator<? extends Module> modulesIterator = schemaContext.findModules(moduleName).iterator();
-        if (!modulesIterator.hasNext()) {
-            throw new RestconfDocumentedException(String.format("Cannot decode value '%s' for identityref type "
-                    + "in %s. Make sure reserved characters such as comma, single-quote, double-quote, colon,"
-                    + " double-quote, space, and forward slash (,'\":\" /) are percent-encoded,"
-                    + " for example ':' is '%%3A'", value, current.getIdentifier().getNodeType()),
-                    ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT);
-        }
-        for (final IdentitySchemaNode identitySchemaNode : modulesIterator.next().getIdentities()) {
-            final QName qName = identitySchemaNode.getQName();
-            if (qName.getLocalName().equals(nodeName)) {
-                return qName;
-            }
-        }
-        return QName.create(schemaNode.getQName().getNamespace(), schemaNode.getQName().getRevision(), nodeName);
-    }
-
-    private static String toNodeName(final String str) {
-        final int idx = str.indexOf(':');
-        if (idx == -1) {
-            return str;
-        }
-
-        if (str.indexOf(':', idx + 1) != -1) {
-            return str;
+        final QNameModule namespace;
+        final String localName;
+        final int firstColon = value.indexOf(':');
+        if (firstColon != -1) {
+            namespace = resolveNamespace(value.substring(0, firstColon));
+            localName = value.substring(firstColon + 1);
+        } else {
+            namespace = schemaNode.getQName().getModule();
+            localName = value;
         }
 
-        return str.substring(idx + 1);
+        return schemaContext.getModuleStatement(namespace)
+            .streamEffectiveSubstatements(IdentityEffectiveStatement.class)
+            .map(IdentityEffectiveStatement::argument)
+            .filter(qname -> localName.equals(qname.getLocalName()))
+            .findFirst()
+            .orElseThrow(() -> new RestconfDocumentedException(
+                "No identity found for '" + localName + "' in namespace " + namespace,
+                ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
     }
 
-    private static String toModuleName(final String str) {
-        final int idx = str.indexOf(':');
-        if (idx == -1) {
-            return null;
-        }
-
-        if (str.indexOf(':', idx + 1) != -1) {
-            return null;
-        }
-
-        return str.substring(0, idx);
+    private @NonNull QNameModule resolveNamespace(final String moduleName) {
+        final var modules = schemaContext.findModules(moduleName);
+        RestconfDocumentedException.throwIf(modules.isEmpty(), ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT,
+            "Failed to lookup for module with name '%s'.", moduleName);
+        return modules.iterator().next().getQNameModule();
     }
 }