Add plain PATCH capability to RFC8040 server 65/89165/2
authorAllan Clarke <aclarke@pobox.com>
Mon, 2 Mar 2020 20:29:11 +0000 (14:29 -0600)
committerRobert Varga <nite@hq.sk>
Tue, 21 Apr 2020 20:12:10 +0000 (20:12 +0000)
Rename constant PATCH to YANG_PATCH - there are now two patch types
Add unit tests for PlainPatchDataTransactionUtil
Update unit tests for RestconfDataService

JIRA: NETCONF-657
Change-Id: I0356e7d4947f89ed603d71e722d845e709ea4e03
Signed-off-by: Allan Clarke <aclarke@pobox.com>
Signed-off-by: Jamo Luhrsen <jluhrsen@gmail.com>
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
(cherry picked from commit fe6e7e380f5948e2582d97df5cb2498c59353869)

12 files changed:
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/Rfc8040.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/errors/RestconfDocumentedExceptionMapper.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/patch/JsonToPatchBodyReader.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/patch/PatchJsonBodyWriter.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/patch/PatchXmlBodyWriter.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/patch/XmlToPatchBodyReader.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/api/RestconfDataService.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImpl.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/PlainPatchDataTransactionUtil.java [new file with mode: 0644]
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/RestconfDataServiceConstant.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/services/wrapper/ServicesWrapper.java
restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/PlainPatchDataTransactionUtilTest.java [new file with mode: 0644]

index 0d055c43a9453cb3a8ca6b86fa05f71acc1a4021..e3ce8a43665bcf7f122c65f3040722a9dc855599 100644 (file)
@@ -40,8 +40,8 @@ public final class Rfc8040 {
         }
 
         public static final String DATA = "application/yang-data";
-        public static final String PATCH = "application/yang.patch";
-        public static final String PATCH_STATUS = "application/yang.patch-status";
+        public static final String YANG_PATCH = "application/yang.patch";
+        public static final String YANG_PATCH_STATUS = "application/yang.patch-status";
         public static final String YIN = "application/yin";
         public static final String YANG = "application/yang";
     }
index f1f9fa017dc8889fd2db7151d24042970b95fc0c..a9a640e24c89a43a488ec01b385df07e56246071 100644 (file)
@@ -61,9 +61,9 @@ public final class RestconfDocumentedExceptionMapper implements ExceptionMapper<
     @VisibleForTesting
     static final MediaType YANG_DATA_XML_TYPE = MediaType.valueOf(MediaTypes.DATA + RestconfConstants.XML);
     @VisibleForTesting
-    static final MediaType YANG_PATCH_JSON_TYPE = MediaType.valueOf(MediaTypes.PATCH + RestconfConstants.JSON);
+    static final MediaType YANG_PATCH_JSON_TYPE = MediaType.valueOf(MediaTypes.YANG_PATCH + RestconfConstants.JSON);
     @VisibleForTesting
-    static final MediaType YANG_PATCH_XML_TYPE = MediaType.valueOf(MediaTypes.PATCH + RestconfConstants.XML);
+    static final MediaType YANG_PATCH_XML_TYPE = MediaType.valueOf(MediaTypes.YANG_PATCH + RestconfConstants.XML);
 
     private static final Logger LOG = LoggerFactory.getLogger(RestconfDocumentedExceptionMapper.class);
     private static final MediaType DEFAULT_MEDIA_TYPE = MediaType.APPLICATION_JSON_TYPE;
