Bug 509: Added support for merge operation to InMemoryData Store 38/6338/3
authorTony Tkacik <ttkacik@cisco.com>
Wed, 23 Apr 2014 14:17:31 +0000 (16:17 +0200)
committerTony Tkacik <ttkacik@cisco.com>
Thu, 24 Apr 2014 08:38:04 +0000 (10:38 +0200)
Merge operation support is critical for backwards compatibility
and for Binding-Aware mapping and Restconf, which are not aware
of Mixin nodes representing lists, leaflists and choices.

Semantics of merge operation are similar to subtree modified,
but when node is not existing it creates one based on
state provided by user of the Data Broker APIs.

Merge operation also fixes concurrent creations of mixin nodes
as demonstrated in ConcurrentImplicitCreateTest,
where two separate applications may indirectly create same
root node.

Change-Id: I9ab290b512601bd99750a68ee53564e67ef0c879
Signed-off-by: Tony Tkacik <ttkacik@cisco.com>
12 files changed:
opendaylight/md-sal/sal-binding-broker/src/main/java/org/opendaylight/controller/md/sal/binding/impl/AbstractForwardedTransaction.java
opendaylight/md-sal/sal-binding-dom-it/src/test/java/org/opendaylight/controller/md/sal/binding/data/ConcurrentImplicitCreateTest.java [new file with mode: 0644]
opendaylight/md-sal/sal-dom-broker/src/main/java/org/opendaylight/controller/md/sal/dom/broker/impl/DOMDataBrokerImpl.java
opendaylight/md-sal/sal-dom-broker/src/main/java/org/opendaylight/controller/md/sal/dom/broker/impl/compat/BackwardsCompatibleTransaction.java
opendaylight/md-sal/sal-dom-broker/src/main/java/org/opendaylight/controller/md/sal/dom/store/impl/InMemoryDOMDataStore.java
opendaylight/md-sal/sal-dom-broker/src/main/java/org/opendaylight/controller/md/sal/dom/store/impl/MutableDataTree.java
opendaylight/md-sal/sal-dom-broker/src/main/java/org/opendaylight/controller/md/sal/dom/store/impl/OperationWithModification.java
opendaylight/md-sal/sal-dom-broker/src/main/java/org/opendaylight/controller/md/sal/dom/store/impl/ResolveDataChangeEventsTask.java
opendaylight/md-sal/sal-dom-broker/src/main/java/org/opendaylight/controller/md/sal/dom/store/impl/SchemaAwareApplyOperation.java
opendaylight/md-sal/sal-dom-broker/src/main/java/org/opendaylight/controller/md/sal/dom/store/impl/tree/ModificationType.java
opendaylight/md-sal/sal-dom-broker/src/main/java/org/opendaylight/controller/md/sal/dom/store/impl/tree/NodeModification.java
opendaylight/md-sal/sal-dom-spi/src/main/java/org/opendaylight/controller/sal/core/spi/data/DOMStoreWriteTransaction.java

index 6334457fd4961ae34d913fb9138a8313f6eb30b1..6bd6e6aaf52294ed0ada1b2809995ea1b7877260 100644 (file)
@@ -132,7 +132,7 @@ public class AbstractForwardedTransaction<T extends AsyncTransaction<org.openday
             }
 
             if (!d.isPresent() && iterator.hasNext()) {
-                writeTransaction.put(store, currentPath, currentOp.createDefault(currentArg));
+                writeTransaction.merge(store, currentPath, currentOp.createDefault(currentArg));
             }
         }
         //LOG .info("Tx: {} : Putting data {}",getDelegate().getIdentifier(),normalized.getKey());
