Use Optional.isEmpty()
[netconf.git] / restconf / restconf-nb-rfc8040 / src / main / java / org / opendaylight / restconf / nb / rfc8040 / utils / parser / YangInstanceIdentifierDeserializer.java
index 7f73387d1072785eeaa017a013da11233abe998f..de31eb364ddf2936c2b0629854ad6ee56af57119 100644 (file)
@@ -7,26 +7,22 @@
  */
 package org.opendaylight.restconf.nb.rfc8040.utils.parser;
 
-import static com.google.common.base.Preconditions.checkArgument;
 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.util.ArrayList;
 import java.util.Iterator;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Optional;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
-import org.opendaylight.restconf.common.errors.RestconfError;
+import org.opendaylight.restconf.common.errors.RestconfError.ErrorTag;
+import org.opendaylight.restconf.common.errors.RestconfError.ErrorType;
 import org.opendaylight.restconf.common.util.RestUtil;
 import org.opendaylight.restconf.common.util.RestconfSchemaUtil;
-import org.opendaylight.restconf.common.validation.RestconfValidationUtils;
 import org.opendaylight.restconf.nb.rfc8040.codecs.RestCodec;
-import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants;
-import org.opendaylight.restconf.nb.rfc8040.utils.parser.builder.ParserBuilderConstants;
-import org.opendaylight.yangtools.concepts.Codec;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
@@ -35,9 +31,10 @@ 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.ContainerSchemaNode;
+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;
@@ -52,132 +49,126 @@ import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
 import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
 
 /**
- * Deserializer for {@link String} to {@link YangInstanceIdentifier} for
- * restconf.
- *
+ * Deserializer for {@link String} to {@link YangInstanceIdentifier} for restconf.
  */
 public final class YangInstanceIdentifierDeserializer {
-
-    private YangInstanceIdentifierDeserializer() {
-        throw new UnsupportedOperationException("Util class.");
+    private static final CharMatcher IDENTIFIER_PREDICATE =
+            CharMatcher.noneOf(ParserConstants.RFC3986_RESERVED_CHARACTERS).precomputed();
+    private static final String PARSING_FAILED_MESSAGE = "Could not parse Instance Identifier '%s'. "
+            + "Offset: '%d' : Reason: ";
+    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 EffectiveModelContext schemaContext;
+    private final String data;
+
+    private DataSchemaContextNode<?> current;
+    private int offset;
+
+    private YangInstanceIdentifierDeserializer(final EffectiveModelContext schemaContext, final String data) {
+        this.schemaContext = requireNonNull(schemaContext);
+        this.data = requireNonNull(data);
+        current = DataSchemaContextTree.from(schemaContext).getRoot();
     }
 
     /**
-     * Method to create {@link Iterable} from {@link PathArgument} which are
-     * parsing from data by {@link SchemaContext}.
+     * Method to create {@link Iterable} from {@link PathArgument} which are parsing from data by {@link SchemaContext}.
      *
-     * @param schemaContext
-     *             for validate of parsing path arguments
-     * @param data
-     *             path to data
+     * @param schemaContext for validate of parsing path arguments
+     * @param data path to data, in URL string form
      * @return {@link Iterable} of {@link PathArgument}
      */
-    public static Iterable<PathArgument> create(final SchemaContext schemaContext, final String data) {
-        final List<PathArgument> path = new LinkedList<>();
-        final MainVarsWrapper variables = new YangInstanceIdentifierDeserializer.MainVarsWrapper(
-                data, DataSchemaContextTree.from(schemaContext).getRoot(),
-                YangInstanceIdentifierDeserializer.MainVarsWrapper.STARTING_OFFSET, schemaContext);
+    public static Iterable<PathArgument> create(final EffectiveModelContext schemaContext, final String data) {
+        return new YangInstanceIdentifierDeserializer(schemaContext, data).parse();
+    }
+
+    private List<PathArgument> parse() {
+        final List<PathArgument> path = new ArrayList<>();
 
-        while (!variables.allCharsConsumed()) {
-            variables.validArg();
-            final QName qname = prepareQName(variables);
+        while (!allCharsConsumed()) {
+            validArg();
+            final QName qname = prepareQName();
 
             // this is the last identifier (input is consumed) or end of identifier (slash)
-            if (variables.allCharsConsumed() || variables.currentChar() == RestconfConstants.SLASH) {
-                prepareIdentifier(qname, path, variables);
-                if (variables.getCurrent() == null) {
-                    path.add(NodeIdentifier.create(qname));
+            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 {
-                    path.add(variables.getCurrent().getIdentifier());
-                }
-            } else if (variables.currentChar() == ParserBuilderConstants.Deserializer.EQUAL) {
-                if (nextContextNode(qname, path, variables).getDataSchemaNode() instanceof ListSchemaNode) {
-                    prepareNodeWithPredicates(qname, path, variables,
-                            (ListSchemaNode) variables.getCurrent().getDataSchemaNode());
-                } else {
-                    prepareNodeWithValue(qname, path, variables);
+                    prepareNodeWithValue(qname, path);
                 }
             } else {
-                throw new IllegalArgumentException(
-                        "Bad char " + variables.currentChar() + " on position " + variables.getOffset() + ".");
+                throw getParsingCharFailedException();
             }
         }
 
         return ImmutableList.copyOf(path);
     }
 
-    private static void prepareNodeWithPredicates(final QName qname, final List<PathArgument> path,
-            final MainVarsWrapper variables, final ListSchemaNode listSchemaNode) {
-        variables.checkValid(listSchemaNode != null, "Data schema node is null");
+    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
-        variables.skipCurrentChar();
+        skipCurrentChar();
 
         // read key value separated by comma
-        while (keys.hasNext() && !variables.allCharsConsumed() && variables.currentChar() != RestconfConstants.SLASH) {
+        while (keys.hasNext() && !allCharsConsumed() && currentChar() != '/') {
 
             // empty key value
-            if (variables.currentChar() == ParserBuilderConstants.Deserializer.COMMA) {
-                values.put(keys.next(), ParserBuilderConstants.Deserializer.EMPTY_STRING);
-                variables.skipCurrentChar();
+            if (currentChar() == ',') {
+                values.put(keys.next(), "");
+                skipCurrentChar();
                 continue;
             }
 
             // check if next value is parsable
-            RestconfValidationUtils.checkDocumentedError(
-                    ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE.matches(variables.currentChar()),
-                    RestconfError.ErrorType.PROTOCOL,
-                    RestconfError.ErrorTag.MALFORMED_MESSAGE,
-                    ""
-            );
+            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);
-            if (!leafSchemaNode.isPresent()) {
-                throw new RestconfDocumentedException("Schema not found for " + key,
-                        RestconfError.ErrorType.PROTOCOL, RestconfError.ErrorTag.BAD_ELEMENT);
-            }
+            RestconfDocumentedException.throwIf(leafSchemaNode.isEmpty(), ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT,
+                    "Schema not found for %s", key);
 
-            final String value = findAndParsePercentEncoded(variables.nextIdentifierFromNextSequence(
-                    ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE));
-            final Object valueByType = prepareValueByType(leafSchemaNode.get(), value, variables);
+            final String value = findAndParsePercentEncoded(nextIdentifierFromNextSequence(IDENTIFIER_PREDICATE));
+            final Object valueByType = prepareValueByType(leafSchemaNode.get(), value);
             values.put(key, valueByType);
 
-
             // skip comma
-            if (keys.hasNext() && !variables.allCharsConsumed()
-                    && variables.currentChar() == ParserBuilderConstants.Deserializer.COMMA) {
-                variables.skipCurrentChar();
+            if (keys.hasNext() && !allCharsConsumed() && currentChar() == ',') {
+                skipCurrentChar();
             }
         }
 
         // the last key is considered to be empty
         if (keys.hasNext()) {
-            if (variables.allCharsConsumed() || variables.currentChar() == RestconfConstants.SLASH) {
-                values.put(keys.next(), ParserBuilderConstants.Deserializer.EMPTY_STRING);
-            }
+            // 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
-            RestconfValidationUtils.checkDocumentedError(
-                    !keys.hasNext(),
-                    RestconfError.ErrorType.PROTOCOL,
-                    RestconfError.ErrorTag.MISSING_ATTRIBUTE,
-                    "Key value missing for: " + qname
-            );
+            RestconfDocumentedException.throwIf(keys.hasNext(), ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE,
+                    "Cannot parse input identifier '%s'. Key value is missing for QName: %s", data, qname);
         }
 
-        path.add(new YangInstanceIdentifier.NodeIdentifierWithPredicates(qname, values.build()));
+        path.add(YangInstanceIdentifier.NodeIdentifierWithPredicates.of(qname, values.build()));
     }
 
-    private static Object prepareValueByType(final DataSchemaNode schemaNode, final String value,
-            final MainVarsWrapper vars) {
-        Object decoded = null;
+    private Object prepareValueByType(final DataSchemaNode schemaNode, final String value) {
+        Object decoded;
 
-        TypeDefinition<? extends TypeDefinition<?>> typedef = null;
+        TypeDefinition<? extends TypeDefinition<?>> typedef;
         if (schemaNode instanceof LeafListSchemaNode) {
             typedef = ((LeafListSchemaNode) schemaNode).getType();
         } else {
@@ -185,136 +176,80 @@ public final class YangInstanceIdentifierDeserializer {
         }
         final TypeDefinition<?> baseType = RestUtil.resolveBaseTypeFrom(typedef);
         if (baseType instanceof LeafrefTypeDefinition) {
-            typedef = SchemaContextUtil.getBaseTypeForLeafRef((LeafrefTypeDefinition) baseType, vars.getSchemaContext(),
+            typedef = SchemaContextUtil.getBaseTypeForLeafRef((LeafrefTypeDefinition) baseType, schemaContext,
                     schemaNode);
         }
-        final Codec<Object, Object> codec = RestCodec.from(typedef, null, vars.getSchemaContext());
-        decoded = codec.deserialize(value);
-        if (decoded == null) {
-            if (baseType instanceof IdentityrefTypeDefinition) {
-                decoded = toQName(value, schemaNode, vars.getSchemaContext());
-            }
+        decoded = RestCodec.from(typedef, null, schemaContext).deserialize(value);
+        if (decoded == null && typedef instanceof IdentityrefTypeDefinition) {
+            decoded = toIdentityrefQName(value, schemaNode);
         }
         return decoded;
     }
 
-    private static Object toQName(final String value, final DataSchemaNode schemaNode,
-            final SchemaContext schemaContext) {
-        final String moduleName = toModuleName(value);
-        final String nodeName = toNodeName(value);
-        final Module module = schemaContext.findModules(moduleName).iterator().next();
-        for (final IdentitySchemaNode identitySchemaNode : module.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;
-        }
-
-        return str.substring(idx + 1);
-    }
-
-    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 static QName prepareQName(final MainVarsWrapper variables) {
-        variables.checkValid(ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR.matches(variables.currentChar()),
-                "Identifier must start with character from set 'a-zA-Z_'");
-        final String preparedPrefix = variables.nextIdentifierFromNextSequence(
-                ParserBuilderConstants.Deserializer.IDENTIFIER);
+    private QName prepareQName() {
+        checkValidIdentifierStart();
+        final String preparedPrefix = nextIdentifierFromNextSequence(ParserConstants.YANG_IDENTIFIER_PART);
         final String prefix;
         final String localName;
 
-        if (variables.allCharsConsumed()) {
-            return getQNameOfDataSchemaNode(preparedPrefix, variables);
+        if (allCharsConsumed()) {
+            return getQNameOfDataSchemaNode(preparedPrefix);
         }
 
-        switch (variables.currentChar()) {
-            case RestconfConstants.SLASH:
-            case ParserBuilderConstants.Deserializer.EQUAL:
+        switch (currentChar()) {
+            case '/':
+            case '=':
                 prefix = preparedPrefix;
-                return getQNameOfDataSchemaNode(prefix, variables);
-            case ParserBuilderConstants.Deserializer.COLON:
+                return getQNameOfDataSchemaNode(prefix);
+            case ':':
                 prefix = preparedPrefix;
-                variables.skipCurrentChar();
-                variables.checkValid(
-                        ParserBuilderConstants.Deserializer.IDENTIFIER_FIRST_CHAR.matches(variables.currentChar()),
-                        "Identifier must start with character from set 'a-zA-Z_'");
-                localName = variables.nextIdentifierFromNextSequence(ParserBuilderConstants.Deserializer.IDENTIFIER);
-
-                if (!variables.allCharsConsumed()
-                        && variables.currentChar() == ParserBuilderConstants.Deserializer.EQUAL) {
-                    return getQNameOfDataSchemaNode(localName, variables);
+                skipCurrentChar();
+                checkValidIdentifierStart();
+                localName = nextIdentifierFromNextSequence(ParserConstants.YANG_IDENTIFIER_PART);
+
+                if (!allCharsConsumed() && currentChar() == '=') {
+                    return getQNameOfDataSchemaNode(localName);
                 } else {
-                    final Module module = moduleForPrefix(prefix, variables.getSchemaContext());
-                    checkArgument(module != null, "Failed to lookup prefix %s", prefix);
+                    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 new IllegalArgumentException("Failed build path.");
+                throw getParsingCharFailedException();
         }
     }
 
-    private static void prepareNodeWithValue(final QName qname, final List<PathArgument> path,
-            final MainVarsWrapper variables) {
-        variables.skipCurrentChar();
-        final String value = variables.nextIdentifierFromNextSequence(
-                ParserBuilderConstants.Deserializer.IDENTIFIER_PREDICATE);
+    private void prepareNodeWithValue(final QName qname, final List<PathArgument> path) {
+        skipCurrentChar();
+        final String value = nextIdentifierFromNextSequence(IDENTIFIER_PREDICATE);
 
         // exception if value attribute is missing
-        RestconfValidationUtils.checkDocumentedError(
-                !value.isEmpty(),
-                RestconfError.ErrorType.PROTOCOL,
-                RestconfError.ErrorTag.MISSING_ATTRIBUTE,
-                "Value missing for: " + qname
-        );
-        final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
-        final Object valueByType = prepareValueByType(dataSchemaNode, findAndParsePercentEncoded(value), variables);
+        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 static void prepareIdentifier(final QName qname, final List<PathArgument> path,
-            final MainVarsWrapper variables) {
-        final DataSchemaContextNode<?> currentNode = nextContextNode(qname, path, variables);
-        if (currentNode == null) {
-            return;
+    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);
         }
-        variables.checkValid(!currentNode.isKeyedEntry(),
-            "Entry " + qname + " requires key or value predicate to be present");
     }
 
-    @SuppressFBWarnings("NP_NULL_ON_SOME_PATH") // code does check for null 'current' but FB doesn't recognize it
-    private static DataSchemaContextNode<?> nextContextNode(final QName qname, final List<PathArgument> path,
-            final MainVarsWrapper variables) {
-        final DataSchemaContextNode<?> initialContext = variables.getCurrent();
+    @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();
 
-        DataSchemaContextNode<?> current = initialContext.getChild(qname);
-        variables.setCurrent(current);
+        current = initialContext.getChild(qname);
 
         if (current == null) {
-            final Optional<Module> module = variables.getSchemaContext().findModule(qname.getModule());
+            final Optional<Module> module = schemaContext.findModule(qname.getModule());
             if (module.isPresent()) {
                 for (final RpcDefinition rpcDefinition : module.get().getRpcs()) {
                     if (rpcDefinition.getQName().getLocalName().equals(qname.getLocalName())) {
@@ -326,41 +261,79 @@ public final class YangInstanceIdentifierDeserializer {
                 return null;
             }
         }
-        variables.checkValid(current != null, qname + " is not correct schema node identifier.");
+        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);
-            variables.setCurrent(current);
         }
         return current;
     }
 
-    private static String findAndParsePercentEncoded(final String preparedPrefix) {
-        if (!preparedPrefix.contains(String.valueOf(ParserBuilderConstants.Deserializer.PERCENT_ENCODING))) {
-            return preparedPrefix;
-        }
+    private Module moduleForPrefix(final String prefix) {
+        return schemaContext.findModules(prefix).stream().findFirst().orElse(null);
+    }
 
-        final StringBuilder parsedPrefix = new StringBuilder(preparedPrefix);
-        final CharMatcher matcher = CharMatcher.is(ParserBuilderConstants.Deserializer.PERCENT_ENCODING);
+    private boolean allCharsConsumed() {
+        return offset == data.length();
+    }
 
-        while (matcher.matchesAnyOf(parsedPrefix)) {
-            final int percentCharPosition = matcher.indexIn(parsedPrefix);
-            parsedPrefix.replace(
-                    percentCharPosition,
-                    percentCharPosition + ParserBuilderConstants.Deserializer.LAST_ENCODED_CHAR,
-                    String.valueOf((char) Integer.parseInt(parsedPrefix.substring(
-                            percentCharPosition + ParserBuilderConstants.Deserializer.FIRST_ENCODED_CHAR,
-                            percentCharPosition + ParserBuilderConstants.Deserializer.LAST_ENCODED_CHAR),
-                            ParserBuilderConstants.Deserializer.PERCENT_ENCODED_RADIX)));
+    private void checkValid(final boolean condition, final ErrorTag errorTag, final String errorMsg,
+                            final Object... messageArgs) {
+        final Object[] allMessageArguments = new Object[messageArgs.length + 2];
+        allMessageArguments[0] = data;
+        allMessageArguments[1] = offset;
+        System.arraycopy(messageArgs, 0, allMessageArguments, 2, messageArgs.length);
+        RestconfDocumentedException.throwIf(!condition, ErrorType.PROTOCOL, errorTag,
+                PARSING_FAILED_MESSAGE + errorMsg, allMessageArguments);
+    }
+
+    private void checkValidIdentifierStart() {
+        checkValid(ParserConstants.YANG_IDENTIFIER_START.matches(currentChar()), ErrorTag.MALFORMED_MESSAGE,
+                "Identifier must start with character from set 'a-zA-Z_'");
+    }
+
+    private RestconfDocumentedException getParsingCharFailedException() {
+        return new RestconfDocumentedException(String.format(PARSING_FAILED_MESSAGE, data, offset)
+                + String.format("Bad char '%c' on the current position.", currentChar()),
+                ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
+    }
+
+    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);
+    }
 
-        return parsedPrefix.toString();
+    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 static QName getQNameOfDataSchemaNode(final String nodeName, final MainVarsWrapper variables) {
-        final DataSchemaNode dataSchemaNode = variables.getCurrent().getDataSchemaNode();
-        if (dataSchemaNode instanceof ContainerSchemaNode) {
-            return getQNameOfDataSchemaNode((ContainerSchemaNode) dataSchemaNode, nodeName);
+    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);
         }
@@ -369,8 +342,8 @@ public final class YangInstanceIdentifierDeserializer {
     }
 
     private static <T extends DataNodeContainer & SchemaNode & ActionNodeContainer> QName getQNameOfDataSchemaNode(
-            final T parent, String nodeName) {
-        final Optional<ActionDefinition> actionDef = findActionDefinition(parent, nodeName);
+            final T parent, final String nodeName) {
+        final Optional<? extends ActionDefinition> actionDef = findActionDefinition(parent, nodeName);
         final SchemaNode node;
         if (actionDef.isPresent()) {
             node = actionDef.get();
@@ -380,11 +353,7 @@ public final class YangInstanceIdentifierDeserializer {
         return node.getQName();
     }
 
-    private static Module moduleForPrefix(final String prefix, final SchemaContext schemaContext) {
-        return schemaContext.findModules(prefix).stream().findFirst().orElse(null);
-    }
-
-    private static Optional<ActionDefinition> findActionDefinition(final SchemaNode dataSchemaNode,
+    private static Optional<? extends ActionDefinition> findActionDefinition(final SchemaNode dataSchemaNode,
             final String nodeName) {
         requireNonNull(dataSchemaNode, "DataSchema Node must not be null.");
         if (dataSchemaNode instanceof ActionNodeContainer) {
@@ -394,78 +363,68 @@ public final class YangInstanceIdentifierDeserializer {
         return Optional.empty();
     }
 
-    private static final class MainVarsWrapper {
-        private static final int STARTING_OFFSET = 0;
-
-        private final SchemaContext schemaContext;
-        private final String data;
-
-        private DataSchemaContextNode<?> current;
-        private int offset;
-
-        MainVarsWrapper(final String data, final DataSchemaContextNode<?> current, final int offset,
-                final SchemaContext schemaContext) {
-            this.data = data;
-            this.current = current;
-            this.offset = offset;
-            this.schemaContext = schemaContext;
-        }
-
-        boolean allCharsConsumed() {
-            return offset == data.length();
-        }
-
-        void checkValid(final boolean condition, final String errorMsg) {
-            checkArgument(condition, "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s", data, offset,
-                errorMsg);
+    private static String findAndParsePercentEncoded(final String preparedPrefix) {
+        if (preparedPrefix.indexOf('%') == -1) {
+            return preparedPrefix;
         }
 
-        char currentChar() {
-            return data.charAt(offset);
+        // 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)));
         }
 
-        void skipCurrentChar() {
-            offset++;
-        }
+        return parsedPrefix.toString();
+    }
 
-        String nextIdentifierFromNextSequence(final CharMatcher matcher) {
-            final int start = offset;
-            while (!allCharsConsumed() && matcher.matches(currentChar())) {
-                skipCurrentChar();
+    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 data.substring(start, offset);
         }
+        return QName.create(schemaNode.getQName().getNamespace(), schemaNode.getQName().getRevision(), nodeName);
+    }
 
-        void validArg() {
-            // every identifier except of the first MUST start with slash
-            if (offset != MainVarsWrapper.STARTING_OFFSET) {
-                checkValid(RestconfConstants.SLASH == currentChar(), "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() && RestconfConstants.SLASH == currentChar()) {
-                    skipCurrentChar();
-                }
-
-                // check if slash is not also the last char in identifier
-                checkValid(!allCharsConsumed(), "Identifier cannot end with '/'.");
-            }
+    private static String toNodeName(final String str) {
+        final int idx = str.indexOf(':');
+        if (idx == -1) {
+            return str;
         }
 
-        public DataSchemaContextNode<?> getCurrent() {
-            return current;
+        if (str.indexOf(':', idx + 1) != -1) {
+            return str;
         }
 
-        public void setCurrent(final DataSchemaContextNode<?> current) {
-            this.current = current;
-        }
+        return str.substring(idx + 1);
+    }
 
-        public int getOffset() {
-            return offset;
+    private static String toModuleName(final String str) {
+        final int idx = str.indexOf(':');
+        if (idx == -1) {
+            return null;
         }
 
-        public SchemaContext getSchemaContext() {
-            return schemaContext;
+        if (str.indexOf(':', idx + 1) != -1) {
+            return null;
         }
+
+        return str.substring(0, idx);
     }
 }