HONEYCOMB-92: Process modifications recursively
authorMaros Marsalek <[email protected]>
Tue, 14 Jun 2016 08:48:26 +0000 (10:48 +0200)
committerMaros Marsalek <[email protected]>
Wed, 15 Jun 2016 06:59:04 +0000 (08:59 +0200)
+ Fix update subtree, child writer lookup
+ Change initializers operation to merge

Change-Id: I6ece7eb3d17d5a0b4a413189ddd383567d7e2270
Signed-off-by: Maros Marsalek <[email protected]>
v3po/data-impl/src/main/java/io/fd/honeycomb/v3po/data/impl/DataTreeUtils.java [deleted file]
v3po/data-impl/src/main/java/io/fd/honeycomb/v3po/data/impl/ModifiableDataTreeDelegator.java
v3po/data-impl/src/test/java/io/fd/honeycomb/v3po/data/impl/DataTreeUtilsTest.java [deleted file]
v3po/data-impl/src/test/java/io/fd/honeycomb/v3po/data/impl/ModifiableDataTreeDelegatorTest.java
v3po/data-impl/src/test/java/io/fd/honeycomb/v3po/data/impl/ModificationDiffTest.java [new file with mode: 0644]
v3po/data-impl/src/test/resources/test-diff.yang [new file with mode: 0644]
v3po/translate-impl/src/main/java/io/fd/honeycomb/v3po/translate/impl/write/AbstractCompositeWriter.java
v3po/translate-utils/src/main/java/io/fd/honeycomb/v3po/translate/util/write/DelegatingWriterRegistry.java
v3po/vpp-cfg-init/src/main/java/io/fd/honeycomb/v3po/vpp/data/init/AbstractDataTreeConverter.java

diff --git a/v3po/data-impl/src/main/java/io/fd/honeycomb/v3po/data/impl/DataTreeUtils.java b/v3po/data-impl/src/main/java/io/fd/honeycomb/v3po/data/impl/DataTreeUtils.java
deleted file mode 100644 (file)
index 5833459..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (c) 2016 Cisco and/or its affiliates.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at:
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package io.fd.honeycomb.v3po.data.impl;
-
-import com.google.common.base.Preconditions;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import javax.annotation.Nonnull;
-import org.opendaylight.yangtools.binding.data.codec.api.BindingNormalizedNodeSerializer;
-import org.opendaylight.yangtools.yang.binding.DataObject;
-import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
-import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
-import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Utility class for various operations on DataTree.
- */
-final class DataTreeUtils {
-
-    private static final Logger LOG = LoggerFactory.getLogger(DataTreeUtils.class);
-
-    private DataTreeUtils() {
-        throw new UnsupportedOperationException("Can't instantiate util class");
-    }
-
-    /**
-     * Translates children of supplied YANG ContainerNode into Binding data.
-     *
-     * @param parent     ContainerNode representing data
-     * @param serializer service for serialization between Java Binding Data representation and NormalizedNode
-     *                   representation.
-     * @return NormalizedNode representation of parent's node children
-     */
-    static Map<InstanceIdentifier<?>, DataObject> childrenFromNormalized(@Nonnull final DataContainerNode parent,
-                                                                         @Nonnull final BindingNormalizedNodeSerializer serializer) {
-
-        Preconditions.checkNotNull(parent, "parent node should not be null");
-        Preconditions.checkNotNull(serializer, "serializer should not be null");
-
-        final Map<InstanceIdentifier<?>, DataObject> map = new HashMap<>();
-
-        final Collection<DataContainerChild<? extends YangInstanceIdentifier.PathArgument, ?>> children =
-                parent.getValue();
-
-        for (final DataContainerChild<? extends YangInstanceIdentifier.PathArgument, ?> child : children) {
-            final YangInstanceIdentifier.PathArgument pathArgument = child.getIdentifier();
-            final YangInstanceIdentifier identifier = YangInstanceIdentifier.create(pathArgument);
-            LOG.debug("DataTreeUtils.childrenFromNormalized() child={}, pathArgument={}, identifier={}", child,
-                    pathArgument, identifier);
-
-            final Map.Entry<InstanceIdentifier<?>, DataObject> entry = serializer.fromNormalizedNode(identifier, child);
-            if (entry != null) {
-                map.put(entry.getKey(), entry.getValue());
-            }
-        }
-
-        return map;
-    }
-}
index 75ffb70..c8d258b 100644 (file)
 
 package io.fd.honeycomb.v3po.data.impl;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.util.concurrent.Futures.immediateCheckedFuture;
-import static io.fd.honeycomb.v3po.data.impl.DataTreeUtils.childrenFromNormalized;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Optional;
+import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.CheckedFuture;
 import io.fd.honeycomb.v3po.data.DataModification;
 import io.fd.honeycomb.v3po.data.ReadableDataManager;
@@ -30,6 +32,8 @@ import io.fd.honeycomb.v3po.translate.util.write.TransactionWriteContext;
 import io.fd.honeycomb.v3po.translate.write.WriteContext;
 import io.fd.honeycomb.v3po.translate.write.WriterRegistry;
 import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
 import java.util.Map;
 import javax.annotation.Nonnull;
 import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException;
@@ -38,11 +42,12 @@ import org.opendaylight.yangtools.binding.data.codec.api.BindingNormalizedNodeSe
 import org.opendaylight.yangtools.yang.binding.DataObject;
 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
-import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTree;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateNode;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.ModificationType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -55,10 +60,10 @@ public final class ModifiableDataTreeDelegator extends ModifiableDataTreeManager
     private static final Logger LOG = LoggerFactory.getLogger(ModifiableDataTreeDelegator.class);
     private static final ReadableDataManager EMPTY_OPERATIONAL = p -> immediateCheckedFuture(Optional.absent());
 
-    // TODO what to use instead of deprecated BindingNormalizedNodeSerializer ?
-    private final BindingNormalizedNodeSerializer serializer;
     private final WriterRegistry writerRegistry;
     private final org.opendaylight.controller.md.sal.binding.api.DataBroker contextBroker;
