Move patch target resolution
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / server / spi / ApiPathNormalizer.java
index 3a7acd415ee7d1f1687d2b0c92b28eafafcfcded..b54c081d96324a9514c1dd335759cb7ad7cbad04 100644 (file)
@@ -12,45 +12,51 @@ import static com.google.common.base.Verify.verifyNotNull;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.VerifyException;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import java.io.IOException;
+import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.List;
 import org.eclipse.jdt.annotation.NonNull;
-import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.opendaylight.restconf.api.ApiPath;
+import org.opendaylight.restconf.api.ApiPath.ApiIdentifier;
 import org.opendaylight.restconf.api.ApiPath.ListInstance;
+import org.opendaylight.restconf.api.ApiPath.Step;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.nb.rfc8040.Insert.PointNormalizer;
 import org.opendaylight.restconf.server.api.DatabindContext;
-import org.opendaylight.restconf.server.spi.ApiPathNormalizer.Path.Data;
-import org.opendaylight.restconf.server.spi.ApiPathNormalizer.Path.Rpc;
+import org.opendaylight.restconf.server.api.DatabindPath;
+import org.opendaylight.restconf.server.api.DatabindPath.Action;
+import org.opendaylight.restconf.server.api.DatabindPath.Data;
+import org.opendaylight.restconf.server.api.DatabindPath.InstanceReference;
+import org.opendaylight.restconf.server.api.DatabindPath.Rpc;
 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.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.codec.gson.JSONCodec;
 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
+import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.Composite;
 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
-import org.opendaylight.yangtools.yang.model.api.EffectiveStatementInference;
 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.TypedDataSchemaNode;
-import org.opendaylight.yangtools.yang.model.api.stmt.ActionEffectiveStatement;
-import org.opendaylight.yangtools.yang.model.api.stmt.InputEffectiveStatement;
-import org.opendaylight.yangtools.yang.model.api.stmt.OutputEffectiveStatement;
 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaTreeEffectiveStatement;
 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
