BUG-4295: instantiate MERGE operations lazily 52/32752/2
authorPeter Kajsa <pkajsa@cisco.com>
Tue, 5 Jan 2016 09:14:00 +0000 (10:14 +0100)
committerTony Tkacik <ttkacik@cisco.com>
Tue, 19 Jan 2016 15:28:56 +0000 (15:28 +0000)
This patch reworks how merges are done in a DataTreeModification by
moving the logic to SchemaAwareApplyOperation, which is the final
recipient of the resulting ModifiedNode.

This way the code is co-located and can be specialized based on
information available for that particular node, and the container merge
code is cleanly separated from the leaf node code, which turns each
merge into a write.

When a merge occurs on a previously-written node, we graft all merged
children onto the write, using recursion only when necessary.

checkPresentChild method renamed.
Fix of ModifiedNode and added unit test.

Change-Id: I674e3d2150e796472e831abdcfa0fad582b69759
Signed-off-by: Robert Varga <rovarga@cisco.com>
Signed-off-by: Filip.Gregor <fgregor@cisco.com>
Signed-off-by: Peter Kajsa <pkajsa@cisco.com>
23 files changed:
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/AbstractNodeContainerModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/AbstractValueNodeModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/AlwaysFailOperation.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/AugmentationModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/ChoiceModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/ContainerModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/ListEntryModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/MinMaxElementsValidation.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/ModificationApplyOperation.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/ModifiedNode.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/OperationWithModification.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/OrderedLeafSetModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/OrderedMapModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/RootModificationApplyOperation.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/SchemaAwareApplyOperation.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/StructuralContainerModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UnkeyedListItemModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UnkeyedListModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UnorderedLeafSetModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UnorderedMapModificationStrategy.java
yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/Bug4295Test.java [new file with mode: 0644]
yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/Bug4454Test.java
yang/yang-data-impl/src/test/resources/bug-4295/foo.yang [new file with mode: 0644]

index b7f2f6003c385d69429c83d69c8e07e28ec60667..59ed52d04a9d242c3149a006b858233da2f21c13 100644 (file)
@@ -11,8 +11,10 @@ import static com.google.common.base.Preconditions.checkArgument;
 
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
+import com.google.common.base.Verify;
 import java.util.Collection;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.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.api.schema.tree.ConflictingModificationAppliedException;
@@ -58,6 +60,21 @@ abstract class AbstractNodeContainerModificationStrategy extends SchemaAwareAppl
         }
     }
 
+    protected void recursivelyVerifyStructure(NormalizedNode<?, ?> value) {
+        final NormalizedNodeContainer container = (NormalizedNodeContainer) value;
+        for (final Object child : container.getValue()) {
+            checkArgument(child instanceof NormalizedNode);
+            final NormalizedNode<?, ?> castedChild = (NormalizedNode<?, ?>) child;
+            final Optional<ModificationApplyOperation> childOp = getChild(castedChild.getIdentifier());
+            if (childOp.isPresent()) {
+                childOp.get().recursivelyVerifyStructure(castedChild);
+            } else {
+                throw new SchemaValidationFailedException(
+                        String.format("Child %s is not valid child according to schema.", castedChild.getIdentifier()));
+            }
+        }
+    }
+
     @Override
     protected TreeNode applyWrite(final ModifiedNode modification,
             final Optional<TreeNode> currentMeta, final Version version) {
@@ -131,12 +148,100 @@ abstract class AbstractNodeContainerModificationStrategy extends SchemaAwareAppl
     }
 
     @Override
