Bug 3866: Support for Restconf HTTP Patch
[netconf.git] / opendaylight / restconf / sal-rest-connector / src / main / java / org / opendaylight / netconf / sal / restconf / impl / BrokerFacade.java
index 0a5409cb10cde37af69f37fef0f08e450a5295a4..b4be7c683ccc119d6b75e302f09d2f7122abdcdb 100644 (file)
@@ -1,4 +1,4 @@
-/**
+/*
  * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
  *
  * This program and the accompanying materials are made available under the
@@ -11,6 +11,7 @@ import static org.opendaylight.controller.md.sal.common.api.data.LogicalDatastor
 import static org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType.OPERATIONAL;
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.CheckedFuture;
 import com.google.common.util.concurrent.ListenableFuture;
 import java.util.ArrayList;
@@ -126,6 +127,88 @@ public class BrokerFacade {
         throw new RestconfDocumentedException(errMsg);
     }
 
+    public PATCHStatusContext patchConfigurationDataWithinTransaction(final PATCHContext context,
+                                                                      final SchemaContext globalSchema) {
+        final DOMDataReadWriteTransaction patchTransaction = domDataBroker.newReadWriteTransaction();
+        List<PATCHStatusEntity> editCollection = new ArrayList<>();
+        List<RestconfError> editErrors;
+        List<RestconfError> globalErrors = null;
+        int errorCounter = 0;
+
+        for (PATCHEntity patchEntity : context.getData()) {
+            final PATCHEditOperation operation = PATCHEditOperation.valueOf(patchEntity.getOperation().toUpperCase());
+
+            switch (operation) {
+                case CREATE:
+                    if (errorCounter == 0) {
+                        try {
+                            postDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity.getTargetNode(),
+                                    patchEntity.getNode(), globalSchema);
+                            editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null));
+                        } catch (RestconfDocumentedException e) {
+                            editErrors = new ArrayList<>();
+                            editErrors.addAll(e.getErrors());
+                            editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), false, editErrors));
+                            errorCounter++;
+                        }
+                    }
+                    break;
+                case REPLACE:
+                    if (errorCounter == 0) {
+                        try {
+                            putDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity
+                                    .getTargetNode(), patchEntity.getNode(), globalSchema);
+                            editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null));
+                        } catch (RestconfDocumentedException e) {
+                            editErrors = new ArrayList<>();
+                            editErrors.addAll(e.getErrors());
+                            editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), false, editErrors));
+                            errorCounter++;
+                        }
+                    }
+                    break;
+                case DELETE:
+                    if (errorCounter == 0) {
+                        try {
+                            deleteDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity
+                                    .getTargetNode());
+                            editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null));
+                        } catch (RestconfDocumentedException e) {
+                            editErrors = new ArrayList<>();
+                            editErrors.addAll(e.getErrors());
+                            editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), false, editErrors));
+                            errorCounter++;
+                        }
+                    }
+                    break;
+                case REMOVE:
+                    if (errorCounter == 0) {
+                        try {
+                            deleteDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity
+                                    .getTargetNode());
+                            editCollection.add(new PATCHStatusEntity(patchEntity.getEditId(), true, null));
+                        } catch (RestconfDocumentedException e) {
+                            LOG.error("Error removing {} by {} operation", patchEntity.getTargetNode().toString(),
+                                    patchEntity.getEditId(), e);
+                        }
+                    }
+                    break;
+            }
+        }
+
+        //TODO: make sure possible global errors are filled up correctly and decide transaction submission based on that
+        //globalErrors = new ArrayList<>();
+        if (errorCounter == 0) {
+            final CheckedFuture<Void, TransactionCommitFailedException> submit = patchTransaction.submit();
+            return new PATCHStatusContext(context.getPatchId(), ImmutableList.copyOf(editCollection), true,
+                    globalErrors);
+        } else {
+            patchTransaction.cancel();
+            return new PATCHStatusContext(context.getPatchId(), ImmutableList.copyOf(editCollection), false,
+                    globalErrors);
+        }
+    }
+
     // POST configuration
     public CheckedFuture<Void, TransactionCommitFailedException> commitConfigurationDataPost(
             final SchemaContext globalSchema, final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload) {
@@ -190,7 +273,7 @@ public class BrokerFacade {
 
     private NormalizedNode<?, ?> readDataViaTransaction(final DOMDataReadTransaction transaction,
             final LogicalDatastoreType datastore, final YangInstanceIdentifier path) {
-        LOG.trace("Read " + datastore.name() + " via Restconf: {}", path);
+        LOG.trace("Read {} via Restconf: {}", datastore.name(), path);
         final ListenableFuture<Optional<NormalizedNode<?, ?>>> listenableFuture = transaction.read(datastore, path);
         if (listenableFuture != null) {
             Optional<NormalizedNode<?, ?>> optional;
@@ -198,7 +281,7 @@ public class BrokerFacade {
                 LOG.debug("Reading result data from transaction.");
                 optional = listenableFuture.get();
             } catch (InterruptedException | ExecutionException e) {
-                LOG.warn("Exception by reading " + datastore.name() + " via Restconf: {}", path, e);
+                LOG.warn("Exception by reading {} via Restconf: {}", datastore.name(), path, e);
                 throw new RestconfDocumentedException("Problem to get data from transaction.", e.getCause());
 
             }
@@ -218,7 +301,7 @@ public class BrokerFacade {
         //        not sure if this will work for choice case
         DOMDataReadWriteTransaction transaction = domDataBroker.newReadWriteTransaction();
         if(payload instanceof MapNode) {
-            LOG.trace("POST " + datastore.name() + " via Restconf: {} with payload {}", path, payload);
+            LOG.trace("POST {} via Restconf: {} with payload {}", datastore.name(), path, payload);
             final NormalizedNode<?, ?> emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path);
             try {
                 transaction.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree);
@@ -249,18 +332,40 @@ public class BrokerFacade {
         return transaction.submit();
     }
 
+    private void postDataWithinTransaction(
+            final DOMDataReadWriteTransaction rWTransaction, final LogicalDatastoreType datastore,
+            final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload, final SchemaContext schemaContext) {
+        // FIXME: This is doing correct post for container and list children
+        //        not sure if this will work for choice case
+        if(payload instanceof MapNode) {
+            LOG.trace("POST {} within Restconf PATCH: {} with payload {}", datastore.name(), path, payload);
+            final NormalizedNode<?, ?> emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path);
+            rWTransaction.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree);
+            ensureParentsByMerge(datastore, path, rWTransaction, schemaContext);
+            for(final MapEntryNode child : ((MapNode) payload).getValue()) {
+                final YangInstanceIdentifier childPath = path.node(child.getIdentifier());
+                checkItemDoesNotExists(rWTransaction, datastore, childPath);
+                rWTransaction.put(datastore, childPath, child);
+            }
+        } else {
+            checkItemDoesNotExists(rWTransaction,datastore, path);
+            ensureParentsByMerge(datastore, path, rWTransaction, schemaContext);
+            rWTransaction.put(datastore, path, payload);
+        }
+    }
+
     private void checkItemDoesNotExists(final DOMDataReadWriteTransaction rWTransaction,final LogicalDatastoreType store, final YangInstanceIdentifier path) {
         final ListenableFuture<Boolean> futureDatastoreData = rWTransaction.exists(store, path);
         try {
             if (futureDatastoreData.get()) {
                 final String errMsg = "Post Configuration via Restconf was not executed because data already exists";
-                LOG.trace(errMsg + ":{}", path);
+                LOG.trace("{}:{}", errMsg, path);
                 rWTransaction.cancel();
                 throw new RestconfDocumentedException("Data already exists for path: " + path, ErrorType.PROTOCOL,
                         ErrorTag.DATA_EXISTS);
             }
         } catch (InterruptedException | ExecutionException e) {
-            LOG.warn("It wasn't possible to get data loaded from datastore at path " + path, e);
+            LOG.warn("It wasn't possible to get data loaded from datastore at path {}", path, e);
         }
 
     }
@@ -270,7 +375,7 @@ public class BrokerFacade {
             final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload, final SchemaContext schemaContext)
     {
         DOMDataReadWriteTransaction transaction = domDataBroker.newReadWriteTransaction();
-        LOG.trace("Put " + datastore.name() + " via Restconf: {} with payload {}", path, payload);
+        LOG.trace("Put {} via Restconf: {} with payload {}", datastore.name(), path, payload);
         if (!ensureParentsByMerge(datastore, path, transaction, schemaContext)) {
             transaction.cancel();
             transaction = domDataBroker.newReadWriteTransaction();
@@ -279,14 +384,29 @@ public class BrokerFacade {
         return transaction.submit();
     }
 
+    private void putDataWithinTransaction(
+            final DOMDataReadWriteTransaction writeTransaction, final LogicalDatastoreType datastore,
+            final YangInstanceIdentifier path, final NormalizedNode<?, ?> payload, final SchemaContext schemaContext) {
+        LOG.trace("Put {} within Restconf PATCH: {} with payload {}", datastore.name(), path, payload);
+        ensureParentsByMerge(datastore, path, writeTransaction, schemaContext);
+        writeTransaction.put(datastore, path, payload);
+    }
+
     private CheckedFuture<Void, TransactionCommitFailedException> deleteDataViaTransaction(
             final DOMDataWriteTransaction writeTransaction, final LogicalDatastoreType datastore,
             final YangInstanceIdentifier path) {
-        LOG.trace("Delete " + datastore.name() + " via Restconf: {}", path);
+        LOG.trace("Delete {} via Restconf: {}", datastore.name(), path);
         writeTransaction.delete(datastore, path);
         return writeTransaction.submit();
     }
 
+    private void deleteDataWithinTransaction(
+            final DOMDataWriteTransaction writeTransaction, final LogicalDatastoreType datastore,
+            final YangInstanceIdentifier path) {
+        LOG.trace("Delete {} within Restconf PATCH: {}", datastore.name(), path);
+        writeTransaction.delete(datastore, path);
+    }
+
     public void setDomDataBroker(final DOMDataBroker domDataBroker) {
         this.domDataBroker = domDataBroker;
     }