-import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
 
 /**
  * Utility for normalizing {@link ApiPath}s. An {@link ApiPath} can represent a number of different constructs, as
- * denoted to in the {@link Path} interface hierarchy.
+ * denoted to in the {@link DatabindPath} interface hierarchy.
  *
  * <p>
  * This process is governed by
@@ -58,142 +64,16 @@ import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference
  * equivalent of NETCONF XML filter encoding, with data values being escaped RFC7891 strings.
  */
 public final class ApiPathNormalizer implements PointNormalizer {
-    /**
-     * A normalized {@link ApiPath}. This can be either
-     * <ul>
-     *   <li>a {@link Data} pointing to a datastore resource, or</li>
-     *   <li>an {@link Rpc} pointing to a YANG {@code rpc} statement, or</li>
-     *   <li>an {@link Action} pointing to an instantiation of a YANG {@code action} statement</li>
-     * </ul>
-     */
-    @NonNullByDefault
-    public sealed interface Path {
-        /**
-         * Returns the {@link EffectiveStatementInference} made by this path.
-         *
-         * @return the {@link EffectiveStatementInference} made by this path
-         */
-        Inference inference();
-
-        /**
-         * A {@link Path} denoting an invocation of a YANG {@code action}.
-         *
-         * @param inference the {@link EffectiveStatementInference} made by this path
-         * @param instance the {@link YangInstanceIdentifier} of the instance being referenced, guaranteed to be
-         *        non-empty
-         * @param action the {@code action}
-         */
-        record Action(Inference inference, YangInstanceIdentifier instance, ActionEffectiveStatement action)
-                implements OperationPath, InstanceReference {
-            public Action {
-                requireNonNull(inference);
-                requireNonNull(action);
-                if (instance.isEmpty()) {
-                    throw new IllegalArgumentException("action must be instantiated on a data resource");
-                }
-            }
-
-            @Override
-            public InputEffectiveStatement inputStatement() {
-                return action.input();
-            }
-
-            @Override
-            public OutputEffectiveStatement outputStatement() {
-                return action.output();
-            }
-        }
-
-        /**
-         * A {@link Path} denoting a datastore instance.
-         *
-         * @param inference the {@link EffectiveStatementInference} made by this path
-         * @param instance the {@link YangInstanceIdentifier} of the instance being referenced,
-         *                 {@link YangInstanceIdentifier#empty()} denotes the datastore
-         * @param schema the {@link DataSchemaContext} of the datastore instance
-         */
-        // FIXME: split into 'Datastore' and 'Data' with non-empty instance, so we can bind to correct
-        //        instance-identifier semantics, which does not allow YangInstanceIdentifier.empty()
-        record Data(Inference inference, YangInstanceIdentifier instance, DataSchemaContext schema)
-                implements InstanceReference {
-            public Data {
-                requireNonNull(inference);
-                requireNonNull(instance);
-                requireNonNull(schema);
-            }
-        }
-
-        /**
-         * A {@link Path} denoting an invocation of a YANG {@code rpc}.
-         *
-         * @param inference the {@link EffectiveStatementInference} made by this path
-         * @param rpc the {@code rpc}
-         */
-        record Rpc(Inference inference, RpcEffectiveStatement rpc) implements OperationPath {
-            public Rpc {
-                requireNonNull(inference);
-                requireNonNull(rpc);
-            }
-
-            @Override
-            public InputEffectiveStatement inputStatement() {
-                return rpc.input();
-            }
-
-            @Override
-            public OutputEffectiveStatement outputStatement() {
-                return rpc.output();
-            }
-        }
-    }
-
-    /**
-     * An intermediate trait of {@link Path}s which are referencing a YANG data resource. This can be either
-     * a {@link Data}, or an {@link Action}}.
-     */
-    @NonNullByDefault
-    public sealed interface InstanceReference extends Path {
-        /**
-         * Returns the {@link YangInstanceIdentifier} of the instance being referenced.
-         *
-         * @return the {@link YangInstanceIdentifier} of the instance being referenced,
-         *         {@link YangInstanceIdentifier#empty()} denotes the datastora
-         */
-        YangInstanceIdentifier instance();
-    }
-
-    /**
-     * An intermediate trait of {@link Path}s which are referencing a YANG operation. This can be either
-     * an {@link Action} on an {@link Rpc}.
-     */
-    @NonNullByDefault
-    public sealed interface OperationPath extends Path {
-        /**
-         * Returns the {@code input} statement of this operation.
-         *
-         * @return the {@code input} statement of this operation
-         */
-        InputEffectiveStatement inputStatement();
-
-        /**
-         * Returns the {@code output} statement of this operation.
-         *
-         * @return the {@code output} statement of this operation
-         */
-        OutputEffectiveStatement outputStatement();
-    }
-
     private final @NonNull DatabindContext databind;
 
     public ApiPathNormalizer(final DatabindContext databind) {
         this.databind = requireNonNull(databind);
     }
 
-    public @NonNull Path normalizePath(final ApiPath apiPath) {
+    public @NonNull DatabindPath normalizePath(final ApiPath apiPath) {
         final var it = apiPath.steps().iterator();
         if (!it.hasNext()) {
-            return new Data(Inference.ofDataTreePath(databind.modelContext()), YangInstanceIdentifier.of(),
-                databind.schemaTree().getRoot());
+            return new Data(databind);
         }
 
         // First step is somewhat special:
@@ -232,7 +112,7 @@ public final class ApiPathNormalizer implements PointNormalizer {
             final var stack = SchemaInferenceStack.of(modelContext);
             final var stmt = stack.enterSchemaTree(rpc.argument());
             verify(rpc.equals(stmt), "Expecting %s, inferred %s", rpc, stmt);
-            return new OperationPath.Rpc(stack.toInference(), rpc);
+            return new Rpc(databind, stack.toInference(), rpc);
         }
 
         final var stack = SchemaInferenceStack.of(modelContext);
@@ -259,7 +139,7 @@ public final class ApiPathNormalizer implements PointNormalizer {
                     final var actionStmt = action.asEffectiveStatement();
                     verify(actionStmt.equals(stmt), "Expecting %s, inferred %s", actionStmt, stmt);
 
-                    return new OperationPath.Action(stack.toInference(), YangInstanceIdentifier.of(path), actionStmt);
+                    return new Action(databind, stack.toInference(), YangInstanceIdentifier.of(path), actionStmt);
                 }
             }
 
@@ -308,7 +188,7 @@ public final class ApiPathNormalizer implements PointNormalizer {
             path.add(pathArg);
 
             if (!it.hasNext()) {
-                return new Data(stack.toInference(), YangInstanceIdentifier.of(path), childNode);
+                return new Data(databind, stack.toInference(), YangInstanceIdentifier.of(path), childNode);
             }
 
             parentNode = childNode;
@@ -331,6 +211,29 @@ public final class ApiPathNormalizer implements PointNormalizer {
             ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
     }
 
+    public static @NonNull Data normalizeSubResource(final Data resource, final ApiPath subResource) {
+        // If subResource is empty just return the resource
+        final var urlPath = resource.instance();
+        if (subResource.steps().isEmpty()) {
+            return resource;
+        }
+        final var normalizer = new ApiPathNormalizer(resource.databind());
+        if (urlPath.isEmpty()) {
+            // URL indicates the datastore resource, let's just normalize targetPath
+            return normalizer.normalizeDataPath(subResource);
+        }
+
+        // FIXME: We are re-parsing the concatenation. We should provide enough context for the bottom half of
+        //        normalizePath() logic instead
+        final String targetUrl = normalizer.canonicalize(urlPath).toString() + "/" + subResource.toString();
+        try {
+            return normalizer.normalizeDataPath(ApiPath.parse(targetUrl));
+        } catch (ParseException e) {
+            throw new RestconfDocumentedException("Failed to parse target " + targetUrl,
+                ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e);
+        }
+    }
+
     @Override
     public PathArgument normalizePoint(final ApiPath value) {
         final var path = normalizePath(value);
@@ -344,7 +247,7 @@ public final class ApiPathNormalizer implements PointNormalizer {
         throw new IllegalArgumentException("Point '" + value + "' resolves to non-data " + path);
     }
 
-    public Path.@NonNull Rpc normalizeRpcPath(final ApiPath apiPath) {
+    public @NonNull Rpc normalizeRpcPath(final ApiPath apiPath) {
         final var steps = apiPath.steps();
         return switch (steps.size()) {
             case 0 -> throw new RestconfDocumentedException("RPC name must be present", ErrorType.PROTOCOL,
@@ -355,7 +258,7 @@ public final class ApiPathNormalizer implements PointNormalizer {
         };
     }
 
-    public Path.@NonNull Rpc normalizeRpcPath(final ApiPath.Step step) {
+    public @NonNull Rpc normalizeRpcPath(final ApiPath.Step step) {
         final var firstModule = step.module();
         if (firstModule == null) {
             throw new RestconfDocumentedException(
@@ -374,7 +277,7 @@ public final class ApiPathNormalizer implements PointNormalizer {
                 ErrorTag.DATA_MISSING, e);
         }
         if (stmt instanceof RpcEffectiveStatement rpc) {
-            return new Rpc(stack.toInference(), rpc);
+            return new Rpc(databind, stack.toInference(), rpc);
         }
         throw new RestconfDocumentedException(qname + " does not refer to an RPC", ErrorType.PROTOCOL,
             ErrorTag.DATA_MISSING);
@@ -386,12 +289,143 @@ public final class ApiPathNormalizer implements PointNormalizer {
         if (path instanceof Data dataPath) {
             return dataPath;
         }
-        if (path instanceof OperationPath.Action actionPath) {
+        if (path instanceof Action actionPath) {
             return actionPath;
         }
         throw new RestconfDocumentedException("Unexpected path " + path, ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
     }
 
+    /**
+     * Return the canonical {@link ApiPath} for specified {@link YangInstanceIdentifier}.
+     *
+     * @param path {@link YangInstanceIdentifier} to canonicalize
+     * @return An {@link ApiPath}
+     */
+    public @NonNull ApiPath canonicalize(final YangInstanceIdentifier path) {
+        final var it = path.getPathArguments().iterator();
+        if (!it.hasNext()) {
+            return ApiPath.empty();
+        }
+
+        final var stack = SchemaInferenceStack.of(databind.modelContext());
+        final var builder = ImmutableList.<Step>builder();
+        DataSchemaContext context = databind.schemaTree().getRoot();
+        QNameModule parentModule = null;
+        do {
+            final var arg = it.next();
+
+            // get module of the parent
+            if (!(context instanceof PathMixin)) {
+                parentModule = context.dataSchemaNode().getQName().getModule();
+            }
+
+            final var childContext = context instanceof Composite composite ? composite.enterChild(stack, arg) : null;
+            if (childContext == null) {
+                throw new RestconfDocumentedException(
+                    "Invalid input '%s': schema for argument '%s' (after '%s') not found".formatted(path, arg,
+                        ApiPath.of(builder.build())), ErrorType.APPLICATION, ErrorTag.UNKNOWN_ELEMENT);
+            }
+
+            context = childContext;
+            if (childContext instanceof PathMixin) {
+                // This PathArgument is a mixed-in YangInstanceIdentifier, do not emit anything and continue
+                continue;
+            }
+
+            builder.add(canonicalize(arg, parentModule, stack, context));
+        } while (it.hasNext());
+
+        return new ApiPath(builder.build());
+    }
+
+    private @NonNull Step canonicalize(final PathArgument arg, final QNameModule prevNamespace,
+            final SchemaInferenceStack stack, final DataSchemaContext context) {
+        // append namespace before every node which is defined in other module than its parent
+        // condition is satisfied also for the first path argument
+        final var nodeType = arg.getNodeType();
+        final var module = nodeType.getModule().equals(prevNamespace) ? null : resolvePrefix(nodeType);
+        final var identifier = nodeType.unbind();
+
+        // NodeIdentifier maps to an ApiIdentifier
+        if (arg instanceof NodeIdentifier) {
+            return new ApiIdentifier(module, identifier);
+        }
+
+        // NodeWithValue addresses a LeafSetEntryNode and maps to a ListInstance with a single value
+        final var schema = context.dataSchemaNode();
+        if (arg instanceof NodeWithValue<?> withValue) {
+            if (!(schema instanceof LeafListSchemaNode leafList)) {
+                throw new RestconfDocumentedException(
+                    "Argument '%s' does not map to a leaf-list, but %s".formatted(arg, schema),
+                    ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
+            }
+            return ListInstance.of(module, identifier, encodeValue(stack, leafList, withValue.getValue()));
+        }
+
+        // The only remaining case is NodeIdentifierWrithPredicates, verify that instead of an explicit cast
+        if (!(arg instanceof NodeIdentifierWithPredicates withPredicates)) {
+            throw new VerifyException("Unhandled " + arg);
+        }
+        // A NodeIdentifierWithPredicates adresses a MapEntryNode and maps to a ListInstance with one or more values:
+        // 1) schema has to be a ListSchemaNode
+        if (!(schema instanceof ListSchemaNode list)) {
+            throw new RestconfDocumentedException(
+                "Argument '%s' does not map to a list, but %s".formatted(arg, schema),
+                ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
+        }
+        // 2) the key definition must be non-empty
+        final var keyDef = list.getKeyDefinition();
+        final var size = keyDef.size();
+        if (size == 0) {
+            throw new RestconfDocumentedException(
+                "Argument '%s' maps a list without any keys %s".formatted(arg, schema),
+                ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
+        }
+        // 3) the number of predicates has to match the number of keys
+        if (size != withPredicates.size()) {
+            throw new RestconfDocumentedException(
+                "Argument '%s' does not match required keys %s".formatted(arg, keyDef),
+                ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
+        }
+
+        // ListSchemaNode implies the context is a composite, verify that instead of an unexplained cast when we look
+        // up the schema for individual keys
+        if (!(context instanceof Composite composite)) {
+            throw new VerifyException("Unexpected non-composite " + context + " with " + list);
+        }
+
+        final var builder = ImmutableList.<String>builderWithExpectedSize(size);
+        for (var key : keyDef) {
+            final var value = withPredicates.getValue(key);
+            if (value == null) {
+                throw new RestconfDocumentedException("Argument '%s' is missing predicate for %s".formatted(arg, key),
+                    ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
+            }
+
+            final var tmpStack = stack.copy();
+            final var keyContext = composite.enterChild(tmpStack, key);
+            if (keyContext == null) {
+                throw new VerifyException("Failed to find key " + key + " in " + composite);
+            }
+            if (!(keyContext.dataSchemaNode() instanceof LeafSchemaNode leaf)) {
+                throw new VerifyException("Key " + key + " maps to non-leaf context " + keyContext);
+            }
+            builder.add(encodeValue(tmpStack, leaf, value));
+        }
+        return ListInstance.of(module, identifier, builder.build());
+    }
+
+    private String encodeValue(final SchemaInferenceStack stack, final TypedDataSchemaNode schema, final Object value) {
+        @SuppressWarnings("unchecked")
+        final var codec = (JSONCodec<Object>) databind.jsonCodecs().codecFor(schema, stack);
+        try (var jsonWriter = new HackJsonWriter()) {
+            codec.writeValue(jsonWriter, value);
+            return jsonWriter.acquireCaptured().rawString();
+        } catch (IOException e) {
+            throw new IllegalStateException("Failed to serialize '" + value + "'", e);
+        }
+    }
+
     private NodeIdentifierWithPredicates prepareNodeWithPredicates(final SchemaInferenceStack stack, final QName qname,
             final @NonNull ListSchemaNode schema, final List<@NonNull String> keyValues) {
         final var keyDef = schema.getKeyDefinition();
@@ -449,4 +483,14 @@ public final class ApiPathNormalizer implements PointNormalizer {
         throw new RestconfDocumentedException("Failed to lookup for module with name '" + moduleName + "'.",
             ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT);
     }
+
+    /**
+     * Create prefix of namespace from {@link QName}.
+     *
+     * @param qname {@link QName}
+     * @return {@link String}
+     */
+    private @NonNull String resolvePrefix(final QName qname) {
+        return databind.modelContext().findModuleStatement(qname.getModule()).orElseThrow().argument().getLocalName();
+    }
 }