diff --git a/opendaylight/md-sal/sal-binding-dom-it/src/test/java/org/opendaylight/controller/md/sal/binding/data/ConcurrentImplicitCreateTest.java b/opendaylight/md-sal/sal-binding-dom-it/src/test/java/org/opendaylight/controller/md/sal/binding/data/ConcurrentImplicitCreateTest.java
new file mode 100644 (file)
index 0000000..61a73d6
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2014 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.controller.md.sal.binding.data;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+import org.junit.Test;
+import org.opendaylight.controller.md.sal.common.api.TransactionStatus;
+import org.opendaylight.controller.sal.binding.api.data.DataModificationTransaction;
+import org.opendaylight.controller.sal.binding.test.AbstractDataServiceTest;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.inventory.rev130819.NodeId;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.inventory.rev130819.Nodes;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.inventory.rev130819.nodes.Node;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.inventory.rev130819.nodes.NodeBuilder;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.inventory.rev130819.nodes.NodeKey;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+import org.opendaylight.yangtools.yang.common.RpcResult;
+
+public class ConcurrentImplicitCreateTest extends AbstractDataServiceTest {
+
+    private static final NodeKey NODE_FOO_KEY = new NodeKey(new NodeId("foo"));
+    private static final NodeKey NODE_BAR_KEY = new NodeKey(new NodeId("foo"));
+    private static InstanceIdentifier<Nodes> NODES_PATH = InstanceIdentifier.builder(Nodes.class).build();
+    private static InstanceIdentifier<Node> NODE_FOO_PATH = InstanceIdentifier.builder(NODES_PATH)
+            .child(Node.class, NODE_FOO_KEY).build();
+    private static InstanceIdentifier<Node> NODE_BAR_PATH = InstanceIdentifier.builder(NODES_PATH)
+            .child(Node.class, NODE_FOO_KEY).build();
+
+    @Test
+    public void testConcurrentCreate() throws InterruptedException, ExecutionException {
+
+        DataModificationTransaction fooTx = baDataService.beginTransaction();
+        DataModificationTransaction barTx = baDataService.beginTransaction();
+
+        fooTx.putOperationalData(NODE_FOO_PATH, new NodeBuilder().setKey(NODE_FOO_KEY).build());
+        barTx.putOperationalData(NODE_BAR_PATH, new NodeBuilder().setKey(NODE_BAR_KEY).build());
+
+        Future<RpcResult<TransactionStatus>> fooFuture = fooTx.commit();
+        Future<RpcResult<TransactionStatus>> barFuture = barTx.commit();
+
+        RpcResult<TransactionStatus> fooResult = fooFuture.get();
+        RpcResult<TransactionStatus> barResult = barFuture.get();
+
+        assertTrue(fooResult.isSuccessful());
+        assertTrue(barResult.isSuccessful());
+
+        assertEquals(TransactionStatus.COMMITED, fooResult.getResult());
+        assertEquals(TransactionStatus.COMMITED, barResult.getResult());
+
+    }
+}
index 64a5606e3fb613a04a70a76cff65a83759cdd589..608ac9bc68d120c39d96b0389bbed324ba01652e 100644 (file)
@@ -217,8 +217,7 @@ public class DOMDataBrokerImpl implements DOMDataBroker, AutoCloseable {
         @Override
         public void merge(final LogicalDatastoreType store, final InstanceIdentifier path,
                 final NormalizedNode<?, ?> data) {
-            // TODO Auto-generated method stub
-            throw new UnsupportedOperationException("Not implemented yet.");
+            getSubtransaction(store).merge(path,data);
         }
 
         @Override
@@ -251,12 +250,6 @@ public class DOMDataBrokerImpl implements DOMDataBroker, AutoCloseable {
                 final InstanceIdentifier path) {
             return getSubtransaction(store).read(path);
         }
-
-        @Override
-        public void merge(final LogicalDatastoreType store, final InstanceIdentifier path,
-                final NormalizedNode<?, ?> data) {
-
-        }
     }
 
     private final class CommitCoordination implements Callable<RpcResult<TransactionStatus>> {
index b905d2f673d77e01a4b8ee56cd0ca09ffddc14c8..29f2af511e8b8bb03ab0921837b8acae80aa59a7 100644 (file)
@@ -32,21 +32,13 @@ import org.opendaylight.yangtools.concepts.ListenerRegistration;
 import org.opendaylight.yangtools.yang.common.RpcResult;
 import org.opendaylight.yangtools.yang.data.api.CompositeNode;
 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier;
-import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.AugmentationIdentifier;
-import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.NodeIdentifier;
 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.PathArgument;
-import org.opendaylight.yangtools.yang.data.api.schema.AugmentationNode;
-import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
-import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
-import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
-import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
-import com.google.common.collect.Iterables;
 import com.google.common.util.concurrent.ListenableFuture;
 
 public abstract class BackwardsCompatibleTransaction<T extends DOMDataReadTransaction> implements
@@ -249,7 +241,7 @@ public abstract class BackwardsCompatibleTransaction<T extends DOMDataReadTransa
                 InstanceIdentifier currentPath = new InstanceIdentifier(currentArguments);
                 boolean isPresent = getDelegate().read(store, currentPath).get().isPresent();
                 if(isPresent == false && iterator.hasNext()) {
-                    getDelegate().put(store, currentPath, currentOp.createDefault(currentArg));
+                    getDelegate().merge(store, currentPath, currentOp.createDefault(currentArg));
                 }
             }
             } catch (InterruptedException | ExecutionException e) {
@@ -259,48 +251,6 @@ public abstract class BackwardsCompatibleTransaction<T extends DOMDataReadTransa
             getDelegate().put(store, normalizedPath, normalizedData);
         }
 