+    // TODO what to use instead of deprecated BindingNormalizedNodeSerializer ?
+    private final BindingNormalizedNodeSerializer serializer;
 
     /**
      * Creates configuration data tree instance.
@@ -107,14 +112,16 @@ public final class ModifiableDataTreeDelegator extends ModifiableDataTreeManager
             throws TranslationException {
             final DataTreeCandidateNode rootNode = candidate.getRootNode();
             final YangInstanceIdentifier rootPath = candidate.getRootPath();
-            final Optional<NormalizedNode<?, ?>> normalizedDataBefore = rootNode.getDataBefore();
-            final Optional<NormalizedNode<?, ?>> normalizedDataAfter = rootNode.getDataAfter();
-            LOG.debug("ConfigDataTree.modify() rootPath={}, rootNode={}, dataBefore={}, dataAfter={}",
-                rootPath, rootNode, normalizedDataBefore, normalizedDataAfter);
+            LOG.trace("ConfigDataTree.modify() rootPath={}, rootNode={}, dataBefore={}, dataAfter={}",
+                rootPath, rootNode, rootNode.getDataBefore(), rootNode.getDataAfter());
 
-            final Map<InstanceIdentifier<?>, DataObject> nodesBefore = toBindingAware(normalizedDataBefore);
+            final ModificationDiff modificationDiff =
+                    ModificationDiff.recursivelyFromCandidate(YangInstanceIdentifier.EMPTY, rootNode);
+            LOG.debug("ConfigDataTree.modify() diff: {}", modificationDiff);
+
+            final Map<InstanceIdentifier<?>, DataObject> nodesBefore = toBindingAware(modificationDiff.getModificationsBefore());
             LOG.debug("ConfigDataTree.modify() extracted nodesBefore={}", nodesBefore.keySet());
-            final Map<InstanceIdentifier<?>, DataObject> nodesAfter = toBindingAware(normalizedDataAfter);
+            final Map<InstanceIdentifier<?>, DataObject> nodesAfter = toBindingAware(modificationDiff.getModificationsAfter());
             LOG.debug("ConfigDataTree.modify() extracted nodesAfter={}", nodesAfter.keySet());
 
             try (final WriteContext ctx = getTransactionWriteContext()) {
@@ -160,12 +167,175 @@ public final class ModifiableDataTreeDelegator extends ModifiableDataTreeManager
             return new TransactionWriteContext(serializer, beforeTx, afterTx, mappingContext);
         }
 
-        private Map<InstanceIdentifier<?>, DataObject> toBindingAware(final Optional<NormalizedNode<?, ?>> parentOptional) {
-            if (parentOptional.isPresent()) {
-                final DataContainerNode parent = (DataContainerNode) parentOptional.get();
-                return childrenFromNormalized(parent, serializer);
+        private Map<InstanceIdentifier<?>, DataObject> toBindingAware(final Map<YangInstanceIdentifier, NormalizedNode<?, ?>> biNodes) {
+            return ModifiableDataTreeDelegator.toBindingAware(biNodes, serializer);
+        }
+    }
+
+    @VisibleForTesting
+    static Map<InstanceIdentifier<?>, DataObject> toBindingAware(final Map<YangInstanceIdentifier, NormalizedNode<?, ?>> biNodes,
+                                                                 final BindingNormalizedNodeSerializer serializer) {
+        final HashMap<InstanceIdentifier<?>, DataObject> transformed = new HashMap<>(biNodes.size());
+        for (Map.Entry<YangInstanceIdentifier, NormalizedNode<?, ?>> biEntry : biNodes.entrySet()) {
+            final Map.Entry<InstanceIdentifier<?>, DataObject> baEntry = serializer.fromNormalizedNode(biEntry.getKey(), biEntry.getValue());
+            if(baEntry != null) {
+                transformed.put(baEntry.getKey(), baEntry.getValue());
+            }
+        }
+        return transformed;
+    }
+
+    @VisibleForTesting
+    static final class ModificationDiff {
+
+        private static final ModificationDiff EMPTY_DIFF = new ModificationDiff(Collections.emptyMap(), Collections.emptyMap());
+        private static final EnumSet LEAF_MODIFICATIONS = EnumSet.of(ModificationType.WRITE, ModificationType.DELETE);
+
+        private final Map<YangInstanceIdentifier, NormalizedNode<?, ?>> modificationsBefore;
+        private final Map<YangInstanceIdentifier, NormalizedNode<?, ?>> modificationsAfter;
+
+        private ModificationDiff(@Nonnull final Map<YangInstanceIdentifier, NormalizedNode<?, ?>> modificationsBefore,
+                                 @Nonnull final Map<YangInstanceIdentifier, NormalizedNode<?, ?>> modificationsAfter) {
+            this.modificationsBefore = modificationsBefore;
+            this.modificationsAfter = modificationsAfter;
+        }
+
+        Map<YangInstanceIdentifier, NormalizedNode<?, ?>> getModificationsBefore() {
+            return modificationsBefore;
+        }
+
+        Map<YangInstanceIdentifier, NormalizedNode<?, ?>> getModificationsAfter() {
+            return modificationsAfter;
+        }
+
+        private ModificationDiff merge(final ModificationDiff other) {
+            if (this == EMPTY_DIFF) {
+                return other;
             }
-            return Collections.emptyMap();
+
+            if (other == EMPTY_DIFF) {
+                return this;
+            }
+
+            return new ModificationDiff(join(modificationsBefore, other.modificationsBefore),
+                    join(modificationsAfter, other.modificationsAfter));
+        }
+
+        private static Map<YangInstanceIdentifier, NormalizedNode<?, ?>> join(
+                final Map<YangInstanceIdentifier, NormalizedNode<?, ?>> mapOne,
+                final Map<YangInstanceIdentifier, NormalizedNode<?, ?>> mapTwo) {
+            // Check unique modifications
+            // TODO Probably not necessary to check
+            final Sets.SetView<YangInstanceIdentifier> duplicates = Sets.intersection(mapOne.keySet(), mapTwo.keySet());
+            checkArgument(duplicates.size() == 0, "Duplicates detected: %s. In maps: %s and %s", duplicates, mapOne, mapTwo);
+            final HashMap<YangInstanceIdentifier, NormalizedNode<?, ?>> joined = new HashMap<>();
+            joined.putAll(mapOne);
+            joined.putAll(mapTwo);
+            return joined;
+        }
+
+        private static ModificationDiff createFromBefore(YangInstanceIdentifier idBefore, DataTreeCandidateNode candidate) {
+            return new ModificationDiff(
+                    Collections.singletonMap(idBefore, candidate.getDataBefore().get()),
+                    Collections.emptyMap());
+        }
+
+        private static ModificationDiff create(YangInstanceIdentifier id, DataTreeCandidateNode candidate) {
+            return new ModificationDiff(
+                    Collections.singletonMap(id, candidate.getDataBefore().get()),
+                    Collections.singletonMap(id, candidate.getDataAfter().get()));
+        }
+
+        private static ModificationDiff createFromAfter(YangInstanceIdentifier idAfter, DataTreeCandidateNode candidate) {
+            return new ModificationDiff(
+                    Collections.emptyMap(),
+                    Collections.singletonMap(idAfter, candidate.getDataAfter().get()));
+        }
+
+        /**
+         * Produce a diff from a candidate node recursively
+         */
+        @Nonnull
+        static ModificationDiff recursivelyFromCandidate(@Nonnull final YangInstanceIdentifier yangIid,
+                                                         @Nonnull final DataTreeCandidateNode currentCandidate) {
+            switch (currentCandidate.getModificationType()) {
+                case APPEARED:
+                case DISAPPEARED:
+                case UNMODIFIED: {
+                    // (dis)appeared nodes are not important, no real data to process
+                    return ModificationDiff.EMPTY_DIFF;
+                }
+                case WRITE: {
+                    return currentCandidate.getDataBefore().isPresent() ?
+                            ModificationDiff.create(yangIid, currentCandidate) :
+                            ModificationDiff.createFromAfter(yangIid, currentCandidate);
+                    // TODO HONEYCOMB-94 process children recursively to get modifications for child nodes
+                }
+                case DELETE:
+                    return ModificationDiff.createFromBefore(yangIid, currentCandidate);
+                case SUBTREE_MODIFIED: {
+                    // Modifications here are presented also for leaves. However that kind of granularity is not required
+                    // So if there's a modified leaf, mark current complex node also as modification
+                    java.util.Optional<Boolean> leavesModified = currentCandidate.getChildNodes().stream()
+                            .filter(ModificationDiff::isLeaf)
+                            // For some reason, we get modifications on unmodified list keys TODO debug and report ODL bug
+                            // and that messes up our modifications collection here, so we need to skip
+                            .filter(ModificationDiff::isModification)
+                            .map(child -> LEAF_MODIFICATIONS.contains(child.getModificationType()))
+                            .reduce((aBoolean, aBoolean2) -> aBoolean || aBoolean2);
+
+                    //
+                    if(leavesModified.isPresent() && leavesModified.get()) {
+                        return ModificationDiff.create(yangIid, currentCandidate);
+                        // TODO HONEYCOMB-94 process children recursively to get modifications for child nodes even if current
+                        // was modified
+                    } else {
+                        // SUBTREE MODIFIED, no modification on current, but process children recursively
+                        return currentCandidate.getChildNodes().stream()
+                                // not interested in modifications to leaves
+                                .filter(child -> !isLeaf(child))
+                                .map(candidate -> recursivelyFromCandidate(yangIid.node(candidate.getIdentifier()), candidate))
+                                .reduce(ModificationDiff::merge)
+                                .orElse(EMPTY_DIFF);
+                    }
+                }
+                default:
+                    throw new IllegalStateException("Unknown modification type: "
+                            + currentCandidate.getModificationType() + ". Unsupported");
+            }
+        }
+
+        /**
+         * Check whether candidate.before and candidate.after is different. If not
+         * return false.
+         */
+        private static boolean isModification(final DataTreeCandidateNode a) {
+            if(a.getDataBefore().isPresent()) {
+                if(a.getDataAfter().isPresent()) {
+                    return !a.getDataAfter().get().equals(a.getDataBefore().get());
+                } else {
+                    return true;
+                }
+            }
+
+            return true;
+        }
+
+        /**
+         * Check whether candidate node is for a leaf type node
+         */
+        private static boolean isLeaf(final DataTreeCandidateNode a) {
+            return a.getDataAfter().isPresent()
+                    ? (a.getDataAfter().get() instanceof LeafNode<?>)
+                    : (a.getDataBefore().get() instanceof LeafNode<?>);
+        }
+
+        @Override
+        public String toString() {
+            return "ModificationDiff{" +
+                    "modificationsBefore=" + modificationsBefore +
+                    ", modificationsAfter=" + modificationsAfter +
+                    '}';
         }
     }
 }