-    protected TreeNode applyMerge(final ModifiedNode modification, final TreeNode currentMeta,
-            final Version version) {
-        // For Node Containers - merge is same as subtree change - we only replace children.
+    protected TreeNode applyMerge(final ModifiedNode modification, final TreeNode currentMeta, final Version version) {
+        /*
+         * The node which we are merging exists. We now need to expand any child operations implied by the value. Once
+         * we do that, ModifiedNode children will look like this node were a TOUCH and we will let applyTouch() do the
+         * heavy lifting of applying the children recursively (either through here or through applyWrite().
+         */
+        final NormalizedNode<?, ?> value = modification.getWrittenValue();
+
+        Verify.verify(value instanceof NormalizedNodeContainer, "Attempted to merge non-container %s", value);
+        @SuppressWarnings({"unchecked", "rawtypes"})
+        final Collection<NormalizedNode<?, ?>> children = ((NormalizedNodeContainer) value).getValue();
+        for (NormalizedNode<?, ?> c : children) {
+            final PathArgument id = c.getIdentifier();
+            modification.modifyChild(id, resolveChildOperation(id).getChildPolicy(), version);
+        }
         return applyTouch(modification, currentMeta, version);
     }
 
+    private void mergeChildrenIntoModification(final ModifiedNode modification,
+            final Collection<NormalizedNode<?, ?>> children, final Version version) {
+        for (NormalizedNode<?, ?> c : children) {
+            final ModificationApplyOperation childOp = resolveChildOperation(c.getIdentifier());
+            final ModifiedNode childNode = modification.modifyChild(c.getIdentifier(), childOp.getChildPolicy(), version);
+            childOp.mergeIntoModifiedNode(childNode, c, version);
+        }
+    }
+
+    @Override
+    final void mergeIntoModifiedNode(final ModifiedNode modification, final NormalizedNode<?, ?> value,
+            final Version version) {
+        @SuppressWarnings({ "unchecked", "rawtypes" })
+        final Collection<NormalizedNode<?, ?>> children = ((NormalizedNodeContainer)value).getValue();
+
+        switch (modification.getOperation()) {
+        case NONE:
+            // Fresh node, just record a MERGE with a value
+                recursivelyVerifyStructure(value);
+            modification.updateValue(LogicalOperation.MERGE, value);
+            return;
+        case TOUCH:
+
+                mergeChildrenIntoModification(modification, children, version);
+                // We record empty merge value, since real children merges
+                // are already expanded. This is needed to satisfy non-null for merge
+                // original merge value can not be used since it mean different
+                // order of operation - parent changes are always resolved before
+                // children ones, and having node in TOUCH means children was modified
+                // before.
+                modification.updateValue(LogicalOperation.MERGE, createEmptyValue(value));
+                return;
+        case MERGE:
+            // Merging into an existing node. Merge data children modifications (maybe recursively) and mark as MERGE,
+            // invalidating cached snapshot
+            mergeChildrenIntoModification(modification, children, version);
+                modification.updateOperationType(LogicalOperation.MERGE);
+            return;
+        case DELETE:
+            // Delete performs a data dependency check on existence of the node. Performing a merge on DELETE means we
+            // are really performing a write. One thing that ruins that are any child modifications. If there are any,
+            // we will perform a read() to get the current state of affairs, turn this into into a WRITE and then
+            // append any child entries.
+            if (!modification.getChildren().isEmpty()) {
+                // Version does not matter here as we'll throw it out
+                final Optional<TreeNode> current = apply(modification, modification.getOriginal(), Version.initial());
+                if (current.isPresent()) {
+                    modification.updateValue(LogicalOperation.WRITE, current.get().getData());
+                    mergeChildrenIntoModification(modification, children, version);
+                    return;
+                }
+            }
+
+            modification.updateValue(LogicalOperation.WRITE, value);
+            return;
+        case WRITE:
+            // We are augmenting a previous write. We'll just walk value's children, get the corresponding ModifiedNode
+            // and run recursively on it
+            mergeChildrenIntoModification(modification, children, version);
+            modification.updateOperationType(LogicalOperation.WRITE);
+            return;
+        }
+
+        throw new IllegalArgumentException("Unsupported operation " + modification.getOperation());
+    }
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    private NormalizedNode<?, ?> createEmptyValue(NormalizedNode<?, ?> value,
+            Collection<NormalizedNode<?, ?>> children) {
+        NormalizedNodeContainerBuilder builder = createBuilder(value);
+        for (NormalizedNode<?, ?> child : children) {
+            builder.removeChild(child.getIdentifier());
+        }
+        return builder.build();
+    }
+
     @Override
     protected TreeNode applyTouch(final ModifiedNode modification, final TreeNode currentMeta, final Version version) {
         /*
@@ -220,4 +325,6 @@ abstract class AbstractNodeContainerModificationStrategy extends SchemaAwareAppl
 
     @SuppressWarnings("rawtypes")
     protected abstract NormalizedNodeContainerBuilder createBuilder(NormalizedNode<?, ?> original);
+
+    protected abstract NormalizedNode<?, ?> createEmptyValue(NormalizedNode<?, ?> original);
 }
index bd23a4e46c905739d39a38bfadb81371545736e3..e4b80b2fb5beb1372a447ff7146ab8a843e643bb 100644 (file)
@@ -72,4 +72,25 @@ abstract class AbstractValueNodeModificationStrategy<T extends DataSchemaNode> e
             final Optional<TreeNode> current) throws IncorrectDataStructureException {
         throw new IncorrectDataStructureException(path, "Subtree modification is not allowed.");
     }
-}
\ No newline at end of file
+
+    @Override
+    void mergeIntoModifiedNode(final ModifiedNode node, final NormalizedNode<?, ?> value, final Version version) {
+
+        switch (node.getOperation()) {
+            // Delete performs a data dependency check on existence of the node. Performing a merge
+            // on DELETE means we
+            // are really performing a write.
+            case DELETE:
+            case WRITE:
+                node.write(value);
+                break;
+            default:
+                node.updateValue(LogicalOperation.MERGE, value);
+        }
+    }
+
+    @Override
+    void recursivelyVerifyStructure(NormalizedNode<?, ?> value) {
+        verifyStructure(value, false);
+    }
+}
index b91d28f9b7d4516d095c37d9ad189024a492729a..cd2455695ae8cda0ddddb55de1cffcff7cc95af0 100644 (file)
@@ -34,7 +34,8 @@ final class AlwaysFailOperation extends ModificationApplyOperation {
     }
 
     @Override
-    void checkApplicable(final YangInstanceIdentifier path,final NodeModification modification, final Optional<TreeNode> storeMetadata) {
+    void checkApplicable(final YangInstanceIdentifier path,final NodeModification modification,
+            final Optional<TreeNode> storeMetadata) {
         throw new IllegalStateException("Schema Context is not available.");
     }
 
@@ -52,4 +53,14 @@ final class AlwaysFailOperation extends ModificationApplyOperation {
     ChildTrackingPolicy getChildPolicy() {
         throw new IllegalStateException("Schema Context is not available.");
     }
+
+    @Override
+    void mergeIntoModifiedNode(final ModifiedNode node, final NormalizedNode<?, ?> value, final Version version) {
+        throw new IllegalStateException("Schema Context is not available.");
+    }
+
+    @Override
+    void recursivelyVerifyStructure(NormalizedNode<?, ?> value) {
+        throw new IllegalStateException("Schema Context is not available.");
+    }
 }
\ No newline at end of file
index 83d460c09b635e693d23d50e64b00c66a21de961..91fd2e557c62198b728f7fce7a421300a3356c43 100644 (file)
@@ -33,6 +33,13 @@ final class AugmentationModificationStrategy extends AbstractDataNodeContainerMo
         return ImmutableAugmentationNodeBuilder.create((AugmentationNode) original);
     }
 
+    @Override
+    protected NormalizedNode<?, ?> createEmptyValue(NormalizedNode<?, ?> original) {
+        checkArgument(original instanceof AugmentationNode);
+        return ImmutableAugmentationNodeBuilder.create()
+                .withNodeIdentifier(((AugmentationNode) original).getIdentifier()).build();
+    }
+
     private static AugmentationSchema createAugmentProxy(final AugmentationSchema schema, final DataNodeContainer resolved) {
         final Set<DataSchemaNode> realChildSchemas = new HashSet<>();
         for(final DataSchemaNode augChild : schema.getChildNodes()) {
index 38edd7bb2f088f5b7cb4f20d10edf1de37223f49..62c6ebcad98463b5394419441184ceb8ac73ed25 100644 (file)
@@ -135,4 +135,11 @@ final class ChoiceModificationStrategy extends AbstractNodeContainerModification
         enforceCases(ret);
         return ret;
     }
+
+    @Override
+    protected NormalizedNode<?, ?> createEmptyValue(NormalizedNode<?, ?> original) {
+        checkArgument(original instanceof ChoiceNode);
+        return ImmutableChoiceNodeBuilder.create().withNodeIdentifier(((ChoiceNode) original).getIdentifier()).build();
+    }
 }
+
index 58671b8c3e84460690d20ca37d2e5925c30b7795..8e28fdd86810222fd84dd49893c7dc163d04127b 100644 (file)
@@ -8,6 +8,8 @@
 package org.opendaylight.yangtools.yang.data.impl.schema.tree;
 
 import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.base.Preconditions;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.TreeType;
@@ -30,4 +32,11 @@ class ContainerModificationStrategy extends AbstractDataNodeContainerModificatio
         checkArgument(original instanceof ContainerNode);
         return ImmutableContainerNodeBuilder.create((ContainerNode) original);
     }
+
+    @Override
+    protected NormalizedNode<?, ?> createEmptyValue(NormalizedNode<?, ?> original) {
+        Preconditions.checkArgument(original instanceof ContainerNode);
+        return ImmutableContainerNodeBuilder.create().withNodeIdentifier(((ContainerNode) original).getIdentifier())
+                .build();
+    }
 }
index 30b17b1c0e4aa76eebbaa6c9d16bb3b23ead3fde..f2ba17be3717a081f975c04d915d064d8a43e928 100644 (file)
@@ -54,4 +54,11 @@ final class ListEntryModificationStrategy extends AbstractDataNodeContainerModif
         checkArgument(original instanceof MapEntryNode);
         return ImmutableMapEntryNodeBuilder.create((MapEntryNode) original);
     }
+
+    @Override
+    protected NormalizedNode<?, ?> createEmptyValue(NormalizedNode<?, ?> original) {
+        checkArgument(original instanceof MapEntryNode);
+        return ImmutableMapEntryNodeBuilder.create().withNodeIdentifier(((MapEntryNode) original).getIdentifier())
+                .build();
+    }
 }
\ No newline at end of file
index 932f20a810092d5d9543a5cabb94ebe0446deaf9..ede5bcc067b227719a92655359c82df1a013ad75 100644 (file)
@@ -114,17 +114,17 @@ final class MinMaxElementsValidation extends SchemaAwareApplyOperation {
         for (final ModifiedNode modChild : modification.getChildren()) {
             switch (modChild.getOperation()) {
                 case WRITE:
-                    if (!modChild.getOriginal().isPresent()) {
+                    if (!checkOriginalPresent(modChild)) {
                         result++;
                     }
                     break;
                 case MERGE:
-                    if (!current.isPresent()) {
+                    if (!checkOriginalPresent(modChild)) {
                         result++;
                     }
                     break;
                 case DELETE:
-                    if (modChild.getOriginal().isPresent()) {
+                    if (checkOriginalPresent(modChild)) {
                         result--;
                     }
                     break;
@@ -139,6 +139,10 @@ final class MinMaxElementsValidation extends SchemaAwareApplyOperation {
         return result;
     }
 
+    private static boolean checkOriginalPresent(ModifiedNode child) {
+        return child.getOriginal().isPresent();
+    }
+
     @Override
     protected void checkTouchApplicable(final YangInstanceIdentifier path, final NodeModification modification,
             final Optional<TreeNode> current) throws DataValidationFailedException {
@@ -191,4 +195,14 @@ final class MinMaxElementsValidation extends SchemaAwareApplyOperation {
     protected ChildTrackingPolicy getChildPolicy() {
         return delegate.getChildPolicy();
     }
+
+    @Override
+    void mergeIntoModifiedNode(final ModifiedNode node, final NormalizedNode<?, ?> value, final Version version) {
+        delegate.mergeIntoModifiedNode(node, value, version);
+    }
+
+    @Override
+    void recursivelyVerifyStructure(NormalizedNode<?, ?> value) {
+        delegate.recursivelyVerifyStructure(value);
+    }
 }
index cdeaa7c01adee901b4be7663c3b10afd37caaddf..933bf9c5704a9f788d0882dd2b1e6d4d06966bd5 100644 (file)
@@ -88,6 +88,17 @@ abstract class ModificationApplyOperation implements StoreTreeNode<ModificationA
      */
     abstract ChildTrackingPolicy getChildPolicy();
 
+    /**
+     * Stage a merge operation into a {@link ModifiedNode} such that it will be processed correctly by
+     * {@link #apply(ModifiedNode, Optional, Version)}. This method is the context which is introducing this operation,
+     * and so any overheads are charged to whoever is in control of the access pattern.
+     *
+     * @param modification Original modification node
+     * @param value Value which should be merge into the modification
+     * @param version Data version as carried in the containing {@link InMemoryDataTreeModification}
+     */
+    abstract void mergeIntoModifiedNode(ModifiedNode modification, NormalizedNode<?, ?> value, Version version);
+
     /**
      * Returns a suboperation for specified tree node
      *
@@ -96,4 +107,6 @@ abstract class ModificationApplyOperation implements StoreTreeNode<ModificationA
      */
     @Override
     public abstract Optional<ModificationApplyOperation> getChild(PathArgument child);
+
+    abstract void recursivelyVerifyStructure(NormalizedNode<?, ?> value);
 }
index 23674daf32655142e17815d6ca3c52486e3b6485..cae3e94d1c7bdd7aa2dc5a5550cb2e2b0d8166dd 100644 (file)
@@ -16,7 +16,7 @@ import javax.annotation.Nonnull;
 import javax.annotation.concurrent.NotThreadSafe;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
-import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodes;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodeContainer;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.ModificationType;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.StoreTreeNode;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.spi.TreeNode;
@@ -26,11 +26,14 @@ import org.opendaylight.yangtools.yang.data.api.schema.tree.spi.Version;
 /**
  * Node Modification Node and Tree
  *
- * Tree which structurally resembles data tree and captures client modifications
- * to the data store tree.
+ * Tree which structurally resembles data tree and captures client modifications to the data store tree. This tree is
+ * lazily created and populated via {@link #modifyChild(PathArgument)} and {@link TreeNode} which represents original
+ * state as tracked by {@link #getOriginal()}.
  *
- * This tree is lazily created and populated via {@link #modifyChild(PathArgument)}
- * and {@link TreeNode} which represents original state as tracked by {@link #getOriginal()}.
+ * The contract is that the state information exposed here preserves the temporal ordering of whatever modifications
+ * were executed. A child's effects pertain to data node as modified by its ancestors. This means that in order to
+ * reconstruct the effective data node presentation, it is sufficient to perform a depth-first pre-order traversal of
+ * the tree.
  */
 @NotThreadSafe
 final class ModifiedNode extends NodeModification implements StoreTreeNode<ModifiedNode> {
@@ -69,28 +72,31 @@ final class ModifiedNode extends NodeModification implements StoreTreeNode<Modif
         this.children = childPolicy.createMap();
     }
 
-    /**
-     * Return the value which was written to this node.
-     *
-     * @return Currently-written value
-     */
-    public NormalizedNode<?, ?> getWrittenValue() {
-        return value;
-    }
-
     @Override
     public PathArgument getIdentifier() {
         return identifier;
     }
 
+    @Override
+    LogicalOperation getOperation() {
+        return operation;
+    }
+
     @Override
     Optional<TreeNode> getOriginal() {
         return original;
     }
 
-    @Override
-    LogicalOperation getOperation() {
-        return operation;
+    /**
+     * Return the value which was written to this node. The returned object is only valid for
+     * {@link LogicalOperation#MERGE} and {@link LogicalOperation#WRITE}.
+     * operations. It should only be consulted when this modification is going to end up being
+     * {@link ModificationType#WRITE}.
+     *
+     * @return Currently-written value
+     */
+    NormalizedNode<?, ?> getWrittenValue() {
+        return value;
     }
 
     /**
@@ -137,15 +143,8 @@ final class ModifiedNode extends NodeModification implements StoreTreeNode<Modif
             return Optional.absent();
         case NONE:
         case TOUCH:
-            return metadataFromSnapshot(child);
         case MERGE:
-            // MERGE is half-way between TOUCH and WRITE. If the child exists in data, it behaves as a WRITE, otherwise
-            // it behaves as a TOUCH
-            if (NormalizedNodes.findNode(value, child).isPresent()) {
-                return metadataFromData(child, modVersion);
-            } else {
-                return metadataFromSnapshot(child);
-            }
+            return metadataFromSnapshot(child);
         case WRITE:
             // WRITE implies presence based on written data
             return metadataFromData(child, modVersion);
@@ -183,6 +182,18 @@ final class ModifiedNode extends NodeModification implements StoreTreeNode<Modif
 
 
         final ModifiedNode newlyCreated = new ModifiedNode(child, currentMetadata, childPolicy);
+        if (operation == LogicalOperation.MERGE && value != null) {
+            /*
+             * We are attempting to modify a previously-unmodified part of a MERGE node. If the
+             * value contains this component, we need to materialize it as a MERGE modification.
+             */
+            @SuppressWarnings({ "rawtypes", "unchecked" })
+            final Optional<NormalizedNode<?, ?>> childData = ((NormalizedNodeContainer)value).getChild(child);
+            if (childData.isPresent()) {
+                newlyCreated.updateValue(LogicalOperation.MERGE, childData.get());
+            }
+        }
+
         children.put(child, newlyCreated);
         return newlyCreated;
     }