index f336b7c5eaac774454371ecca16e7f8a0750671e..14fd87ce2f2efe53f947d767852589af81ed7c88 100644 (file)
@@ -55,7 +55,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 @Provider
-@Consumes({Rfc8040.MediaTypes.PATCH + RestconfConstants.JSON})
+@Consumes({Rfc8040.MediaTypes.YANG_PATCH + RestconfConstants.JSON})
 public class JsonToPatchBodyReader extends AbstractToPatchBodyReader {
     private static final Logger LOG = LoggerFactory.getLogger(JsonToPatchBodyReader.class);
 
index 805b339fc0f0452005c01f062b619987e6229ed5..99710ce87f7ede8b540b2769a6fa4e058d58475a 100644 (file)
@@ -31,7 +31,7 @@ import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory;
 
 
 @Provider
-@Produces({ Rfc8040.MediaTypes.PATCH_STATUS + RestconfConstants.JSON })
+@Produces({ Rfc8040.MediaTypes.YANG_PATCH_STATUS + RestconfConstants.JSON })
 public class PatchJsonBodyWriter implements MessageBodyWriter<PatchStatusContext> {
 
     @Override
index aa936e59193cea70020ef2b2a6905f1db67a346a..0d6ce8455a852dc2c7bc7881e9cfae134e717eea 100644 (file)
@@ -30,7 +30,7 @@ import org.opendaylight.restconf.nb.rfc8040.Rfc8040;
 import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants;
 
 @Provider
-@Produces({ Rfc8040.MediaTypes.PATCH_STATUS + RestconfConstants.XML })
+@Produces({ Rfc8040.MediaTypes.YANG_PATCH_STATUS + RestconfConstants.XML })
 public class PatchXmlBodyWriter implements MessageBodyWriter<PatchStatusContext> {
 
     private static final XMLOutputFactory XML_FACTORY;
index 33153a7ef8f472d86044102711eb4656b79c1dc7..322000d7e6c0995bf2a98be6e3d129ca90c6df97 100644 (file)
@@ -62,7 +62,7 @@ import org.w3c.dom.NodeList;
 import org.xml.sax.SAXException;
 
 @Provider
-@Consumes({Rfc8040.MediaTypes.PATCH + RestconfConstants.XML})
+@Consumes({Rfc8040.MediaTypes.YANG_PATCH + RestconfConstants.XML})
 public class XmlToPatchBodyReader extends AbstractToPatchBodyReader {
     private static final Logger LOG = LoggerFactory.getLogger(XmlToPatchBodyReader.class);
     private static final Splitter SLASH_SPLITTER = Splitter.on('/');
index 87407ac46308a595ccb3ca039ac093d211e29e2b..1c5b17c0b8071d9d3dfe6ed2d7b570a4cd7d0a74 100644 (file)
@@ -137,9 +137,10 @@ public interface RestconfDataService extends UpdateHandlers {
      */
     @Patch
     @Path("/data/{identifier:.+}")
-    @Consumes({ Rfc8040.MediaTypes.PATCH + RestconfConstants.JSON, Rfc8040.MediaTypes.PATCH + RestconfConstants.XML })
-    @Produces({ Rfc8040.MediaTypes.PATCH_STATUS + RestconfConstants.JSON,
-            Rfc8040.MediaTypes.PATCH_STATUS + RestconfConstants.XML })
+    @Consumes({ Rfc8040.MediaTypes.YANG_PATCH + RestconfConstants.JSON,
+            Rfc8040.MediaTypes.YANG_PATCH + RestconfConstants.XML })
+    @Produces({ Rfc8040.MediaTypes.YANG_PATCH_STATUS + RestconfConstants.JSON,
+            Rfc8040.MediaTypes.YANG_PATCH_STATUS + RestconfConstants.XML })
     PatchStatusContext patchData(@Encoded @PathParam("identifier") String identifier, PatchContext context,
                                  @Context UriInfo uriInfo);
 
@@ -154,8 +155,26 @@ public interface RestconfDataService extends UpdateHandlers {
      */
     @Patch
     @Path("/data")
-    @Consumes({ Rfc8040.MediaTypes.PATCH + RestconfConstants.JSON, Rfc8040.MediaTypes.PATCH + RestconfConstants.XML })
-    @Produces({ Rfc8040.MediaTypes.PATCH_STATUS + RestconfConstants.JSON,
-            Rfc8040.MediaTypes.PATCH_STATUS + RestconfConstants.XML })
+    @Consumes({ Rfc8040.MediaTypes.YANG_PATCH + RestconfConstants.JSON,
+            Rfc8040.MediaTypes.YANG_PATCH + RestconfConstants.XML })
+    @Produces({ Rfc8040.MediaTypes.YANG_PATCH_STATUS + RestconfConstants.JSON,
+            Rfc8040.MediaTypes.YANG_PATCH_STATUS + RestconfConstants.XML })
     PatchStatusContext patchData(PatchContext context, @Context UriInfo uriInfo);
+
+
+    /**
+     * Partially modify the target data resource.
+     *
+     * @param identifier
+     *            path to target
+     * @param payload
+     *            data node for put to config DS
+     * @return {@link Response}
+     */
+    @Patch
+    @Path("/data/{identifier:.+}")
+    @Consumes({ Rfc8040.MediaTypes.DATA + RestconfConstants.JSON, Rfc8040.MediaTypes.DATA, MediaType.APPLICATION_JSON,
+            MediaType.APPLICATION_XML, MediaType.TEXT_XML })
+    Response patchData(@Encoded @PathParam("identifier") String identifier, NormalizedNodeContext payload,
+                       @Context UriInfo uriInfo);
 }
index 2b81a48f6e74402e972d767ccd7606053cc9f404..3324904029a37d6f9544e765425bdad2e7d40e33 100644 (file)
@@ -8,6 +8,8 @@
 package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
 
 import static java.util.Objects.requireNonNull;
+import static org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfDataServiceConstant.PostPutQueryParameters.INSERT;
+import static org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfDataServiceConstant.PostPutQueryParameters.POINT;
 import static org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfStreamsConstants.CREATE_NOTIFICATION_STREAM;
 import static org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfStreamsConstants.STREAM_ACCESS_PATH_PART;
 import static org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfStreamsConstants.STREAM_LOCATION_PATH_PART;
@@ -24,6 +26,7 @@ import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
 import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.mdsal.dom.api.DOMActionResult;
 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
@@ -46,6 +49,7 @@ import org.opendaylight.restconf.nb.rfc8040.rests.services.api.RestconfStreamsSu
 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.TransactionVarsWrapper;
 import org.opendaylight.restconf.nb.rfc8040.rests.utils.DeleteDataTransactionUtil;
 import org.opendaylight.restconf.nb.rfc8040.rests.utils.PatchDataTransactionUtil;
+import org.opendaylight.restconf.nb.rfc8040.rests.utils.PlainPatchDataTransactionUtil;
 import org.opendaylight.restconf.nb.rfc8040.rests.utils.PostDataTransactionUtil;
 import org.opendaylight.restconf.nb.rfc8040.rests.utils.PutDataTransactionUtil;
 import org.opendaylight.restconf.nb.rfc8040.rests.utils.ReadDataTransactionUtil;
@@ -53,6 +57,7 @@ import org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfDataServiceConst
 import org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfInvokeOperationsUtil;
 import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants;
 import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
+import org.opendaylight.yangtools.concepts.Immutable;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.common.Revision;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
@@ -69,6 +74,15 @@ import org.slf4j.LoggerFactory;
  */
 @Path("/")
 public class RestconfDataServiceImpl implements RestconfDataService {
+    private static final class QueryParams implements Immutable {
+        final @Nullable String point;
+        final @Nullable String insert;
+
+        QueryParams(final @Nullable String insert, final @Nullable String point) {
+            this.insert = insert;
+            this.point = point;
+        }
+    }
 
     private static final Logger LOG = LoggerFactory.getLogger(RestconfDataServiceImpl.class);
     private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss");
@@ -157,6 +171,32 @@ public class RestconfDataServiceImpl implements RestconfDataService {
     public Response putData(final String identifier, final NormalizedNodeContext payload, final UriInfo uriInfo) {
         requireNonNull(payload);
 
+        final QueryParams checkedParms = checkQueryParameters(uriInfo);
+
+        final InstanceIdentifierContext<? extends SchemaNode> iid = payload
+                .getInstanceIdentifierContext();
+
+        PutDataTransactionUtil.validInputData(iid.getSchemaNode(), payload);
+        PutDataTransactionUtil.validTopLevelNodeName(iid.getInstanceIdentifier(), payload);
+        PutDataTransactionUtil.validateListKeysEqualityInPayloadAndUri(payload);
+
+        final DOMMountPoint mountPoint = payload.getInstanceIdentifierContext().getMountPoint();
+        final TransactionChainHandler localTransactionChainHandler;
+        final SchemaContextRef ref;
+        if (mountPoint == null) {
+            localTransactionChainHandler = this.transactionChainHandler;
+            ref = new SchemaContextRef(this.schemaContextHandler.get());
+        } else {
+            localTransactionChainHandler = transactionChainOfMountPoint(mountPoint);
+            ref = new SchemaContextRef(mountPoint.getSchemaContext());
+        }
+
+        final TransactionVarsWrapper transactionNode = new TransactionVarsWrapper(
+                payload.getInstanceIdentifierContext(), mountPoint, localTransactionChainHandler);
+        return PutDataTransactionUtil.putData(payload, ref, transactionNode, checkedParms.insert, checkedParms.point);
+    }
+
+    private static QueryParams checkQueryParameters(final UriInfo uriInfo) {
         boolean insertUsed = false;
         boolean pointUsed = false;
         String insert = null;
@@ -164,19 +204,19 @@ public class RestconfDataServiceImpl implements RestconfDataService {
 
         for (final Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
             switch (entry.getKey()) {
-                case "insert":
+                case INSERT:
                     if (!insertUsed) {
                         insertUsed = true;
-                        insert = entry.getValue().iterator().next();
+                        insert = entry.getValue().get(0);
                     } else {
                         throw new RestconfDocumentedException("Insert parameter can be used only once.",
                                 RestconfError.ErrorType.PROTOCOL, RestconfError.ErrorTag.BAD_ELEMENT);
                     }
                     break;
-                case "point":
+                case POINT:
                     if (!pointUsed) {
                         pointUsed = true;
-                        point = entry.getValue().iterator().next();
+                        point = entry.getValue().get(0);
                     } else {
                         throw new RestconfDocumentedException("Point parameter can be used only once.",
                                 RestconfError.ErrorType.PROTOCOL, RestconfError.ErrorTag.BAD_ELEMENT);
@@ -189,28 +229,7 @@ public class RestconfDataServiceImpl implements RestconfDataService {
         }
 
         checkQueryParams(insertUsed, pointUsed, insert);
-
-        final InstanceIdentifierContext<? extends SchemaNode> iid = payload
-                .getInstanceIdentifierContext();
-
-        PutDataTransactionUtil.validInputData(iid.getSchemaNode(), payload);
-        PutDataTransactionUtil.validTopLevelNodeName(iid.getInstanceIdentifier(), payload);
-        PutDataTransactionUtil.validateListKeysEqualityInPayloadAndUri(payload);
-
-        final DOMMountPoint mountPoint = payload.getInstanceIdentifierContext().getMountPoint();
-        final TransactionChainHandler localTransactionChainHandler;
-        final SchemaContextRef ref;
-        if (mountPoint == null) {
-            localTransactionChainHandler = this.transactionChainHandler;
-            ref = new SchemaContextRef(this.schemaContextHandler.get());
-        } else {
-            localTransactionChainHandler = transactionChainOfMountPoint(mountPoint);
-            ref = new SchemaContextRef(mountPoint.getSchemaContext());
-        }
-
-        final TransactionVarsWrapper transactionNode = new TransactionVarsWrapper(
-                payload.getInstanceIdentifierContext(), mountPoint, localTransactionChainHandler);
-        return PutDataTransactionUtil.putData(payload, ref, transactionNode, insert, point);
+        return new QueryParams(insert, point);
     }
 
     private static void checkQueryParams(final boolean insertUsed, final boolean pointUsed, final String insert) {
@@ -236,44 +255,14 @@ public class RestconfDataServiceImpl implements RestconfDataService {
         if (payload.getInstanceIdentifierContext().getSchemaNode() instanceof ActionDefinition) {
             return invokeAction(payload, uriInfo);
         }
-        boolean insertUsed = false;
-        boolean pointUsed = false;
-        String insert = null;
-        String point = null;
-
-        for (final Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
-            switch (entry.getKey()) {
-                case "insert":
-                    if (!insertUsed) {
-                        insertUsed = true;
-                        insert = entry.getValue().iterator().next();
-                    } else {
-                        throw new RestconfDocumentedException("Insert parameter can be used only once.",
-                                RestconfError.ErrorType.PROTOCOL, RestconfError.ErrorTag.BAD_ELEMENT);
-                    }
-                    break;
-                case "point":
-                    if (!pointUsed) {
-                        pointUsed = true;
-                        point = entry.getValue().iterator().next();
-                    } else {
-                        throw new RestconfDocumentedException("Point parameter can be used only once.",
-                                RestconfError.ErrorType.PROTOCOL, RestconfError.ErrorTag.BAD_ELEMENT);
-                    }
-                    break;
-                default:
-                    throw new RestconfDocumentedException("Bad parameter for post: " + entry.getKey(),
-                            RestconfError.ErrorType.PROTOCOL, RestconfError.ErrorTag.BAD_ELEMENT);
-            }
-        }
 
-        checkQueryParams(insertUsed, pointUsed, insert);
+        final QueryParams checkedParms = checkQueryParameters(uriInfo);
 
         final DOMMountPoint mountPoint = payload.getInstanceIdentifierContext().getMountPoint();
         final TransactionVarsWrapper transactionNode = new TransactionVarsWrapper(
                 payload.getInstanceIdentifierContext(), mountPoint, getTransactionChainHandler(mountPoint));
         return PostDataTransactionUtil.postData(uriInfo, payload, transactionNode,
-                getSchemaContext(mountPoint), insert, point);
+                getSchemaContext(mountPoint), checkedParms.insert, checkedParms.point);
     }
 
     @Override
@@ -308,6 +297,34 @@ public class RestconfDataServiceImpl implements RestconfDataService {
         return PatchDataTransactionUtil.patchData(context, transactionNode, getSchemaContext(mountPoint));
     }
 
+    @Override
+    public Response patchData(final String identifier, final NormalizedNodeContext payload, final UriInfo uriInfo) {
+        requireNonNull(payload);
+
+        final InstanceIdentifierContext<? extends SchemaNode> iid = payload
+                .getInstanceIdentifierContext();
+
+        PutDataTransactionUtil.validInputData(iid.getSchemaNode(), payload);
+        PutDataTransactionUtil.validTopLevelNodeName(iid.getInstanceIdentifier(), payload);
+        PutDataTransactionUtil.validateListKeysEqualityInPayloadAndUri(payload);
+
+        final DOMMountPoint mountPoint = payload.getInstanceIdentifierContext().getMountPoint();
+        final TransactionChainHandler localTransactionChainHandler;
+        final SchemaContextRef ref;
+        if (mountPoint == null) {
+            localTransactionChainHandler = this.transactionChainHandler;
+            ref = new SchemaContextRef(this.schemaContextHandler.get());
+        } else {
+            localTransactionChainHandler = transactionChainOfMountPoint(mountPoint);
+            ref = new SchemaContextRef(mountPoint.getSchemaContext());
+        }
+
+        final TransactionVarsWrapper transactionNode = new TransactionVarsWrapper(
+                payload.getInstanceIdentifierContext(), mountPoint, localTransactionChainHandler);
+
+        return PlainPatchDataTransactionUtil.patchData(payload, transactionNode, ref);
+    }
+
     private TransactionChainHandler getTransactionChainHandler(final DOMMountPoint mountPoint) {
         return mountPoint == null ? transactionChainHandler : transactionChainOfMountPoint(mountPoint);
     }
diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/PlainPatchDataTransactionUtil.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/PlainPatchDataTransactionUtil.java
new file mode 100644 (file)
index 0000000..04adf47
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2020 Lumina Networks, Inc. and others.  All rights reserved.
+ * Copyright (c) 2016 Cisco Systems, Inc. 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.nb.rfc8040.rests.utils;
+
+import com.google.common.util.concurrent.FluentFuture;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.opendaylight.mdsal.common.api.CommitInfo;
+import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
+import org.opendaylight.mdsal.dom.api.DOMDataTreeReadWriteTransaction;
+import org.opendaylight.mdsal.dom.api.DOMDataTreeWriteTransaction;
+import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
+import org.opendaylight.restconf.common.context.NormalizedNodeContext;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.nb.rfc8040.references.SchemaContextRef;
+import org.opendaylight.restconf.nb.rfc8040.rests.transactions.TransactionVarsWrapper;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Util class for plain patch data to DS.
+ *
+ */
+public final class PlainPatchDataTransactionUtil {
+
+    private static final Logger LOG = LoggerFactory.getLogger(PlainPatchDataTransactionUtil.class);
+
+    private PlainPatchDataTransactionUtil() {
+    }
+
+    /**
+     * Prepare variables for put data to DS. Close {@link DOMTransactionChain} inside of object
+     * {@link TransactionVarsWrapper} provided as a parameter.
+     *
+     * @param payload
+     *             data to put
+     * @param schemaContextRef
+     *             reference to {@link SchemaContext}
+     * @param transactionNode
+     *             wrapper of variables for transaction
+     * @return {@link Response}
+     */
+    public static Response patchData(final NormalizedNodeContext payload,
+                                     final TransactionVarsWrapper transactionNode,
+                                     final SchemaContextRef schemaContextRef) {
+
+        final DOMTransactionChain transactionChain = transactionNode.getTransactionChain();
+        final DOMDataTreeReadWriteTransaction tx = transactionChain.newReadWriteTransaction();
+
+        YangInstanceIdentifier path = payload.getInstanceIdentifierContext().getInstanceIdentifier();
+        NormalizedNode<?, ?> data = payload.getData();
+
+        try {
+            mergeDataWithinTransaction(LogicalDatastoreType.CONFIGURATION,
+                    path, data, tx, schemaContextRef);
+        } catch (final RestconfDocumentedException e) {
+            tx.cancel();
+            transactionChain.close();
+
+            throw new IllegalArgumentException(e);
+        }
+
+        final FluentFuture<? extends CommitInfo> future = tx.commit();
+        final ResponseFactory response = new ResponseFactory(Status.OK);
+
+        FutureCallbackTx.addCallback(future,
+                RestconfDataServiceConstant.PatchData.PATCH_TX_TYPE,
+                response,
+                transactionChain); // closes transactionChain, may throw
+
+        return response.build();
+    }
+
+    /**
+     * Merge data within one transaction.
+     * @param dataStore Datastore to merge data to
+     * @param path Path for data to be merged
+     * @param payload Data to be merged
+     * @param writeTransaction Transaction
+     * @param schemaContextRef Soft reference for global schema context
+     */
+    private static void mergeDataWithinTransaction(final LogicalDatastoreType dataStore,
+                                                   final YangInstanceIdentifier path,
+                                                   final NormalizedNode<?, ?> payload,
+                                                   final DOMDataTreeWriteTransaction writeTransaction,
+                                                   final SchemaContextRef schemaContextRef) {
+        LOG.trace("Merge {} within Restconf Patch: {} with payload {}", dataStore.name(), path, payload);
+        TransactionUtil.ensureParentsByMerge(path, schemaContextRef.get(), writeTransaction);
+        writeTransaction.merge(dataStore, path, payload);
+    }
+}
index 1b18839512f2527d00d978d3eb3d607362630824..ade44ca81ce0b14c8d71c5f418a88fb1a4bcb9d9 100644 (file)
@@ -51,6 +51,18 @@ public final class RestconfDataServiceConstant {
         }
     }
 
+    /**
+     * Common for PostData and PutData.
+     */
+    public static final class PostPutQueryParameters {
+        public static final String INSERT = "insert";
+        public static final String POINT = "point";
+
+        private PostPutQueryParameters() {
+            // Hidden on purpose
+        }
+    }
+
     /**
      * Constants for data to put.
      *
index 0c60f0ee771be26b65c3862c4dd8d5e723ed3ea3..d6271ca9bbd32389a9eff9850351b7994fcdea59 100644 (file)
@@ -145,6 +145,11 @@ public final class ServicesWrapper implements BaseServicesWrapper, TransactionSe
         return this.delegRestconfDataService.patchData(context, uriInfo);
     }
 
+    @Override
+    public Response patchData(final String identifier, final NormalizedNodeContext payload, final UriInfo uriInfo) {
+        return this.delegRestconfDataService.patchData(identifier, payload, uriInfo);
+    }
+
     @Override
     public NormalizedNodeContext invokeRpc(final String identifier, final NormalizedNodeContext payload,
             final UriInfo uriInfo) {
diff --git a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/PlainPatchDataTransactionUtilTest.java b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/rests/utils/PlainPatchDataTransactionUtilTest.java
new file mode 100644 (file)
index 0000000..6c0ba76
--- /dev/null
@@ -0,0 +1,229 @@
+/*
+ * Copyright (c) 2020 Lumina Networks, Inc. and others. All rights reserved.
+ * Copyright (c) 2016 Cisco Systems, Inc. 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.nb.rfc8040.rests.utils;
+
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFalseFluentFuture;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.opendaylight.mdsal.common.api.CommitInfo;
+import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
+import org.opendaylight.mdsal.dom.api.DOMDataBroker;
+import org.opendaylight.mdsal.dom.api.DOMDataTreeReadTransaction;
+import org.opendaylight.mdsal.dom.api.DOMDataTreeReadWriteTransaction;
+import org.opendaylight.mdsal.dom.api.DOMDataTreeWriteTransaction;
+import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
+import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
+import org.opendaylight.restconf.common.context.NormalizedNodeContext;
+import org.opendaylight.restconf.nb.rfc8040.TestRestconfUtils;
+import org.opendaylight.restconf.nb.rfc8040.handlers.TransactionChainHandler;
+import org.opendaylight.restconf.nb.rfc8040.references.SchemaContextRef;
+import org.opendaylight.restconf.nb.rfc8040.rests.transactions.TransactionVarsWrapper;
+import org.opendaylight.yangtools.yang.common.QName;
+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.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
+import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
+import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
+import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
+
+public class PlainPatchDataTransactionUtilTest {
+
+    private static final String PATH_FOR_NEW_SCHEMA_CONTEXT = "/jukebox";
+
+    @Mock
+    private DOMTransactionChain transactionChain;
+    @Mock
+    private DOMDataTreeReadWriteTransaction readWrite;
+    @Mock
+    private DOMDataTreeReadTransaction read;
+    @Mock
+    private DOMDataTreeWriteTransaction write;
+    @Mock
+    private DOMDataBroker mockDataBroker;
+
+    private TransactionChainHandler transactionChainHandler;
+    private SchemaContextRef refSchemaCtx;
+    private LeafNode leafGap;
+    private ContainerNode jukeboxContainerWithPlayer;
+    private ContainerNode jukeboxContainerWithPlaylist;
+    private SchemaContext schema;
+    private DataSchemaNode schemaNodeForGap;
+    private YangInstanceIdentifier iidGap;
+    private DataSchemaNode schemaNodeForJukebox;
+    private YangInstanceIdentifier iidJukebox;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        this.refSchemaCtx = new SchemaContextRef(
+                YangParserTestUtils.parseYangFiles(TestRestconfUtils.loadFiles(PATH_FOR_NEW_SCHEMA_CONTEXT)));
+        this.schema = this.refSchemaCtx.get();
+
+        final QName qnJukebox = QName.create("http://example.com/ns/example-jukebox", "2015-04-04", "jukebox");
+        final QName qnPlayer = QName.create(qnJukebox, "player");
+        final QName qnGap = QName.create(qnJukebox, "gap");
+        final QName qnPlaylist = QName.create(qnJukebox, "playlist");
+        final QName qnPlaylistKey = QName.create(qnJukebox, "name");
+
+        final NodeIdentifierWithPredicates nidBandA =
+                NodeIdentifierWithPredicates.of(qnPlaylist, qnPlaylistKey, "MyFavoriteBand-A");
+        final NodeIdentifierWithPredicates nidBandB =
+                NodeIdentifierWithPredicates.of(qnPlaylist, qnPlaylistKey, "MyFavoriteBand-B");
+
+        this.iidGap = YangInstanceIdentifier.builder()
+                .node(qnJukebox)
+                .node(qnPlayer)
+                .node(qnGap)
+                .build();
+        this.schemaNodeForGap = DataSchemaContextTree.from(this.schema).getChild(this.iidGap).getDataSchemaNode();
+
+        this.iidJukebox = YangInstanceIdentifier.builder()
+                .node(qnJukebox)
+                .build();
+        this.schemaNodeForJukebox = DataSchemaContextTree.from(this.schema)
+                .getChild(this.iidJukebox).getDataSchemaNode();
+
+        this.leafGap = Builders.leafBuilder()
+                .withNodeIdentifier(new NodeIdentifier(qnGap))
+                .withValue(0.2)
+                .build();
+        final ContainerNode playerContainer = Builders.containerBuilder()
+                .withNodeIdentifier(new NodeIdentifier(qnPlayer))
+                .withChild(this.leafGap)
+                .build();
+        this.jukeboxContainerWithPlayer = Builders.containerBuilder()
+                .withNodeIdentifier(new NodeIdentifier(qnJukebox))
+                .withChild(playerContainer)
+                .build();
+
+        // ----------
+
+        final LeafNode<Object> leafBandA = Builders.leafBuilder()
+                .withNodeIdentifier(new NodeIdentifier(QName.create(qnJukebox, "name")))
+                .withValue("MyFavoriteBand-A")
+                .build();
+        final LeafNode<Object> leafDescriptionA = Builders.leafBuilder()
+                .withNodeIdentifier(new NodeIdentifier(QName.create(qnJukebox, "description")))
+                .withValue("band description A")
+                .build();
+        final MapEntryNode entryBandA = Builders.mapEntryBuilder()
+                .withNodeIdentifier(nidBandA)
+                .withChild(leafBandA)
+                .withChild(leafDescriptionA)
+                .build();
+
+        final LeafNode<Object> leafBandB = Builders.leafBuilder()
+                .withNodeIdentifier(new NodeIdentifier(QName.create(qnJukebox, "name")))
+                .withValue("MyFavoriteBand-B")
+                .build();
+        final LeafNode<Object> leafDescriptionB = Builders.leafBuilder()
+                .withNodeIdentifier(new NodeIdentifier(QName.create(qnJukebox, "description")))
+                .withValue("band description B")
+                .build();
+        final MapEntryNode entryBandB = Builders.mapEntryBuilder()
+                .withNodeIdentifier(nidBandB)
+                .withChild(leafBandB)
+                .withChild(leafDescriptionB)
+                .build();
+
+        final MapNode listBands = Builders.mapBuilder()
+                .withNodeIdentifier(new NodeIdentifier(qnPlaylist))
+                .withChild(entryBandA)
+                .withChild(entryBandB)
+                .build();
+        this.jukeboxContainerWithPlaylist = Builders.containerBuilder()
+                .withNodeIdentifier(new NodeIdentifier(qnJukebox))
+                .withChild(listBands)
+                .build();
+
+        Mockito.doReturn(transactionChain).when(mockDataBroker).createTransactionChain(Mockito.any());
+        transactionChainHandler = new TransactionChainHandler(mockDataBroker);
+    }
+
+    @Test
+    public void testPatchContainerData() {
+        final InstanceIdentifierContext<DataSchemaNode> iidContext =
+                new InstanceIdentifierContext<>(this.iidJukebox, this.schemaNodeForJukebox, null, this.schema);
+        final NormalizedNodeContext payload = new NormalizedNodeContext(iidContext, this.jukeboxContainerWithPlayer);
+
+        doReturn(this.readWrite).when(this.transactionChain).newReadWriteTransaction();
+        doReturn(this.read).when(this.transactionChain).newReadOnlyTransaction();
+        doReturn(this.write).when(this.transactionChain).newWriteOnlyTransaction();
+        doReturn(immediateFalseFluentFuture())
+                .when(this.readWrite).exists(LogicalDatastoreType.CONFIGURATION, this.iidJukebox);
+        doNothing().when(this.readWrite).put(LogicalDatastoreType.CONFIGURATION,
+                payload.getInstanceIdentifierContext().getInstanceIdentifier(), payload.getData());
+        doReturn(CommitInfo.emptyFluentFuture()).when(this.readWrite).commit();
+
+        PlainPatchDataTransactionUtil.patchData(payload,
+                new TransactionVarsWrapper(payload.getInstanceIdentifierContext(), null, transactionChainHandler),
+                this.refSchemaCtx);
+
+        verify(this.readWrite).merge(LogicalDatastoreType.CONFIGURATION,
+                payload.getInstanceIdentifierContext().getInstanceIdentifier(), payload.getData());
+    }
+
+    @Test
+    public void testPatchLeafData() {
+        final InstanceIdentifierContext<DataSchemaNode> iidContext =
+                new InstanceIdentifierContext<>(this.iidGap, this.schemaNodeForGap, null, this.schema);
+        final NormalizedNodeContext payload = new NormalizedNodeContext(iidContext, this.leafGap);
+
+        doReturn(this.readWrite).when(this.transactionChain).newReadWriteTransaction();
+        doReturn(this.read).when(this.transactionChain).newReadOnlyTransaction();
+        doReturn(this.write).when(this.transactionChain).newWriteOnlyTransaction();
+        doReturn(immediateFalseFluentFuture())
+                .when(this.readWrite).exists(LogicalDatastoreType.CONFIGURATION, this.iidGap);
+        doNothing().when(this.readWrite).put(LogicalDatastoreType.CONFIGURATION,
+                payload.getInstanceIdentifierContext().getInstanceIdentifier(), payload.getData());
+        doReturn(CommitInfo.emptyFluentFuture()).when(this.readWrite).commit();
+
+        PlainPatchDataTransactionUtil.patchData(payload,
+                new TransactionVarsWrapper(payload.getInstanceIdentifierContext(), null, transactionChainHandler),
+                this.refSchemaCtx);
+
+        verify(this.readWrite).merge(LogicalDatastoreType.CONFIGURATION,
+                payload.getInstanceIdentifierContext().getInstanceIdentifier(), payload.getData());
+    }
+
+    @Test
+    public void testPatchListData() {
+        final InstanceIdentifierContext<DataSchemaNode> iidContext =
+                new InstanceIdentifierContext<>(this.iidJukebox, this.schemaNodeForJukebox, null, this.schema);
+        final NormalizedNodeContext payload = new NormalizedNodeContext(iidContext, this.jukeboxContainerWithPlaylist);
+
+        doReturn(this.readWrite).when(this.transactionChain).newReadWriteTransaction();
+        doReturn(this.read).when(this.transactionChain).newReadOnlyTransaction();
+        doReturn(this.write).when(this.transactionChain).newWriteOnlyTransaction();
+        doReturn(immediateFalseFluentFuture())
+                .when(this.readWrite).exists(LogicalDatastoreType.CONFIGURATION, this.iidJukebox);
+        doNothing().when(this.readWrite).put(LogicalDatastoreType.CONFIGURATION,
+                payload.getInstanceIdentifierContext().getInstanceIdentifier(), payload.getData());
+        doReturn(CommitInfo.emptyFluentFuture()).when(this.readWrite).commit();
+
+        PlainPatchDataTransactionUtil.patchData(payload,
+                new TransactionVarsWrapper(payload.getInstanceIdentifierContext(), null, transactionChainHandler),
+                this.refSchemaCtx);
+
+        verify(this.readWrite).merge(LogicalDatastoreType.CONFIGURATION, this.iidJukebox, payload.getData());
+    }
+}