diff --git a/v3po/data-impl/src/test/java/io/fd/honeycomb/v3po/data/impl/DataTreeUtilsTest.java b/v3po/data-impl/src/test/java/io/fd/honeycomb/v3po/data/impl/DataTreeUtilsTest.java
deleted file mode 100644 (file)
index 40792a1..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (c) 2016 Cisco and/or its affiliates.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at:
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package io.fd.honeycomb.v3po.data.impl;
-
-import static org.junit.Assert.assertEquals;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Map;
-import org.junit.Test;
-import org.mockito.Matchers;
-import org.mockito.Mockito;
-import org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeSerializer;
-import org.opendaylight.yangtools.yang.binding.DataObject;
-import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
-import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
-import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
-
-public class DataTreeUtilsTest {
-
-    @Test
-    public void testChildrenFromNormalized() throws Exception {
-        final ContainerNode parent = Mockito.mock(ContainerNode.class);
-        final BindingNormalizedNodeSerializer serializer = Mockito.mock(BindingNormalizedNodeSerializer.class);
-
-        final Collection<DataContainerChild> list = new ArrayList<>();
-        Mockito.doReturn(list).when(parent).getValue();
-
-        // init child1 (will not be serialized)
-        final DataContainerChild child1 = Mockito.mock(DataContainerChild.class);
-        Mockito.when(child1.getIdentifier()).thenReturn(Mockito.mock(YangInstanceIdentifier.PathArgument.class));
-        Mockito.when(serializer.fromNormalizedNode(Matchers.any(YangInstanceIdentifier.class), Matchers.eq(child1))).thenReturn(null);
-        list.add(child1);
-
-        // init child 2 (will be serialized)
-        final DataContainerChild child2 = Mockito.mock(DataContainerChild.class);
-        Mockito.when(child2.getIdentifier()).thenReturn(Mockito.mock(YangInstanceIdentifier.PathArgument.class));
-
-        final Map.Entry entry = Mockito.mock(Map.Entry.class);
-        final InstanceIdentifier<?> id = Mockito.mock(InstanceIdentifier.class);
-        Mockito.doReturn(id).when(entry).getKey();
-        final DataObject data = Mockito.mock(DataObject.class);
-        Mockito.doReturn(data).when(entry).getValue();
-        Mockito.when(serializer.fromNormalizedNode(Matchers.any(YangInstanceIdentifier.class), Matchers.eq(child2))).thenReturn(entry);
-
-        list.add(child2);
-
-        // run tested method
-        final Map<InstanceIdentifier<?>, DataObject> map = DataTreeUtils.childrenFromNormalized(parent, serializer);
-        assertEquals(1, map.size());
-        assertEquals(data, map.get(id));
-    }
-}
\ No newline at end of file
index fed32da..086636d 100644 (file)
@@ -37,9 +37,8 @@ import io.fd.honeycomb.v3po.data.DataModification;
 import io.fd.honeycomb.v3po.translate.TranslationException;
 import io.fd.honeycomb.v3po.translate.write.WriteContext;
 import io.fd.honeycomb.v3po.translate.write.WriterRegistry;
-import java.util.ArrayList;
 import java.util.Collections;
-import java.util.List;
+import java.util.HashMap;
 import java.util.Map;
 import org.junit.Before;
 import org.junit.Test;