@@ -238,39 +249,10 @@ final class ModifiedNode extends NodeModification implements StoreTreeNode<Modif
      * @param value
      */
     void write(final NormalizedNode<?, ?> value) {
-        pushWrite(value);
+        updateValue(LogicalOperation.WRITE, value);
         children.clear();
     }
 
-    // Promote the node to write, but do not lose children
-    void pushWrite(final NormalizedNode<?, ?> value) {
-        clearSnapshot();
-        updateOperationType(LogicalOperation.WRITE);
-        this.value = value;
-    }
-
-    void merge(final NormalizedNode<?, ?> value) {
-        clearSnapshot();
-        updateOperationType(LogicalOperation.MERGE);
-
-        /*
-         * Blind overwrite of any previous data is okay, no matter whether the node
-         * is simple or complex type.
-         *
-         * If this is a simple or complex type with unkeyed children, this merge will
-         * be turned into a write operation, overwriting whatever was there before.
-         *
-         * If this is a container with keyed children, there are two possibilities:
-         * - if it existed before, this value will never be consulted and the children
-         *   will get explicitly merged onto the original data.
-         * - if it did not exist before, this value will be used as a seed write and
-         *   children will be merged into it.
-         * In either case we rely on OperationWithModification to manipulate the children
-         * before calling this method, so unlike a write we do not want to clear them.
-         */
-        this.value = value;
-    }
-
     /**
      * Seal the modification node and prune any children which has not been modified.
      *
@@ -314,7 +296,7 @@ final class ModifiedNode extends NodeModification implements StoreTreeNode<Modif
         return snapshot;
     }
 
-    private void updateOperationType(final LogicalOperation type) {
+    void updateOperationType(final LogicalOperation type) {
         operation = type;
         modType = null;
 
@@ -333,6 +315,17 @@ final class ModifiedNode extends NodeModification implements StoreTreeNode<Modif
         modType = type;
     }
 
+    /**
+     * Update this node's value and operation type without disturbing any of its child modifications.
+     *
+     * @param type New operation type
+     * @param value New node value
+     */
+    void updateValue(final LogicalOperation type, final NormalizedNode<?, ?> value) {
+        this.value = Preconditions.checkNotNull(value);
+        updateOperationType(type);
+    }
+
     /**
      * Return the physical modification done to data. May return null if the
      * operation has not been applied to the underlying tree. This is different
@@ -345,30 +338,6 @@ final class ModifiedNode extends NodeModification implements StoreTreeNode<Modif
         return modType;
     }
 
-    /**
-     * Create a node which will reflect the state of this node, except it will behave as newly-written
-     * value. This is useful only for merge validation.
-     *
-     * @param value Value associated with the node
-     * @return An isolated node. This node should never reach a datatree.
-     */
-    ModifiedNode asNewlyWritten(final NormalizedNode<?, ?> value) {
-        /*
-         * We are instantiating an "equivalent" of this node. Currently the only callsite does not care
-         * about the actual iteration order, so we do not have to specify the same tracking policy as
-         * we were instantiated with. Since this is the only time we need to know that policy (it affects
-         * only things in constructor), we do not want to retain it (saves some memory on per-instance
-         * basis).
-         *
-         * We could reconstruct it using two instanceof checks (to undo what the constructor has done),
-         * which would give perfect results. The memory saving would be at most 32 bytes of a short-lived
-         * object, so let's not bother with that.
-         */
-        final ModifiedNode ret = new ModifiedNode(getIdentifier(), Optional.<TreeNode>absent(), ChildTrackingPolicy.UNORDERED);
-        ret.write(value);
-        return ret;
-    }
-
     public static ModifiedNode createUnmodified(final TreeNode metadataTree, final ChildTrackingPolicy childPolicy) {
         return new ModifiedNode(metadataTree.getIdentifier(), Optional.of(metadataTree), childPolicy);
     }
