Rework YangInstanceIdentifier/URI path conversion
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / server / spi / ApiPathNormalizer.java
1 /*
2  * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.restconf.server.spi;
9
10 import static com.google.common.base.Verify.verify;
11 import static com.google.common.base.Verify.verifyNotNull;
12 import static java.util.Objects.requireNonNull;
13
14 import com.google.common.base.VerifyException;
15 import com.google.common.collect.ImmutableList;
16 import com.google.common.collect.ImmutableMap;
17 import java.io.IOException;
18 import java.util.ArrayList;
19 import java.util.List;
20 import org.eclipse.jdt.annotation.NonNull;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.opendaylight.restconf.api.ApiPath;
23 import org.opendaylight.restconf.api.ApiPath.ApiIdentifier;
24 import org.opendaylight.restconf.api.ApiPath.ListInstance;
25 import org.opendaylight.restconf.api.ApiPath.Step;
26 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
27 import org.opendaylight.restconf.nb.rfc8040.Insert.PointNormalizer;
28 import org.opendaylight.restconf.server.api.DatabindContext;
29 import org.opendaylight.restconf.server.spi.ApiPathNormalizer.Path.Action;
30 import org.opendaylight.restconf.server.spi.ApiPathNormalizer.Path.Data;
31 import org.opendaylight.restconf.server.spi.ApiPathNormalizer.Path.Rpc;
32 import org.opendaylight.yangtools.yang.common.ErrorTag;
33 import org.opendaylight.yangtools.yang.common.ErrorType;
34 import org.opendaylight.yangtools.yang.common.QName;
35 import org.opendaylight.yangtools.yang.common.QNameModule;
36 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
37 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
38 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
39 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
40 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
41 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodec;
42 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
43 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.Composite;
44 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
45 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
46 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
47 import org.opendaylight.yangtools.yang.model.api.EffectiveStatementInference;
48 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
49 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
50 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
51 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
52 import org.opendaylight.yangtools.yang.model.api.stmt.ActionEffectiveStatement;
53 import org.opendaylight.yangtools.yang.model.api.stmt.InputEffectiveStatement;
54 import org.opendaylight.yangtools.yang.model.api.stmt.OutputEffectiveStatement;
55 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
56 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaTreeEffectiveStatement;
57 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
58 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
59
60 /**
61  * Utility for normalizing {@link ApiPath}s. An {@link ApiPath} can represent a number of different constructs, as
62  * denoted to in the {@link Path} interface hierarchy.
63  *
64  * <p>
65  * This process is governed by
66  * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-3.5.3">RFC8040, section 3.5.3</a>. The URI provides the
67  * equivalent of NETCONF XML filter encoding, with data values being escaped RFC7891 strings.
68  */
69 public final class ApiPathNormalizer implements PointNormalizer {
70     /**
71      * A normalized {@link ApiPath}. This can be either
72      * <ul>
73      *   <li>a {@link Data} pointing to a datastore resource, or</li>
74      *   <li>an {@link Rpc} pointing to a YANG {@code rpc} statement, or</li>
75      *   <li>an {@link Action} pointing to an instantiation of a YANG {@code action} statement</li>
76      * </ul>
77      */
78     @NonNullByDefault
79     public sealed interface Path {
80         /**
81          * Returns the {@link EffectiveStatementInference} made by this path.
82          *
83          * @return the {@link EffectiveStatementInference} made by this path
84          */
85         Inference inference();
86
87         /**
88          * A {@link Path} denoting an invocation of a YANG {@code action}.
89          *
90          * @param inference the {@link EffectiveStatementInference} made by this path
91          * @param instance the {@link YangInstanceIdentifier} of the instance being referenced, guaranteed to be
92          *        non-empty
93          * @param action the {@code action}
94          */
95         record Action(Inference inference, YangInstanceIdentifier instance, ActionEffectiveStatement action)
96                 implements OperationPath, InstanceReference {
97             public Action {
98                 requireNonNull(inference);
99                 requireNonNull(action);
100                 if (instance.isEmpty()) {
101                     throw new IllegalArgumentException("action must be instantiated on a data resource");
102                 }
103             }
104
105             @Override
106             public InputEffectiveStatement inputStatement() {
107                 return action.input();
108             }
109
110             @Override
111             public OutputEffectiveStatement outputStatement() {
112                 return action.output();
113             }
114         }
115
116         /**
117          * A {@link Path} denoting a datastore instance.
118          *
119          * @param inference the {@link EffectiveStatementInference} made by this path
120          * @param instance the {@link YangInstanceIdentifier} of the instance being referenced,
121          *                 {@link YangInstanceIdentifier#empty()} denotes the datastore
122          * @param schema the {@link DataSchemaContext} of the datastore instance
123          */
124         // FIXME: split into 'Datastore' and 'Data' with non-empty instance, so we can bind to correct
125         //        instance-identifier semantics, which does not allow YangInstanceIdentifier.empty()
126         record Data(Inference inference, YangInstanceIdentifier instance, DataSchemaContext schema)
127                 implements InstanceReference {
128             public Data {
129                 requireNonNull(inference);
130                 requireNonNull(instance);
131                 requireNonNull(schema);
132             }
133         }
134
135         /**
136          * A {@link Path} denoting an invocation of a YANG {@code rpc}.
137          *
138          * @param inference the {@link EffectiveStatementInference} made by this path
139          * @param rpc the {@code rpc}
140          */
141         record Rpc(Inference inference, RpcEffectiveStatement rpc) implements OperationPath {
142             public Rpc {
143                 requireNonNull(inference);
144                 requireNonNull(rpc);
145             }
146
147             @Override
148             public InputEffectiveStatement inputStatement() {
149                 return rpc.input();
150             }
151
152             @Override
153             public OutputEffectiveStatement outputStatement() {
154                 return rpc.output();
155             }
156         }
157     }
158
159     /**
160      * An intermediate trait of {@link Path}s which are referencing a YANG data resource. This can be either
161      * a {@link Data}, or an {@link Action}}.
162      */
163     @NonNullByDefault
164     public sealed interface InstanceReference extends Path {
165         /**
166          * Returns the {@link YangInstanceIdentifier} of the instance being referenced.
167          *
168          * @return the {@link YangInstanceIdentifier} of the instance being referenced,
169          *         {@link YangInstanceIdentifier#empty()} denotes the datastora
170          */
171         YangInstanceIdentifier instance();
172     }
173
174     /**
175      * An intermediate trait of {@link Path}s which are referencing a YANG operation. This can be either
176      * an {@link Action} on an {@link Rpc}.
177      */
178     @NonNullByDefault
179     public sealed interface OperationPath extends Path {
180         /**
181          * Returns the {@code input} statement of this operation.
182          *
183          * @return the {@code input} statement of this operation
184          */
185         InputEffectiveStatement inputStatement();
186
187         /**
188          * Returns the {@code output} statement of this operation.
189          *
190          * @return the {@code output} statement of this operation
191          */
192         OutputEffectiveStatement outputStatement();
193     }
194
195     private final @NonNull DatabindContext databind;
196
197     public ApiPathNormalizer(final DatabindContext databind) {
198         this.databind = requireNonNull(databind);
199     }
200
201     public @NonNull Path normalizePath(final ApiPath apiPath) {
202         final var it = apiPath.steps().iterator();
203         if (!it.hasNext()) {
204             return new Data(Inference.ofDataTreePath(databind.modelContext()), YangInstanceIdentifier.of(),
205                 databind.schemaTree().getRoot());
206         }
207
208         // First step is somewhat special:
209         // - it has to contain a module qualifier
210         // - it has to consider RPCs, for which we need SchemaContext
211         //
212         // We therefore peel that first iteration here and not worry about those details in further iterations
213         var step = it.next();
214         final var firstModule = step.module();
215         if (firstModule == null) {
216             throw new RestconfDocumentedException(
217                 "First member must use namespace-qualified form, '" + step.identifier() + "' does not",
218                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
219         }
220
221         var namespace = resolveNamespace(firstModule);
222         var qname = step.identifier().bindTo(namespace);
223
224         // We go through more modern APIs here to get this special out of the way quickly
225         final var modelContext = databind.modelContext();
226         final var optRpc = modelContext.findModuleStatement(namespace).orElseThrow()
227             .findSchemaTreeNode(RpcEffectiveStatement.class, qname);
228         if (optRpc.isPresent()) {
229             final var rpc = optRpc.orElseThrow();
230
231             // We have found an RPC match,
232             if (it.hasNext()) {
233                 throw new RestconfDocumentedException("First step in the path resolves to RPC '" + qname + "' and "
234                     + "therefore it must be the only step present", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
235             }
236             if (step instanceof ListInstance) {
237                 throw new RestconfDocumentedException("First step in the path resolves to RPC '" + qname + "' and "
238                     + "therefore it must not contain key values", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
239             }
240
241             final var stack = SchemaInferenceStack.of(modelContext);
242             final var stmt = stack.enterSchemaTree(rpc.argument());
243             verify(rpc.equals(stmt), "Expecting %s, inferred %s", rpc, stmt);
244             return new Rpc(stack.toInference(), rpc);
245         }
246
247         final var stack = SchemaInferenceStack.of(modelContext);
248         final var path = new ArrayList<PathArgument>();
249         DataSchemaContext parentNode = databind.schemaTree().getRoot();
250         while (true) {
251             final var parentSchema = parentNode.dataSchemaNode();
252             if (parentSchema instanceof ActionNodeContainer actionParent) {
253                 final var optAction = actionParent.findAction(qname);
254                 if (optAction.isPresent()) {
255                     final var action = optAction.orElseThrow();
256
257                     if (it.hasNext()) {
258                         throw new RestconfDocumentedException("Request path resolves to action '" + qname + "' and "
259                             + "therefore it must not continue past it", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
260                     }
261                     if (step instanceof ListInstance) {
262                         throw new RestconfDocumentedException("Request path resolves to action '" + qname + "' and "
263                             + "therefore it must not contain key values",
264                             ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
265                     }
266
267                     final var stmt = stack.enterSchemaTree(qname);
268                     final var actionStmt = action.asEffectiveStatement();
269                     verify(actionStmt.equals(stmt), "Expecting %s, inferred %s", actionStmt, stmt);
270
271                     return new Action(stack.toInference(), YangInstanceIdentifier.of(path), actionStmt);
272                 }
273             }
274
275             // Resolve the child step with respect to data schema tree
276             final var found = parentNode instanceof DataSchemaContext.Composite composite
277                 ? composite.enterChild(stack, qname) : null;
278             if (found == null) {
279                 throw new RestconfDocumentedException("Schema for '" + qname + "' not found",
280                     ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
281             }
282
283             // Now add all mixins encountered to the path
284             var childNode = found;
285             while (childNode instanceof PathMixin currentMixin) {
286                 path.add(currentMixin.mixinPathStep());
287                 childNode = verifyNotNull(currentMixin.enterChild(stack, qname),
288                     "Mixin %s is missing child for %s while resolving %s", childNode, qname, found);
289             }
290
291             final PathArgument pathArg;
292             if (step instanceof ListInstance listStep) {
293                 final var values = listStep.keyValues();
294                 final var schema = childNode.dataSchemaNode();
295                 if (schema instanceof ListSchemaNode listSchema) {
296                     pathArg = prepareNodeWithPredicates(stack, qname, listSchema, values);
297                 } else if (schema instanceof LeafListSchemaNode leafListSchema) {
298                     if (values.size() != 1) {
299                         throw new RestconfDocumentedException("Entry '" + qname + "' requires one value predicate.",
300                             ErrorType.PROTOCOL, ErrorTag.BAD_ATTRIBUTE);
301                     }
302                     pathArg = new NodeWithValue<>(qname, parserJsonValue(stack, leafListSchema, values.get(0)));
303                 } else {
304                     throw new RestconfDocumentedException(
305                         "Entry '" + qname + "' does not take a key or value predicate.",
306                         ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE);
307                 }
308             } else {
309                 if (childNode.dataSchemaNode() instanceof ListSchemaNode list && !list.getKeyDefinition().isEmpty()) {
310                     throw new RestconfDocumentedException(
311                         "Entry '" + qname + "' requires key or value predicate to be present.",
312                         ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE);
313                 }
314                 pathArg = childNode.getPathStep();
315             }
316
317             path.add(pathArg);
318
319             if (!it.hasNext()) {
320                 return new Data(stack.toInference(), YangInstanceIdentifier.of(path), childNode);
321             }
322
323             parentNode = childNode;
324             step = it.next();
325             final var module = step.module();
326             if (module != null) {
327                 namespace = resolveNamespace(module);
328             }
329
330             qname = step.identifier().bindTo(namespace);
331         }
332     }
333
334     public @NonNull Data normalizeDataPath(final ApiPath apiPath) {
335         final var path = normalizePath(apiPath);
336         if (path instanceof Data dataPath) {
337             return dataPath;
338         }
339         throw new RestconfDocumentedException("Point '" + apiPath + "' resolves to non-data " + path,
340             ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
341     }
342
343     @Override
344     public PathArgument normalizePoint(final ApiPath value) {
345         final var path = normalizePath(value);
346         if (path instanceof Data dataPath) {
347             final var lastArg = dataPath.instance().getLastPathArgument();
348             if (lastArg != null) {
349                 return lastArg;
350             }
351             throw new IllegalArgumentException("Point '" + value + "' resolves to an empty path");
352         }
353         throw new IllegalArgumentException("Point '" + value + "' resolves to non-data " + path);
354     }
355
356     public Path.@NonNull Rpc normalizeRpcPath(final ApiPath apiPath) {
357         final var steps = apiPath.steps();
358         return switch (steps.size()) {
359             case 0 -> throw new RestconfDocumentedException("RPC name must be present", ErrorType.PROTOCOL,
360                 ErrorTag.DATA_MISSING);
361             case 1 -> normalizeRpcPath(steps.get(0));
362             default -> throw new RestconfDocumentedException(apiPath + " does not refer to an RPC", ErrorType.PROTOCOL,
363                 ErrorTag.DATA_MISSING);
364         };
365     }
366
367     public Path.@NonNull Rpc normalizeRpcPath(final ApiPath.Step step) {
368         final var firstModule = step.module();
369         if (firstModule == null) {
370             throw new RestconfDocumentedException(
371                 "First member must use namespace-qualified form, '" + step.identifier() + "' does not",
372                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
373         }
374
375         final var namespace = resolveNamespace(firstModule);
376         final var qname = step.identifier().bindTo(namespace);
377         final var stack = SchemaInferenceStack.of(databind.modelContext());
378         final SchemaTreeEffectiveStatement<?> stmt;
379         try {
380             stmt = stack.enterSchemaTree(qname);
381         } catch (IllegalArgumentException e) {
382             throw new RestconfDocumentedException(qname + " does not refer to an RPC", ErrorType.PROTOCOL,
383                 ErrorTag.DATA_MISSING, e);
384         }
385         if (stmt instanceof RpcEffectiveStatement rpc) {
386             return new Rpc(stack.toInference(), rpc);
387         }
388         throw new RestconfDocumentedException(qname + " does not refer to an RPC", ErrorType.PROTOCOL,
389             ErrorTag.DATA_MISSING);
390     }
391
392     public @NonNull InstanceReference normalizeDataOrActionPath(final ApiPath apiPath) {
393         // FIXME: optimize this
394         final var path = normalizePath(apiPath);
395         if (path instanceof Data dataPath) {
396             return dataPath;
397         }
398         if (path instanceof Action actionPath) {
399             return actionPath;
400         }
401         throw new RestconfDocumentedException("Unexpected path " + path, ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
402     }
403
404     /**
405      * Return the canonical {@link ApiPath} for specified {@link YangInstanceIdentifier}.
406      *
407      * @param path {@link YangInstanceIdentifier} to canonicalize
408      * @return An {@link ApiPath}
409      */
410     public @NonNull ApiPath canonicalize(final YangInstanceIdentifier path) {
411         final var it = path.getPathArguments().iterator();
412         if (!it.hasNext()) {
413             return ApiPath.empty();
414         }
415
416         final var stack = SchemaInferenceStack.of(databind.modelContext());
417         final var builder = ImmutableList.<Step>builder();
418         DataSchemaContext context = databind.schemaTree().getRoot();
419         QNameModule parentModule = null;
420         do {
421             final var arg = it.next();
422
423             // get module of the parent
424             if (!(context instanceof PathMixin)) {
425                 parentModule = context.dataSchemaNode().getQName().getModule();
426             }
427
428             final var childContext = context instanceof Composite composite ? composite.enterChild(stack, arg) : null;
429             if (childContext == null) {
430                 throw new RestconfDocumentedException(
431                     "Invalid input '%s': schema for argument '%s' (after '%s') not found".formatted(path, arg,
432                         ApiPath.of(builder.build())), ErrorType.APPLICATION, ErrorTag.UNKNOWN_ELEMENT);
433             }
434
435             context = childContext;
436             if (childContext instanceof PathMixin) {
437                 // This PathArgument is a mixed-in YangInstanceIdentifier, do not emit anything and continue
438                 continue;
439             }
440
441             builder.add(canonicalize(arg, parentModule, stack, context));
442         } while (it.hasNext());
443
444         return new ApiPath(builder.build());
445     }
446
447     private @NonNull Step canonicalize(final PathArgument arg, final QNameModule prevNamespace,
448             final SchemaInferenceStack stack, final DataSchemaContext context) {
449         // append namespace before every node which is defined in other module than its parent
450         // condition is satisfied also for the first path argument
451         final var nodeType = arg.getNodeType();
452         final var module = nodeType.getModule().equals(prevNamespace) ? null : resolvePrefix(nodeType);
453         final var identifier = nodeType.unbind();
454
455         // NodeIdentifier maps to an ApiIdentifier
456         if (arg instanceof NodeIdentifier) {
457             return new ApiIdentifier(module, identifier);
458         }
459
460         // NodeWithValue addresses a LeafSetEntryNode and maps to a ListInstance with a single value
461         final var schema = context.dataSchemaNode();
462         if (arg instanceof NodeWithValue<?> withValue) {
463             if (!(schema instanceof LeafListSchemaNode leafList)) {
464                 throw new RestconfDocumentedException(
465                     "Argument '%s' does not map to a leaf-list, but %s".formatted(arg, schema),
466                     ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
467             }
468             return ListInstance.of(module, identifier, encodeValue(stack, leafList, withValue.getValue()));
469         }
470
471         // The only remaining case is NodeIdentifierWrithPredicates, verify that instead of an explicit cast
472         if (!(arg instanceof NodeIdentifierWithPredicates withPredicates)) {
473             throw new VerifyException("Unhandled " + arg);
474         }
475         // A NodeIdentifierWithPredicates adresses a MapEntryNode and maps to a ListInstance with one or more values:
476         // 1) schema has to be a ListSchemaNode
477         if (!(schema instanceof ListSchemaNode list)) {
478             throw new RestconfDocumentedException(
479                 "Argument '%s' does not map to a list, but %s".formatted(arg, schema),
480                 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
481         }
482         // 2) the key definition must be non-empty
483         final var keyDef = list.getKeyDefinition();
484         final var size = keyDef.size();
485         if (size == 0) {
486             throw new RestconfDocumentedException(
487                 "Argument '%s' maps a list without any keys %s".formatted(arg, schema),
488                 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
489         }
490         // 3) the number of predicates has to match the number of keys
491         if (size != withPredicates.size()) {
492             throw new RestconfDocumentedException(
493                 "Argument '%s' does not match required keys %s".formatted(arg, keyDef),
494                 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
495         }
496
497         // ListSchemaNode implies the context is a composite, verify that instead of an unexplained cast when we look
498         // up the schema for individual keys
499         if (!(context instanceof Composite composite)) {
500             throw new VerifyException("Unexpected non-composite " + context + " with " + list);
501         }
502
503         final var builder = ImmutableList.<String>builderWithExpectedSize(size);
504         for (var key : keyDef) {
505             final var value = withPredicates.getValue(key);
506             if (value == null) {
507                 throw new RestconfDocumentedException("Argument '%s' is missing predicate for %s".formatted(arg, key),
508                     ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
509             }
510
511             final var tmpStack = stack.copy();
512             final var keyContext = composite.enterChild(tmpStack, key);
513             if (keyContext == null) {
514                 throw new VerifyException("Failed to find key " + key + " in " + composite);
515             }
516             if (!(keyContext.dataSchemaNode() instanceof LeafSchemaNode leaf)) {
517                 throw new VerifyException("Key " + key + " maps to non-leaf context " + keyContext);
518             }
519             builder.add(encodeValue(tmpStack, leaf, value));
520         }
521         return ListInstance.of(module, identifier, builder.build());
522     }
523
524     private String encodeValue(final SchemaInferenceStack stack, final TypedDataSchemaNode schema, final Object value) {
525         @SuppressWarnings("unchecked")
526         final var codec = (JSONCodec<Object>) databind.jsonCodecs().codecFor(schema, stack);
527         try (var jsonWriter = new HackJsonWriter()) {
528             codec.writeValue(jsonWriter, value);
529             return jsonWriter.acquireCaptured().rawString();
530         } catch (IOException e) {
531             throw new IllegalStateException("Failed to serialize '" + value + "'", e);
532         }
533     }
534
535     private NodeIdentifierWithPredicates prepareNodeWithPredicates(final SchemaInferenceStack stack, final QName qname,
536             final @NonNull ListSchemaNode schema, final List<@NonNull String> keyValues) {
537         final var keyDef = schema.getKeyDefinition();
538         final var keySize = keyDef.size();
539         final var varSize = keyValues.size();
540         if (keySize != varSize) {
541             throw new RestconfDocumentedException(
542                 "Schema for " + qname + " requires " + keySize + " key values, " + varSize + " supplied",
543                 ErrorType.PROTOCOL, keySize > varSize ? ErrorTag.MISSING_ATTRIBUTE : ErrorTag.UNKNOWN_ATTRIBUTE);
544         }
545
546         final var values = ImmutableMap.<QName, Object>builderWithExpectedSize(keySize);
547         final var tmp = stack.copy();
548         for (int i = 0; i < keySize; ++i) {
549             final QName keyName = keyDef.get(i);
550             final var child = schema.getDataChildByName(keyName);
551             tmp.enterSchemaTree(keyName);
552             values.put(keyName, prepareValueByType(tmp, child, keyValues.get(i)));
553             tmp.exit();
554         }
555
556         return NodeIdentifierWithPredicates.of(qname, values.build());
557     }
558
559     private Object prepareValueByType(final SchemaInferenceStack stack, final DataSchemaNode schemaNode,
560             final @NonNull String value) {
561         if (schemaNode instanceof TypedDataSchemaNode typedSchema) {
562             return parserJsonValue(stack, typedSchema, value);
563         }
564         throw new VerifyException("Unhandled schema " + schemaNode + " decoding '" + value + "'");
565     }
566
567     private Object parserJsonValue(final SchemaInferenceStack stack, final TypedDataSchemaNode schemaNode,
568             final String value) {
569         // As per https://www.rfc-editor.org/rfc/rfc8040#page-29:
570         //            The syntax for
571         //            "api-identifier" and "key-value" MUST conform to the JSON identifier
572         //            encoding rules in Section 4 of [RFC7951]: The RESTCONF root resource
573         //            path is required.  Additional sub-resource identifiers are optional.
574         //            The characters in a key value string are constrained, and some
575         //            characters need to be percent-encoded, as described in Section 3.5.3.
576         try {
577             return databind.jsonCodecs().codecFor(schemaNode, stack).parseValue(null, value);
578         } catch (IllegalArgumentException e) {
579             throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
580                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
581         }
582     }
583
584     private @NonNull QNameModule resolveNamespace(final String moduleName) {
585         final var it = databind.modelContext().findModuleStatements(moduleName).iterator();
586         if (it.hasNext()) {
587             return it.next().localQNameModule();
588         }
589         throw new RestconfDocumentedException("Failed to lookup for module with name '" + moduleName + "'.",
590             ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT);
591     }
592
593     /**
594      * Create prefix of namespace from {@link QName}.
595      *
596      * @param qname {@link QName}
597      * @return {@link String}
598      */
599     private @NonNull String resolvePrefix(final QName qname) {
600         return databind.modelContext().findModuleStatement(qname.getModule()).orElseThrow().argument().getLocalName();
601     }
602 }