Fix 500 server error in PUT request
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / server / api / PatchBody.java
index 1c156eeaf7cc1f225992dc5adb1e5282ce924c4d..fefdf7cfb5bafd8100b51752180552f2e8d8155c 100644 (file)
@@ -7,62 +7,89 @@
  */
 package org.opendaylight.restconf.server.api;
 
-import static com.google.common.base.Verify.verify;
+import static java.util.Objects.requireNonNull;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.text.ParseException;
 import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.opendaylight.restconf.api.ApiPath;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.patch.PatchContext;
-import org.opendaylight.restconf.server.spi.ApiPathNormalizer;
+import org.opendaylight.restconf.server.api.DatabindPath.Data;
 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.concepts.Immutable;
 import org.opendaylight.yangtools.yang.common.ErrorTag;
 import org.opendaylight.yangtools.yang.common.ErrorType;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 
 /**
  * A YANG Patch body.
  */
-public abstract sealed class PatchBody extends AbstractBody permits JsonPatchBody, XmlPatchBody {
+public abstract sealed class PatchBody extends RequestBody permits JsonPatchBody, XmlPatchBody {
+    /**
+     * Resource context needed to completely resolve a {@link PatchBody}.
+     */
+    @NonNullByDefault
+    public abstract static class ResourceContext implements Immutable {
+        protected final Data path;
+
+        protected ResourceContext(final Data path) {
+            this.path = requireNonNull(path);
+        }
+
+        /**
+         * Return a {@link ResourceContext} for a sub-resource identified by an {@link ApiPath}.
+         *
+         * @param apiPath sub-resource
+         * @return A {@link ResourceContext}
+         * @throws RestconfDocumentedException if the sub-resource cannot be resolved
+         */
+        protected abstract ResourceContext resolveRelative(ApiPath apiPath);
+    }
+
     PatchBody(final InputStream inputStream) {
         super(inputStream);
     }
 
-    public final @NonNull PatchContext toPatchContext(final @NonNull DataPatchPath path) throws IOException {
-        try (var is = acquireStream()) {
-            return toPatchContext(path, is);
+    public final @NonNull PatchContext toPatchContext(final @NonNull ResourceContext resource) throws IOException {
+        try (var is = consume()) {
+            return toPatchContext(resource, is);
         }
     }
 
-    abstract @NonNull PatchContext toPatchContext(@NonNull DataPatchPath path, @NonNull InputStream inputStream)
+    abstract @NonNull PatchContext toPatchContext(@NonNull ResourceContext resource, @NonNull InputStream inputStream)
         throws IOException;
 
-    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;
-        }
-
-        // FIXME: NETCONF-1157: these two operations should really be a single ApiPathNormalizer step -- but for that
-        //                      we need to switch to ApiPath forms
-        final var pathNormalizer = new ApiPathNormalizer(path.databind());
-        final String targetUrl;
-        if (urlPath.isEmpty()) {
-            targetUrl = target.startsWith("/") ? target.substring(1) : target;
-        } else {
-            targetUrl = pathNormalizer.canonicalize(urlPath).toString() + target;
+    static final Data parsePatchTarget(final @NonNull ResourceContext resource, final String target) {
+        // As per: https://www.rfc-editor.org/rfc/rfc8072#page-18:
+        //
+        //        "Identifies the target data node for the edit
+        //        operation.  If the target has the value '/', then
+        //        the target data node is the target resource.
+        //        The target node MUST identify a data resource,
+        //        not the datastore resource.";
+        //
+        final ApiPath targetPath;
+        try {
+            targetPath = ApiPath.parse(target.startsWith("/") ? target.substring(1) : target);
+        } catch (ParseException e) {
+            throw new RestconfDocumentedException("Failed to parse edit target '" + target + "'",
+                ErrorType.RPC, ErrorTag.MALFORMED_MESSAGE, e);
         }
 
+        final Data result;
         try {
-            return pathNormalizer.normalizeDataPath(ApiPath.parse(targetUrl)).instance();
-        } catch (ParseException | RestconfDocumentedException e) {
-            throw new RestconfDocumentedException("Failed to parse target " + target,
+            result = resource.resolveRelative(targetPath).path;
+        } catch (RestconfDocumentedException e) {
+            throw new RestconfDocumentedException("Invalid edit target '" + targetPath + "'",
                 ErrorType.RPC, ErrorTag.MALFORMED_MESSAGE, e);
         }
+        if (result.instance().isEmpty()) {
+            throw new RestconfDocumentedException("Target node resource must not be a datastore resource",
+                ErrorType.RPC, ErrorTag.MALFORMED_MESSAGE);
+        }
+        return result;
     }
 
     /**