index fbfb499c6dcd484caccebc9a41014f54c6dc8c1b..d22854dc889b21d06e57ca11928dd74e9281badf 100644 (file)
@@ -12,7 +12,6 @@ import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.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.api.schema.tree.spi.TreeNode;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.spi.Version;
 
@@ -40,48 +39,6 @@ final class OperationWithModification {
         applyOperation.verifyStructure(value, false);
     }
 
-    private void recursiveMerge(final NormalizedNode<?,?> data, final Version version) {
-        if (data instanceof NormalizedNodeContainer) {
-            @SuppressWarnings({ "rawtypes", "unchecked" })
-            final NormalizedNodeContainer<?,?, NormalizedNode<PathArgument, ?>> dataContainer =
-                    (NormalizedNodeContainer) data;
-
-            /*
-             * If there was write before on this node and it is of NormalizedNodeContainer type merge would overwrite
-             * our changes. So we create write modifications from data children to retain children created by previous
-             * write operation. These writes will then be pushed down in the tree while there are merge modifications
-             * on these children
-             */
-            if (modification.getOperation() == LogicalOperation.WRITE) {
-                @SuppressWarnings({ "rawtypes", "unchecked" })
-                final NormalizedNodeContainer<?,?, NormalizedNode<PathArgument, ?>> oldDataContainer =
-                        (NormalizedNodeContainer) modification.getWrittenValue();
-                for (final NormalizedNode<PathArgument, ?> c : oldDataContainer.getValue()) {
-                    final PathArgument childId = c.getIdentifier();
-
-                    // Acquire the child operation type if available, fall back to NONE
-                    final Optional<ModifiedNode> maybeChild = modification.getChild(childId);
-                    if (maybeChild.isPresent()) {
-                        final ModifiedNode child = maybeChild.get();
-                        final LogicalOperation op = child.getOperation();
-                        if (op == LogicalOperation.TOUCH || op == LogicalOperation.NONE) {
-                            child.pushWrite(c);
-                        }
-                    } else {
-                        // Not present, issue a write
-                        forChild(childId, version).write(c);
-                    }
-                }
-            }
-            for (final NormalizedNode<PathArgument, ?> child : dataContainer.getValue()) {
-                final PathArgument childId = child.getIdentifier();
-                forChild(childId, version).recursiveMerge(child, version);
-            }
-        }
-
-        modification.merge(data);
-    }
-
     void merge(final NormalizedNode<?, ?> data, final Version version) {
         /*
          * A merge operation will end up overwriting parts of the tree, retaining others. We want to
@@ -89,11 +46,9 @@ final class OperationWithModification {
          * written. In order to do that, we first pretend the data was written, run verification and
          * then perform the merge -- with the explicit assumption that adding the newly-validated
          * data with the previously-validated data will not result in invalid data.
-         *
-         * FIXME: Should be this moved to recursive merge and run for each node?
          */
-        applyOperation.verifyStructure(data, false);
-        recursiveMerge(data, version);
+        applyOperation.verifyStructure(data, true);
+        applyOperation.mergeIntoModifiedNode(modification, data, version);
     }
 
     void delete() {
@@ -150,15 +105,4 @@ final class OperationWithModification {
             final ModifiedNode modification) {
         return new OperationWithModification(operation, modification);
     }
-
-    private OperationWithModification forChild(final PathArgument childId, final Version version) {
-        final Optional<ModificationApplyOperation> maybeChildOp = applyOperation.getChild(childId);
-        Preconditions.checkArgument(maybeChildOp.isPresent(),
-            "Attempted to apply operation to non-existent child %s", childId);
-
-        final ModificationApplyOperation childOp = maybeChildOp.get();
-        final ModifiedNode childMod = modification.modifyChild(childId, childOp.getChildPolicy(), version);
-
-        return from(childOp, childMod);
-    }
 }