@@ -58,6 +57,7 @@ import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTree;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateNode;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.ModificationType;
 
 public class ModifiableDataTreeDelegatorTest {
 
@@ -109,7 +109,7 @@ public class ModifiableDataTreeDelegatorTest {
         when(dataTree.takeSnapshot()).thenReturn(snapshot);
         when(snapshot.newModification()).thenReturn(modification);
 
-        final DataModification dataTreeSnapshot = configDataTree.newModification();
+        configDataTree.newModification();
         // Snapshot captured twice, so that original data could be provided to translation layer without any possible
         // modification
         verify(dataTree, times(2)).takeSnapshot();
@@ -135,12 +135,19 @@ public class ModifiableDataTreeDelegatorTest {
 
         // Prepare modification:
         final DataTreeCandidateNode rootNode = mockRootNode();
+        doReturn(ModificationType.SUBTREE_MODIFIED).when(rootNode).getModificationType();
+        doReturn(mock(YangInstanceIdentifier.PathArgument.class)).when(rootNode).getIdentifier();
+        final DataTreeCandidateNode childNode = mock(DataTreeCandidateNode.class);
+        doReturn(mock(YangInstanceIdentifier.PathArgument.class)).when(childNode).getIdentifier();
+        doReturn(Collections.singleton(childNode)).when(rootNode).getChildNodes();
+        doReturn(ModificationType.WRITE).when(childNode).getModificationType();
+
         // data before:
         final ContainerNode nodeBefore = mockContainerNode(dataBefore);
-        when(rootNode.getDataBefore()).thenReturn(Optional.<NormalizedNode<?, ?>>fromNullable(nodeBefore));
+        when(childNode.getDataBefore()).thenReturn(Optional.fromNullable(nodeBefore));
         // data after:
         final ContainerNode nodeAfter = mockContainerNode(dataAfter);
-        when(rootNode.getDataAfter()).thenReturn(Optional.<NormalizedNode<?, ?>>fromNullable(nodeAfter));
+        when(childNode.getDataAfter()).thenReturn(Optional.fromNullable(nodeAfter));
 
         // Run the test
         doReturn(rootNode).when(prepare).getRootNode();
@@ -160,9 +167,7 @@ public class ModifiableDataTreeDelegatorTest {
     }
 
     private Map<InstanceIdentifier<?>, DataObject> mapOf(final DataObject dataBefore, final Class<Ethernet> type) {
-        return eq(
-                Collections.<InstanceIdentifier<?>, DataObject>singletonMap(InstanceIdentifier.create(type),
-                        dataBefore));
+        return eq(Collections.singletonMap(InstanceIdentifier.create(type),dataBefore));
     }
 
     private DataObject mockDataObject(final String name, final Class<? extends DataObject> classToMock) {
@@ -194,12 +199,19 @@ public class ModifiableDataTreeDelegatorTest {
 
         // Prepare modification:
         final DataTreeCandidateNode rootNode = mockRootNode();
+        doReturn(ModificationType.SUBTREE_MODIFIED).when(rootNode).getModificationType();
+        doReturn(mock(YangInstanceIdentifier.PathArgument.class)).when(rootNode).getIdentifier();
+        final DataTreeCandidateNode childNode = mock(DataTreeCandidateNode.class);
+        doReturn(mock(YangInstanceIdentifier.PathArgument.class)).when(childNode).getIdentifier();
+        doReturn(Collections.singleton(childNode)).when(rootNode).getChildNodes();
+        doReturn(ModificationType.WRITE).when(childNode).getModificationType();
+
         // data before:
         final ContainerNode nodeBefore = mockContainerNode(dataBefore);
-        when(rootNode.getDataBefore()).thenReturn(Optional.<NormalizedNode<?, ?>>fromNullable(nodeBefore));
+        when(childNode.getDataBefore()).thenReturn(Optional.fromNullable(nodeBefore));
         // data after:
         final ContainerNode nodeAfter = mockContainerNode(dataAfter);
-        when(rootNode.getDataAfter()).thenReturn(Optional.<NormalizedNode<?, ?>>fromNullable(nodeAfter));
+        when(childNode.getDataAfter()).thenReturn(Optional.fromNullable(nodeAfter));
 
         // Run the test
         try {
@@ -245,12 +257,19 @@ public class ModifiableDataTreeDelegatorTest {
 
         // Prepare modification:
         final DataTreeCandidateNode rootNode = mockRootNode();
+        doReturn(ModificationType.SUBTREE_MODIFIED).when(rootNode).getModificationType();
+        doReturn(mock(YangInstanceIdentifier.PathArgument.class)).when(rootNode).getIdentifier();
+        final DataTreeCandidateNode childNode = mock(DataTreeCandidateNode.class);
+        doReturn(mock(YangInstanceIdentifier.PathArgument.class)).when(childNode).getIdentifier();
+        doReturn(Collections.singleton(childNode)).when(rootNode).getChildNodes();
+        doReturn(ModificationType.WRITE).when(childNode).getModificationType();
+
         // data before:
         final ContainerNode nodeBefore = mockContainerNode(dataBefore);
-        when(rootNode.getDataBefore()).thenReturn(Optional.<NormalizedNode<?, ?>>fromNullable(nodeBefore));
+        when(childNode.getDataBefore()).thenReturn(Optional.fromNullable(nodeBefore));
         // data after:
         final ContainerNode nodeAfter = mockContainerNode(dataAfter);
-        when(rootNode.getDataAfter()).thenReturn(Optional.<NormalizedNode<?, ?>>fromNullable(nodeAfter));
+        when(childNode.getDataAfter()).thenReturn(Optional.fromNullable(nodeAfter));
 
         // Run the test
         try {
@@ -267,6 +286,37 @@ public class ModifiableDataTreeDelegatorTest {
         fail("RevertFailedException was expected");
     }
 
+    @Test
+    public void testChildrenFromNormalized() throws Exception {
+        final BindingNormalizedNodeSerializer serializer = mock(BindingNormalizedNodeSerializer.class);
+
+        final Map<YangInstanceIdentifier, NormalizedNode<?, ?>> map = new HashMap<>();
+
+        // init child1 (will not be serialized)
+        final DataContainerChild child1 = mock(DataContainerChild.class);
+        when(child1.getIdentifier()).thenReturn(mock(YangInstanceIdentifier.PathArgument.class));
+        when(serializer.fromNormalizedNode(any(YangInstanceIdentifier.class), eq(child1))).thenReturn(null);
+        map.put(mock(YangInstanceIdentifier.class), child1);
+
+        // init child 2 (will be serialized)
+        final DataContainerChild child2 = mock(DataContainerChild.class);
+        when(child2.getIdentifier()).thenReturn(mock(YangInstanceIdentifier.PathArgument.class));
+
+        final Map.Entry entry = mock(Map.Entry.class);
+        final InstanceIdentifier<?> id = mock(InstanceIdentifier.class);
+        doReturn(id).when(entry).getKey();
+        final DataObject data = mock(DataObject.class);
+        doReturn(data).when(entry).getValue();
+        when(serializer.fromNormalizedNode(any(YangInstanceIdentifier.class), eq(child2))).thenReturn(entry);
+        map.put(mock(YangInstanceIdentifier.class), child2);
+
+        // run tested method
+        final Map<InstanceIdentifier<?>, DataObject> baMap =
+                ModifiableDataTreeDelegator.toBindingAware(map, serializer);
+        assertEquals(1, baMap.size());
+        assertEquals(data, baMap.get(id));
+    }
+
     private DataTreeCandidateNode mockRootNode() {
         final DataTreeCandidate candidate = mock(DataTreeCandidate.class);
         when(dataTree.prepare(modification)).thenReturn(candidate);
@@ -277,32 +327,22 @@ public class ModifiableDataTreeDelegatorTest {
         return rootNode;
     }
 
-    private ContainerNode mockContainerNode(DataObject... modifications) {
-        final int numberOfChildren = modifications.length;
-
+    private ContainerNode mockContainerNode(DataObject modification) {
         final YangInstanceIdentifier.NodeIdentifier identifier =
                 YangInstanceIdentifier.NodeIdentifier.create(QName.create("/"));
 
         final ContainerNode node = mock(ContainerNode.class);
         when(node.getIdentifier()).thenReturn(identifier);
 
-        final List<DataContainerChild> list = new ArrayList<>(numberOfChildren);
-        doReturn(list).when(node).getValue();
+        final Map.Entry entry = mock(Map.Entry.class);
+        final Class<? extends DataObject> implementedInterface =
+                (Class<? extends DataObject>) modification.getImplementedInterface();
+        final InstanceIdentifier<?> id = InstanceIdentifier.create(implementedInterface);
 
-        for (DataObject modification : modifications) {
-            final DataContainerChild child = mock(DataContainerChild.class);
-            when(child.getIdentifier()).thenReturn(mock(YangInstanceIdentifier.PathArgument.class));
-            list.add(child);
+        doReturn(id).when(entry).getKey();
+        doReturn(modification).when(entry).getValue();
+        doReturn(entry).when(serializer).fromNormalizedNode(any(YangInstanceIdentifier.class), eq(node));
 
-            final Map.Entry entry = mock(Map.Entry.class);
-            final Class<? extends DataObject> implementedInterface =
-                    (Class<? extends DataObject>) modification.getImplementedInterface();
-            final InstanceIdentifier<?> id = InstanceIdentifier.create(implementedInterface);
-
-            doReturn(id).when(entry).getKey();
-            doReturn(modification).when(entry).getValue();
-            doReturn(entry).when(serializer).fromNormalizedNode(any(YangInstanceIdentifier.class), eq(child));
-        }
         return node;
     }
 }
diff --git a/v3po/data-impl/src/test/java/io/fd/honeycomb/v3po/data/impl/ModificationDiffTest.java b/v3po/data-impl/src/test/java/io/fd/honeycomb/v3po/data/impl/ModificationDiffTest.java
new file mode 100644 (file)
index 0000000..f5c2706
--- /dev/null
@@ -0,0 +1,376 @@
+package io.fd.honeycomb.v3po.data.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Map;
+import org.junit.Test;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+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.api.schema.tree.DataTreeCandidateTip;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeModification;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeSnapshot;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataValidationFailedException;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.TipProducingDataTree;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.TreeType;
+import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
+import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
+import org.opendaylight.yangtools.yang.data.impl.schema.tree.InMemoryDataTreeFactory;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.parser.spi.meta.ReactorException;
+import org.opendaylight.yangtools.yang.parser.stmt.reactor.CrossSourceStatementReactor;
+import org.opendaylight.yangtools.yang.parser.stmt.rfc6020.YangInferencePipeline;
+import org.opendaylight.yangtools.yang.parser.stmt.rfc6020.YangStatementSourceImpl;
+
+public class ModificationDiffTest {
+
+    private static final QName TOP_CONTAINER_QNAME = QName.create("urn:opendaylight:params:xml:ns:yang:test:diff", "2015-01-05", "top-container");
+    private static final QName STRING_LEAF_QNAME = QName.create(TOP_CONTAINER_QNAME, "string");
+    private static final QName NAME_LEAF_QNAME = QName.create(TOP_CONTAINER_QNAME, "name");
+    private static final QName TEXT_LEAF_QNAME = QName.create(TOP_CONTAINER_QNAME, "text");
+    private static final QName NESTED_LIST_QNAME = QName.create(TOP_CONTAINER_QNAME, "nested-list");
+    private static final QName DEEP_LIST_QNAME = QName.create(TOP_CONTAINER_QNAME, "deep-list");
+
+    private YangInstanceIdentifier topContainerId = YangInstanceIdentifier.of(TOP_CONTAINER_QNAME);;
+
+    @Test
+    public void testInitialWrite() throws Exception {
+        final TipProducingDataTree dataTree = getDataTree();
+        final DataTreeSnapshot dataTreeSnapshot = dataTree.takeSnapshot();
+        final DataTreeModification dataTreeModification = dataTreeSnapshot.newModification();
+        final NormalizedNode<?, ?> topContainer = getTopContainer("string1");
+        final YangInstanceIdentifier topContainerId = YangInstanceIdentifier.of(TOP_CONTAINER_QNAME);
+        dataTreeModification.write(topContainerId, topContainer);
+        dataTreeModification.ready();
+        dataTree.validate(dataTreeModification);
+        final DataTreeCandidateTip prepare = dataTree.prepare(dataTreeModification);
+
+        final ModifiableDataTreeDelegator.ModificationDiff modificationDiff =
+                ModifiableDataTreeDelegator.ModificationDiff
+                        .recursivelyFromCandidate(YangInstanceIdentifier.EMPTY, prepare.getRootNode());
+
+        assertTrue(modificationDiff.getModificationsBefore().isEmpty());
+        assertAfter(topContainer, topContainerId, modificationDiff);
+    }
+
+    @Test
+    public void testUpdateWrite() throws Exception {
+        final TipProducingDataTree dataTree = getDataTree();
+        final NormalizedNode<?, ?> topContainerBefore = addTopContainer(dataTree);
+
+        final DataTreeSnapshot dataTreeSnapshot = dataTree.takeSnapshot();
+        final DataTreeModification dataTreeModification = dataTreeSnapshot.newModification();
+        final NormalizedNode<?, ?> topContainerAfter = getTopContainer("string2");
+        dataTreeModification.write(topContainerId, topContainerAfter);
+        dataTreeModification.ready();
+        dataTree.validate(dataTreeModification);
+        final DataTreeCandidateTip prepare = dataTree.prepare(dataTreeModification);
+
+        final ModifiableDataTreeDelegator.ModificationDiff modificationDiff =
+                ModifiableDataTreeDelegator.ModificationDiff
+                        .recursivelyFromCandidate(YangInstanceIdentifier.EMPTY, prepare.getRootNode());
+
+        assertBefore(topContainerBefore, topContainerId, modificationDiff);
+        assertAfter(topContainerAfter, topContainerId, modificationDiff);
+    }
+
+    @Test
+    public void testUpdateMerge() throws Exception {
+        final TipProducingDataTree dataTree = getDataTree();
+        final NormalizedNode<?, ?> topContainerBefore = addTopContainer(dataTree);
+
+        final DataTreeSnapshot dataTreeSnapshot = dataTree.takeSnapshot();
+        final DataTreeModification dataTreeModification = dataTreeSnapshot.newModification();
+        final NormalizedNode<?, ?> topContainerAfter = getTopContainer("string2");
+        dataTreeModification.merge(topContainerId, topContainerAfter);
+        dataTreeModification.ready();
+        dataTree.validate(dataTreeModification);
+        final DataTreeCandidateTip prepare = dataTree.prepare(dataTreeModification);
+
+        final ModifiableDataTreeDelegator.ModificationDiff modificationDiff =
+                ModifiableDataTreeDelegator.ModificationDiff
+                        .recursivelyFromCandidate(YangInstanceIdentifier.EMPTY, prepare.getRootNode());
+
+        assertBefore(topContainerBefore, topContainerId, modificationDiff);
+        assertAfter(topContainerAfter, topContainerId, modificationDiff);
+    }
+
+    @Test
+    public void testUpdateDelete() throws Exception {
+        final TipProducingDataTree dataTree = getDataTree();
+        final NormalizedNode<?, ?> topContainerBefore = addTopContainer(dataTree);
+
+        final DataTreeSnapshot dataTreeSnapshot = dataTree.takeSnapshot();
+        final DataTreeModification dataTreeModification = dataTreeSnapshot.newModification();
+        dataTreeModification.delete(topContainerId);
+        dataTreeModification.ready();
+        dataTree.validate(dataTreeModification);
+        final DataTreeCandidateTip prepare = dataTree.prepare(dataTreeModification);
+
+        final ModifiableDataTreeDelegator.ModificationDiff modificationDiff =
+                ModifiableDataTreeDelegator.ModificationDiff
+                        .recursivelyFromCandidate(YangInstanceIdentifier.EMPTY, prepare.getRootNode());
+
+        assertBefore(topContainerBefore, topContainerId, modificationDiff);
+        assertTrue(modificationDiff.getModificationsAfter().isEmpty());
+    }
+
+    @Test
+    public void testWriteAndUpdateInnerList() throws Exception {
+        final TipProducingDataTree dataTree = getDataTree();
+        addTopContainer(dataTree);
+
+        DataTreeSnapshot dataTreeSnapshot = dataTree.takeSnapshot();
+        DataTreeModification dataTreeModification = dataTreeSnapshot.newModification();
+        final YangInstanceIdentifier listId =
+                YangInstanceIdentifier.create(
+                        new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME),
+                        new YangInstanceIdentifier.NodeIdentifier(NESTED_LIST_QNAME));
+
+        final MapNode mapNode = getNestedList("name1", "text");
+        dataTreeModification.write(listId, mapNode);
+        dataTreeModification.ready();
+        dataTree.validate(dataTreeModification);
+        DataTreeCandidateTip prepare = dataTree.prepare(dataTreeModification);
+
+        ModifiableDataTreeDelegator.ModificationDiff modificationDiff =
+                ModifiableDataTreeDelegator.ModificationDiff
+                        .recursivelyFromCandidate(YangInstanceIdentifier.EMPTY, prepare.getRootNode());
+
+        assertTrue(modificationDiff.getModificationsBefore().isEmpty());
+        assertAfter(mapNode, listId, modificationDiff);
+
+        // Commit so that update can be tested next
+        dataTree.commit(prepare);
+
+        YangInstanceIdentifier listItemId = listId.node(
+                new YangInstanceIdentifier.NodeIdentifierWithPredicates(NESTED_LIST_QNAME, NAME_LEAF_QNAME, "name1"));
+        MapEntryNode mapEntryNode =
+                getNestedList("name1", "text-update").getValue().iterator().next();
+
+        dataTreeSnapshot = dataTree.takeSnapshot();
+        dataTreeModification = dataTreeSnapshot.newModification();
+        dataTreeModification.write(listItemId, mapEntryNode);
+        dataTreeModification.ready();
+        dataTree.validate(dataTreeModification);
+        prepare = dataTree.prepare(dataTreeModification);
+
+        modificationDiff =
+                ModifiableDataTreeDelegator.ModificationDiff
+                        .recursivelyFromCandidate(YangInstanceIdentifier.EMPTY, prepare.getRootNode());
+
+        assertBefore(mapNode.getValue().iterator().next(), listItemId, modificationDiff);
+        assertAfter(mapEntryNode, listItemId, modificationDiff);
+    }
+
+    @Test
+    public void testWriteTopContainerAndInnerList() throws Exception {
+        final TipProducingDataTree dataTree = getDataTree();
+
+        DataTreeSnapshot dataTreeSnapshot = dataTree.takeSnapshot();
+        DataTreeModification dataTreeModification = dataTreeSnapshot.newModification();
+
+        final ContainerNode topContainer = getTopContainer("string1");
+        dataTreeModification.write(topContainerId, topContainer);
+
+        final YangInstanceIdentifier listId =
+                YangInstanceIdentifier.create(
+                        new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME),
+                        new YangInstanceIdentifier.NodeIdentifier(NESTED_LIST_QNAME));
+
+        final MapNode mapNode = getNestedList("name1", "text");
+        dataTreeModification.write(listId, mapNode);
+
+        dataTreeModification.ready();
+        dataTree.validate(dataTreeModification);
+        final DataTreeCandidateTip prepare = dataTree.prepare(dataTreeModification);
+
+        final ModifiableDataTreeDelegator.ModificationDiff modificationDiff =
+                ModifiableDataTreeDelegator.ModificationDiff
+                        .recursivelyFromCandidate(YangInstanceIdentifier.EMPTY, prepare.getRootNode());
+
+        assertTrue(modificationDiff.getModificationsBefore().isEmpty());
+
+        // TODO HONEYCOMB-94 2 after modifications should appear, for top-container and nested-list entry
+        assertAfter(Builders.containerBuilder(topContainer)
+                        .withChild(mapNode)
+                        .build(),
+                topContainerId, modificationDiff);
+    }
+
+    @Test
+    public void testWriteDeepList() throws Exception {
+        final TipProducingDataTree dataTree = getDataTree();
+        addTopContainer(dataTree);
+
+        DataTreeSnapshot dataTreeSnapshot = dataTree.takeSnapshot();
+        DataTreeModification dataTreeModification = dataTreeSnapshot.newModification();
+
+        YangInstanceIdentifier listId =
+                YangInstanceIdentifier.create(
+                        new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME),
+                        new YangInstanceIdentifier.NodeIdentifier(NESTED_LIST_QNAME));
+
+        MapNode mapNode = getNestedList("name1", "text");
+        dataTreeModification.write(listId, mapNode);
+
+        dataTreeModification.ready();
+        dataTree.validate(dataTreeModification);
+        DataTreeCandidateTip prepare = dataTree.prepare(dataTreeModification);
+        dataTree.commit(prepare);
+
+        dataTreeSnapshot = dataTree.takeSnapshot();
+        dataTreeModification = dataTreeSnapshot.newModification();
+
+        final YangInstanceIdentifier.NodeIdentifierWithPredicates nestedListNodeId =
+                new YangInstanceIdentifier.NodeIdentifierWithPredicates(NESTED_LIST_QNAME, NAME_LEAF_QNAME, "name1");
+        listId = YangInstanceIdentifier.create(
+                        new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME),
+                        new YangInstanceIdentifier.NodeIdentifier(NESTED_LIST_QNAME),
+                        nestedListNodeId);
+        final YangInstanceIdentifier deepListId = listId.node(new YangInstanceIdentifier.NodeIdentifier(DEEP_LIST_QNAME));
+        final YangInstanceIdentifier deepListEntryId = deepListId.node(
+                new YangInstanceIdentifier.NodeIdentifierWithPredicates(DEEP_LIST_QNAME, NAME_LEAF_QNAME,"name1"));
+
+        final MapEntryNode deepListEntry = getDeepList("name1").getValue().iterator().next();
+        // Merge parent list, just to see no modifications on it
+        dataTreeModification.merge(
+                listId,
+                Builders.mapEntryBuilder().withNodeIdentifier(nestedListNodeId).withChild(ImmutableNodes.leafNode(NAME_LEAF_QNAME, "name1")).build());
+        dataTreeModification.merge(
+                deepListId,
+                Builders.mapBuilder().withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(DEEP_LIST_QNAME)).build());
+        dataTreeModification.merge(
+                deepListEntryId,
+                deepListEntry);
+
+        dataTreeModification.ready();
+        dataTree.validate(dataTreeModification);
+        prepare = dataTree.prepare(dataTreeModification);
+        dataTree.commit(prepare);
+
+        final ModifiableDataTreeDelegator.ModificationDiff modificationDiff = ModifiableDataTreeDelegator.ModificationDiff
+                        .recursivelyFromCandidate(YangInstanceIdentifier.EMPTY, prepare.getRootNode());
+
+        assertTrue(modificationDiff.getModificationsBefore().isEmpty());
+        assertAfter(getDeepList("name1"), deepListId, modificationDiff);
+    }
+
+    @Test
+    public void testDeleteInnerListItem() throws Exception {
+        final TipProducingDataTree dataTree = getDataTree();
+        addTopContainer(dataTree);
+
+        DataTreeSnapshot dataTreeSnapshot = dataTree.takeSnapshot();
+        DataTreeModification dataTreeModification = dataTreeSnapshot.newModification();
+        final YangInstanceIdentifier listId =
+                YangInstanceIdentifier.create(
+                        new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME),
+                        new YangInstanceIdentifier.NodeIdentifier(NESTED_LIST_QNAME));
+
+        final MapNode mapNode = getNestedList("name1", "text");
+        dataTreeModification.write(listId, mapNode);
+        dataTreeModification.ready();
+        dataTree.validate(dataTreeModification);
+        DataTreeCandidateTip prepare = dataTree.prepare(dataTreeModification);
+
+        // Commit so that update can be tested next
+        dataTree.commit(prepare);
+
+        YangInstanceIdentifier listItemId = listId.node(
+                new YangInstanceIdentifier.NodeIdentifierWithPredicates(NESTED_LIST_QNAME, NAME_LEAF_QNAME, "name1"));
+
+        dataTreeSnapshot = dataTree.takeSnapshot();
+        dataTreeModification = dataTreeSnapshot.newModification();
+        dataTreeModification.delete(listItemId);
+        dataTreeModification.ready();
+        dataTree.validate(dataTreeModification);
+        prepare = dataTree.prepare(dataTreeModification);
+
+        final ModifiableDataTreeDelegator.ModificationDiff modificationDiff =
+                ModifiableDataTreeDelegator.ModificationDiff
+                        .recursivelyFromCandidate(YangInstanceIdentifier.EMPTY, prepare.getRootNode());
+
+        assertBefore(mapNode.getValue().iterator().next(), listItemId, modificationDiff);
+        assertTrue(modificationDiff.getModificationsAfter().isEmpty());
+    }
+
+    private NormalizedNode<?, ?> addTopContainer(final TipProducingDataTree dataTree) throws DataValidationFailedException {
+        DataTreeSnapshot dataTreeSnapshot = dataTree.takeSnapshot();
+        DataTreeModification dataTreeModification = dataTreeSnapshot.newModification();
+        final NormalizedNode<?, ?> topContainerBefore = getTopContainer("string1");
+        dataTreeModification.write(topContainerId, topContainerBefore);
+        dataTreeModification.ready();
+        dataTree.validate(dataTreeModification);
+        DataTreeCandidateTip prepare = dataTree.prepare(dataTreeModification);
+        dataTree.commit(prepare);
+        return topContainerBefore;
+    }
+
+    private void assertAfter(final NormalizedNode<?, ?> topContainer, final YangInstanceIdentifier topContainerId,
+                             final ModifiableDataTreeDelegator.ModificationDiff modificationDiff) {
+        assertModification(topContainer, topContainerId, modificationDiff.getModificationsAfter());
+    }
+
+    private void assertModification(final NormalizedNode<?, ?> topContainer,
+                                    final YangInstanceIdentifier topContainerId,
+                                    final Map<YangInstanceIdentifier, NormalizedNode<?, ?>> modificationMap) {
+        assertEquals(1, modificationMap.keySet().size());
+        assertEquals(topContainerId, modificationMap.keySet().iterator().next());
+        assertEquals(topContainer, modificationMap.values().iterator().next());
+    }
+
+    private void assertBefore(final NormalizedNode<?, ?> topContainerBefore,
+                              final YangInstanceIdentifier topContainerId,
+                              final ModifiableDataTreeDelegator.ModificationDiff modificationDiff) {
+        assertModification(topContainerBefore, topContainerId, modificationDiff.getModificationsBefore());
+    }
+
+    private TipProducingDataTree getDataTree() throws ReactorException {
+        final TipProducingDataTree dataTree = InMemoryDataTreeFactory.getInstance().create(TreeType.CONFIGURATION);
+        dataTree.setSchemaContext(getSchemaCtx());
+        return dataTree;
+    }
+
+    private ContainerNode getTopContainer(final String stringValue) {
+        return Builders.containerBuilder()
+                .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME))
+                .withChild(ImmutableNodes.leafNode(STRING_LEAF_QNAME, stringValue))
+                .build();
+    }
+
+    private MapNode getNestedList(final String listItemName, final String text) {
+        return Builders.mapBuilder()
+                .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_LIST_QNAME))
+                .withChild(
+                    Builders.mapEntryBuilder()
+                        .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifierWithPredicates(NESTED_LIST_QNAME, NAME_LEAF_QNAME, listItemName))
+                        .withChild(ImmutableNodes.leafNode(NAME_LEAF_QNAME, listItemName))
+                        .withChild(ImmutableNodes.leafNode(TEXT_LEAF_QNAME, text))
+                        .build()
+                )
+                .build();
+    }
+
+    private MapNode getDeepList(final String listItemName) {
+        return Builders.mapBuilder()
+                .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(DEEP_LIST_QNAME))
+                .withChild(
+                    Builders.mapEntryBuilder()
+                        .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifierWithPredicates(DEEP_LIST_QNAME, NAME_LEAF_QNAME, listItemName))
+                        .withChild(ImmutableNodes.leafNode(NAME_LEAF_QNAME, listItemName))
+                        .build()
+                )
+                .build();
+    }
+
+    private SchemaContext getSchemaCtx() throws ReactorException {
+        final CrossSourceStatementReactor.BuildAction buildAction = YangInferencePipeline.RFC6020_REACTOR.newBuild();
+        buildAction.addSource(new YangStatementSourceImpl(getClass().getResourceAsStream("/test-diff.yang")));
+        return buildAction.buildEffective();
+    }
+}
\ No newline at end of file
diff --git a/v3po/data-impl/src/test/resources/test-diff.yang b/v3po/data-impl/src/test/resources/test-diff.yang
new file mode 100644 (file)
index 0000000..7e8721f
--- /dev/null
@@ -0,0 +1,37 @@
+module test-diff {
+    yang-version 1;
+    namespace "urn:opendaylight:params:xml:ns:yang:test:diff";
+    prefix "td";
+
+    revision "2015-01-05" {
+        description "Initial revision";
+    }
+
+    container top-container {
+        leaf string {
+            type string;
+        }
+
+        list nested-list {
+            key "name";
+
+            leaf name {
+                type string;
+            }
+
+            leaf text {
+                type string;
+            }
+
+            list deep-list {
+                key "name";
+
+                leaf name {
+                    type string;
+                }
+
+            }
+        }
+    }
+
+}
index 2a08da1..0212c08 100644 (file)
@@ -220,11 +220,16 @@ public abstract class AbstractCompositeWriter<D extends DataObject> implements W
     private void writeSubtree(final InstanceIdentifier<? extends DataObject> id,
                               final DataObject dataAfter, final WriteContext ctx) throws WriteFailedException {
         LOG.debug("{}: Writing subtree: {}", this, id);
-        final Writer<? extends ChildOf<D>> writer = getNextWriter(id);
+
+        final Writer<? extends ChildOf<D>> writer = getNextChildWriter(id);
+        final Writer<? extends Augmentation<D>> augWriter = getNextAgumentationWriter(id);
 
         if (writer != null) {
             LOG.debug("{}: Writing subtree: {} in: {}", this, id, writer);
             writer.update(id, null, dataAfter, ctx);
+        } else if (augWriter != null) {
+            LOG.debug("{}: Updating augmented subtree: {} in: {}", this, id, augWriter);
+            augWriter.update(id, null, dataAfter, ctx);
         } else {
             // If there's no dedicated writer, use write current
             // But we need current data after to do so
@@ -243,12 +248,17 @@ public abstract class AbstractCompositeWriter<D extends DataObject> implements W
     private void deleteSubtree(final InstanceIdentifier<? extends DataObject> id,
                                final DataObject dataBefore, final WriteContext ctx) throws WriteFailedException {
         LOG.debug("{}: Deleting subtree: {}", this, id);
-        final Writer<? extends ChildOf<D>> writer = getNextWriter(id);
+
+        final Writer<? extends ChildOf<D>> writer = getNextChildWriter(id);
+        final Writer<? extends Augmentation<D>> augWriter = getNextAgumentationWriter(id);
 
         if (writer != null) {
             LOG.debug("{}: Deleting subtree: {} in: {}", this, id, writer);
             writer.update(id, dataBefore, null, ctx);
-        } else {
+        } else if (augWriter != null) {
+            LOG.debug("{}: Updating augmented subtree: {} in: {}", this, id, augWriter);
+            augWriter.update(id, dataBefore, null, ctx);
+        }  else {
             updateSubtreeFromCurrent(id, ctx);
         }
     }
@@ -270,21 +280,30 @@ public abstract class AbstractCompositeWriter<D extends DataObject> implements W
                                final DataObject dataAfter,
                                final WriteContext ctx) throws WriteFailedException {
         LOG.debug("{}: Updating subtree: {}", this, id);
-        final Writer<? extends ChildOf<D>> writer = getNextWriter(id);
+        final Writer<? extends ChildOf<D>> writer = getNextChildWriter(id);
+        final Writer<? extends Augmentation<D>> augWriter = getNextAgumentationWriter(id);
 
         if (writer != null) {
             LOG.debug("{}: Updating subtree: {} in: {}", this, id, writer);
             writer.update(id, dataBefore, dataAfter, ctx);
+        } else if (augWriter != null) {
+            LOG.debug("{}: Updating augmented subtree: {} in: {}", this, id, augWriter);
+            augWriter.update(id, dataBefore, dataAfter, ctx);
         } else {
             updateSubtreeFromCurrent(id, ctx);
         }
     }
 
-    private Writer<? extends ChildOf<D>> getNextWriter(final InstanceIdentifier<? extends DataObject> id) {
+    private Writer<? extends ChildOf<D>> getNextChildWriter(final InstanceIdentifier<? extends DataObject> id) {
         final Class<? extends DataObject> next = RWUtils.getNextId(id, getManagedDataObjectType()).getType();
         return childWriters.get(next);
     }
 
+    private Writer<? extends Augmentation<D>> getNextAgumentationWriter(final InstanceIdentifier<? extends DataObject> id) {
+        final Class<? extends DataObject> next = RWUtils.getNextId(id, getManagedDataObjectType()).getType();
+        return augWriters.get(next);
+    }
+
     private static <T> List<T> reverseCollection(final Collection<T> original) {
         // TODO find a better reverse mechanism (probably a different collection for child writers is necessary)
         final ArrayList<T> list = Lists.newArrayList(original);
index 8b981af..061d3fa 100644 (file)
@@ -19,11 +19,10 @@ package io.fd.honeycomb.v3po.translate.util.write;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 
-import com.google.common.base.Function;
-import com.google.common.collect.Collections2;
+import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
+import com.google.common.collect.Multimap;
 import io.fd.honeycomb.v3po.translate.util.RWUtils;
 import io.fd.honeycomb.v3po.translate.write.WriteContext;
 import io.fd.honeycomb.v3po.translate.write.WriteFailedException;
@@ -32,7 +31,6 @@ import io.fd.honeycomb.v3po.translate.write.WriterRegistry;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import org.opendaylight.yangtools.yang.binding.DataObject;
@@ -88,8 +86,15 @@ public final class DelegatingWriterRegistry implements WriterRegistry {
     public void update(@Nonnull final Map<InstanceIdentifier<?>, DataObject> nodesBefore,
                        @Nonnull final Map<InstanceIdentifier<?>, DataObject> nodesAfter,
                        @Nonnull final WriteContext ctx) throws WriteFailedException {
-        checkAllWritersPresent(nodesBefore);
-        checkAllWritersPresent(nodesAfter);
+
+        Multimap<InstanceIdentifier<?>, InstanceIdentifier<?>> rootIdToNestedIds = HashMultimap.create();
+        try {
+            checkAllWritersPresent(nodesBefore, rootIdToNestedIds);
+            checkAllWritersPresent(nodesAfter, rootIdToNestedIds);
+        } catch (IllegalArgumentException e) {
+            LOG.warn("Unable to process update", e);
+            throw e;
+        }
 
         final List<InstanceIdentifier<?>> processedNodes = Lists.newArrayList();
 
@@ -97,36 +102,41 @@ public final class DelegatingWriterRegistry implements WriterRegistry {
                 .entrySet()) {
 
             final InstanceIdentifier<? extends DataObject> id = rootWriterEntry.getValue().getManagedDataObjectType();
+            // FIXME !! this is not ideal, we are not handling nested updates in expected order
+            // Root writers are invoked in order they were registered, but nested updates are not, since they are
+            // iterated here.
+            //
+            for (InstanceIdentifier<?> specificInstanceIdentifier : rootIdToNestedIds.get(id)) {
+                final DataObject dataBefore = nodesBefore.get(specificInstanceIdentifier);
+                final DataObject dataAfter = nodesAfter.get(specificInstanceIdentifier);
+
+                // No change to current writer
+                if (dataBefore == null && dataAfter == null) {
+                    continue;
+                }
 
-            final DataObject dataBefore = nodesBefore.get(id);
-            final DataObject dataAfter = nodesAfter.get(id);
-
-            // No change to current writer
-            if (dataBefore == null && dataAfter == null) {
-                continue;
-            }
-
-            LOG.debug("ChangesProcessor.applyChanges() processing dataBefore={}, dataAfter={}", dataBefore, dataAfter);
-
-            try {
-                update(id, dataBefore, dataAfter, ctx);
-                processedNodes.add(id);
-            } catch (Exception e) {
-                LOG.error("Error while processing data change of: {} (before={}, after={})",
-                        id, dataBefore, dataAfter, e);
-                throw new BulkUpdateException(
-                        id, new ReverterImpl(this, processedNodes, nodesBefore, nodesAfter, ctx), e);
+                try {
+                    LOG.debug("ChangesProcessor.applyChanges() processing dataBefore={}, dataAfter={}", dataBefore, dataAfter);
+                    update(specificInstanceIdentifier, dataBefore, dataAfter, ctx);
+                    processedNodes.add(id);
+                } catch (Exception e) {
+                    LOG.error("Error while processing data change of: {} (before={}, after={})", id, dataBefore, dataAfter, e);
+                    throw new BulkUpdateException(id, new ReverterImpl(this, processedNodes, nodesBefore, nodesAfter, ctx), e);
+                }
             }
         }
     }
 
-    private void checkAllWritersPresent(final @Nonnull Map<InstanceIdentifier<?>, DataObject> nodesBefore) {
-        final Set<Class<? extends DataObject>> nodesBeforeClasses =
-            Sets.newHashSet(Collections2.transform(nodesBefore.keySet(),
-                (Function<InstanceIdentifier<?>, Class<? extends DataObject>>) InstanceIdentifier::getTargetType));
-        checkArgument(rootWriters.keySet().containsAll(nodesBeforeClasses),
-                "Unable to handle all changes. Missing dedicated writers for: %s",
-                Sets.difference(nodesBeforeClasses, rootWriters.keySet()));
+    private void checkAllWritersPresent(final @Nonnull Map<InstanceIdentifier<?>, DataObject> nodesBefore,
+                                        final @Nonnull Multimap<InstanceIdentifier<?>, InstanceIdentifier<?>> rootIdToNestedIds) {
+        for (final InstanceIdentifier<?> changeId : nodesBefore.keySet()) {
+            final InstanceIdentifier.PathArgument first = Iterables.getFirst(changeId.getPathArguments(), null);
+            checkNotNull(first, "Empty identifier detected");
+            final InstanceIdentifier<? extends DataObject> rootId = InstanceIdentifier.create(first.getType());
+            checkArgument(rootWriters.keySet().contains(first.getType()),
+                    "Unable to handle change. Missing dedicated writer for: %s", first.getType());
+            rootIdToNestedIds.put(rootId, changeId);
+        }
     }
 
     private static final class ReverterImpl implements Reverter {
index 575f1d8..f005826 100644 (file)
@@ -97,7 +97,10 @@ public abstract class AbstractDataTreeConverter<O extends DataObject, C extends
 
     private void writeData(final C configData) throws TransactionCommitFailedException {
         final WriteTransaction writeTx = bindingDataBroker.newWriteOnlyTransaction();
-        writeTx.put(LogicalDatastoreType.CONFIGURATION, idConfig, configData);
+        // Merge(instead of put) has to be used due to dynamic start, this might be executed multiple times
+        // and might overwrite config restored from persisted file with the same incomplete config.
+        // Making the entire configuration trigger VPP twice (on second persis ... and VPP does not like that
+        writeTx.merge(LogicalDatastoreType.CONFIGURATION, idConfig, configData);
         writeTx.submit().checkedGet();
     }