MDSAL-298: properly handle unkeyed lists 73/67673/4
authorRobert Varga <robert.varga@pantheon.tech>
Mon, 29 Jan 2018 13:18:05 +0000 (14:18 +0100)
committerRobert Varga <nite@hq.sk>
Sun, 25 Feb 2018 18:52:33 +0000 (18:52 +0000)
Unkeyed lists are not representable in binding, which means they
cannot be reported as modified children, either. This has implications
for any data change which contains unrepresentable fields as child
modifications.

Previously we would report SUBTREE_MODIFIED for the container node,
but would fail to report any children, which is obviously wrong, as the
user is left guessing as to what exactly happened.

This patch modifies LazyDataObjectModification to report a WRITE event
if modifications to unrepresentable children are found in SUBTREE_MODIFIED
case. Since this is a potentially expensive operation, we cache a child
addressability summary in BindingCodecTreeNode, so that we go to this
slow path only when needed.

We also expose BindingStructuralType enumeration, so controller's
sal-binding-broker can reuse the implementation rather than having its
own copy.

Change-Id: I6642166cd262d0dddb1b2ed6d73a20785d0efff6
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
(cherry picked from commit 972489522808ad9777d4c190a2a0c3896ed0fca4)
(cherry picked from commit 076dcd04523bd003935796c3795ea4f81dfa8898)

binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/BindingStructuralType.java
binding/mdsal-binding-dom-adapter/src/main/java/org/opendaylight/mdsal/binding/dom/adapter/LazyDataObjectModification.java
binding/mdsal-binding-dom-adapter/src/test/java/org/opendaylight/mdsal/binding/dom/adapter/test/Mdsal298Test.java [new file with mode: 0644]
binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/api/BindingCodecTreeNode.java
binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/yangtools/binding/data/codec/impl/DataContainerCodecContext.java
binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/yangtools/binding/data/codec/impl/DataContainerCodecPrototype.java
binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/yangtools/binding/data/codec/impl/LeafNodeCodecContext.java
binding/mdsal-binding-test-model/src/main/yang/opendaylight-mdsal298.yang [new file with mode: 0644]

index f6577a5e5923cdba57b93210e24f3d662fb9efa0..d925a5119984b90acd6ac576dccafcab2e678a68 100644 (file)
@@ -7,6 +7,7 @@
  */
 package org.opendaylight.mdsal.binding.dom.adapter;
 
+import com.google.common.annotations.Beta;
 import com.google.common.base.Optional;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
@@ -22,71 +23,53 @@ import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListNode;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateNode;
 
 /**
- * Defines structural mapping of Normalized Node to Binding data
- * addressable by Instance Identifier.
+ * Defines structural mapping of Normalized Node to Binding data addressable by Instance Identifier. Not all binding
+ * data are addressable by instance identifier and there are some differences.
  *
  * <p>
- * Not all binding data are addressable by instance identifier
- * and there are some differences.
+ * See {@link #NOT_ADDRESSABLE},{@link #INVISIBLE_CONTAINER},{@link #VISIBLE_CONTAINER} for more details.
  *
  * <p>
- * See {@link #NOT_ADDRESSABLE},{@link #INVISIBLE_CONTAINER},{@link #VISIBLE_CONTAINER}
- * for more details.
+ * NOTE: this class is exposed for migration purposes only, no new users outside of its package should be introduced.
  */
