Bug 5968: Mandatory leaf enforcement does not work in some cases 80/47980/1
authorPeter Kajsa <pkajsa@cisco.com>
Thu, 28 Jul 2016 13:05:16 +0000 (15:05 +0200)
committerRobert Varga <nite@hq.sk>
Fri, 4 Nov 2016 10:05:29 +0000 (10:05 +0000)
When MapEntry nodes are directly written into datatree, mandatory leaf
enforcement works correctly. However, when a List, Container or any other
parent node containing MapEntry nodes as its children is written into datatree,
datatree does not check presence of mandatory nodes of in this way written
map entry nodes. This patch provides fix which ensures mandatory node validation
in all cases above.

In addition, full validation on unsealed modification was performed after each MERGE
operation performed on this modification, which may lead to undesirable results,
because unsealed modification does not have to contain all elements, which can be
added during next operations on this modification. This patch fixes this behavior of
MERGE operation and performs full validation during seal() method.

Change-Id: If653eeda5378cf1ed1418123eb33713ee80976d9
Signed-off-by: Peter Kajsa <pkajsa@cisco.com>
Signed-off-by: Robert Varga <rovarga@cisco.com>
Signed-off-by: Peter Kajsa <pkajsa@cisco.com>
(cherry picked from commit 5ec45595d85f7c42310707ea058dac8a6be62acb)

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/ListEntryModificationStrategy.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/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/Bug5968MergeTest.java [new file with mode: 0644]
yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/Bug5968Test.java [new file with mode: 0644]
yang/yang-data-impl/src/test/resources/bug5968/foo.yang [new file with mode: 0644]

index f908483b409a22cb54f0a0476c4b90873322316b..cf1b9edeaa2d22265353f44bbed7d63aefc4ae39 100644 (file)
@@ -18,10 +18,10 @@ 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.NormalizedNodeContainer;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.ConflictingModificationAppliedException;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeConfiguration;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataValidationFailedException;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.ModificationType;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.ModifiedNodeDoesNotExistException;
-import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeConfiguration;
 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;
@@ -34,18 +34,22 @@ abstract class AbstractNodeContainerModificationStrategy extends SchemaAwareAppl
     private final Class<? extends NormalizedNode<?, ?>> nodeClass;
     private final boolean verifyChildrenStructure;
 