index 2e57dbbfc50d0007f109ffb09db7a4315b8d85ec..488cc2281f01c1851f8142228016920871315028 100644 (file)
@@ -40,6 +40,13 @@ final class OrderedLeafSetModificationStrategy extends AbstractNodeContainerModi
         return ImmutableOrderedLeafSetNodeBuilder.create((OrderedLeafSetNode<?>) original);
     }
 
+    @Override
+    protected NormalizedNode<?, ?> createEmptyValue(NormalizedNode<?, ?> original) {
+        checkArgument(original instanceof OrderedLeafSetNode<?>);
+        return ImmutableOrderedLeafSetNodeBuilder.create()
+                .withNodeIdentifier(((OrderedLeafSetNode<?>) original).getIdentifier()).build();
+    }
+
     @Override
     public Optional<ModificationApplyOperation> getChild(final YangInstanceIdentifier.PathArgument identifier) {
         if (identifier instanceof YangInstanceIdentifier.NodeWithValue) {
index 686258a7909ee2a9db52e847f534dbd54811cdc9..c69a170f246550b8a2f53ef5899ad47c7cc86ce0 100644 (file)
@@ -39,6 +39,13 @@ final class OrderedMapModificationStrategy extends AbstractNodeContainerModifica
         return ImmutableOrderedMapNodeBuilder.create((OrderedMapNode) original);
     }
 
+    @Override
+    protected NormalizedNode<?, ?> createEmptyValue(NormalizedNode<?, ?> original) {
+        checkArgument(original instanceof OrderedMapNode);
+        return ImmutableOrderedMapNodeBuilder.create().withNodeIdentifier(((OrderedMapNode) original).getIdentifier())
+                .build();
+    }
+
     @Override
     public Optional<ModificationApplyOperation> getChild(final YangInstanceIdentifier.PathArgument identifier) {
         if (identifier instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates) {
index 693ace4df2759b44f0b380ab70bd67d2bf6aa2e9..6fc704c053c32bb38212673580bbb88948955bba 100644 (file)
@@ -99,11 +99,21 @@ abstract class RootModificationApplyOperation extends ModificationApplyOperation
         getDelegate().verifyStructure(modification, verifyChildren);
     }
 
+    @Override
+    void recursivelyVerifyStructure(NormalizedNode<?, ?> value) {
+        getDelegate().recursivelyVerifyStructure(value);
+    }
+
     @Override
     final ChildTrackingPolicy getChildPolicy() {
         return getDelegate().getChildPolicy();
     }
 
+    @Override
+    final void mergeIntoModifiedNode(final ModifiedNode node, final NormalizedNode<?, ?> value, final Version version) {
+        getDelegate().mergeIntoModifiedNode(node, value, version);
+    }
+
     /**
      * Return the underlying delegate.
      *
index f07844c43515a2e2ac29b599757070b2747931f5..a511ddf064b719cb1c0f1320b6da2ec21b98e959 100644 (file)
@@ -16,7 +16,6 @@ import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.Augmentat
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.ConflictingModificationAppliedException;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataValidationFailedException;
-import org.opendaylight.yangtools.yang.data.api.schema.tree.IncorrectDataStructureException;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.ModificationType;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.TreeType;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.spi.TreeNode;
@@ -169,8 +168,8 @@ abstract class SchemaAwareApplyOperation extends ModificationApplyOperation {
             checkNotConflicting(path, original.get(), current.get());
         } else if(original.isPresent()) {
             throw new ConflictingModificationAppliedException(path,"Node was deleted by other transaction.");
-        } else if(current.isPresent()) {
-            throw new ConflictingModificationAppliedException(path,"Node was created by other transaction.");
+        } else if (current.isPresent()) {
+            throw new ConflictingModificationAppliedException(path, "Node was created by other transaction.");
         }
     }
 
@@ -203,7 +202,7 @@ abstract class SchemaAwareApplyOperation extends ModificationApplyOperation {
 
             // This is a slight optimization: a merge on a non-existing node equals to a write
             if (currentMeta.isPresent()) {
-                result = applyMerge(modification,currentMeta.get(), version);
+                result = applyMerge(modification, currentMeta.get(), version);
             } else {
                 modification.resolveModificationType(ModificationType.WRITE);
                 result = applyWrite(modification, currentMeta, version);
index f2eefd293c7ea9b2982adf1538f2f60f817cd715..be35b5aa673402019e2b2210365194577f223c42 100644 (file)
@@ -103,11 +103,21 @@ final class StructuralContainerModificationStrategy extends ModificationApplyOpe
         delegate.verifyStructure(modification, verifyChildren);
     }
 
+    @Override
+    void recursivelyVerifyStructure(NormalizedNode<?, ?> value) {
+        delegate.recursivelyVerifyStructure(value);
+    }
+
     @Override
     ChildTrackingPolicy getChildPolicy() {
         return delegate.getChildPolicy();
     }
 
+    @Override
+    void mergeIntoModifiedNode(ModifiedNode modification, NormalizedNode<?, ?> value, Version version) {
+        delegate.mergeIntoModifiedNode(modification, value, version);
+    }
+
     @Override
     public Optional<ModificationApplyOperation> getChild(final PathArgument child) {
         return delegate.getChild(child);
index 6439ed67cb20ab7078e40587523837a1363a1cd5..1e270900582951b8c8f4cc1dc7b46ce87f73d974 100644 (file)
@@ -27,4 +27,11 @@ final class UnkeyedListItemModificationStrategy extends AbstractDataNodeContaine
         checkArgument(original instanceof UnkeyedListEntryNode);
         return ImmutableUnkeyedListEntryNodeBuilder.create((UnkeyedListEntryNode) original);
     }
+
+    @Override
+    protected NormalizedNode<?, ?> createEmptyValue(NormalizedNode<?, ?> original) {
+        checkArgument(original instanceof UnkeyedListEntryNode);
+        return ImmutableUnkeyedListEntryNodeBuilder.create()
+                .withNodeIdentifier(((UnkeyedListEntryNode) original).getIdentifier()).build();
+    }
 }
\ No newline at end of file
index 32d73c9b61876aeeae38438c08a2eb71e1e87034..dacba5aefe3b295d08f1bd565be5be61f677b077 100644 (file)
@@ -14,7 +14,6 @@ import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgum
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.IncorrectDataStructureException;
-import org.opendaylight.yangtools.yang.data.api.schema.tree.ModificationType;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.TreeType;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.spi.MutableTreeNode;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.spi.TreeNode;
@@ -38,10 +37,8 @@ final class UnkeyedListModificationStrategy extends SchemaAwareApplyOperation {
     }
 
     @Override
-    protected TreeNode applyMerge(final ModifiedNode modification, final TreeNode currentMeta,
-            final Version version) {
-        modification.resolveModificationType(ModificationType.WRITE);
-        return applyWrite(modification, Optional.of(currentMeta), version);
+    protected TreeNode applyMerge(final ModifiedNode modification, final TreeNode currentMeta, final Version version) {
+        throw new IllegalStateException("Invalid merge into unkeyed list");
     }
 
     @Override
@@ -127,9 +124,20 @@ final class UnkeyedListModificationStrategy extends SchemaAwareApplyOperation {
 
     }
 
+    @Override
+    void recursivelyVerifyStructure(NormalizedNode<?, ?> value) {
+        // NOOP
+    }
+
     @Override
     protected void checkTouchApplicable(final YangInstanceIdentifier path, final NodeModification modification,
             final Optional<TreeNode> current) throws IncorrectDataStructureException {
         throw new IncorrectDataStructureException(path, "Subtree modification is not allowed.");
     }
+
+    @Override
+    void mergeIntoModifiedNode(final ModifiedNode node, final NormalizedNode<?, ?> value, final Version version) {
+        // Unkeyed lists are always replaced
+        node.write(value);
+    }
 }
index 8e001d0c7c3ab214f061c99c49bf5e9a3b3c0483..e52325ea79a7936b74f06f79ef78cab587609017 100644 (file)
@@ -34,6 +34,13 @@ final class UnorderedLeafSetModificationStrategy extends AbstractNodeContainerMo
         return ImmutableLeafSetNodeBuilder.create((LeafSetNode<?>) original);
     }
 
+    @Override
+    protected NormalizedNode<?, ?> createEmptyValue(NormalizedNode<?, ?> original) {
+        checkArgument(original instanceof LeafSetNode<?>);
+        return ImmutableLeafSetNodeBuilder.create().withNodeIdentifier(((LeafSetNode<?>) original).getIdentifier())
+                .build();
+    }
+
     @Override
     public Optional<ModificationApplyOperation> getChild(final YangInstanceIdentifier.PathArgument identifier) {
         if (identifier instanceof YangInstanceIdentifier.NodeWithValue) {
index a264e06f63bbe3242bc18951914aaf6c80823c80..601939bc9710910153a99daf3812040f60c1bf17 100644 (file)
@@ -34,6 +34,12 @@ final class UnorderedMapModificationStrategy extends AbstractNodeContainerModifi
         return ImmutableMapNodeBuilder.create((MapNode) original);
     }
 
+    @Override
+    protected NormalizedNode<?, ?> createEmptyValue(final NormalizedNode<?, ?> original) {
+        checkArgument(original instanceof MapNode);
+        return ImmutableMapNodeBuilder.create().withNodeIdentifier(((MapNode) original).getIdentifier()).build();
+    }
+
     @Override
     public Optional<ModificationApplyOperation> getChild(final YangInstanceIdentifier.PathArgument identifier) {
         if (identifier instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates) {
diff --git a/yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/Bug4295Test.java b/yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/Bug4295Test.java
new file mode 100644 (file)
index 0000000..9ac489c
--- /dev/null
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2015 Pantheon Technologies s.r.o. 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.yangtools.yang.data.impl.schema.tree;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMap.Builder;
+import java.io.File;
+import java.net.URI;
+import org.junit.Before;
+import org.junit.Test;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.QNameModule;
+import org.opendaylight.yangtools.yang.common.SimpleDateFormatUtil;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
+import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeModification;
+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.RetestUtils;
+import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
+import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.CollectionNodeBuilder;
+import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.DataContainerNodeAttrBuilder;
+import org.opendaylight.yangtools.yang.data.impl.schema.builder.impl.ImmutableContainerNodeBuilder;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+
+public class Bug4295Test {
+
+    private TipProducingDataTree inMemoryDataTree;
+    private SchemaContext context;
+    private QName root;
+    private QName subRoot;
+    private QName outerList;
+    private QName innerList;
+    private QName oId;
+    private QName iId;
+    private QName oLeaf;
+    private QName iLeaf;
+    private QNameModule foo;
+
+    @Before
+    public void init() throws Exception {
+        final File resourceFile = new File(Bug4295Test.class.getResource("/bug-4295/foo.yang")
+                .toURI());
+        context = RetestUtils.parseYangSources(resourceFile);
+        foo = QNameModule.create(new URI("foo"), SimpleDateFormatUtil.getRevisionFormat().parse("1970-01-01"));
+        root = QName.create(foo, "root");
+        subRoot = QName.create(foo, "sub-root");
+        outerList = QName.create(foo, "outer-list");
+        innerList = QName.create(foo, "inner-list");
+        oId = QName.create(foo, "o-id");
+        iId = QName.create(foo, "i-id");
+        oLeaf = QName.create(foo, "o");
+        iLeaf = QName.create(foo, "i");
+        inMemoryDataTree = InMemoryDataTreeFactory.getInstance().create(TreeType.OPERATIONAL);
+        inMemoryDataTree.setSchemaContext(context);
+    }
+
+    @Test
+    public void test() throws DataValidationFailedException {
+        firstModification();
+        secondModification(1);
+        secondModification(2);
+        secondModification(3);
+    }
+
+
+    private void firstModification() throws DataValidationFailedException {
+        /*  MERGE */
+        MapNode outerListNode = ImmutableNodes.mapNodeBuilder().withNodeIdentifier(NodeIdentifier.create(outerList))
+                .withChild(createOuterListEntry("1", "o-1"))
+                .withChild(createOuterListEntry("2", "o-2"))
+                .withChild(createOuterListEntry("3", "o-3"))
+                .build();
+        ContainerNode rootContainerNode = createRootContainerBuilder()
+        .withChild(
+                createSubRootContainerBuilder()
+                .withChild(outerListNode)
+                .build())
+        .build();
+        YangInstanceIdentifier path = YangInstanceIdentifier.of(root);
+        DataTreeModification modification = inMemoryDataTree.takeSnapshot().newModification();
+        modification.merge(path, rootContainerNode);
+
+        /*  WRITE INNER LIST WITH ENTRIES*/
+        MapNode innerListNode = createInnerListBuilder()
+            .withChild(createInnerListEntry("a", "i-a"))
+            .withChild(createInnerListEntry("b", "i-b"))
+            .build();
+        path = YangInstanceIdentifier.of(root).node(subRoot).node(outerList).node(createOuterListEntryPath("2")).node(innerList);
+        modification.write(path, innerListNode);
+
+        /*  COMMIT */
+        modification.ready();
+        inMemoryDataTree.validate(modification);
+        inMemoryDataTree.commit(inMemoryDataTree.prepare(modification));
+    }
+
+    private void secondModification(int testScenarioNumber) throws DataValidationFailedException {
+        /*  MERGE */
+        MapNode outerListNode = ImmutableNodes.mapNodeBuilder().withNodeIdentifier(NodeIdentifier.create(outerList))
+                .withChild(createOuterListEntry("3", "o-3"))
+                .withChild(createOuterListEntry("4", "o-4"))
+                .withChild(createOuterListEntry("5", "o-5"))
+                .build();
+
+        ContainerNode rootContainerNode = createRootContainerBuilder()
+        .withChild(
+                createSubRootContainerBuilder()
+                .withChild(outerListNode)
+                .build())
+        .build();
+
+        YangInstanceIdentifier path = YangInstanceIdentifier.of(root);
+        DataTreeModification modification = inMemoryDataTree.takeSnapshot().newModification();
+        modification.merge(path, rootContainerNode);
+
+        if (testScenarioNumber == 1) {
+            /* WRITE EMPTY INNER LIST */
+            writeEmptyInnerList(modification, "2");
+        } else if (testScenarioNumber == 2) {
+            /* WRITE INNER LIST ENTRY */
+            MapEntryNode innerListEntryA = createInnerListEntry("a", "i-a-2");
+            path = YangInstanceIdentifier.of(root).node(subRoot).node(outerList).node(createOuterListEntryPath("2"))
+                    .node(innerList).node(createInnerListEntryPath("a"));
+            modification.write(path, innerListEntryA);
+        } else if (testScenarioNumber == 3) {
+            /* WRITE INNER LIST WITH ENTRIES */
+            MapNode innerListNode = createInnerListBuilder().withChild(createInnerListEntry("a", "i-a-3"))
+                    .withChild(createInnerListEntry("c", "i-c")).build();
+            path = YangInstanceIdentifier.of(root).node(subRoot).node(outerList).node(createOuterListEntryPath("2"))
+                    .node(innerList);
+            modification.write(path, innerListNode);
+        }
+
+        /*  COMMIT */
+        modification.ready();
+        inMemoryDataTree.validate(modification);
+        inMemoryDataTree.commit(inMemoryDataTree.prepare(modification));
+    }
+
+    private void writeEmptyInnerList(DataTreeModification modification, String outerListEntryKey) {
+        YangInstanceIdentifier path = YangInstanceIdentifier.of(root).node(subRoot).node(outerList)
+                .node(createOuterListEntryPath(outerListEntryKey)).node(innerList);
+        modification.write(path, createInnerListBuilder().build());
+    }
+
+    private DataContainerNodeAttrBuilder<NodeIdentifier, ContainerNode> createRootContainerBuilder() {
+        return ImmutableContainerNodeBuilder.create().withNodeIdentifier(
+                new YangInstanceIdentifier.NodeIdentifier(root));
+    }
+
+    private DataContainerNodeAttrBuilder<NodeIdentifier, ContainerNode> createSubRootContainerBuilder() {
+        return ImmutableContainerNodeBuilder.create().withNodeIdentifier(
+                new YangInstanceIdentifier.NodeIdentifier(subRoot));
+    }
+
+    private CollectionNodeBuilder<MapEntryNode, MapNode> createInnerListBuilder() {
+        return ImmutableNodes.mapNodeBuilder().withNodeIdentifier(NodeIdentifier.create(innerList));
+    }
+
+    private NodeIdentifierWithPredicates createInnerListEntryPath(String keyValue) {
+        Builder<QName, Object> builder = ImmutableMap.builder();
+        ImmutableMap<QName, Object> keys = builder.put(iId, keyValue).build();
+        return new YangInstanceIdentifier.NodeIdentifierWithPredicates(innerList, keys);
+    }
+
+    private NodeIdentifierWithPredicates createOuterListEntryPath(String keyValue) {
+        Builder<QName, Object> builder = ImmutableMap.builder();
+        ImmutableMap<QName, Object> keys = builder.put(oId, keyValue).build();
+        return new YangInstanceIdentifier.NodeIdentifierWithPredicates(outerList, keys);
+    }
+
+    private MapEntryNode createOuterListEntry(String keyValue, String leafValue) {
+        return ImmutableNodes.mapEntryBuilder(outerList, oId, keyValue)
+                .withChild(ImmutableNodes.leafNode(oLeaf, leafValue)).build();
+    }
+
+    private MapEntryNode createInnerListEntry(String keyValue, String leafValue) {
+        return ImmutableNodes.mapEntryBuilder(innerList, iId, keyValue)
+                .withChild(ImmutableNodes.leafNode(iLeaf, leafValue)).build();
+    }
+}
\ No newline at end of file
index 92906c6e722154f578b906cc7456b6b4c55cc63d..1a835c85b30f5016c9ed92a263752d12bde20442 100644 (file)
@@ -328,7 +328,7 @@ public class Bug4454Test {
                 .node(mapEntryPath2).build();
 
         key.clear();
-        key.put(MIN_MAX_KEY_LEAF_QNAME, "bar");
+        key.put(MIN_MAX_KEY_LEAF_QNAME, "baz");
 
         mapEntryPath2 = new YangInstanceIdentifier.NodeIdentifierWithPredicates(MIN_MAX_LIST_QNAME, key);
 
diff --git a/yang/yang-data-impl/src/test/resources/bug-4295/foo.yang b/yang/yang-data-impl/src/test/resources/bug-4295/foo.yang
new file mode 100644 (file)
index 0000000..be6abf3
--- /dev/null
@@ -0,0 +1,27 @@
+module foo {
+    namespace "foo";
+    prefix foo;
+
+    container root {
+        container sub-root {
+            list outer-list {
+                key "o-id";
+                leaf o-id {
+                    type string;
+                }
+                list inner-list {
+                    key "i-id";
+                    leaf i-id {
+                        type string;
+                    }
+                    leaf i {
+                        type string;
+                    }
+                }
+                leaf o {
+                    type string;
+                }
+            }
+        }
+    }
+}