-enum BindingStructuralType {
-
+@Beta
+public enum BindingStructuralType {
     /**
-     * DOM Item is not addressable in Binding Instance Identifier,
-     * data is not lost, but are available only via parent object.
-     *
-     * <p>
-     * Such types of data are leaf-lists, leafs, list without keys
-     * or anyxml.
-     *
+     * DOM Item is not addressable in Binding InstanceIdentifier, data is not lost, but are available only via parent
+     * object. Such types of data are leaf-lists, leafs, list without keys or anyxml.
      */
     NOT_ADDRESSABLE,
     /**
-     * Data container is addressable in NormalizedNode format,
-     * but in Binding it is not represented in Instance Identifier.
+     * Data container is addressable in NormalizedNode format, but in Binding it is not represented in
+     * InstanceIdentifier. These are choice / case nodes.
      *
      * <p>
-     * This are choice / case nodes.
-     *
-     * <p>
-     * This data is still accessible using parent object and their
-     * children are addressable.
-     *
+     * This data is still accessible using parent object and their children are addressable.
      */
     INVISIBLE_CONTAINER,
     /**
-     * Data container is addressable in NormalizedNode format,
-     * but in Binding it is not represented in Instance Identifier.
-     *
-     * <p>
-     * This are list nodes.
+     * Data container is addressable in NormalizedNode format, but in Binding it is not represented in
+     * InstanceIdentifier. These are list nodes.
      *
      * <p>
-     * This data is still accessible using parent object and their
-     * children are addressable.
-     *
+     * This data is still accessible using parent object and their children are addressable.
      */
     INVISIBLE_LIST,
     /**
-     * Data container is addressable in Binding Instance Identifier format
-     * and also YangInstanceIdentifier format.
-     *
+     * Data container is addressable in Binding InstanceIdentifier format and also YangInstanceIdentifier format.
      */
     VISIBLE_CONTAINER,
     /**
-     * Mapping algorithm was unable to detect type or was not updated after introduction
-     * of new NormalizedNode type.
+     * Mapping algorithm was unable to detect type or was not updated after introduction of new NormalizedNode type.
      */
     UNKNOWN;
 
-    static BindingStructuralType from(final DataTreeCandidateNode domChildNode) {
+    public static BindingStructuralType from(final DataTreeCandidateNode domChildNode) {
         final Optional<NormalizedNode<?, ?>> dataBased = domChildNode.getDataAfter().or(domChildNode.getDataBefore());
         if (dataBased.isPresent()) {
             return from(dataBased.get());
@@ -120,6 +103,35 @@ enum BindingStructuralType {
         return UNKNOWN;
     }
 
+    public static BindingStructuralType recursiveFrom(final DataTreeCandidateNode node) {
+        final BindingStructuralType type = BindingStructuralType.from(node);
+        switch (type) {
+            case INVISIBLE_CONTAINER:
+            case INVISIBLE_LIST:
+                // This node is invisible, try to resolve using a child node
+                for (final DataTreeCandidateNode child : node.getChildNodes()) {
+                    final BindingStructuralType childType = recursiveFrom(child);
+                    switch (childType) {
+                        case INVISIBLE_CONTAINER:
+                        case INVISIBLE_LIST:
+                            // Invisible nodes are not addressable
+                            return BindingStructuralType.NOT_ADDRESSABLE;
+                        case NOT_ADDRESSABLE:
+                        case UNKNOWN:
+                        case VISIBLE_CONTAINER:
+                            return childType;
+                        default:
+                            throw new IllegalStateException("Unhandled child type " + childType + " for child "
+                                    + child);
+                    }
+                }
+
+                return BindingStructuralType.NOT_ADDRESSABLE;
+            default:
+                return type;
+        }
+    }
+
     private static boolean isVisibleContainer(final NormalizedNode<?, ?> data) {
         return data instanceof MapEntryNode || data instanceof ContainerNode || data instanceof AugmentationNode;
     }
@@ -128,7 +140,8 @@ enum BindingStructuralType {
         return normalizedNode instanceof LeafNode
                 || normalizedNode instanceof AnyXmlNode
                 || normalizedNode instanceof LeafSetNode
-                || normalizedNode instanceof LeafSetEntryNode;
+                || normalizedNode instanceof LeafSetEntryNode
+                || normalizedNode instanceof UnkeyedListNode
+                || normalizedNode instanceof UnkeyedListEntryNode;
     }
-
 }
index b4e14b6d772e1d729194fbde694d486bd9e6b246..dc47be3aff1a33c90f57c39d73fc0051e5c17758 100644 (file)
@@ -45,7 +45,9 @@ final class LazyDataObjectModification<T extends DataObject> implements DataObje
     private final BindingCodecTreeNode<T> codec;
     private final DataTreeCandidateNode domData;
     private final PathArgument identifier;
-    private Collection<DataObjectModification<? extends DataObject>> childNodesCache;
+
+    private volatile Collection<DataObjectModification<? extends DataObject>> childNodesCache;
+    private volatile ModificationType modificationType;
 
     private LazyDataObjectModification(final BindingCodecTreeNode<T> codec, final DataTreeCandidateNode domData) {
         this.codec = Preconditions.checkNotNull(codec);
@@ -137,29 +139,68 @@ final class LazyDataObjectModification<T extends DataObject> implements DataObje
     }
 
     @Override
-    public DataObjectModification.ModificationType getModificationType() {
+    public ModificationType getModificationType() {
+        ModificationType localType = modificationType;
+        if (localType != null) {
+            return localType;
+        }
+
         switch (domData.getModificationType()) {
             case APPEARED:
             case WRITE:
-                return DataObjectModification.ModificationType.WRITE;
-            case SUBTREE_MODIFIED:
-                return DataObjectModification.ModificationType.SUBTREE_MODIFIED;
+                localType = ModificationType.WRITE;
+                break;
             case DISAPPEARED:
             case DELETE:
-                return DataObjectModification.ModificationType.DELETE;
-
+                localType = ModificationType.DELETE;
+                break;
+            case SUBTREE_MODIFIED:
+                localType = resolveSubtreeModificationType();
+                break;
             default:
                 // TODO: Should we lie about modification type instead of exception?
                 throw new IllegalStateException("Unsupported DOM Modification type " + domData.getModificationType());
         }
+
+        modificationType = localType;
+        return localType;
+    }
+
+    private ModificationType resolveSubtreeModificationType() {
+        switch (codec.getChildAddressabilitySummary()) {
+            case ADDRESSABLE:
+                // All children are addressable, it is safe to report SUBTREE_MODIFIED
+                return ModificationType.SUBTREE_MODIFIED;
+            case UNADDRESSABLE:
+                // All children are non-addressable, report WRITE
+                return ModificationType.WRITE;
+            case MIXED:
+                // This case is not completely trivial, as we may have NOT_ADDRESSABLE nodes underneath us. If that
+                // is the case, we need to turn this modification into a WRITE operation, so that the user is able
+                // to observe those nodes being introduced. This is not efficient, but unfortunately unavoidable,
+                // as we cannot accurately represent such changes.
+                for (DataTreeCandidateNode child : domData.getChildNodes()) {
+                    if (BindingStructuralType.recursiveFrom(child) == BindingStructuralType.NOT_ADDRESSABLE) {
+                        // We have a non-addressable child, turn this modification into a write
+                        return ModificationType.WRITE;
+                    }
+                }
+
+                // No unaddressable children found, proceed in addressed mode
+                return ModificationType.SUBTREE_MODIFIED;
+            default:
+                throw new IllegalStateException("Unsupported child addressability summary "
+                        + codec.getChildAddressabilitySummary());
+        }
     }
 
     @Override
     public Collection<DataObjectModification<? extends DataObject>> getModifiedChildren() {
-        if (childNodesCache == null) {
-            childNodesCache = from(codec, domData.getChildNodes());
+        Collection<DataObjectModification<? extends DataObject>> local = childNodesCache;
+        if (local == null) {
+            childNodesCache = local = from(codec, domData.getChildNodes());
         }
-        return childNodesCache;
+        return local;
     }
 
     @SuppressWarnings("unchecked")
diff --git a/binding/mdsal-binding-dom-adapter/src/test/java/org/opendaylight/mdsal/binding/dom/adapter/test/Mdsal298Test.java b/binding/mdsal-binding-dom-adapter/src/test/java/org/opendaylight/mdsal/binding/dom/adapter/test/Mdsal298Test.java
new file mode 100644 (file)
index 0000000..7da7f65
--- /dev/null
@@ -0,0 +1,385 @@
+/*
+ * Copyright (c) 2018 Pantheon Technologies, s.ro.. 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.mdsal.binding.dom.adapter.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.opendaylight.mdsal.common.api.LogicalDatastoreType.CONFIGURATION;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.opendaylight.mdsal.binding.api.DataObjectModification;
+import org.opendaylight.mdsal.binding.api.DataObjectModification.ModificationType;
+import org.opendaylight.mdsal.binding.api.DataTreeChangeListener;
+import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
+import org.opendaylight.mdsal.binding.api.DataTreeModification;
+import org.opendaylight.mdsal.binding.api.WriteTransaction;
+import org.opendaylight.mdsal.dom.api.DOMDataTreeWriteTransaction;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal298.rev180129.AddressableCont;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal298.rev180129.AddressableContBuilder;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal298.rev180129.Container;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal298.rev180129.ContainerBuilder;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal298.rev180129.UnaddressableCont;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal298.rev180129.UnaddressableContBuilder;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal298.rev180129.WithChoice;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal298.rev180129.WithChoiceBuilder;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal298.rev180129.addressable.cont.AddressableChild;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal298.rev180129.addressable.cont.AddressableChildBuilder;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal298.rev180129.container.Keyed;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal298.rev180129.container.KeyedBuilder;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal298.rev180129.container.KeyedKey;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal298.rev180129.container.Unkeyed;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal298.rev180129.container.UnkeyedBuilder;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal298.rev180129.with.choice.Foo;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal298.rev180129.with.choice.foo.addressable._case.Addressable;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal298.rev180129.with.choice.foo.addressable._case.AddressableBuilder;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier.Item;
+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.impl.schema.Builders;
+import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
+
+public class Mdsal298Test extends AbstractDataBrokerTest {
+    private static final InstanceIdentifier<Container> CONTAINER = InstanceIdentifier.create(Container.class);
+    private static final DataTreeIdentifier<Container> CONTAINER_TID = DataTreeIdentifier.create(CONFIGURATION,
+        CONTAINER);
+    private static final NodeIdentifier CONTAINER_NID = new NodeIdentifier(Container.QNAME);
+    private static final QName FOO_QNAME = QName.create(Container.QNAME, "foo");
+    private static final QName BAZ_QNAME = QName.create(UnaddressableCont.QNAME, "baz");
+
+    private static final InstanceIdentifier<WithChoice> CHOICE_CONTAINER = InstanceIdentifier.create(WithChoice.class);
+    private static final DataTreeIdentifier<WithChoice> CHOICE_CONTAINER_TID = DataTreeIdentifier.create(CONFIGURATION,
+        CHOICE_CONTAINER);
+    private static final NodeIdentifier CHOICE_CONTAINER_NID = new NodeIdentifier(WithChoice.QNAME);
+    private static final NodeIdentifier CHOICE_NID = new NodeIdentifier(Foo.QNAME);
+    private static final InstanceIdentifier<Addressable> ADDRESSABLE_CASE = CHOICE_CONTAINER
+            .child((Class)Addressable.class);
+
+    private static final InstanceIdentifier<AddressableCont> ADDRESSABLE_CONTAINER =
+            InstanceIdentifier.create(AddressableCont.class);
+    private static final DataTreeIdentifier<AddressableCont> ADDRESSABLE_CONTAINER_TID = DataTreeIdentifier.create(
+        CONFIGURATION, ADDRESSABLE_CONTAINER);
+    private static final NodeIdentifier ADDRESSABLE_CONTAINER_NID = new NodeIdentifier(AddressableCont.QNAME);
+
+    private static final InstanceIdentifier<UnaddressableCont> UNADDRESSABLE_CONTAINER =
+            InstanceIdentifier.create(UnaddressableCont.class);
+    private static final DataTreeIdentifier<UnaddressableCont> UNADDRESSABLE_CONTAINER_TID = DataTreeIdentifier.create(
+        CONFIGURATION, UNADDRESSABLE_CONTAINER);
+    private static final NodeIdentifier UNADDRESSABLE_CONTAINER_NID = new NodeIdentifier(UnaddressableCont.QNAME);
+
+    @Test
+    public void testKeyedDataTreeModification() throws InterruptedException, ExecutionException {
+        final DataTreeChangeListener<Container> listener = assertWrittenContainer(Container.QNAME, Container.class,
+            new ContainerBuilder().build());
+
+        final DOMDataTreeWriteTransaction domTx = getDomBroker().newWriteOnlyTransaction();
+        domTx.put(CONFIGURATION, YangInstanceIdentifier.create(CONTAINER_NID).node(Keyed.QNAME),
+            Builders.orderedMapBuilder()
+            .withNodeIdentifier(new NodeIdentifier(Keyed.QNAME))
+            .addChild(Builders.mapEntryBuilder()
+                .withNodeIdentifier(new NodeIdentifierWithPredicates(Keyed.QNAME, ImmutableMap.of(FOO_QNAME, "foo")))
+                .addChild(ImmutableNodes.leafNode(FOO_QNAME, "foo"))
+                .build())
+            .addChild(Builders.mapEntryBuilder()
+                .withNodeIdentifier(new NodeIdentifierWithPredicates(Keyed.QNAME, ImmutableMap.of(FOO_QNAME, "bar")))
+                .addChild(ImmutableNodes.leafNode(FOO_QNAME, "bar"))
+                .build())
+            .build());
+        domTx.submit().get();
+
+        final ArgumentCaptor<Collection> captor = ArgumentCaptor.forClass(Collection.class);
+        verify(listener).onDataTreeChanged(captor.capture());
+        Collection<DataTreeModification<Container>> capture = captor.getValue();
+        assertEquals(1, capture.size());
+
+        final DataTreeModification<Container> change = capture.iterator().next();
+        assertEquals(CONTAINER_TID, change.getRootPath());
+        final DataObjectModification<Container> changedContainer = change.getRootNode();
+        assertEquals(new Item<>(Container.class), changedContainer.getIdentifier());
+        assertEquals(ModificationType.SUBTREE_MODIFIED, changedContainer.getModificationType());
+
+        final Container containerAfter = changedContainer.getDataAfter();
+        assertEquals(new ContainerBuilder()
+            .setKeyed(ImmutableList.of(
+                new KeyedBuilder().setFoo("foo").setKey(new KeyedKey("foo")).build(),
+                new KeyedBuilder().setFoo("bar").setKey(new KeyedKey("bar")).build()))
+            .build(), containerAfter);
+
+        final Collection<DataObjectModification<?>> changedChildren = changedContainer.getModifiedChildren();
+        assertEquals(2, changedChildren.size());
+
+        final Iterator<DataObjectModification<?>> it = changedChildren.iterator();
+        final DataObjectModification<?> changedChild1 = it.next();
+        assertEquals(ModificationType.WRITE, changedChild1.getModificationType());
+        assertEquals(Collections.emptyList(), changedChild1.getModifiedChildren());
+        final Keyed child1After = (Keyed) changedChild1.getDataAfter();
+        assertEquals("foo", child1After.getFoo());
+
+        final DataObjectModification<?> changedChild2 = it.next();
+        assertEquals(ModificationType.WRITE, changedChild2.getModificationType());
+        assertEquals(Collections.emptyList(), changedChild2.getModifiedChildren());
+        final Keyed child2After = (Keyed) changedChild2.getDataAfter();
+        assertEquals("bar", child2After.getFoo());
+    }
+
+    @Test
+    public void testUnkeyedDataTreeModification() throws InterruptedException, ExecutionException {
+        final DataTreeChangeListener<Container> listener = assertWrittenContainer(Container.QNAME, Container.class,
+            new ContainerBuilder().build());
+
+        final DOMDataTreeWriteTransaction domTx = getDomBroker().newWriteOnlyTransaction();
+        domTx.put(CONFIGURATION, YangInstanceIdentifier.create(CONTAINER_NID).node(Unkeyed.QNAME),
+            Builders.unkeyedListBuilder()
+            .withNodeIdentifier(new NodeIdentifier(Unkeyed.QNAME))
+            .withChild(Builders.unkeyedListEntryBuilder()
+                .withNodeIdentifier(new NodeIdentifier(Unkeyed.QNAME))
+                .addChild(ImmutableNodes.leafNode(FOO_QNAME, "foo"))
+                .build())
+            .withChild(Builders.unkeyedListEntryBuilder()
+                .withNodeIdentifier(new NodeIdentifier(Unkeyed.QNAME))
+                .addChild(ImmutableNodes.leafNode(FOO_QNAME, "bar"))
+                .build())
+            .build());
+        domTx.submit().get();
+
+        final ArgumentCaptor<Collection> captor = ArgumentCaptor.forClass(Collection.class);
+        verify(listener).onDataTreeChanged(captor.capture());
+        Collection<DataTreeModification<Container>> capture = captor.getValue();
+        assertEquals(1, capture.size());
+
+        final DataTreeModification<Container> change = capture.iterator().next();
+        assertEquals(CONTAINER_TID, change.getRootPath());
+        final DataObjectModification<Container> changedContainer = change.getRootNode();
+        assertEquals(new Item<>(Container.class), changedContainer.getIdentifier());
+        assertEquals(ModificationType.WRITE, changedContainer.getModificationType());
+
+        final Container containerAfter = changedContainer.getDataAfter();
+        assertEquals(new ContainerBuilder()
+                .setUnkeyed(ImmutableList.of(
+                    new UnkeyedBuilder().setFoo("foo").build(),
+                    new UnkeyedBuilder().setFoo("bar").build()))
+                .build(), containerAfter);
+
+        final Collection<DataObjectModification<?>> changedChildren = changedContainer.getModifiedChildren();
+        assertEquals(0, changedChildren.size());
+    }
+
+    @Test
+    public void testChoiceDataTreeModificationAddressable() throws InterruptedException, ExecutionException {
+        final DataTreeChangeListener<WithChoice> listener = assertWrittenWithChoice();
+
+        doNothing().when(listener).onDataTreeChanged(any(Collection.class));
+
+        final WriteTransaction writeTx = getDataBroker().newWriteOnlyTransaction();
+        writeTx.put(CONFIGURATION, ADDRESSABLE_CASE, new AddressableBuilder().build());
+        writeTx.submit().get();
+
+        final ArgumentCaptor<Collection> captor = ArgumentCaptor.forClass(Collection.class);
+        verify(listener).onDataTreeChanged(captor.capture());
+        Collection<DataTreeModification<WithChoice>> capture = captor.getValue();
+        assertEquals(1, capture.size());
+
+        final DataTreeModification<WithChoice> choiceChange = capture.iterator().next();
+        assertEquals(CHOICE_CONTAINER_TID, choiceChange.getRootPath());
+        final DataObjectModification<WithChoice> changedContainer = choiceChange.getRootNode();
+        assertEquals(ModificationType.SUBTREE_MODIFIED, changedContainer.getModificationType());
+        assertEquals(new Item<>(WithChoice.class), changedContainer.getIdentifier());
+
+        final Collection<DataObjectModification<?>> choiceChildren = changedContainer.getModifiedChildren();
+        assertEquals(1, choiceChildren.size());
+
+        final DataObjectModification<Addressable> changedCase = (DataObjectModification<Addressable>) choiceChildren
+                .iterator().next();
+        assertEquals(ModificationType.WRITE, changedCase.getModificationType());
+        assertEquals(new Item<>(Addressable.class), changedCase.getIdentifier());
+        assertEquals(new AddressableBuilder().build(), changedCase.getDataAfter());
+    }
+
+    @Test
+    public void testDataTreeModificationAddressable() throws InterruptedException, ExecutionException {
+        final DataTreeChangeListener<AddressableCont> listener = assertWrittenContainer(AddressableCont.QNAME,
+            AddressableCont.class, new AddressableContBuilder().build());
+
+        doNothing().when(listener).onDataTreeChanged(any(Collection.class));
+
+        final WriteTransaction writeTx = getDataBroker().newWriteOnlyTransaction();
+        writeTx.put(CONFIGURATION, ADDRESSABLE_CONTAINER.child(AddressableChild.class),
+            new AddressableChildBuilder().build());
+        writeTx.submit().get();
+
+        final ArgumentCaptor<Collection> captor = ArgumentCaptor.forClass(Collection.class);
+        verify(listener).onDataTreeChanged(captor.capture());
+        Collection<DataTreeModification<AddressableCont>> capture = captor.getValue();
+        assertEquals(1, capture.size());
+
+        final DataTreeModification<AddressableCont> contChange = capture.iterator().next();
+        assertEquals(ADDRESSABLE_CONTAINER_TID, contChange.getRootPath());
+        final DataObjectModification<AddressableCont> changedContainer = contChange.getRootNode();
+        assertEquals(ModificationType.SUBTREE_MODIFIED, changedContainer.getModificationType());
+        assertEquals(new Item<>(AddressableCont.class), changedContainer.getIdentifier());
+
+        final Collection<DataObjectModification<?>> contChildren = changedContainer.getModifiedChildren();
+        assertEquals(1, contChildren.size());
+
+        final DataObjectModification<Addressable> changedChild = (DataObjectModification<Addressable>) contChildren
+                .iterator().next();
+        assertEquals(ModificationType.WRITE, changedChild.getModificationType());
+        assertEquals(new Item<>(AddressableChild.class), changedChild.getIdentifier());
+        assertEquals(new AddressableChildBuilder().build(), changedChild.getDataAfter());
+    }
+
+    @Test
+    public void testDataTreeModificationUnaddressable() throws InterruptedException, ExecutionException {
+        final DataTreeChangeListener<UnaddressableCont> listener = assertWrittenContainer(UnaddressableCont.QNAME,
+            UnaddressableCont.class, new UnaddressableContBuilder().build());
+
+        doNothing().when(listener).onDataTreeChanged(any(Collection.class));
+
+        final DOMDataTreeWriteTransaction domTx = getDomBroker().newWriteOnlyTransaction();
+        domTx.put(CONFIGURATION, YangInstanceIdentifier.create(UNADDRESSABLE_CONTAINER_NID)
+            .node(QName.create(UnaddressableCont.QNAME, "baz")),
+            ImmutableNodes.leafNode(BAZ_QNAME, "baz"));
+        domTx.submit().get();
+
+        final ArgumentCaptor<Collection> captor = ArgumentCaptor.forClass(Collection.class);
+        verify(listener).onDataTreeChanged(captor.capture());
+        Collection<DataTreeModification<UnaddressableCont>> capture = captor.getValue();
+        assertEquals(1, capture.size());
+
+        final DataTreeModification<UnaddressableCont> contChange = capture.iterator().next();
+        assertEquals(UNADDRESSABLE_CONTAINER_TID, contChange.getRootPath());
+        final DataObjectModification<UnaddressableCont> changedContainer = contChange.getRootNode();
+        assertEquals(ModificationType.WRITE, changedContainer.getModificationType());
+        assertEquals(new Item<>(UnaddressableCont.class), changedContainer.getIdentifier());
+
+        final Collection<DataObjectModification<?>> contChildren = changedContainer.getModifiedChildren();
+        assertEquals(0, contChildren.size());
+    }
+
+    @Test
+    public void testChoiceDataTreeModificationUnaddressable() throws InterruptedException, ExecutionException {
+        final DataTreeChangeListener<WithChoice> listener = assertWrittenWithChoice();
+
+        doNothing().when(listener).onDataTreeChanged(any(Collection.class));
+
+        final DOMDataTreeWriteTransaction domTx = getDomBroker().newWriteOnlyTransaction();
+        domTx.put(CONFIGURATION, YangInstanceIdentifier.create(CHOICE_CONTAINER_NID).node(Foo.QNAME),
+            Builders.choiceBuilder()
+            .withNodeIdentifier(new NodeIdentifier(Foo.QNAME))
+            .withChild(Builders.leafSetBuilder()
+                .withNodeIdentifier(new NodeIdentifier(QName.create(Foo.QNAME, "unaddressable")))
+                .withChildValue("foo")
+                .build())
+            .build());
+        domTx.submit().get();
+
+        final ArgumentCaptor<Collection> captor = ArgumentCaptor.forClass(Collection.class);
+        verify(listener).onDataTreeChanged(captor.capture());
+        Collection<DataTreeModification<WithChoice>> capture = captor.getValue();
+        assertEquals(1, capture.size());
+
+        final DataTreeModification<WithChoice> choiceChange = capture.iterator().next();
+        assertEquals(CHOICE_CONTAINER_TID, choiceChange.getRootPath());
+        final DataObjectModification<WithChoice> changedContainer = choiceChange.getRootNode();
+
+        // Should be write
+        assertEquals(ModificationType.WRITE, changedContainer.getModificationType());
+        assertEquals(new Item<>(WithChoice.class), changedContainer.getIdentifier());
+
+        final Collection<DataObjectModification<?>> choiceChildren = changedContainer.getModifiedChildren();
+        assertEquals(0, choiceChildren.size());
+    }
+
+    private <T extends DataObject> DataTreeChangeListener<T> assertWrittenContainer(final QName qname,
+            final Class<T> bindingClass, final T expected)
+            throws InterruptedException, ExecutionException {
+        final DataTreeChangeListener<T> listener = mock(DataTreeChangeListener.class);
+        doNothing().when(listener).onDataTreeChanged(any(Collection.class));
+
+        final DataTreeIdentifier<T> dti = DataTreeIdentifier.create(CONFIGURATION,
+            InstanceIdentifier.create(bindingClass));
+        getDataBroker().registerDataTreeChangeListener(dti, listener);
+
+        final DOMDataTreeWriteTransaction domTx = getDomBroker().newWriteOnlyTransaction();
+        domTx.put(CONFIGURATION, YangInstanceIdentifier.create(new NodeIdentifier(qname)),
+            ImmutableNodes.containerNode(qname));
+        domTx.submit().get();
+
+        final ArgumentCaptor<Collection> captor = ArgumentCaptor.forClass(Collection.class);
+        verify(listener).onDataTreeChanged(captor.capture());
+        Collection<DataTreeModification<T>> capture = captor.getValue();
+        assertEquals(1, capture.size());
+
+        final DataTreeModification<T> change = capture.iterator().next();
+        assertEquals(dti, change.getRootPath());
+        final DataObjectModification<T> changedContainer = change.getRootNode();
+        assertEquals(ModificationType.WRITE, changedContainer.getModificationType());
+        assertEquals(new Item<>(bindingClass), changedContainer.getIdentifier());
+
+        final T containerAfter = changedContainer.getDataAfter();
+        assertEquals(expected, containerAfter);
+
+        // No further modifications should occur
+        assertEquals(Collections.emptyList(), changedContainer.getModifiedChildren());
+
+        reset(listener);
+        doNothing().when(listener).onDataTreeChanged(any(Collection.class));
+        return listener;
+    }
+
+    private DataTreeChangeListener<WithChoice> assertWrittenWithChoice() throws InterruptedException,
+            ExecutionException {
+        final DataTreeChangeListener<WithChoice> listener = mock(DataTreeChangeListener.class);
+        doNothing().when(listener).onDataTreeChanged(any(Collection.class));
+        getDataBroker().registerDataTreeChangeListener(CHOICE_CONTAINER_TID, listener);
+
+        final DOMDataTreeWriteTransaction domTx = getDomBroker().newWriteOnlyTransaction();
+        domTx.put(CONFIGURATION, YangInstanceIdentifier.create(CHOICE_CONTAINER_NID),
+            Builders.containerBuilder()
+            .withNodeIdentifier(CHOICE_CONTAINER_NID)
+            .withChild(Builders.choiceBuilder().withNodeIdentifier(CHOICE_NID).build())
+            .build());
+        domTx.submit().get();
+
+        final ArgumentCaptor<Collection> captor = ArgumentCaptor.forClass(Collection.class);
+        verify(listener).onDataTreeChanged(captor.capture());
+        Collection<DataTreeModification<WithChoice>> capture = captor.getValue();
+        assertEquals(1, capture.size());
+
+        final DataTreeModification<WithChoice> change = capture.iterator().next();
+        assertEquals(CHOICE_CONTAINER_TID, change.getRootPath());
+        final DataObjectModification<WithChoice> changedContainer = change.getRootNode();
+        assertEquals(ModificationType.WRITE, changedContainer.getModificationType());
+        assertEquals(new Item<>(WithChoice.class), changedContainer.getIdentifier());
+
+        final WithChoice containerAfter = changedContainer.getDataAfter();
+        assertEquals(new WithChoiceBuilder().build(), containerAfter);
+
+        // No further modifications should occur
+        assertEquals(Collections.emptyList(), changedContainer.getModifiedChildren());
+
+        reset(listener);
+        doNothing().when(listener).onDataTreeChanged(any(Collection.class));
+
+        return listener;
+    }
+}
index 9345c2ab9bf3c3074f1ffa334165a974c1d9535b..89590630c741a15b58cee810a2d8c3747bb6069b 100644 (file)
@@ -155,4 +155,33 @@ public interface BindingCodecTreeNode<T extends DataObject> extends BindingNorma
 
     @Override
     Object getSchema();
+
+    /**
+     * Return a summary of addressability of potential children. Binding specification does not allow all DOM tree
+     * elements to be directly addressed, which means some recursive tree operations, like data tree changes do not
+     * have a one-to-one mapping from DOM to binding in all cases. This method provides an optimization hint to guide
+     * translation of data structures, allowing for fast paths when all children are known to either be addressable
+     * or non-addressable.
+     *
+     * @return Summary children addressability.
+     */
+    @Nonnull ChildAddressabilitySummary getChildAddressabilitySummary();
+
+    /**
+     * Enumeration of possible addressability attribute of all children.
+     */
+    enum ChildAddressabilitySummary {
+        /**
+         * All children are addressable.
+         */
+        ADDRESSABLE,
+        /**
+         * All children are non-addressable, including the case when this node does not have any children.
+         */
+        UNADDRESSABLE,
+        /**
+         * Mixed children, some are addressable and some are not.
+         */
+        MIXED
+    }
 }
index 933c6a8630db779092e29a53ff5eec68cb183672..27bbe9428b29b4982768285959a3d9781035a2aa 100644 (file)
@@ -42,6 +42,11 @@ abstract class DataContainerCodecContext<D extends DataObject,T> extends NodeCod
         return prototype.getSchema();
     }
 
+    @Override
+    public final ChildAddressabilitySummary getChildAddressabilitySummary() {
+        return prototype.getChildAddressabilitySummary();
+    }
+
     protected final QNameModule namespace() {
         return prototype.getNamespace();
     }
index 4560bd6274822ce1a3d6c1db3819c7a2eff40940..88b5cd984d538a169d70f69b6e3b4b18ce642457 100644 (file)
@@ -9,6 +9,7 @@ package org.opendaylight.yangtools.binding.data.codec.impl;
 
 import com.google.common.collect.Iterables;
 import javax.annotation.concurrent.GuardedBy;
+import org.opendaylight.mdsal.binding.dom.codec.api.BindingCodecTreeNode.ChildAddressabilitySummary;
 import org.opendaylight.yangtools.binding.data.codec.impl.NodeCodecContext.CodecContextFactory;
 import org.opendaylight.yangtools.yang.binding.DataRoot;
 import org.opendaylight.yangtools.yang.binding.Identifiable;
@@ -18,16 +19,23 @@ import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
+import org.opendaylight.yangtools.yang.model.api.AnyDataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.AnyXmlSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.AugmentationSchema;
 import org.opendaylight.yangtools.yang.model.api.ChoiceCaseNode;
 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.NotificationDefinition;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.model.api.TypedSchemaNode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 final class DataContainerCodecPrototype<T> implements NodeContextSupplier {
+    private static final Logger LOG = LoggerFactory.getLogger(DataContainerCodecPrototype.class);
 
     private final T schema;
     private final QNameModule namespace;
@@ -35,7 +43,9 @@ final class DataContainerCodecPrototype<T> implements NodeContextSupplier {
     private final Class<?> bindingClass;
     private final InstanceIdentifier.Item<?> bindingArg;
     private final YangInstanceIdentifier.PathArgument yangArg;
-    private volatile DataContainerCodecContext<?,T> instance = null;
+    private final ChildAddressabilitySummary childAddressabilitySummary;
+
+    private volatile DataContainerCodecContext<?, T> instance = null;
 
     @SuppressWarnings({"rawtypes", "unchecked"})
     private DataContainerCodecPrototype(final Class<?> cls, final YangInstanceIdentifier.PathArgument arg, final T nodeSchema,
@@ -52,6 +62,81 @@ final class DataContainerCodecPrototype<T> implements NodeContextSupplier {
         } else {
             this.namespace = arg.getNodeType().getModule();
         }
+
+        this.childAddressabilitySummary = computeChildAddressabilitySummary(nodeSchema);
+    }
+
+    private static ChildAddressabilitySummary computeChildAddressabilitySummary(final Object nodeSchema) {
+        if (nodeSchema instanceof DataNodeContainer) {
+            boolean haveAddressable = false;
+            boolean haveUnaddressable = false;
+            for (DataSchemaNode child : ((DataNodeContainer) nodeSchema).getChildNodes()) {
+                if (child instanceof ContainerSchemaNode || child instanceof AugmentationSchema) {
+                    haveAddressable = true;
+                } else if (child instanceof ListSchemaNode) {
+                    if (((ListSchemaNode) child).getKeyDefinition().isEmpty()) {
+                        haveUnaddressable = true;
+                    } else {
+                        haveAddressable = true;
+                    }
+                } else if (child instanceof AnyDataSchemaNode || child instanceof AnyXmlSchemaNode
+                        || child instanceof TypedSchemaNode) {
+                    haveUnaddressable = true;
+                } else if (child instanceof ChoiceSchemaNode) {
+                    switch (computeChildAddressabilitySummary(child)) {
+                        case ADDRESSABLE:
+                            haveAddressable = true;
+                            break;
+                        case MIXED:
+                            haveAddressable = true;
+                            haveUnaddressable = true;
+                            break;
+                        case UNADDRESSABLE:
+                            haveUnaddressable = true;
+                            break;
+                        default:
+                            throw new IllegalStateException("Unhandled accessibility summary for " + child);
+                    }
+                } else {
+                    LOG.warn("Unhandled child node {}", child);
+                }
+            }
+
+            if (!haveAddressable) {
+                // Empty or all are unaddressable
+                return ChildAddressabilitySummary.UNADDRESSABLE;
+            }
+
+            return haveUnaddressable ? ChildAddressabilitySummary.MIXED : ChildAddressabilitySummary.ADDRESSABLE;
+        } else if (nodeSchema instanceof ChoiceSchemaNode) {
+            boolean haveAddressable = false;
+            boolean haveUnaddressable = false;
+            for (ChoiceCaseNode child : ((ChoiceSchemaNode) nodeSchema).getCases()) {
+                switch (computeChildAddressabilitySummary(child)) {
+                    case ADDRESSABLE:
+                        haveAddressable = true;
+                        break;
+                    case UNADDRESSABLE:
+                        haveUnaddressable = true;
+                        break;
+                    case MIXED:
+                        // A child is mixed, which means we are mixed, too
+                        return ChildAddressabilitySummary.MIXED;
+                    default:
+                        throw new IllegalStateException("Unhandled accessibility summary for " + child);
+                }
+            }
+
+            if (!haveAddressable) {
+                // Empty or all are unaddressable
+                return ChildAddressabilitySummary.UNADDRESSABLE;
+            }
+
+            return haveUnaddressable ? ChildAddressabilitySummary.MIXED : ChildAddressabilitySummary.ADDRESSABLE;
+        }
+
+        // No child nodes possible: return unaddressable
+        return ChildAddressabilitySummary.UNADDRESSABLE;
     }
 
     @SuppressWarnings({ "unchecked", "rawtypes" })
@@ -63,7 +148,7 @@ final class DataContainerCodecPrototype<T> implements NodeContextSupplier {
     static DataContainerCodecPrototype<SchemaContext> rootPrototype(final CodecContextFactory factory) {
         final SchemaContext schema = factory.getRuntimeContext().getSchemaContext();
         final NodeIdentifier arg = NodeIdentifier.create(schema.getQName());
-        return new DataContainerCodecPrototype<SchemaContext>(DataRoot.class, arg, schema, factory);
+        return new DataContainerCodecPrototype<>(DataRoot.class, arg, schema, factory);
     }
 
 
@@ -75,13 +160,17 @@ final class DataContainerCodecPrototype<T> implements NodeContextSupplier {
 
     static DataContainerCodecPrototype<NotificationDefinition> from(final Class<?> augClass, final NotificationDefinition schema, final CodecContextFactory factory) {
         final PathArgument arg = NodeIdentifier.create(schema.getQName());
-        return new DataContainerCodecPrototype<NotificationDefinition>(augClass,arg, schema, factory);
+        return new DataContainerCodecPrototype<>(augClass,arg, schema, factory);
     }
 
     protected T getSchema() {
         return schema;
     }
 
+    ChildAddressabilitySummary getChildAddressabilitySummary() {
+        return childAddressabilitySummary;
+    }
+
     protected QNameModule getNamespace() {
         return namespace;
     }
index 7b482ba2e6d3314eece728387b4ecba89ee4445b..d9b63e794928939139b3394f7f09fd240a9199ff 100644 (file)
@@ -226,6 +226,11 @@ final class LeafNodeCodecContext<D extends DataObject> extends NodeCodecContext<
         return schema;
     }
 
+    @Override
+    public ChildAddressabilitySummary getChildAddressabilitySummary() {
+        return ChildAddressabilitySummary.UNADDRESSABLE;
+    }
+
     /**
      * Return the default value object.
      *
diff --git a/binding/mdsal-binding-test-model/src/main/yang/opendaylight-mdsal298.yang b/binding/mdsal-binding-test-model/src/main/yang/opendaylight-mdsal298.yang
new file mode 100644 (file)
index 0000000..cbfdd40
--- /dev/null
@@ -0,0 +1,59 @@
+module opendaylight-mdsal298 {
+    namespace "urn:test:opendaylight-mdsal298";
+    prefix mdsal298;
+
+    revision 2018-01-29;
+
+    container container {
+        presence "for persistence";
+
+        list unkeyed {
+            ordered-by user;
+            leaf foo {
+                type string;
+            }
+        }
+
+        list keyed {
+            ordered-by user;
+            key foo;
+            leaf foo {
+                type string;
+            }
+        }
+    }
+
+    container with-choice {
+        presence "for persistence";
+
+        choice foo {
+            case addressable-case {
+                container addressable {
+                    presence "for persistence";
+                }
+            }
+            case unaddressable-case {
+                leaf-list unaddressable {
+                    type string;
+                }
+            }
+        }
+    }
+
+    container addressable-cont {
+        presence "for persistence";
+
+        container addressable-child {
+            presence "for persistence";
+        }
+    }
+
+    container unaddressable-cont {
+        presence "for persistence";
+
+        leaf baz {
+            type string;
+        }
+    }
+}
+