-        private boolean isAugmentationChild(final InstanceIdentifier normalizedPath) {
-            List<PathArgument> parentArgs = parentPath(normalizedPath).getPath();
-            if(parentArgs.isEmpty()) {
-                return false;
-            }
-            return Iterables.getLast(parentArgs) instanceof AugmentationIdentifier;
-        }
-
-        private void ensureParentNode(final LogicalDatastoreType store, final InstanceIdentifier normalizedPath,
-                final NormalizedNode<?, ?> normalizedData) {
-            InstanceIdentifier parentPath = parentPath(normalizedPath);
-            PathArgument parentType = Iterables.getLast(parentPath.getPath());
-            if(parentType instanceof AugmentationIdentifier) {
-                AugmentationNode node = Builders.augmentationBuilder()
-                        .withNodeIdentifier((AugmentationIdentifier) parentType)
-                        .build();
-                getDelegate().put(store, parentPath, node);
-            }
-            if(normalizedData instanceof MapEntryNode) {
-                MapNode mapNode = Builders.mapBuilder().withNodeIdentifier(new NodeIdentifier(normalizedData.getNodeType())).build();
-                getDelegate().put(store, parentPath, mapNode);
-            } else if (normalizedData instanceof LeafSetNode<?>){
-                LeafSetNode<Object> leafNode = Builders.leafSetBuilder().withNodeIdentifier(new NodeIdentifier(normalizedData.getNodeType())).build();
-                getDelegate().put(store, parentPath, leafNode);
-            }
-
-
-        }
-
-        private InstanceIdentifier parentPath(final InstanceIdentifier normalizedPath) {
-            List<PathArgument> childArgs = normalizedPath.getPath();
-            return new InstanceIdentifier(childArgs.subList(0, childArgs.size() -1));
-        }
-
-        private boolean parentNodeDoesNotExists(final LogicalDatastoreType store, final InstanceIdentifier normalizedPath) {
-            try {
-                return !getDelegate().read(store, parentPath(normalizedPath)).get().isPresent();
-            } catch (InterruptedException | ExecutionException e) {
-                throw new IllegalStateException(e);
-            }
-        }
-
         @Override
         public void removeConfigurationData(final InstanceIdentifier legacyPath) {
             checkNotNull(legacyPath, "Path MUST NOT be null.");
index 6c0e5823a49f885520f902cea0647499e715482b..9bbba1e24d8600f196f23df145105d1b787e9c6e 100644 (file)
@@ -258,6 +258,18 @@ public class InMemoryDOMDataStore implements DOMStore, Identifiable<String>, Sch
             }
         }
 
