Add DataPatchPath 89/109089/1
authorRobert Varga <robert.varga@pantheon.tech>
Tue, 28 Nov 2023 04:11:51 +0000 (05:11 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Tue, 28 Nov 2023 04:12:26 +0000 (05:12 +0100)
This is a minimal capture of state we need, allowing us to reuse codecs.

Change-Id: Iefa54fef563ab1adac3a0cedd166d5aedbd45588
JIRA: NETCONF-1157
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/JsonPatchBody.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/PatchBody.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/databind/XmlPatchBody.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataPatchPath.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/MdsalRestconfServer.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/databind/AbstractPatchBodyTest.java

index 779d994b074d6df5b6c5a13145d7ddddc85f0940..ed1c7f87747d02855f221040493c95a0c2b51ea7 100644 (file)
@@ -26,6 +26,7 @@ import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.patch.PatchContext;
 import org.opendaylight.restconf.common.patch.PatchEntity;
+import org.opendaylight.restconf.server.api.DataPatchPath;
 import org.opendaylight.restconf.server.api.DatabindContext;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.patch.rev170222.yang.patch.yang.patch.Edit.Operation;
 import org.opendaylight.yangtools.yang.common.ErrorTag;
@@ -33,11 +34,9 @@ import org.opendaylight.yangtools.yang.common.ErrorType;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
-import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
 import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream;
 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizationResultHolder;
-import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
 
@@ -47,18 +46,17 @@ public final class JsonPatchBody extends PatchBody {
     }
 
     @Override
-    PatchContext toPatchContext(final DatabindContext databind, final YangInstanceIdentifier urlPath,
-            final InputStream inputStream) throws IOException {
+    PatchContext toPatchContext(final DataPatchPath path, final InputStream inputStream) throws IOException {
         try (var jsonReader = new JsonReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
             final var patchId = new AtomicReference<String>();
-            final var resultList = read(jsonReader, databind, urlPath, patchId);
+            final var resultList = read(jsonReader, path, patchId);
             // Note: patchId side-effect of above
             return new PatchContext(patchId.get(), resultList);
         }
     }
 
-    private static ImmutableList<PatchEntity> read(final JsonReader in, final DatabindContext databind,
-            final YangInstanceIdentifier urlPath, final AtomicReference<String> patchId) throws IOException {
+    private static ImmutableList<PatchEntity> read(final JsonReader in, final DataPatchPath path,
+            final AtomicReference<String> patchId) throws IOException {
         final var edits = ImmutableList.<PatchEntity>builder();
         final var edit = new PatchEdit();
 
@@ -83,7 +81,7 @@ public final class JsonPatchBody extends PatchBody {
                 case END_DOCUMENT:
                     break;
                 case NAME:
-                    parseByName(in.nextName(), edit, in, urlPath, databind, edits, patchId);
+                    parseByName(in.nextName(), edit, in, path, edits, patchId);
                     break;
                 case END_OBJECT:
                     in.endObject();
@@ -102,23 +100,23 @@ public final class JsonPatchBody extends PatchBody {
 
     // Switch value of parsed JsonToken.NAME and read edit definition or patch id
     private static void parseByName(final @NonNull String name, final @NonNull PatchEdit edit,
-            final @NonNull JsonReader in, final @NonNull YangInstanceIdentifier urlPath,
-            final @NonNull DatabindContext databind, final @NonNull Builder<PatchEntity> resultCollection,
-            final @NonNull AtomicReference<String> patchId) throws IOException {
+            final @NonNull JsonReader in, final @NonNull DataPatchPath path,
+            final @NonNull Builder<PatchEntity> resultCollection, final @NonNull AtomicReference<String> patchId)
+                throws IOException {
         switch (name) {
             case "edit":
                 if (in.peek() == JsonToken.BEGIN_ARRAY) {
                     in.beginArray();
 
                     while (in.hasNext()) {
-                        readEditDefinition(edit, in, urlPath, databind);
+                        readEditDefinition(edit, in, path);
                         resultCollection.add(prepareEditOperation(edit));
                         edit.clear();
                     }
 
                     in.endArray();
                 } else {
-                    readEditDefinition(edit, in, urlPath, databind);
+                    readEditDefinition(edit, in, path);
                     resultCollection.add(prepareEditOperation(edit));
                     edit.clear();
                 }
@@ -134,8 +132,7 @@ public final class JsonPatchBody extends PatchBody {
 
     // Read one patch edit object from JSON input
     private static void readEditDefinition(final @NonNull PatchEdit edit, final @NonNull JsonReader in,
-            final @NonNull YangInstanceIdentifier urlPath, final @NonNull DatabindContext databind)
-                throws IOException {
+            final @NonNull DataPatchPath path) throws IOException {
         String deferredValue = null;
         in.beginObject();
 
@@ -150,8 +147,8 @@ public final class JsonPatchBody extends PatchBody {
                     break;
                 case "target":
                     // target can be specified completely in request URI
-                    edit.setTarget(parsePatchTarget(databind, urlPath, in.nextString()));
-                    final var stack = databind.schemaTree().enterPath(edit.getTarget()).orElseThrow().stack();
+                    edit.setTarget(parsePatchTarget(path, in.nextString()));
+                    final var stack = path.databind().schemaTree().enterPath(edit.getTarget()).orElseThrow().stack();
                     if (!stack.isEmpty()) {
                         stack.exit();
                     }
@@ -172,7 +169,7 @@ public final class JsonPatchBody extends PatchBody {
                         deferredValue = readValueNode(in);
                     } else {
                         // We have a target schema node, reuse this reader without buffering the value.
-                        edit.setData(readEditData(in, edit.getTargetSchemaNode(), databind.modelContext()));
+                        edit.setData(readEditData(in, edit.getTargetSchemaNode(), path.databind()));
                     }
                     break;
                 default:
@@ -186,7 +183,7 @@ public final class JsonPatchBody extends PatchBody {
         if (deferredValue != null) {
             // read saved data to normalized node when target schema is already known
             edit.setData(readEditData(new JsonReader(new StringReader(deferredValue)), edit.getTargetSchemaNode(),
-                databind.modelContext()));
+                path.databind()));
         }
     }
 
@@ -290,12 +287,10 @@ public final class JsonPatchBody extends PatchBody {
      * @return NormalizedNode representing data
      */
     private static NormalizedNode readEditData(final @NonNull JsonReader in, final @NonNull Inference targetSchemaNode,
-            final @NonNull EffectiveModelContext context) {
+            final @NonNull DatabindContext databind) {
         final var resultHolder = new NormalizationResultHolder();
         final var writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
-        JsonParserStream.create(writer, JSONCodecFactorySupplier.RFC7951.getShared(context), targetSchemaNode)
-            .parse(in);
-
+        JsonParserStream.create(writer, databind.jsonCodecs(), targetSchemaNode).parse(in);
         return resultHolder.getResult().data();
     }
 
index 3eb1a4bfb19007b7e94e8f7063f940e7ff53fbda..4a2956bcc88cad8f3b8d62ea9b8da757f36bbee6 100644 (file)
@@ -18,7 +18,7 @@ import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.patch.PatchContext;
 import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
 import org.opendaylight.restconf.nb.rfc8040.utils.parser.IdentifierCodec;
-import org.opendaylight.restconf.server.api.DatabindContext;
+import org.opendaylight.restconf.server.api.DataPatchPath;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.patch.rev170222.yang.patch.yang.patch.Edit.Operation;
 import org.opendaylight.yangtools.yang.common.ErrorTag;
 import org.opendaylight.yangtools.yang.common.ErrorType;
@@ -32,24 +32,24 @@ public abstract sealed class PatchBody extends AbstractBody permits JsonPatchBod
         super(inputStream);
     }
 
-    public final @NonNull PatchContext toPatchContext(final @NonNull DatabindContext databind,
-            final @NonNull YangInstanceIdentifier urlPath) throws IOException {
+    public final @NonNull PatchContext toPatchContext(final @NonNull DataPatchPath path) throws IOException {
         try (var is = acquireStream()) {
-            return toPatchContext(databind, urlPath, is);
+            return toPatchContext(path, is);
         }
     }
 
-    abstract @NonNull PatchContext toPatchContext(@NonNull DatabindContext databind,
-        @NonNull YangInstanceIdentifier urlPath, @NonNull InputStream inputStream) throws IOException;
+    abstract @NonNull PatchContext toPatchContext(@NonNull DataPatchPath path, @NonNull InputStream inputStream)
+        throws IOException;
 
-    static final YangInstanceIdentifier parsePatchTarget(final DatabindContext databind,
-            final YangInstanceIdentifier urlPath, final String target) {
+    static final YangInstanceIdentifier parsePatchTarget(final DataPatchPath path, final String target) {
+        final var urlPath = path.instance();
         if (target.equals("/")) {
             verify(!urlPath.isEmpty(),
                 "target resource of URI must not be a datastore resource when target is '/'");
             return urlPath;
         }
 
+        final var databind = path.databind();
         final String targetUrl;
         if (urlPath.isEmpty()) {
             targetUrl = target.startsWith("/") ? target.substring(1) : target;
index 03d76eba7faf39fa5966a91ee4f2870d5ff9e532..4dab95f0d02495395fdf8cd1af58fdf6b568edd4 100644 (file)
@@ -20,12 +20,11 @@ import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.patch.PatchContext;
 import org.opendaylight.restconf.common.patch.PatchEntity;
-import org.opendaylight.restconf.server.api.DatabindContext;
+import org.opendaylight.restconf.server.api.DataPatchPath;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.patch.rev170222.yang.patch.yang.patch.Edit.Operation;
 import org.opendaylight.yangtools.util.xml.UntrustedXML;
 import org.opendaylight.yangtools.yang.common.ErrorTag;
 import org.opendaylight.yangtools.yang.common.ErrorType;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
 import org.opendaylight.yangtools.yang.data.codec.xml.XmlParserStream;
 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
@@ -45,10 +44,9 @@ public final class XmlPatchBody extends PatchBody {
     }
 
     @Override
-    PatchContext toPatchContext(final DatabindContext databind, final YangInstanceIdentifier urlPath,
-            final InputStream inputStream) throws IOException {
+    PatchContext toPatchContext(final DataPatchPath path, final InputStream inputStream) throws IOException {
         try {
-            return parse(databind, urlPath, UntrustedXML.newDocumentBuilder().parse(inputStream));
+            return parse(path, UntrustedXML.newDocumentBuilder().parse(inputStream));
         } catch (XMLStreamException | SAXException | URISyntaxException e) {
             LOG.debug("Failed to parse YANG Patch XML", e);
             throw new RestconfDocumentedException("Error parsing YANG Patch XML: " + e.getMessage(), ErrorType.PROTOCOL,
@@ -56,11 +54,12 @@ public final class XmlPatchBody extends PatchBody {
         }
     }
 
-    private static @NonNull PatchContext parse(final DatabindContext databind, final YangInstanceIdentifier urlPath,
-            final Document doc) throws XMLStreamException, IOException, SAXException, URISyntaxException {
+    private static @NonNull PatchContext parse(final DataPatchPath path, final Document doc)
+            throws XMLStreamException, IOException, SAXException, URISyntaxException {
         final var entities = ImmutableList.<PatchEntity>builder();
         final var patchId = doc.getElementsByTagName("patch-id").item(0).getFirstChild().getNodeValue();
         final var editNodes = doc.getElementsByTagName("edit");
+        final var databind = path.databind();
 
         for (int i = 0; i < editNodes.getLength(); i++) {
             final Element element = (Element) editNodes.item(i);
@@ -72,7 +71,7 @@ public final class XmlPatchBody extends PatchBody {
             final Element firstValueElement = values != null ? values.get(0) : null;
 
             // find complete path to target, it can be also empty (only slash)
-            final var targetII = parsePatchTarget(databind, urlPath, target);
+            final var targetII = parsePatchTarget(path, target);
             // move schema node
             final var lookup = databind.schemaTree().enterPath(targetII).orElseThrow();
 
@@ -85,7 +84,7 @@ public final class XmlPatchBody extends PatchBody {
             if (requiresValue(oper)) {
                 final var resultHolder = new NormalizationResultHolder();
                 final var writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
-                final var xmlParser = XmlParserStream.create(writer, inference);
+                final var xmlParser = XmlParserStream.create(writer, databind.xmlCodecs(), inference);
                 xmlParser.traverse(new DOMSource(firstValueElement));
 
                 final var result = resultHolder.getResult().data();
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataPatchPath.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataPatchPath.java
new file mode 100644 (file)
index 0000000..391f636
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.server.api;
+
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.restconf.api.ApiPath;
+import org.opendaylight.restconf.nb.rfc8040.databind.PatchBody;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+
+/**
+ * An {@link ApiPath} subpath of {@code /data} {@code PUT} HTTP operation, as defined in
+ * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.5">RFC8040 section 4.5</a>.
+ *
+ * @param databind Associated {@link DatabindContext}
+ * @param instance Associated {@link YangInstanceIdentifier}
+ * @see PatchBody
+ */
+@NonNullByDefault
+public record DataPatchPath(DatabindContext databind, YangInstanceIdentifier instance)
+        implements DatabindAware {
+    public DataPatchPath {
+        requireNonNull(databind);
+        requireNonNull(instance);
+    }
+}
index 819acc16626e4ec6d1e1b413b1a7bd473afe86ad..fa50703fc5a730ae83e927005e2bcb0e25bacf5a 100644 (file)
@@ -69,6 +69,7 @@ import org.opendaylight.restconf.nb.rfc8040.rests.transactions.MdsalRestconfStra
 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
 import org.opendaylight.restconf.nb.rfc8040.utils.parser.NetconfFieldsTranslator;
 import org.opendaylight.restconf.nb.rfc8040.utils.parser.WriterFieldsTranslator;
+import org.opendaylight.restconf.server.api.DataPatchPath;
 import org.opendaylight.restconf.server.api.DataPostPath;
 import org.opendaylight.restconf.server.api.DataPostResult;
 import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
@@ -301,9 +302,10 @@ public final class MdsalRestconfServer
 
     private @NonNull RestconfFuture<PatchStatusContext> dataPATCH(final InstanceIdentifierContext reqPath,
             final PatchBody body) {
+        final var patchPath = new DataPatchPath(reqPath.databind(), reqPath.getInstanceIdentifier());
         final PatchContext patch;
         try {
-            patch = body.toPatchContext(reqPath.databind(), reqPath.getInstanceIdentifier());
+            patch = body.toPatchContext(patchPath);
         } catch (IOException e) {
             LOG.debug("Error parsing YANG Patch input", e);
             return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
index 56477be957641a686f6da127f9acf70d7aa92f09..8226f9a2dd6f0fd4d44f87bd91d59f879a5c3d77 100644 (file)
@@ -31,6 +31,7 @@ import org.opendaylight.restconf.api.ApiPath;
 import org.opendaylight.restconf.common.patch.PatchContext;
 import org.opendaylight.restconf.nb.rfc8040.AbstractInstanceIdentifierTest;
 import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
+import org.opendaylight.restconf.server.api.DataPatchPath;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 
 @RunWith(MockitoJUnitRunner.Silent.class)
@@ -85,7 +86,7 @@ abstract class AbstractPatchBodyTest extends AbstractInstanceIdentifierTest {
         final var iid = InstanceIdentifierContext.ofApiPath(apiPath, IID_DATABIND, mountPointService);
 
         try (var body = bodyConstructor.apply(stringInputStream(patchBody))) {
-            return body.toPatchContext(iid.databind(), iid.getInstanceIdentifier());
+            return body.toPatchContext(new DataPatchPath(iid.databind(), iid.getInstanceIdentifier()));
         }
     }
 }