-    protected AbstractNodeContainerModificationStrategy(final Class<? extends NormalizedNode<?, ?>> nodeClass,
+    AbstractNodeContainerModificationStrategy(final Class<? extends NormalizedNode<?, ?>> nodeClass,
             final DataTreeConfiguration treeConfig) {
         this.nodeClass = Preconditions.checkNotNull(nodeClass , "nodeClass");
         this.verifyChildrenStructure = (treeConfig.getTreeType() == TreeType.CONFIGURATION);
     }
 
+    final boolean verifyChildrenStructure() {
+        return verifyChildrenStructure;
+    }
+
     @SuppressWarnings("rawtypes")
     @Override
     void verifyStructure(final NormalizedNode<?, ?> writtenValue, final boolean verifyChildren) {
         checkArgument(nodeClass.isInstance(writtenValue), "Node %s is not of type %s", writtenValue, nodeClass);
         checkArgument(writtenValue instanceof NormalizedNodeContainer);
-        if (verifyChildrenStructure && verifyChildren) {
+        if (verifyChildren && verifyChildrenStructure) {
             final NormalizedNodeContainer container = (NormalizedNodeContainer) writtenValue;
             for (final Object child : container.getValue()) {
                 checkArgument(child instanceof NormalizedNode);
@@ -64,7 +68,7 @@ abstract class AbstractNodeContainerModificationStrategy extends SchemaAwareAppl
 
     @Override
     protected void recursivelyVerifyStructure(final NormalizedNode<?, ?> value) {
-        final NormalizedNodeContainer container = (NormalizedNodeContainer) value;
+        final NormalizedNodeContainer<?, ?, ?> container = (NormalizedNodeContainer<?, ?, ?>) value;
         for (final Object child : container.getValue()) {
             checkArgument(child instanceof NormalizedNode);
             final NormalizedNode<?, ?> castedChild = (NormalizedNode<?, ?>) child;
@@ -314,10 +318,6 @@ abstract class AbstractNodeContainerModificationStrategy extends SchemaAwareAppl
         }
     }
 
-    protected boolean verifyChildrenStructure() {
-        return verifyChildrenStructure;
-    }
-
     @SuppressWarnings("rawtypes")
     protected abstract NormalizedNodeContainerBuilder createBuilder(NormalizedNode<?, ?> original);
 
index 599ef01ae68e26a031e2649814202789b03c37f7..d77f63916731d69fe3bf4596d18ec0e35ee34533 100644 (file)
@@ -27,6 +27,14 @@ final class ListEntryModificationStrategy extends AbstractDataNodeContainerModif
         enforcer = MandatoryLeafEnforcer.forContainer(schema, treeConfig);
     }
 
+    @Override
+    void verifyStructure(final NormalizedNode<?, ?> writtenValue, final boolean verifyChildren) {
+        if (verifyChildren && verifyChildrenStructure()) {
+            enforcer.enforceOnTreeNode(writtenValue);
+        }
+        super.verifyStructure(writtenValue, verifyChildren);
+    }
+
     @Override
     protected TreeNode applyMerge(final ModifiedNode modification, final TreeNode currentMeta, final Version version) {
         final TreeNode ret = super.applyMerge(modification, currentMeta, version);
index 35da5d3650a4c87987b46679acb1625461e5c880..e1757a0a7441a450d7e35bb73147eff0f03effc9 100644 (file)
@@ -275,6 +275,7 @@ final class ModifiedNode extends NodeModification implements StoreTreeNode<Modif
                 if (children.isEmpty()) {
                     updateOperationType(LogicalOperation.NONE);
                 }
+
                 break;
             case WRITE:
                 // A WRITE can collapse all of its children
@@ -285,6 +286,18 @@ final class ModifiedNode extends NodeModification implements StoreTreeNode<Modif
 
                 schema.verifyStructure(value, true);
                 break;
+            /*
+             * Perform full validation in case of merge operation. This validation is performed during sealing of
+             * a ModifiedNode when we run InMemoryDataTreeModification.ready() just as it is in case of write operation
+             * above.
+             *
+             * Some parts of this validation may also be re-done during InMemoryDataTree.prepare() in case when we
+             * merge or write a MapEntry directly (e.g. Bug5968MergeTest.mergeValidMapEntryTest()), however in other
+             * cases full validation is performed only once just here.
+             */
+            case MERGE:
+                schema.verifyStructure(schema.apply(this, getOriginal(), version).get().getData(), true);
+                break;
             default:
                 break;
         }
index 72221770cd0393c06223900b4a4313525bf2ac25..87c196b8b9fde0fd353a65498d3ab88f517b2091 100644 (file)
@@ -38,8 +38,10 @@ 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.
+         *
+         * Fast validation of structure, full validation on data will be run during seal.
          */
-        applyOperation.verifyStructure(data, true);
+        applyOperation.verifyStructure(data, false);
         applyOperation.mergeIntoModifiedNode(modification, data, version);
     }
 
diff --git a/yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/Bug5968MergeTest.java b/yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/Bug5968MergeTest.java
new file mode 100644 (file)
index 0000000..05e9108
--- /dev/null
@@ -0,0 +1,476 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.data.impl.schema.tree;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.Before;
+import org.junit.Ignore;
+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.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.DataTreeCandidate;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeConfiguration;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataValidationFailedException;
+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.builder.api.CollectionNodeBuilder;
+import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.DataContainerNodeAttrBuilder;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.parser.spi.meta.ReactorException;
+
+public class Bug5968MergeTest {
+    private static final String NS = "foo";
+    private static final String REV = "2016-07-28";
+    private static final QName ROOT = QName.create(NS, REV, "root");
+    private static final QName MY_LIST = QName.create(NS, REV, "my-list");
+    private static final QName LIST_ID = QName.create(NS, REV, "list-id");
+    private static final QName MANDATORY_LEAF = QName.create(NS, REV, "mandatory-leaf");
+    private static final QName COMMON_LEAF = QName.create(NS, REV, "common-leaf");
+    private SchemaContext schemaContext;
+
+    @Before
+    public void init() throws ReactorException {
+        this.schemaContext = TestModel.createTestContext("/bug5968/foo.yang");
+        assertNotNull("Schema context must not be null.", this.schemaContext);
+    }
+
+    private static InMemoryDataTree initDataTree(final SchemaContext schemaContext, final boolean withMapNode)
+            throws DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = (InMemoryDataTree) InMemoryDataTreeFactory.getInstance().create(
+                DataTreeConfiguration.DEFAULT_CONFIGURATION);
+        inMemoryDataTree.setSchemaContext(schemaContext);
+
+        final DataContainerNodeAttrBuilder<NodeIdentifier, ContainerNode> root = Builders.containerBuilder()
+                .withNodeIdentifier(new NodeIdentifier(ROOT));
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+        modificationTree.merge(
+                YangInstanceIdentifier.of(ROOT),
+                withMapNode ? root.withChild(
+                        Builders.mapBuilder().withNodeIdentifier(new NodeIdentifier(MY_LIST)).build()).build() : root
+                        .build());
+        modificationTree.ready();
+
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+
+        return inMemoryDataTree;
+    }
+
+    private static InMemoryDataTree emptyDataTree(final SchemaContext schemaContext)
+            throws DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = (InMemoryDataTree) InMemoryDataTreeFactory.getInstance().create(
+                DataTreeConfiguration.DEFAULT_CONFIGURATION);
+        inMemoryDataTree.setSchemaContext(schemaContext);
+
+        return inMemoryDataTree;
+    }
+
+    @Test
+    public void mergeInvalidContainerTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = emptyDataTree(schemaContext);
+
+        final MapNode myList = createMap(true);
+        final DataContainerNodeAttrBuilder<NodeIdentifier, ContainerNode> root = Builders.containerBuilder()
+                .withNodeIdentifier(new NodeIdentifier(ROOT)).withChild(myList);
+
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+        modificationTree.merge(YangInstanceIdentifier.of(ROOT), root.build());
+
+        try {
+            modificationTree.ready();
+            inMemoryDataTree.validate(modificationTree);
+            final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+            inMemoryDataTree.commit(prepare);
+            fail("Should fail due to missing mandatory leaf.");
+        } catch (final IllegalArgumentException e) {
+            assertEquals(
+                    "Node (foo?revision=2016-07-28)my-list[{(foo?revision=2016-07-28)list-id=1}] is missing mandatory "
+                            + "descendant /(foo?revision=2016-07-28)mandatory-leaf", e.getMessage());
+        }
+    }
+
+    @Test
+    public void mergeInvalidMapTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = emptyDataTree(schemaContext);
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+        mergeMap(modificationTree, true);
+
+        try {
+            modificationTree.ready();
+            inMemoryDataTree.validate(modificationTree);
+            final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+            inMemoryDataTree.commit(prepare);
+            fail("Should fail due to missing mandatory leaf.");
+        } catch (final IllegalArgumentException e) {
+            assertEquals(
+                    "Node (foo?revision=2016-07-28)my-list[{(foo?revision=2016-07-28)list-id=1}] is missing mandatory "
+                            + "descendant /(foo?revision=2016-07-28)mandatory-leaf", e.getMessage());
+        }
+    }
+
+    @Test
+    public void mergeInvalidMapEntryTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = initDataTree(schemaContext, true);
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+
+        mergeMapEntry(modificationTree, "1", null, "common-value");
+
+        try {
+            modificationTree.ready();
+            inMemoryDataTree.validate(modificationTree);
+            final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+            inMemoryDataTree.commit(prepare);
+            fail("Should fail due to missing mandatory leaf.");
+        } catch (final IllegalArgumentException e) {
+            assertEquals(
+                    "Node (foo?revision=2016-07-28)my-list[{(foo?revision=2016-07-28)list-id=1}] is missing mandatory "
+                            + "descendant /(foo?revision=2016-07-28)mandatory-leaf", e.getMessage());
+        }
+    }
+
+    private static void mergeMap(final InMemoryDataTreeModification modificationTree, final boolean mandatoryDataMissing)
+            throws DataValidationFailedException {
+        final MapNode myList = createMap(mandatoryDataMissing);
+        modificationTree.merge(YangInstanceIdentifier.of(ROOT).node(MY_LIST), myList);
+    }
+
+    private static MapNode createMap(final boolean mandatoryDataMissing) throws DataValidationFailedException {
+        return Builders
+                .mapBuilder()
+                .withNodeIdentifier(new NodeIdentifier(MY_LIST))
+                .withChild(
+                        mandatoryDataMissing ? createMapEntry("1", "common-value") : createMapEntry("1",
+                                "mandatory-value", "common-value")).build();
+    }
+
+    private static void mergeMapEntry(final InMemoryDataTreeModification modificationTree, final Object listIdValue,
+            final Object mandatoryLeafValue, final Object commonLeafValue) throws DataValidationFailedException {
+        final MapEntryNode taskEntryNode = mandatoryLeafValue == null ? createMapEntry(listIdValue, commonLeafValue)
+                : createMapEntry(listIdValue, mandatoryLeafValue, commonLeafValue);
+
+        modificationTree.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, listIdValue))),
+                taskEntryNode);
+    }
+
+    private static MapEntryNode createMapEntry(final Object listIdValue, final Object mandatoryLeafValue,
+            final Object commonLeafValue) throws DataValidationFailedException {
+        return Builders.mapEntryBuilder()
+                .withNodeIdentifier(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, listIdValue)))
+                .withChild(ImmutableNodes.leafNode(LIST_ID, listIdValue))
+                .withChild(ImmutableNodes.leafNode(MANDATORY_LEAF, mandatoryLeafValue))
+                .withChild(ImmutableNodes.leafNode(COMMON_LEAF, commonLeafValue)).build();
+    }
+
+    private static MapEntryNode createMapEntry(final Object listIdValue, final Object commonLeafValue)
+            throws DataValidationFailedException {
+        return Builders.mapEntryBuilder()
+                .withNodeIdentifier(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, listIdValue)))
+                .withChild(ImmutableNodes.leafNode(LIST_ID, listIdValue))
+                .withChild(ImmutableNodes.leafNode(COMMON_LEAF, commonLeafValue)).build();
+    }
+
+    private static MapEntryNode createMapEntryM(final Object listIdValue, final Object mandatoryLeafValue)
+            throws DataValidationFailedException {
+        return Builders.mapEntryBuilder()
+                .withNodeIdentifier(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, listIdValue)))
+                .withChild(ImmutableNodes.leafNode(LIST_ID, listIdValue))
+                .withChild(ImmutableNodes.leafNode(MANDATORY_LEAF, mandatoryLeafValue)).build();
+    }
+
+    @Test
+    public void mergeValidContainerTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = emptyDataTree(schemaContext);
+
+        final MapNode myList = createMap(false);
+        final DataContainerNodeAttrBuilder<NodeIdentifier, ContainerNode> root = Builders.containerBuilder()
+                .withNodeIdentifier(new NodeIdentifier(ROOT)).withChild(myList);
+
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+        modificationTree.merge(YangInstanceIdentifier.of(ROOT), root.build());
+        modificationTree.ready();
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+    }
+
+    @Test
+    public void mergeValidMapTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = emptyDataTree(schemaContext);
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+        mergeMap(modificationTree, false);
+
+        modificationTree.ready();
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+    }
+
+    @Test
+    public void mergeValidMapEntryTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = initDataTree(schemaContext, true);
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+
+        mergeMapEntry(modificationTree, "1", "mandatory-value", "common-value");
+
+        modificationTree.ready();
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+    }
+
+    @Test
+    public void validMultiStepsMergeTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = emptyDataTree(schemaContext);
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+
+        modificationTree.merge(YangInstanceIdentifier.of(ROOT), createContainerBuilder().build());
+        modificationTree.merge(YangInstanceIdentifier.of(ROOT).node(MY_LIST), createMapBuilder().build());
+        modificationTree.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "1"))),
+                createEmptyMapEntryBuilder("1").build());
+        modificationTree.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "1"))),
+                createMapEntry("1", "mandatory-value", "common-value"));
+
+        modificationTree.ready();
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+    }
+
+    @Test
+    public void invalidMultiStepsMergeTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = emptyDataTree(schemaContext);
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+
+        modificationTree.merge(YangInstanceIdentifier.of(ROOT), createContainerBuilder().build());
+        modificationTree.merge(YangInstanceIdentifier.of(ROOT).node(MY_LIST), createMapBuilder().build());
+        modificationTree.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "1"))),
+                createEmptyMapEntryBuilder("1").build());
+        modificationTree.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "1"))),
+                createMapEntry("1", "common-value"));
+
+        try {
+            modificationTree.ready();
+            inMemoryDataTree.validate(modificationTree);
+            final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+            inMemoryDataTree.commit(prepare);
+            fail("Should fail due to missing mandatory leaf.");
+        } catch (final IllegalArgumentException e) {
+            assertEquals(
+                    "Node (foo?revision=2016-07-28)my-list[{(foo?revision=2016-07-28)list-id=1}] is missing mandatory "
+                            + "descendant /(foo?revision=2016-07-28)mandatory-leaf", e.getMessage());
+        }
+    }
+
+    private static DataContainerNodeAttrBuilder<NodeIdentifierWithPredicates, MapEntryNode> createEmptyMapEntryBuilder(
+            final Object listIdValue) throws DataValidationFailedException {
+        return Builders.mapEntryBuilder()
+                .withNodeIdentifier(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, listIdValue)))
+                .withChild(ImmutableNodes.leafNode(LIST_ID, listIdValue));
+    }
+
+    private static CollectionNodeBuilder<MapEntryNode, MapNode> createMapBuilder() throws DataValidationFailedException {
+        return Builders.mapBuilder().withNodeIdentifier(new NodeIdentifier(MY_LIST));
+    }
+
+    private static DataContainerNodeAttrBuilder<NodeIdentifier, ContainerNode> createContainerBuilder()
+            throws DataValidationFailedException {
+        return Builders.containerBuilder().withNodeIdentifier(new NodeIdentifier(ROOT));
+    }
+
+    @Test
+    public void validMultiStepsWriteAndMergeTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = emptyDataTree(schemaContext);
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+
+        modificationTree.write(YangInstanceIdentifier.of(ROOT), createContainerBuilder().build());
+        modificationTree.merge(YangInstanceIdentifier.of(ROOT).node(MY_LIST), createMapBuilder().build());
+        modificationTree.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "1"))),
+                createEmptyMapEntryBuilder("1").build());
+        modificationTree.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "1"))),
+                createMapEntry("1", "mandatory-value", "common-value"));
+
+        modificationTree.ready();
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+    }
+
+    @Test
+    public void invalidMultiStepsWriteAndMergeTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = emptyDataTree(schemaContext);
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+
+        modificationTree.write(YangInstanceIdentifier.of(ROOT), createContainerBuilder().build());
+        modificationTree.merge(YangInstanceIdentifier.of(ROOT).node(MY_LIST), createMapBuilder().build());
+        modificationTree.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "1"))),
+                createEmptyMapEntryBuilder("1").build());
+        modificationTree.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "1"))),
+                createMapEntry("1", "common-value"));
+
+        try {
+            modificationTree.ready();
+            inMemoryDataTree.validate(modificationTree);
+            final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+            inMemoryDataTree.commit(prepare);
+            fail("Should fail due to missing mandatory leaf.");
+        } catch (final IllegalArgumentException e) {
+            assertEquals(
+                    "Node (foo?revision=2016-07-28)my-list[{(foo?revision=2016-07-28)list-id=1}] is missing mandatory "
+                            + "descendant /(foo?revision=2016-07-28)mandatory-leaf", e.getMessage());
+        }
+    }
+
+    @Test
+    public void validMapEntryMultiCommitMergeTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = emptyDataTree(schemaContext);
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+
+        modificationTree.write(YangInstanceIdentifier.of(ROOT), createContainerBuilder().build());
+        modificationTree.merge(YangInstanceIdentifier.of(ROOT).node(MY_LIST), createMapBuilder().build());
+        modificationTree.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "1"))),
+                createEmptyMapEntryBuilder("1").build());
+        modificationTree.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "1"))),
+                createMapEntryM("1", "mandatory-value"));
+
+        modificationTree.ready();
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+
+        final InMemoryDataTreeModification modificationTree2 = inMemoryDataTree.takeSnapshot().newModification();
+        modificationTree2.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "1"))),
+                createMapEntry("1", "common-value"));
+        modificationTree2.ready();
+        inMemoryDataTree.validate(modificationTree2);
+        final DataTreeCandidate prepare2 = inMemoryDataTree.prepare(modificationTree2);
+        inMemoryDataTree.commit(prepare2);
+    }
+
+    @Test
+    public void invalidMapEntryMultiCommitMergeTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = emptyDataTree(schemaContext);
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+
+        modificationTree.write(YangInstanceIdentifier.of(ROOT), createContainerBuilder().build());
+        modificationTree.merge(YangInstanceIdentifier.of(ROOT).node(MY_LIST), createMapBuilder().build());
+        modificationTree.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "1"))),
+                createEmptyMapEntryBuilder("1").build());
+        modificationTree.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "1"))),
+                createMapEntryM("1", "mandatory-value"));
+
+        modificationTree.ready();
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+
+        final InMemoryDataTreeModification modificationTree2 = inMemoryDataTree.takeSnapshot().newModification();
+        modificationTree2.write(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "1"))),
+                createMapEntry("1", "common-value"));
+        modificationTree2.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "1"))),
+                createMapEntryM("1", "mandatory-value"));
+        modificationTree2.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "2"))),
+                createMapEntry("2", "common-value"));
+        try {
+            modificationTree2.ready();
+            inMemoryDataTree.validate(modificationTree2);
+            final DataTreeCandidate prepare2 = inMemoryDataTree.prepare(modificationTree2);
+            inMemoryDataTree.commit(prepare2);
+            fail("Should fail due to missing mandatory leaf.");
+        } catch (final IllegalArgumentException e) {
+            assertEquals(
+                    "Node (foo?revision=2016-07-28)my-list[{(foo?revision=2016-07-28)list-id=2}] is missing mandatory "
+                            + "descendant /(foo?revision=2016-07-28)mandatory-leaf", e.getMessage());
+        }
+    }
+
+    @Ignore
+    @Test
+    /*
+     * FIXME: This test consists of two transactions (i.e. data tree modifications) on empty data tree.
+     *        The first one writes mandatory data and second one writes common data without any mandatory data.
+     *        Is it correct to rely on other transactions that they fill mandatory data before our transaction is
+     *        commited ?
+     */
+    public void validMapEntryMultiCommitMergeTest2() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = emptyDataTree(schemaContext);
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+        final InMemoryDataTreeModification modificationTree2 = inMemoryDataTree.takeSnapshot().newModification();
+
+        modificationTree.write(YangInstanceIdentifier.of(ROOT), createContainerBuilder().build());
+        modificationTree.merge(YangInstanceIdentifier.of(ROOT).node(MY_LIST), createMapBuilder().build());
+        modificationTree.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "1"))),
+                createEmptyMapEntryBuilder("1").build());
+        modificationTree.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "1"))),
+                createMapEntryM("1", "mandatory-value"));
+
+        modificationTree.ready();
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+        System.out.println(inMemoryDataTree);
+
+        modificationTree2.merge(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, "1"))),
+                createMapEntry("1", "common-value"));
+        modificationTree2.ready();
+        inMemoryDataTree.validate(modificationTree2);
+        final DataTreeCandidate prepare2 = inMemoryDataTree.prepare(modificationTree2);
+        inMemoryDataTree.commit(prepare2);
+
+        System.out.println(inMemoryDataTree);
+    }
+}
diff --git a/yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/Bug5968Test.java b/yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/Bug5968Test.java
new file mode 100644 (file)
index 0000000..523d7d2
--- /dev/null
@@ -0,0 +1,227 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.data.impl.schema.tree;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableMap;
+import org.junit.Before;
+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.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.DataTreeCandidate;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeConfiguration;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataValidationFailedException;
+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.builder.api.DataContainerNodeAttrBuilder;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.parser.spi.meta.ReactorException;
+
+public class Bug5968Test {
+    private static final String NS = "foo";
+    private static final String REV = "2016-07-28";
+    private static final QName ROOT = QName.create(NS, REV, "root");
+    private static final QName MY_LIST = QName.create(NS, REV, "my-list");
+    private static final QName LIST_ID = QName.create(NS, REV, "list-id");
+    private static final QName MANDATORY_LEAF = QName.create(NS, REV, "mandatory-leaf");
+    private static final QName COMMON_LEAF = QName.create(NS, REV, "common-leaf");
+    private SchemaContext schemaContext;
+
+    @Before
+    public void init() throws ReactorException {
+        this.schemaContext = TestModel.createTestContext("/bug5968/foo.yang");
+        assertNotNull("Schema context must not be null.", this.schemaContext);
+    }
+
+    private static InMemoryDataTree initDataTree(final SchemaContext schemaContext, final boolean withMapNode)
+            throws DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = (InMemoryDataTree) InMemoryDataTreeFactory.getInstance().create(
+                DataTreeConfiguration.DEFAULT_CONFIGURATION);
+        inMemoryDataTree.setSchemaContext(schemaContext);
+
+        final DataContainerNodeAttrBuilder<NodeIdentifier, ContainerNode> root = Builders.containerBuilder()
+                .withNodeIdentifier(new NodeIdentifier(ROOT));
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+        modificationTree.write(
+                YangInstanceIdentifier.of(ROOT),
+                withMapNode ? root.withChild(
+                        Builders.mapBuilder().withNodeIdentifier(new NodeIdentifier(MY_LIST)).build()).build() : root
+                        .build());
+        modificationTree.ready();
+
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+
+        return inMemoryDataTree;
+    }
+
+    private static InMemoryDataTree emptyDataTree(final SchemaContext schemaContext)
+            throws DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = (InMemoryDataTree) InMemoryDataTreeFactory.getInstance().create(
+                DataTreeConfiguration.DEFAULT_CONFIGURATION);
+        inMemoryDataTree.setSchemaContext(schemaContext);
+
+        return inMemoryDataTree;
+    }
+
+    @Test
+    public void writeInvalidContainerTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = emptyDataTree(schemaContext);
+
+        final MapNode myList = createMap(true);
+        final DataContainerNodeAttrBuilder<NodeIdentifier, ContainerNode> root = Builders.containerBuilder()
+                .withNodeIdentifier(new NodeIdentifier(ROOT)).withChild(myList);
+
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+        modificationTree.write(YangInstanceIdentifier.of(ROOT), root.build());
+
+        try {
+            modificationTree.ready();
+            inMemoryDataTree.validate(modificationTree);
+            final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+            inMemoryDataTree.commit(prepare);
+            fail("Should fail due to missing mandatory leaf.");
+        } catch (final IllegalArgumentException e) {
+            assertEquals(
+                    "Node (foo?revision=2016-07-28)my-list[{(foo?revision=2016-07-28)list-id=1}] is missing mandatory "
+                            + "descendant /(foo?revision=2016-07-28)mandatory-leaf", e.getMessage());
+        }
+    }
+
+    @Test
+    public void writeInvalidMapTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = emptyDataTree(schemaContext);
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+        writeMap(modificationTree, true);
+
+        try {
+            modificationTree.ready();
+            inMemoryDataTree.validate(modificationTree);
+            final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+            inMemoryDataTree.commit(prepare);
+            fail("Should fail due to missing mandatory leaf.");
+        } catch (final IllegalArgumentException e) {
+            assertEquals(
+                    "Node (foo?revision=2016-07-28)my-list[{(foo?revision=2016-07-28)list-id=1}] is missing mandatory "
+                            + "descendant /(foo?revision=2016-07-28)mandatory-leaf", e.getMessage());
+        }
+    }
+
+    @Test
+    public void writeInvalidMapEntryTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = initDataTree(schemaContext, true);
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+
+        writeMapEntry(modificationTree, "1", null, "common-value");
+
+        try {
+            modificationTree.ready();
+            inMemoryDataTree.validate(modificationTree);
+            final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+            inMemoryDataTree.commit(prepare);
+            fail("Should fail due to missing mandatory leaf.");
+        } catch (final IllegalArgumentException e) {
+            assertEquals(
+                    "Node (foo?revision=2016-07-28)my-list[{(foo?revision=2016-07-28)list-id=1}] is missing mandatory "
+                            + "descendant /(foo?revision=2016-07-28)mandatory-leaf", e.getMessage());
+        }
+    }
+
+    private static void writeMap(final InMemoryDataTreeModification modificationTree, final boolean mandatoryDataMissing)
+            throws DataValidationFailedException {
+        final MapNode myList = createMap(mandatoryDataMissing);
+        modificationTree.write(YangInstanceIdentifier.of(ROOT).node(MY_LIST), myList);
+    }
+
+    private static MapNode createMap(final boolean mandatoryDataMissing) throws DataValidationFailedException {
+        return Builders
+                .mapBuilder()
+                .withNodeIdentifier(new NodeIdentifier(MY_LIST))
+                .withChild(
+                        mandatoryDataMissing ? createMapEntry("1", "common-value") : createMapEntry("1",
+                                "mandatory-value", "common-value")).build();
+    }
+
+    private static void writeMapEntry(final InMemoryDataTreeModification modificationTree, final Object listIdValue,
+            final Object mandatoryLeafValue, final Object commonLeafValue) throws DataValidationFailedException {
+        final MapEntryNode taskEntryNode = mandatoryLeafValue == null ? createMapEntry(listIdValue, commonLeafValue)
+                : createMapEntry(listIdValue, mandatoryLeafValue, commonLeafValue);
+
+        modificationTree.write(
+                YangInstanceIdentifier.of(ROOT).node(MY_LIST)
+                        .node(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, listIdValue))),
+                taskEntryNode);
+    }
+
+    private static MapEntryNode createMapEntry(final Object listIdValue, final Object mandatoryLeafValue,
+            final Object commonLeafValue) throws DataValidationFailedException {
+        return Builders.mapEntryBuilder()
+                .withNodeIdentifier(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, listIdValue)))
+                .withChild(ImmutableNodes.leafNode(LIST_ID, listIdValue))
+                .withChild(ImmutableNodes.leafNode(MANDATORY_LEAF, mandatoryLeafValue))
+                .withChild(ImmutableNodes.leafNode(COMMON_LEAF, commonLeafValue)).build();
+    }
+
+    private static MapEntryNode createMapEntry(final Object listIdValue, final Object commonLeafValue)
+            throws DataValidationFailedException {
+        return Builders.mapEntryBuilder()
+                .withNodeIdentifier(new NodeIdentifierWithPredicates(MY_LIST, ImmutableMap.of(LIST_ID, listIdValue)))
+                .withChild(ImmutableNodes.leafNode(LIST_ID, listIdValue))
+                .withChild(ImmutableNodes.leafNode(COMMON_LEAF, commonLeafValue)).build();
+    }
+
+    @Test
+    public void writeValidContainerTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = emptyDataTree(schemaContext);
+
+        final MapNode myList = createMap(false);
+        final DataContainerNodeAttrBuilder<NodeIdentifier, ContainerNode> root = Builders.containerBuilder()
+                .withNodeIdentifier(new NodeIdentifier(ROOT)).withChild(myList);
+
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+        modificationTree.write(YangInstanceIdentifier.of(ROOT), root.build());
+        modificationTree.ready();
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+    }
+
+    @Test
+    public void writeValidMapTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = emptyDataTree(schemaContext);
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+        writeMap(modificationTree, false);
+
+        modificationTree.ready();
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+    }
+
+    @Test
+    public void writeValidMapEntryTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = initDataTree(schemaContext, true);
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+
+        writeMapEntry(modificationTree, "1", "mandatory-value", "common-value");
+
+        modificationTree.ready();
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+    }
+}
diff --git a/yang/yang-data-impl/src/test/resources/bug5968/foo.yang b/yang/yang-data-impl/src/test/resources/bug5968/foo.yang
new file mode 100644 (file)
index 0000000..a3bdec4
--- /dev/null
@@ -0,0 +1,25 @@
+module foo {
+    yang-version 1;
+    namespace "foo";
+    prefix foo;
+
+    revision 2016-07-28 {
+        description "test";
+    }
+
+    container root {
+        list my-list {
+            key "list-id";
+            leaf list-id {
+                type string;
+            }
+            leaf mandatory-leaf {
+                type string;
+                mandatory true;
+            }
+            leaf common-leaf {
+                type string;
+            }
+        }
+    }
+}