+        @Override
+        public void merge(final InstanceIdentifier path, final NormalizedNode<?, ?> data) {
+            checkNotReady();
+            try {
+                LOG.trace("Tx: {} Merge: {}:{}",getIdentifier(),path,data);
+                mutableTree.merge(path, data);
+              // FIXME: Add checked exception
+            } catch (Exception e) {
+                LOG.error("Tx: {}, failed to write {}:{} in {}",getIdentifier(),path,data,mutableTree,e);
+            }
+        }
+
         @Override
         public void delete(final InstanceIdentifier path) {
             checkNotReady();
index bc7fe7a2c7aa69dc6bf6979497abd480d147863c..b711163b465d3ca07914da3d3472347ab854533e 100644 (file)
@@ -18,6 +18,7 @@ import org.opendaylight.controller.md.sal.dom.store.impl.tree.TreeNodeUtils;
 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.PathArgument;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodeContainer;
 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -47,6 +48,24 @@ class MutableDataTree {
         resolveModificationFor(path).write(value);
     }
 
+    public void merge(final InstanceIdentifier path, final NormalizedNode<?, ?> data) {
+        checkSealed();
+        mergeImpl(resolveModificationFor(path),data);
+    }
+
+    private void mergeImpl(final OperationWithModification op,final NormalizedNode<?,?> data) {
+
+        if(data instanceof NormalizedNodeContainer<?,?,?>) {
+            @SuppressWarnings({ "rawtypes", "unchecked" })
+            NormalizedNodeContainer<?,?,NormalizedNode<PathArgument, ?>> dataContainer = (NormalizedNodeContainer) data;
+            for(NormalizedNode<PathArgument, ?> child : dataContainer.getValue()) {
+                PathArgument childId = child.getIdentifier();
+                mergeImpl(op.forChild(childId), child);
+            }
+        }
+        op.merge(data);
+    }
+
     public void delete(final InstanceIdentifier path) {
         checkSealed();
         resolveModificationFor(path).delete();
index eaf01aeecda8d60e0cb7913678b1747665876ce1..35864b6bc29cfeed97f35690f5548b5bb2c9ad48 100644 (file)
@@ -2,6 +2,7 @@ package org.opendaylight.controller.md.sal.dom.store.impl;
 
 import org.opendaylight.controller.md.sal.dom.store.impl.tree.NodeModification;
 import org.opendaylight.controller.md.sal.dom.store.impl.tree.StoreMetadataNode;
+import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.PathArgument;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 
 import com.google.common.base.Optional;
@@ -10,6 +11,7 @@ import com.google.common.primitives.UnsignedLong;
 public class OperationWithModification {
 
     private final NodeModification modification;
+
     private final ModificationApplyOperation applyOperation;
 
     private OperationWithModification(final ModificationApplyOperation op, final NodeModification mod) {
@@ -28,6 +30,14 @@ public class OperationWithModification {
         return this;
     }
 
+    public NodeModification getModification() {
+        return modification;
+    }
+
+    public ModificationApplyOperation getApplyOperation() {
+        return applyOperation;
+    }
+
     public boolean isApplicable(final Optional<StoreMetadataNode> data) {
         return applyOperation.isApplicable(modification, data);
     }
@@ -41,4 +51,16 @@ public class OperationWithModification {
         return new OperationWithModification(operation, modification);
 
     }
+
+    public void merge(final NormalizedNode<?, ?> data) {
+        modification.merge(data);
+        applyOperation.verifyStructure(modification);
+
+    }
+
+    public OperationWithModification forChild(final PathArgument childId) {
+        NodeModification childMod = modification.modifyChild(childId);
+        Optional<ModificationApplyOperation> childOp = applyOperation.getChild(childId);
+        return from(childOp.get(),childMod);
+    }
 }
\ No newline at end of file
index 520bd1ba1de3d9407718db6a6df64ce128fd1509..c62c27dea8908f8043b05561685b7db5ac555adc 100644 (file)
@@ -308,6 +308,7 @@ public class ResolveDataChangeEventsTask implements Callable<Iterable<ChangeList
         switch (modification.getModificationType()) {
         case SUBTREE_MODIFIED:
             return resolveSubtreeChangeEvent(path, listeners, modification, before.get(), after.get());
+        case MERGE:
         case WRITE:
             if (before.isPresent()) {
                 return resolveReplacedEvent(path, listeners, before.get().getData(), after.get().getData());
@@ -487,6 +488,7 @@ public class ResolveDataChangeEventsTask implements Callable<Iterable<ChangeList
 
             switch (childMod.getModificationType()) {
             case WRITE:
+            case MERGE:
             case DELETE:
                 one.merge(resolveAnyChangeEvent(childPath, childListeners, childMod, childBefore, childAfter));
                 break;
index b9b1ab035e09f2dfc8325a39535a5a30bfc6c570..a5c9b7983a92f93a9a2e2049688e0d33eaed1c8b 100644 (file)
@@ -131,6 +131,8 @@ public abstract class SchemaAwareApplyOperation implements ModificationApplyOper
             return isSubtreeModificationApplicable(modification, current);
         case WRITE:
             return isWriteApplicable(modification, current);
+        case MERGE:
+            return isMergeApplicable(modification,current);
         case UNMODIFIED:
             return true;
         default:
@@ -138,6 +140,16 @@ public abstract class SchemaAwareApplyOperation implements ModificationApplyOper
         }
     }
 
+    private boolean isMergeApplicable(final NodeModification modification, final Optional<StoreMetadataNode> current) {
+        Optional<StoreMetadataNode> original = modification.getOriginal();
+        if (original.isPresent() && current.isPresent()) {
+            return isNotConflicting(original.get(), current.get());
+        } else if (current.isPresent()) {
+            return true;
+        }
+        return true;
+    }
+
     protected boolean isWriteApplicable(final NodeModification modification, final Optional<StoreMetadataNode> current) {
         Optional<StoreMetadataNode> original = modification.getOriginal();
         if (original.isPresent() && current.isPresent()) {
@@ -174,6 +186,10 @@ public abstract class SchemaAwareApplyOperation implements ModificationApplyOper
                     modification);
             return modification.storeSnapshot(Optional.of(applySubtreeChange(modification, currentMeta.get(),
                     subtreeVersion)));
+        case MERGE:
+            if(currentMeta.isPresent()) {
+                return modification.storeSnapshot(Optional.of(applyMerge(modification,currentMeta.get(),subtreeVersion)));
+            } // Fallback to write is intentional - if node is not preexisting merge is same as write
         case WRITE:
             return modification.storeSnapshot(Optional.of(applyWrite(modification, currentMeta, subtreeVersion)));
         case UNMODIFIED:
@@ -183,6 +199,9 @@ public abstract class SchemaAwareApplyOperation implements ModificationApplyOper
         }
     }
 
+    protected abstract StoreMetadataNode applyMerge(NodeModification modification,
+            StoreMetadataNode currentMeta, UnsignedLong subtreeVersion);
+
     protected abstract StoreMetadataNode applyWrite(NodeModification modification,
             Optional<StoreMetadataNode> currentMeta, UnsignedLong subtreeVersion);
 
@@ -219,14 +238,16 @@ public abstract class SchemaAwareApplyOperation implements ModificationApplyOper
                     + "is leaf type node. Subtree change is not allowed.");
         }
 
+        @Override
+        protected StoreMetadataNode applyMerge(final NodeModification modification, final StoreMetadataNode currentMeta,
+                final UnsignedLong subtreeVersion) {
+            return applyWrite(modification, Optional.of(currentMeta), subtreeVersion);
+        }
+
         @Override
         protected StoreMetadataNode applyWrite(final NodeModification modification,
                 final Optional<StoreMetadataNode> currentMeta, final UnsignedLong subtreeVersion) {
             UnsignedLong nodeVersion = subtreeVersion;
-            if (currentMeta.isPresent()) {
-                nodeVersion = StoreUtils.increase(currentMeta.get().getNodeVersion());
-            }
-
             return StoreMetadataNode.builder().setNodeVersion(nodeVersion).setSubtreeVersion(subtreeVersion)
                     .setData(modification.getWritenValue()).build();
         }
@@ -314,6 +335,13 @@ public abstract class SchemaAwareApplyOperation implements ModificationApplyOper
 
         }
 
+        @Override
+        protected StoreMetadataNode applyMerge(final NodeModification modification, final StoreMetadataNode currentMeta,
+                final UnsignedLong subtreeVersion) {
+            // For Node Containers - merge is same as subtree change - we only replace children.
+            return applySubtreeChange(modification, currentMeta, subtreeVersion);
+        }
+
         @Override
         public StoreMetadataNode applySubtreeChange(final NodeModification modification,
                 final StoreMetadataNode currentMeta, final UnsignedLong subtreeVersion) {
@@ -569,7 +597,11 @@ public abstract class SchemaAwareApplyOperation implements ModificationApplyOper
             entryStrategy = Optional.<ModificationApplyOperation> of(new UnkeyedListItemModificationStrategy(schema));
         }
 
-
+        @Override
+        protected StoreMetadataNode applyMerge(final NodeModification modification, final StoreMetadataNode currentMeta,
+                final UnsignedLong subtreeVersion) {
+            return applyWrite(modification, Optional.of(currentMeta), subtreeVersion);
+        }
 
         @Override
         protected StoreMetadataNode applySubtreeChange(final NodeModification modification,
index 199d90252ea8a2399baf9cad41e88dd02f561548..b16e907120ac098ee81a3f9fec4dd83469971f69 100644 (file)
@@ -32,5 +32,12 @@ public enum ModificationType {
      * Tree node is to be deleted.
      *
      */
-    DELETE
+    DELETE,
+
+    /**
+     *
+     * Tree node is to be merged with existing one.
+     *
+     */
+    MERGE
 }
index 04fb3b7c6c50499ad0b96b220c2685b597bdcb9d..4f650c171107abdbffd552a11f7e5531a1f1f8b8 100644 (file)
@@ -20,7 +20,6 @@ import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 
 import com.google.common.base.Optional;
 import com.google.common.base.Predicate;
-import com.google.common.primitives.UnsignedLong;
 
 /**
  * Node Modification Node and Tree
@@ -37,7 +36,9 @@ public class NodeModification implements StoreTreeNode<NodeModification>, Identi
     public static final Predicate<NodeModification> IS_TERMINAL_PREDICATE = new Predicate<NodeModification>() {
         @Override
         public boolean apply(final NodeModification input) {
-            return input.getModificationType() == ModificationType.WRITE || input.getModificationType() == ModificationType.DELETE;
+            return input.getModificationType() == ModificationType.WRITE //
+                    || input.getModificationType() == ModificationType.DELETE //
+                    || input.getModificationType() == ModificationType.MERGE;
         }
     };
     private final PathArgument identifier;
@@ -48,7 +49,6 @@ public class NodeModification implements StoreTreeNode<NodeModification>, Identi
 
     private NormalizedNode<?, ?> value;
 
-    private UnsignedLong subtreeVersion;
     private Optional<StoreMetadataNode> snapshotCache;
 
     private final Map<PathArgument, NodeModification> childModification;
@@ -176,6 +176,14 @@ public class NodeModification implements StoreTreeNode<NodeModification>, Identi
         this.value = value;
     }
 
+    public synchronized void merge(final NormalizedNode<?, ?> data) {
+        checkSealed();
+        clearSnapshot();
+        updateModificationType(ModificationType.MERGE);
+        // FIXME: Probably merge with previous value.
+        this.value = data;
+    }
+
     @GuardedBy("this")
     private void checkSealed() {
         checkState(!sealed, "Node Modification is sealed. No further changes allowed.");
index 6761bc1968778a8894c1a475176b66c054ce92ed..ddabbc6c030f3f8ef098d207b2b250f737288ddc 100644 (file)
@@ -33,6 +33,25 @@ public interface DOMStoreWriteTransaction extends DOMStoreTransaction {
      */
     void write(InstanceIdentifier path, NormalizedNode<?, ?> data);
 
+    /**
+     * Store a provided data at specified path. This acts as a add / replace
+     * operation, which is to say that whole subtree will be replaced by
+     * specified path.
+     *
+     * If you need add or merge of current object with specified use
+     * {@link #merge(LogicalDatastoreType, Path, Object)}
+     *
+     *
+     * @param path
+     * @param data
+     *            Data object to be written
+     *
+     * @throws IllegalStateException
+     *             if the client code already sealed transaction and invoked
+     *             {@link #ready()}
+     */
+    void merge(InstanceIdentifier path, NormalizedNode<?, ?> data);
+
     /**
      *
      * Deletes data and whole subtree located at provided path.