From 9f6b16d6e8ade6dfa40e9bbf4196d55adf8f2fec Mon Sep 17 00:00:00 2001 From: Maros Marsalek Date: Wed, 29 Jun 2016 09:14:51 +0200 Subject: HONEYCOMB-94 Reimplement writer registry with better ordering options Now the registry is flat and allows for full control of writer execution order Change-Id: I864e1d676588ffe59b596145e0829e81b1a1ed2f Signed-off-by: Maros Marsalek --- .../data/impl/ModifiableDataTreeDelegator.java | 228 ++++--------- .../honeycomb/v3po/data/impl/ModificationDiff.java | 278 ++++++++++++++++ .../data/impl/rev160411/ConfigDataTreeModule.java | 4 +- v3po/data-impl/src/main/yang/data-impl.yang | 4 +- .../data/impl/ModifiableDataTreeDelegatorTest.java | 367 ++++++++------------- .../v3po/data/impl/ModificationDiffTest.java | 215 +++++++----- v3po/data-impl/src/test/resources/test-diff.yang | 17 + 7 files changed, 631 insertions(+), 482 deletions(-) create mode 100644 v3po/data-impl/src/main/java/io/fd/honeycomb/v3po/data/impl/ModificationDiff.java (limited to 'v3po/data-impl') diff --git a/v3po/data-impl/src/main/java/io/fd/honeycomb/v3po/data/impl/ModifiableDataTreeDelegator.java b/v3po/data-impl/src/main/java/io/fd/honeycomb/v3po/data/impl/ModifiableDataTreeDelegator.java index 91de1885e..77aa12aba 100644 --- a/v3po/data-impl/src/main/java/io/fd/honeycomb/v3po/data/impl/ModifiableDataTreeDelegator.java +++ b/v3po/data-impl/src/main/java/io/fd/honeycomb/v3po/data/impl/ModifiableDataTreeDelegator.java @@ -16,38 +16,36 @@ package io.fd.honeycomb.v3po.data.impl; -import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.util.concurrent.Futures.immediateCheckedFuture; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; -import com.google.common.collect.Sets; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; import com.google.common.util.concurrent.CheckedFuture; import io.fd.honeycomb.v3po.data.DataModification; import io.fd.honeycomb.v3po.data.ReadableDataManager; import io.fd.honeycomb.v3po.translate.TranslationException; +import io.fd.honeycomb.v3po.translate.util.RWUtils; import io.fd.honeycomb.v3po.translate.util.write.TransactionMappingContext; import io.fd.honeycomb.v3po.translate.util.write.TransactionWriteContext; +import io.fd.honeycomb.v3po.translate.write.DataObjectUpdate; import io.fd.honeycomb.v3po.translate.write.WriteContext; import io.fd.honeycomb.v3po.translate.write.WriterRegistry; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; import java.util.Map; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException; import org.opendaylight.controller.md.sal.dom.api.DOMDataReadOnlyTransaction; import org.opendaylight.yangtools.binding.data.codec.api.BindingNormalizedNodeSerializer; import org.opendaylight.yangtools.yang.binding.DataObject; import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; -import org.opendaylight.yangtools.yang.data.api.schema.LeafNode; import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTree; import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate; import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateNode; -import org.opendaylight.yangtools.yang.data.api.schema.tree.ModificationType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -110,6 +108,7 @@ public final class ModifiableDataTreeDelegator extends ModifiableDataTreeManager @Override protected void processCandidate(final DataTreeCandidate candidate) throws TranslationException { + final DataTreeCandidateNode rootNode = candidate.getRootNode(); final YangInstanceIdentifier rootPath = candidate.getRootPath(); LOG.trace("ConfigDataTree.modify() rootPath={}, rootNode={}, dataBefore={}, dataAfter={}", @@ -119,13 +118,12 @@ public final class ModifiableDataTreeDelegator extends ModifiableDataTreeManager ModificationDiff.recursivelyFromCandidate(YangInstanceIdentifier.EMPTY, rootNode); LOG.debug("ConfigDataTree.modify() diff: {}", modificationDiff); - final Map, DataObject> nodesBefore = toBindingAware(modificationDiff.getModificationsBefore()); - LOG.debug("ConfigDataTree.modify() extracted nodesBefore={}", nodesBefore.keySet()); - final Map, DataObject> nodesAfter = toBindingAware(modificationDiff.getModificationsAfter()); - LOG.debug("ConfigDataTree.modify() extracted nodesAfter={}", nodesAfter.keySet()); + // Distinguish between updates (create + update) and deletes + final WriterRegistry.DataObjectUpdates baUpdates = toBindingAware(modificationDiff.getUpdates()); + LOG.debug("ConfigDataTree.modify() extracted updates={}", baUpdates); try (final WriteContext ctx = getTransactionWriteContext()) { - writerRegistry.update(nodesBefore, nodesAfter, ctx); + writerRegistry.update(baUpdates, ctx); final CheckedFuture contextUpdateResult = ((TransactionMappingContext) ctx.getMappingContext()).submit(); @@ -152,7 +150,7 @@ public final class ModifiableDataTreeDelegator extends ModifiableDataTreeManager LOG.error(msg, e); throw new TranslationException(msg, e); } catch (TranslationException e) { - LOG.error("Error while processing data change (before={}, after={})", nodesBefore, nodesAfter, e); + LOG.error("Error while processing data change (updates={})", baUpdates, e); throw e; } } @@ -167,177 +165,71 @@ public final class ModifiableDataTreeDelegator extends ModifiableDataTreeManager return new TransactionWriteContext(serializer, beforeTx, afterTx, mappingContext); } - private Map, DataObject> toBindingAware(final Map> biNodes) { + private WriterRegistry.DataObjectUpdates toBindingAware( + final Map biNodes) { return ModifiableDataTreeDelegator.toBindingAware(biNodes, serializer); } } @VisibleForTesting - static Map, DataObject> toBindingAware(final Map> biNodes, - final BindingNormalizedNodeSerializer serializer) { - final HashMap, DataObject> transformed = new HashMap<>(biNodes.size()); - for (Map.Entry> biEntry : biNodes.entrySet()) { - final Map.Entry, DataObject> baEntry = serializer.fromNormalizedNode(biEntry.getKey(), biEntry.getValue()); - if (baEntry != null) { - transformed.put(baEntry.getKey(), baEntry.getValue()); + static WriterRegistry.DataObjectUpdates toBindingAware( + final Map biNodes, + final BindingNormalizedNodeSerializer serializer) { + + final Multimap, DataObjectUpdate> dataObjectUpdates = HashMultimap.create(); + final Multimap, DataObjectUpdate.DataObjectDelete> dataObjectDeletes = + HashMultimap.create(); + + for (Map.Entry biEntry : biNodes.entrySet()) { + final InstanceIdentifier unkeyedIid = + RWUtils.makeIidWildcarded(serializer.fromYangInstanceIdentifier(biEntry.getKey())); + + ModificationDiff.NormalizedNodeUpdate normalizedNodeUpdate = biEntry.getValue(); + final DataObjectUpdate dataObjectUpdate = toDataObjectUpdate(normalizedNodeUpdate, serializer); + if (dataObjectUpdate != null) { + if (dataObjectUpdate instanceof DataObjectUpdate.DataObjectDelete) { + dataObjectDeletes.put(unkeyedIid, ((DataObjectUpdate.DataObjectDelete) dataObjectUpdate)); + } else { + dataObjectUpdates.put(unkeyedIid, dataObjectUpdate); + } } } - return transformed; + return new WriterRegistry.DataObjectUpdates(dataObjectUpdates, dataObjectDeletes); } - @VisibleForTesting - static final class ModificationDiff { - - private static final ModificationDiff EMPTY_DIFF = new ModificationDiff(Collections.emptyMap(), Collections.emptyMap()); - private static final EnumSet LEAF_MODIFICATIONS = EnumSet.of(ModificationType.WRITE, ModificationType.DELETE); - - private final Map> modificationsBefore; - private final Map> modificationsAfter; + @Nullable + private static DataObjectUpdate toDataObjectUpdate( + final ModificationDiff.NormalizedNodeUpdate normalizedNodeUpdate, + final BindingNormalizedNodeSerializer serializer) { - private ModificationDiff(@Nonnull final Map> modificationsBefore, - @Nonnull final Map> modificationsAfter) { - this.modificationsBefore = modificationsBefore; - this.modificationsAfter = modificationsAfter; - } - - Map> getModificationsBefore() { - return modificationsBefore; - } + InstanceIdentifier baId = serializer.fromYangInstanceIdentifier(normalizedNodeUpdate.getId()); + checkNotNull(baId, "Unable to transform instance identifier: %s into BA", normalizedNodeUpdate.getId()); - Map> getModificationsAfter() { - return modificationsAfter; - } - - private ModificationDiff merge(final ModificationDiff other) { - if (this == EMPTY_DIFF) { - return other; - } + DataObject dataObjectBefore = getDataObject(serializer, + normalizedNodeUpdate.getDataBefore(), normalizedNodeUpdate.getId()); + DataObject dataObjectAfter = + getDataObject(serializer, normalizedNodeUpdate.getDataAfter(), normalizedNodeUpdate.getId()); - if (other == EMPTY_DIFF) { - return this; - } - - return new ModificationDiff(join(modificationsBefore, other.modificationsBefore), - join(modificationsAfter, other.modificationsAfter)); - } - - private static Map> join( - final Map> mapOne, - final Map> mapTwo) { - // Check unique modifications - // TODO Probably not necessary to check - final Sets.SetView duplicates = Sets.intersection(mapOne.keySet(), mapTwo.keySet()); - checkArgument(duplicates.size() == 0, "Duplicates detected: %s. In maps: %s and %s", duplicates, mapOne, mapTwo); - final HashMap> joined = new HashMap<>(); - joined.putAll(mapOne); - joined.putAll(mapTwo); - return joined; - } - - private static ModificationDiff createFromBefore(YangInstanceIdentifier idBefore, DataTreeCandidateNode candidate) { - return new ModificationDiff( - Collections.singletonMap(idBefore, candidate.getDataBefore().get()), - Collections.emptyMap()); - } - - private static ModificationDiff create(YangInstanceIdentifier id, DataTreeCandidateNode candidate) { - return new ModificationDiff( - Collections.singletonMap(id, candidate.getDataBefore().get()), - Collections.singletonMap(id, candidate.getDataAfter().get())); - } - - private static ModificationDiff createFromAfter(YangInstanceIdentifier idAfter, DataTreeCandidateNode candidate) { - return new ModificationDiff( - Collections.emptyMap(), - Collections.singletonMap(idAfter, candidate.getDataAfter().get())); - } - - /** - * Produce a diff from a candidate node recursively. - */ - @Nonnull - static ModificationDiff recursivelyFromCandidate(@Nonnull final YangInstanceIdentifier yangIid, - @Nonnull final DataTreeCandidateNode currentCandidate) { - switch (currentCandidate.getModificationType()) { - case APPEARED: - case DISAPPEARED: - case UNMODIFIED: { - // (dis)appeared nodes are not important, no real data to process - return ModificationDiff.EMPTY_DIFF; - } - case WRITE: { - return currentCandidate.getDataBefore().isPresent() - ? ModificationDiff.create(yangIid, currentCandidate) - : ModificationDiff.createFromAfter(yangIid, currentCandidate); - // TODO HONEYCOMB-94 process children recursively to get modifications for child nodes - } - case DELETE: - return ModificationDiff.createFromBefore(yangIid, currentCandidate); - case SUBTREE_MODIFIED: { - // Modifications here are presented also for leaves. However that kind of granularity is not required - // So if there's a modified leaf, mark current complex node also as modification - java.util.Optional leavesModified = currentCandidate.getChildNodes().stream() - .filter(ModificationDiff::isLeaf) - // For some reason, we get modifications on unmodified list keys TODO debug and report ODL bug - // and that messes up our modifications collection here, so we need to skip - .filter(ModificationDiff::isModification) - .map(child -> LEAF_MODIFICATIONS.contains(child.getModificationType())) - .reduce((boolOne, boolTwo) -> boolOne || boolTwo); - - if (leavesModified.isPresent() && leavesModified.get()) { - return ModificationDiff.create(yangIid, currentCandidate); - // TODO HONEYCOMB-94 process children recursively to get modifications for child nodes even if current - // was modified - } else { - // SUBTREE MODIFIED, no modification on current, but process children recursively - return currentCandidate.getChildNodes().stream() - // not interested in modifications to leaves - .filter(child -> !isLeaf(child)) - .map(candidate -> recursivelyFromCandidate(yangIid.node(candidate.getIdentifier()), candidate)) - .reduce(ModificationDiff::merge) - .orElse(EMPTY_DIFF); - } - } - default: - throw new IllegalStateException("Unknown modification type: " - + currentCandidate.getModificationType() + ". Unsupported"); - } - } + return dataObjectBefore == null && dataObjectAfter == null + ? null + : DataObjectUpdate.create(baId, dataObjectBefore, dataObjectAfter); + } - /** - * Check whether candidate.before and candidate.after is different. If not - * return false. - */ - private static boolean isModification(final DataTreeCandidateNode candidateNode) { - if (candidateNode.getDataBefore().isPresent()) { - if (candidateNode.getDataAfter().isPresent()) { - return !candidateNode.getDataAfter().get().equals(candidateNode.getDataBefore().get()); - } else { - return true; - } + @Nullable + private static DataObject getDataObject(@Nonnull final BindingNormalizedNodeSerializer serializer, + @Nullable final NormalizedNode data, + @Nonnull final YangInstanceIdentifier id) { + DataObject dataObject = null; + if (data != null) { + final Map.Entry, DataObject> dataObjectEntry = + serializer.fromNormalizedNode(id, data); + if (dataObjectEntry != null) { + dataObject = dataObjectEntry.getValue(); } - - // considering not a modification if data after is also null - return candidateNode.getDataAfter().isPresent(); - } - - /** - * Check whether candidate node is for a leaf type node. - */ - private static boolean isLeaf(final DataTreeCandidateNode candidateNode) { - // orNull intentional, some candidate nodes have both data after and data before null - return candidateNode.getDataAfter().orNull() instanceof LeafNode - || candidateNode.getDataBefore().orNull() instanceof LeafNode; - } - - @Override - public String toString() { - return "ModificationDiff{" - + "modificationsBefore=" + modificationsBefore - + ", modificationsAfter=" + modificationsAfter - + '}'; } + return dataObject; } + } diff --git a/v3po/data-impl/src/main/java/io/fd/honeycomb/v3po/data/impl/ModificationDiff.java b/v3po/data-impl/src/main/java/io/fd/honeycomb/v3po/data/impl/ModificationDiff.java new file mode 100644 index 000000000..abc0062de --- /dev/null +++ b/v3po/data-impl/src/main/java/io/fd/honeycomb/v3po/data/impl/ModificationDiff.java @@ -0,0 +1,278 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.v3po.data.impl; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableMap; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.AugmentationNode; +import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode; +import org.opendaylight.yangtools.yang.data.api.schema.LeafNode; +import org.opendaylight.yangtools.yang.data.api.schema.MixinNode; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateNode; +import org.opendaylight.yangtools.yang.data.api.schema.tree.ModificationType; + +/** + * Recursively collects and provides all unique and non-null modifications (modified normalized nodes). + */ +final class ModificationDiff { + + private static final ModificationDiff EMPTY_DIFF = new ModificationDiff(Collections.emptyMap()); + private static final EnumSet LEAF_MODIFICATIONS = EnumSet.of(ModificationType.WRITE, ModificationType.DELETE); + + private final Map updates; + + private ModificationDiff(@Nonnull Map updates) { + this.updates = updates; + } + + /** + * Get processed modifications. + * + * @return mapped modifications, where key is keyed {@link YangInstanceIdentifier}. + */ + Map getUpdates() { + return updates; + } + + private ModificationDiff merge(final ModificationDiff other) { + if (this == EMPTY_DIFF) { + return other; + } + + if (other == EMPTY_DIFF) { + return this; + } + + return new ModificationDiff(join(updates, other.updates)); + } + + private static Map join(Map first, + Map second) { + final Map merged = new HashMap<>(); + merged.putAll(first); + merged.putAll(second); + return merged; + } + + private static ModificationDiff create(YangInstanceIdentifier id, DataTreeCandidateNode candidate) { + return new ModificationDiff(ImmutableMap.of(id, NormalizedNodeUpdate.create(id, candidate))); + } + + /** + * Produce an aggregated diff from a candidate node recursively. MixinNodes are ignored as modifications and so + * are complex nodes which direct leaves were not modified. + */ + @Nonnull + static ModificationDiff recursivelyFromCandidate(@Nonnull final YangInstanceIdentifier yangIid, + @Nonnull final DataTreeCandidateNode currentCandidate) { + // recursively process child nodes for exact modifications + return recursivelyChildrenFromCandidate(yangIid, currentCandidate) + // also add modification on current level, if elligible + .merge(isModification(currentCandidate) + ? ModificationDiff.create(yangIid, currentCandidate) + : EMPTY_DIFF); + } + + /** + * Check whether current node was modified. {@link MixinNode}s are ignored + * and only nodes which direct leaves(or choices) are modified are considered a modification. + */ + private static Boolean isModification(@Nonnull final DataTreeCandidateNode currentCandidate) { + // Mixin nodes are not considered modifications + if (isMixin(currentCandidate) && !isAugment(currentCandidate)) { + return false; + } else { + return isCurrentModified(currentCandidate); + } + } + + private static Boolean isCurrentModified(final @Nonnull DataTreeCandidateNode currentCandidate) { + // Check if there are any modified leaves and if so, consider current node as modified + final Boolean directLeavesModified = currentCandidate.getChildNodes().stream() + .filter(ModificationDiff::isLeaf) + // For some reason, we get modifications on unmodified list keys TODO debug and report ODL bug + // and that messes up our modifications collection here, so we need to skip + .filter(ModificationDiff::isBeforeAndAfterDifferent) + .filter(child -> LEAF_MODIFICATIONS.contains(child.getModificationType())) + .findFirst() + .isPresent(); + + return directLeavesModified + // Also check choices (choices do not exist in BA world and if anything within a choice was modified, + // consider its parent as being modified) + || currentCandidate.getChildNodes().stream() + .filter(ModificationDiff::isChoice) + // Recursively check each choice if there was any change to it + .filter(ModificationDiff::isCurrentModified) + .findFirst() + .isPresent(); + } + + /** + * Process all non-leaf child nodes recursively, creating aggregated {@link ModificationDiff}. + */ + private static ModificationDiff recursivelyChildrenFromCandidate(final @Nonnull YangInstanceIdentifier yangIid, + final @Nonnull DataTreeCandidateNode currentCandidate) { + // recursively process child nodes for specific modifications + return currentCandidate.getChildNodes().stream() + // not interested in modifications to leaves + .filter(child -> !isLeaf(child)) + .map(candidate -> recursivelyFromCandidate(yangIid.node(candidate.getIdentifier()), candidate)) + .reduce(ModificationDiff::merge) + .orElse(EMPTY_DIFF); + } + + /** + * Check whether candidate.before and candidate.after is different. If not return false. + */ + private static boolean isBeforeAndAfterDifferent(@Nonnull final DataTreeCandidateNode candidateNode) { + if (candidateNode.getDataBefore().isPresent()) { + return !candidateNode.getDataBefore().get().equals(candidateNode.getDataAfter().orNull()); + } + + // considering not a modification if data after is also null + return candidateNode.getDataAfter().isPresent(); + } + + /** + * Check whether candidate node is for a leaf type node. + */ + private static boolean isLeaf(final DataTreeCandidateNode candidateNode) { + // orNull intentional, some candidate nodes have both data after and data before null + return candidateNode.getDataAfter().orNull() instanceof LeafNode + || candidateNode.getDataBefore().orNull() instanceof LeafNode; + } + + /** + * Check whether candidate node is for a Mixin type node. + */ + private static boolean isMixin(final DataTreeCandidateNode candidateNode) { + // orNull intentional, some candidate nodes have both data after and data before null + return candidateNode.getDataAfter().orNull() instanceof MixinNode + || candidateNode.getDataBefore().orNull() instanceof MixinNode; + } + + /** + * Check whether candidate node is for an Augmentation type node. + */ + private static boolean isAugment(final DataTreeCandidateNode candidateNode) { + // orNull intentional, some candidate nodes have both data after and data before null + return candidateNode.getDataAfter().orNull() instanceof AugmentationNode + || candidateNode.getDataBefore().orNull() instanceof AugmentationNode; + } + + /** + * Check whether candidate node is for a Choice type node. + */ + private static boolean isChoice(final DataTreeCandidateNode candidateNode) { + // orNull intentional, some candidate nodes have both data after and data before null + return candidateNode.getDataAfter().orNull() instanceof ChoiceNode + || candidateNode.getDataBefore().orNull() instanceof ChoiceNode; + } + + @Override + public String toString() { + return "ModificationDiff{updates=" + updates + '}'; + } + + /** + * Update to a normalized node identifiable by its {@link YangInstanceIdentifier}. + */ + static final class NormalizedNodeUpdate { + + @Nonnull + private final YangInstanceIdentifier id; + @Nullable + private final NormalizedNode dataBefore; + @Nullable + private final NormalizedNode dataAfter; + + private NormalizedNodeUpdate(@Nonnull final YangInstanceIdentifier id, + @Nullable final NormalizedNode dataBefore, + @Nullable final NormalizedNode dataAfter) { + this.id = checkNotNull(id); + this.dataAfter = dataAfter; + this.dataBefore = dataBefore; + } + + @Nullable + public NormalizedNode getDataBefore() { + return dataBefore; + } + + @Nullable + public NormalizedNode getDataAfter() { + return dataAfter; + } + + @Nonnull + public YangInstanceIdentifier getId() { + return id; + } + + static NormalizedNodeUpdate create(@Nonnull final YangInstanceIdentifier id, + @Nonnull final DataTreeCandidateNode candidate) { + return create(id, candidate.getDataBefore().orNull(), candidate.getDataAfter().orNull()); + } + + static NormalizedNodeUpdate create(@Nonnull final YangInstanceIdentifier id, + @Nullable final NormalizedNode dataBefore, + @Nullable final NormalizedNode dataAfter) { + checkArgument(!(dataBefore == null && dataAfter == null), "Both before and after data are null"); + return new NormalizedNodeUpdate(id, dataBefore, dataAfter); + } + + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + + final NormalizedNodeUpdate that = (NormalizedNodeUpdate) other; + + return id.equals(that.id); + + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public String toString() { + return "NormalizedNodeUpdate{" + "id=" + id + + ", dataBefore=" + dataBefore + + ", dataAfter=" + dataAfter + + '}'; + } + } + +} diff --git a/v3po/data-impl/src/main/java/org/opendaylight/yang/gen/v1/urn/honeycomb/params/xml/ns/yang/data/impl/rev160411/ConfigDataTreeModule.java b/v3po/data-impl/src/main/java/org/opendaylight/yang/gen/v1/urn/honeycomb/params/xml/ns/yang/data/impl/rev160411/ConfigDataTreeModule.java index 3e871e09a..eabcdcbc8 100644 --- a/v3po/data-impl/src/main/java/org/opendaylight/yang/gen/v1/urn/honeycomb/params/xml/ns/yang/data/impl/rev160411/ConfigDataTreeModule.java +++ b/v3po/data-impl/src/main/java/org/opendaylight/yang/gen/v1/urn/honeycomb/params/xml/ns/yang/data/impl/rev160411/ConfigDataTreeModule.java @@ -38,8 +38,8 @@ public class ConfigDataTreeModule extends public java.lang.AutoCloseable createInstance() { LOG.debug("ConfigDataTreeModule.createInstance()"); return new CloseableConfigDataTree( - new ModifiableDataTreeDelegator(getSerializerDependency(), getDataTreeDependency(), getWriterRegistryDependency(), - getContextBindingBrokerDependency())); + new ModifiableDataTreeDelegator(getSerializerDependency(), getDataTreeDependency(), + getWriterRegistryBuilderDependency().build(), getContextBindingBrokerDependency())); } private static final class CloseableConfigDataTree implements ModifiableDataManager, AutoCloseable { diff --git a/v3po/data-impl/src/main/yang/data-impl.yang b/v3po/data-impl/src/main/yang/data-impl.yang index 0ea76cf0e..922846371 100644 --- a/v3po/data-impl/src/main/yang/data-impl.yang +++ b/v3po/data-impl/src/main/yang/data-impl.yang @@ -108,11 +108,11 @@ module data-impl { } } - container writer-registry { + container writer-registry-builder { uses config:service-ref { refine type { mandatory true; - config:required-identity tapi:honeycomb-writer-registry; + config:required-identity tapi:honeycomb-writer-registry-builder; } } } diff --git a/v3po/data-impl/src/test/java/io/fd/honeycomb/v3po/data/impl/ModifiableDataTreeDelegatorTest.java b/v3po/data-impl/src/test/java/io/fd/honeycomb/v3po/data/impl/ModifiableDataTreeDelegatorTest.java index 086636de6..c2653661a 100644 --- a/v3po/data-impl/src/test/java/io/fd/honeycomb/v3po/data/impl/ModifiableDataTreeDelegatorTest.java +++ b/v3po/data-impl/src/test/java/io/fd/honeycomb/v3po/data/impl/ModifiableDataTreeDelegatorTest.java @@ -16,27 +16,34 @@ package io.fd.honeycomb.v3po.data.impl; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyMap; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; import com.google.common.base.Optional; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; import com.google.common.util.concurrent.CheckedFuture; import com.google.common.util.concurrent.Futures; import io.fd.honeycomb.v3po.data.DataModification; import io.fd.honeycomb.v3po.translate.TranslationException; +import io.fd.honeycomb.v3po.translate.write.DataObjectUpdate; import io.fd.honeycomb.v3po.translate.write.WriteContext; import io.fd.honeycomb.v3po.translate.write.WriterRegistry; +import java.util.AbstractMap; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -46,18 +53,14 @@ import org.mockito.Mock; import org.opendaylight.controller.md.sal.binding.api.DataBroker; import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException; import org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeSerializer; -import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.v3po.rev150105.interfaces._interface.Ethernet; import org.opendaylight.yangtools.yang.binding.DataObject; import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; -import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; +import org.opendaylight.yangtools.yang.data.api.schema.MapNode; import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTree; -import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate; -import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateNode; -import org.opendaylight.yangtools.yang.data.api.schema.tree.ModificationType; public class ModifiableDataTreeDelegatorTest { @@ -65,112 +68,66 @@ public class ModifiableDataTreeDelegatorTest { private WriterRegistry writer; @Mock private BindingNormalizedNodeSerializer serializer; - @Mock private DataTree dataTree; @Mock private org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeModification modification; @Mock private DataBroker contextBroker; + @Mock + private org.opendaylight.controller.md.sal.binding.api.ReadWriteTransaction tx; private ModifiableDataTreeManager configDataTree; + static final InstanceIdentifier DEFAULT_ID = InstanceIdentifier.create(DataObject.class); + static DataObject DEFAULT_DATA_OBJECT = mockDataObject("serialized", DataObject.class); + @Before - public void setUp() { + public void setUp() throws Exception { initMocks(this); - configDataTree = new ModifiableDataTreeDelegator(serializer, dataTree, writer, contextBroker); - } - - @Test - public void testRead() throws Exception { - final org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeSnapshot - snapshot = mock(org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeSnapshot.class); - when(dataTree.takeSnapshot()).thenReturn(snapshot); - when(snapshot.newModification()).thenReturn(modification); + dataTree = ModificationDiffTest.getDataTree(); + when(contextBroker.newReadWriteTransaction()).thenReturn(tx); + when(tx.submit()).thenReturn(Futures.immediateCheckedFuture(null)); - final YangInstanceIdentifier path = mock(YangInstanceIdentifier.class); - final Optional node = mock(Optional.class); - doReturn(node).when(modification).readNode(path); + when(serializer.fromYangInstanceIdentifier(any(YangInstanceIdentifier.class))).thenReturn(((InstanceIdentifier) DEFAULT_ID)); + final Map.Entry, DataObject> parsed = new AbstractMap.SimpleEntry<>(DEFAULT_ID, DEFAULT_DATA_OBJECT); + when(serializer.fromNormalizedNode(any(YangInstanceIdentifier.class), any(NormalizedNode.class))).thenReturn(parsed); - final DataModification dataTreeSnapshot = configDataTree.newModification(); - final CheckedFuture>, ReadFailedException> future = - dataTreeSnapshot.read(path); - - verify(dataTree, times(2)).takeSnapshot(); - verify(modification).readNode(path); - - assertTrue(future.isDone()); - assertEquals(node, future.get()); + configDataTree = new ModifiableDataTreeDelegator(serializer, dataTree, writer, contextBroker); } @Test - public void testNewModification() throws Exception { - final org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeSnapshot - snapshot = mock(org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeSnapshot.class); - when(dataTree.takeSnapshot()).thenReturn(snapshot); - when(snapshot.newModification()).thenReturn(modification); - - configDataTree.newModification(); - // Snapshot captured twice, so that original data could be provided to translation layer without any possible - // modification - verify(dataTree, times(2)).takeSnapshot(); - verify(snapshot, times(2)).newModification(); + public void testRead() throws Exception { + final ContainerNode topContainer = ModificationDiffTest.getTopContainer("topContainer"); + ModificationDiffTest.addNodeToTree(dataTree, topContainer, ModificationDiffTest.TOP_CONTAINER_ID); + final CheckedFuture>, ReadFailedException> read = + configDataTree.read(ModificationDiffTest.TOP_CONTAINER_ID); + final CheckedFuture>, ReadFailedException> read2 = + configDataTree.newModification().read(ModificationDiffTest.TOP_CONTAINER_ID); + final Optional> normalizedNodeOptional = read.get(); + final Optional> normalizedNodeOptional2 = read2.get(); + + assertEquals(normalizedNodeOptional, normalizedNodeOptional2); + assertTrue(normalizedNodeOptional.isPresent()); + assertEquals(topContainer, normalizedNodeOptional.get()); + assertEquals(dataTree.takeSnapshot().readNode(ModificationDiffTest.TOP_CONTAINER_ID), normalizedNodeOptional); } @Test public void testCommitSuccessful() throws Exception { - final org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeSnapshot - snapshot = mock(org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeSnapshot.class); - when(dataTree.takeSnapshot()).thenReturn(snapshot); - when(snapshot.newModification()).thenReturn(modification); - final DataTreeCandidate prepare = mock(DataTreeCandidate.class); - doReturn(prepare).when(dataTree).prepare(modification); - - final org.opendaylight.controller.md.sal.binding.api.ReadWriteTransaction ctxTransaction = mock( - org.opendaylight.controller.md.sal.binding.api.ReadWriteTransaction.class); - doReturn(ctxTransaction).when(contextBroker).newReadWriteTransaction(); - doReturn(Futures.immediateCheckedFuture(null)).when(ctxTransaction).submit(); - - final DataObject dataBefore = mockDataObject("before", Ethernet.class); - final DataObject dataAfter = mockDataObject("after", Ethernet.class); - - // Prepare modification: - final DataTreeCandidateNode rootNode = mockRootNode(); - doReturn(ModificationType.SUBTREE_MODIFIED).when(rootNode).getModificationType(); - doReturn(mock(YangInstanceIdentifier.PathArgument.class)).when(rootNode).getIdentifier(); - final DataTreeCandidateNode childNode = mock(DataTreeCandidateNode.class); - doReturn(mock(YangInstanceIdentifier.PathArgument.class)).when(childNode).getIdentifier(); - doReturn(Collections.singleton(childNode)).when(rootNode).getChildNodes(); - doReturn(ModificationType.WRITE).when(childNode).getModificationType(); - - // data before: - final ContainerNode nodeBefore = mockContainerNode(dataBefore); - when(childNode.getDataBefore()).thenReturn(Optional.fromNullable(nodeBefore)); - // data after: - final ContainerNode nodeAfter = mockContainerNode(dataAfter); - when(childNode.getDataAfter()).thenReturn(Optional.fromNullable(nodeAfter)); - - // Run the test - doReturn(rootNode).when(prepare).getRootNode(); + final MapNode nestedList = ModificationDiffTest.getNestedList("listEntry", "listValue"); + final DataModification dataModification = configDataTree.newModification(); + dataModification.write(ModificationDiffTest.NESTED_LIST_ID, nestedList); + dataModification.validate(); dataModification.commit(); - // Verify all changes were processed: - verify(writer).update( - mapOf(dataBefore, Ethernet.class), - mapOf(dataAfter, Ethernet.class), - any(WriteContext.class)); - - // Verify modification was validated - verify(dataTree).validate(modification); - // Verify context transaction was finished - verify(ctxTransaction).submit(); - } - - private Map, DataObject> mapOf(final DataObject dataBefore, final Class type) { - return eq(Collections.singletonMap(InstanceIdentifier.create(type),dataBefore)); + final Multimap, DataObjectUpdate> map = HashMultimap.create(); + map.put(DEFAULT_ID, DataObjectUpdate.create(DEFAULT_ID, DEFAULT_DATA_OBJECT, DEFAULT_DATA_OBJECT)); + verify(writer).update(eq(new WriterRegistry.DataObjectUpdates(map, ImmutableMultimap.of())), any(WriteContext.class)); + assertEquals(nestedList, dataTree.takeSnapshot().readNode(ModificationDiffTest.NESTED_LIST_ID).get()); } - private DataObject mockDataObject(final String name, final Class classToMock) { + private static DataObject mockDataObject(final String name, final Class classToMock) { final DataObject dataBefore = mock(classToMock, name); doReturn(classToMock).when(dataBefore).getImplementedInterface(); return dataBefore; @@ -178,171 +135,135 @@ public class ModifiableDataTreeDelegatorTest { @Test public void testCommitUndoSuccessful() throws Exception { - final org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeSnapshot - snapshot = mock(org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeSnapshot.class); - when(dataTree.takeSnapshot()).thenReturn(snapshot); - when(snapshot.newModification()).thenReturn(modification); - final DataTreeCandidate prepare = mock(DataTreeCandidate.class); - doReturn(prepare).when(dataTree).prepare(modification); - - // Prepare data changes: - final DataObject dataBefore = mockDataObject("before", Ethernet.class); - final DataObject dataAfter = mockDataObject("after", Ethernet.class); - - final io.fd.honeycomb.v3po.translate.write.WriterRegistry.Reverter reverter = mock( - io.fd.honeycomb.v3po.translate.write.WriterRegistry.Reverter.class); + final MapNode nestedList = ModificationDiffTest.getNestedList("listEntry", "listValue"); // Fail on update: + final WriterRegistry.Reverter reverter = mock(WriterRegistry.Reverter.class); final TranslationException failedOnUpdateException = new TranslationException("update failed"); - doThrow(new io.fd.honeycomb.v3po.translate.write.WriterRegistry.BulkUpdateException(InstanceIdentifier.create(Ethernet.class), reverter, - failedOnUpdateException)).when(writer).update(anyMap(), anyMap(), any(WriteContext.class)); - - // Prepare modification: - final DataTreeCandidateNode rootNode = mockRootNode(); - doReturn(ModificationType.SUBTREE_MODIFIED).when(rootNode).getModificationType(); - doReturn(mock(YangInstanceIdentifier.PathArgument.class)).when(rootNode).getIdentifier(); - final DataTreeCandidateNode childNode = mock(DataTreeCandidateNode.class); - doReturn(mock(YangInstanceIdentifier.PathArgument.class)).when(childNode).getIdentifier(); - doReturn(Collections.singleton(childNode)).when(rootNode).getChildNodes(); - doReturn(ModificationType.WRITE).when(childNode).getModificationType(); - - // data before: - final ContainerNode nodeBefore = mockContainerNode(dataBefore); - when(childNode.getDataBefore()).thenReturn(Optional.fromNullable(nodeBefore)); - // data after: - final ContainerNode nodeAfter = mockContainerNode(dataAfter); - when(childNode.getDataAfter()).thenReturn(Optional.fromNullable(nodeAfter)); - - // Run the test + doThrow(new WriterRegistry.BulkUpdateException(Collections.singleton(DEFAULT_ID), reverter, failedOnUpdateException)) + .when(writer).update(any(WriterRegistry.DataObjectUpdates.class), any(WriteContext.class)); + try { - doReturn(rootNode).when(prepare).getRootNode(); + // Run the test final DataModification dataModification = configDataTree.newModification(); + dataModification.write(ModificationDiffTest.NESTED_LIST_ID, nestedList); + dataModification.validate(); dataModification.commit(); - } catch (io.fd.honeycomb.v3po.translate.write.WriterRegistry.BulkUpdateException e) { - verify(writer).update(anyMap(), anyMap(), any(WriteContext.class)); + fail("WriterRegistry.BulkUpdateException was expected"); + } catch (WriterRegistry.BulkUpdateException e) { + verify(writer).update(any(WriterRegistry.DataObjectUpdates.class), any(WriteContext.class)); + assertThat(e.getFailedIds(), hasItem(DEFAULT_ID)); verify(reverter).revert(); assertEquals(failedOnUpdateException, e.getCause()); - return; } - - fail("WriterRegistry.BulkUpdateException was expected"); } @Test public void testCommitUndoFailed() throws Exception { - final org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeSnapshot - snapshot = mock(org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeSnapshot.class); - when(dataTree.takeSnapshot()).thenReturn(snapshot); - when(snapshot.newModification()).thenReturn(modification); - final DataTreeCandidate prepare = mock(DataTreeCandidate.class); - doReturn(prepare).when(dataTree).prepare(modification); - - // Prepare data changes: - final DataObject dataBefore = mockDataObject("before", Ethernet.class); - final DataObject dataAfter = mockDataObject("after", Ethernet.class); - - final io.fd.honeycomb.v3po.translate.write.WriterRegistry.Reverter reverter = mock( - io.fd.honeycomb.v3po.translate.write.WriterRegistry.Reverter.class); + final MapNode nestedList = ModificationDiffTest.getNestedList("listEntry", "listValue"); // Fail on update: - doThrow(new io.fd.honeycomb.v3po.translate.write.WriterRegistry.BulkUpdateException(InstanceIdentifier.create(Ethernet.class), reverter, - new TranslationException("update failed"))).when(writer).update(anyMap(), anyMap(), any(WriteContext.class)); + final WriterRegistry.Reverter reverter = mock(WriterRegistry.Reverter.class); + final TranslationException failedOnUpdateException = new TranslationException("update failed"); + doThrow(new WriterRegistry.BulkUpdateException(Collections.singleton(DEFAULT_ID), reverter, failedOnUpdateException)) + .when(writer).update(any(WriterRegistry.DataObjectUpdates.class), any(WriteContext.class)); // Fail on revert: - final TranslationException failedOnRevertException = new TranslationException("update failed"); - final io.fd.honeycomb.v3po.translate.write.WriterRegistry.Reverter.RevertFailedException revertFailedException = - new io.fd.honeycomb.v3po.translate.write.WriterRegistry.Reverter.RevertFailedException(Collections.>emptyList(), - failedOnRevertException); - doThrow(revertFailedException).when(reverter).revert(); - - // Prepare modification: - final DataTreeCandidateNode rootNode = mockRootNode(); - doReturn(ModificationType.SUBTREE_MODIFIED).when(rootNode).getModificationType(); - doReturn(mock(YangInstanceIdentifier.PathArgument.class)).when(rootNode).getIdentifier(); - final DataTreeCandidateNode childNode = mock(DataTreeCandidateNode.class); - doReturn(mock(YangInstanceIdentifier.PathArgument.class)).when(childNode).getIdentifier(); - doReturn(Collections.singleton(childNode)).when(rootNode).getChildNodes(); - doReturn(ModificationType.WRITE).when(childNode).getModificationType(); - - // data before: - final ContainerNode nodeBefore = mockContainerNode(dataBefore); - when(childNode.getDataBefore()).thenReturn(Optional.fromNullable(nodeBefore)); - // data after: - final ContainerNode nodeAfter = mockContainerNode(dataAfter); - when(childNode.getDataAfter()).thenReturn(Optional.fromNullable(nodeAfter)); - - // Run the test + final TranslationException failedOnRevertException = new TranslationException("revert failed"); + doThrow(new WriterRegistry.Reverter.RevertFailedException(Collections.emptySet(), failedOnRevertException)) + .when(reverter).revert(); + try { - doReturn(rootNode).when(prepare).getRootNode(); + // Run the test final DataModification dataModification = configDataTree.newModification(); + dataModification.write(ModificationDiffTest.NESTED_LIST_ID, nestedList); + dataModification.validate(); dataModification.commit(); - } catch (io.fd.honeycomb.v3po.translate.write.WriterRegistry.Reverter.RevertFailedException e) { - verify(writer).update(anyMap(), anyMap(), any(WriteContext.class)); + fail("WriterRegistry.Reverter.RevertFailedException was expected"); + } catch (WriterRegistry.Reverter.RevertFailedException e) { + verify(writer).update(any(WriterRegistry.DataObjectUpdates.class), any(WriteContext.class)); verify(reverter).revert(); assertEquals(failedOnRevertException, e.getCause()); - return; } - - fail("RevertFailedException was expected"); } + private abstract static class DataObject1 implements DataObject {} + private abstract static class DataObject2 implements DataObject {} + private abstract static class DataObject3 implements DataObject {} + @Test - public void testChildrenFromNormalized() throws Exception { - final BindingNormalizedNodeSerializer serializer = mock(BindingNormalizedNodeSerializer.class); - - final Map> map = new HashMap<>(); - - // init child1 (will not be serialized) - final DataContainerChild child1 = mock(DataContainerChild.class); - when(child1.getIdentifier()).thenReturn(mock(YangInstanceIdentifier.PathArgument.class)); - when(serializer.fromNormalizedNode(any(YangInstanceIdentifier.class), eq(child1))).thenReturn(null); - map.put(mock(YangInstanceIdentifier.class), child1); - - // init child 2 (will be serialized) - final DataContainerChild child2 = mock(DataContainerChild.class); - when(child2.getIdentifier()).thenReturn(mock(YangInstanceIdentifier.PathArgument.class)); - - final Map.Entry entry = mock(Map.Entry.class); - final InstanceIdentifier id = mock(InstanceIdentifier.class); - doReturn(id).when(entry).getKey(); - final DataObject data = mock(DataObject.class); - doReturn(data).when(entry).getValue(); - when(serializer.fromNormalizedNode(any(YangInstanceIdentifier.class), eq(child2))).thenReturn(entry); - map.put(mock(YangInstanceIdentifier.class), child2); - - // run tested method - final Map, DataObject> baMap = - ModifiableDataTreeDelegator.toBindingAware(map, serializer); - assertEquals(1, baMap.size()); - assertEquals(data, baMap.get(id)); + public void testToBindingAware() throws Exception { + when(serializer.fromNormalizedNode(any(YangInstanceIdentifier.class), eq(null))).thenReturn(null); + + final Map biNodes = new HashMap<>(); + // delete + final QName nn1 = QName.create("namespace", "nn1"); + final YangInstanceIdentifier yid1 = mockYid(nn1); + final InstanceIdentifier iid1 = mockIid(yid1, DataObject1.class); + final NormalizedNode nn1B = mockNormalizedNode(nn1); + final DataObject1 do1B = mockDataObject(yid1, iid1, nn1B, DataObject1.class); + biNodes.put(yid1, ModificationDiff.NormalizedNodeUpdate.create(yid1, nn1B, null)); + + // create + final QName nn2 = QName.create("namespace", "nn1"); + final YangInstanceIdentifier yid2 = mockYid(nn2); + final InstanceIdentifier iid2 = mockIid(yid2, DataObject2.class);; + final NormalizedNode nn2A = mockNormalizedNode(nn2); + final DataObject2 do2A = mockDataObject(yid2, iid2, nn2A, DataObject2.class); + biNodes.put(yid2, ModificationDiff.NormalizedNodeUpdate.create(yid2, null, nn2A)); + + // update + final QName nn3 = QName.create("namespace", "nn1"); + final YangInstanceIdentifier yid3 = mockYid(nn3); + final InstanceIdentifier iid3 = mockIid(yid3, DataObject3.class); + final NormalizedNode nn3B = mockNormalizedNode(nn3); + final DataObject3 do3B = mockDataObject(yid3, iid3, nn3B, DataObject3.class); + final NormalizedNode nn3A = mockNormalizedNode(nn3); + final DataObject3 do3A = mockDataObject(yid3, iid3, nn3A, DataObject3.class);; + biNodes.put(yid3, ModificationDiff.NormalizedNodeUpdate.create(yid3, nn3B, nn3A)); + + final WriterRegistry.DataObjectUpdates dataObjectUpdates = + ModifiableDataTreeDelegator.toBindingAware(biNodes, serializer); + + assertThat(dataObjectUpdates.getDeletes().size(), is(1)); + assertThat(dataObjectUpdates.getDeletes().keySet(), hasItem(((InstanceIdentifier) iid1))); + assertThat(dataObjectUpdates.getDeletes().values(), hasItem( + ((DataObjectUpdate.DataObjectDelete) DataObjectUpdate.create(iid1, do1B, null)))); + + assertThat(dataObjectUpdates.getUpdates().size(), is(2)); + assertThat(dataObjectUpdates.getUpdates().keySet(), hasItems((InstanceIdentifier) iid2, (InstanceIdentifier) iid3)); + assertThat(dataObjectUpdates.getUpdates().values(), hasItems( + DataObjectUpdate.create(iid2, null, do2A), + DataObjectUpdate.create(iid3, do3B, do3A))); + + assertThat(dataObjectUpdates.getTypeIntersection().size(), is(3)); } - private DataTreeCandidateNode mockRootNode() { - final DataTreeCandidate candidate = mock(DataTreeCandidate.class); - when(dataTree.prepare(modification)).thenReturn(candidate); - - final DataTreeCandidateNode rootNode = mock(DataTreeCandidateNode.class); - when(candidate.getRootNode()).thenReturn(rootNode); - - return rootNode; + private D mockDataObject(final YangInstanceIdentifier yid1, + final InstanceIdentifier iid1, + final NormalizedNode nn1B, + final Class type) { + final D do1B = mock(type); + when(serializer.fromNormalizedNode(yid1, nn1B)).thenReturn(new AbstractMap.SimpleEntry<>(iid1, do1B)); + return do1B; } - private ContainerNode mockContainerNode(DataObject modification) { - final YangInstanceIdentifier.NodeIdentifier identifier = - YangInstanceIdentifier.NodeIdentifier.create(QName.create("/")); - - final ContainerNode node = mock(ContainerNode.class); - when(node.getIdentifier()).thenReturn(identifier); - - final Map.Entry entry = mock(Map.Entry.class); - final Class implementedInterface = - (Class) modification.getImplementedInterface(); - final InstanceIdentifier id = InstanceIdentifier.create(implementedInterface); + private NormalizedNode mockNormalizedNode(final QName nn1) { + final NormalizedNode nn1B = mock(NormalizedNode.class); + when(nn1B.getNodeType()).thenReturn(nn1); + return nn1B; + } - doReturn(id).when(entry).getKey(); - doReturn(modification).when(entry).getValue(); - doReturn(entry).when(serializer).fromNormalizedNode(any(YangInstanceIdentifier.class), eq(node)); + private InstanceIdentifier mockIid(final YangInstanceIdentifier yid1, + final Class type) { + final InstanceIdentifier iid1 = InstanceIdentifier.create(type); + when(serializer.fromYangInstanceIdentifier(yid1)).thenReturn(iid1); + return iid1; + } - return node; + private YangInstanceIdentifier mockYid(final QName nn1) { + final YangInstanceIdentifier yid1 = mock(YangInstanceIdentifier.class); + when(yid1.getLastPathArgument()).thenReturn(new YangInstanceIdentifier.NodeIdentifier(nn1)); + return yid1; } } diff --git a/v3po/data-impl/src/test/java/io/fd/honeycomb/v3po/data/impl/ModificationDiffTest.java b/v3po/data-impl/src/test/java/io/fd/honeycomb/v3po/data/impl/ModificationDiffTest.java index 3475973d6..bc7582e93 100644 --- a/v3po/data-impl/src/test/java/io/fd/honeycomb/v3po/data/impl/ModificationDiffTest.java +++ b/v3po/data-impl/src/test/java/io/fd/honeycomb/v3po/data/impl/ModificationDiffTest.java @@ -1,7 +1,8 @@ package io.fd.honeycomb.v3po.data.impl; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; import java.util.Map; import org.junit.Test; @@ -11,6 +12,8 @@ import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode; import org.opendaylight.yangtools.yang.data.api.schema.MapNode; import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTree; +import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate; import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateTip; import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeModification; import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeSnapshot; @@ -28,15 +31,23 @@ import org.opendaylight.yangtools.yang.parser.stmt.rfc6020.YangStatementSourceIm public class ModificationDiffTest { - private static final QName TOP_CONTAINER_QNAME = + static final QName TOP_CONTAINER_QNAME = QName.create("urn:opendaylight:params:xml:ns:yang:test:diff", "2015-01-05", "top-container"); - private static final QName STRING_LEAF_QNAME = QName.create(TOP_CONTAINER_QNAME, "string"); - private static final QName NAME_LEAF_QNAME = QName.create(TOP_CONTAINER_QNAME, "name"); - private static final QName TEXT_LEAF_QNAME = QName.create(TOP_CONTAINER_QNAME, "text"); - private static final QName NESTED_LIST_QNAME = QName.create(TOP_CONTAINER_QNAME, "nested-list"); - private static final QName DEEP_LIST_QNAME = QName.create(TOP_CONTAINER_QNAME, "deep-list"); + static final QName STRING_LEAF_QNAME = QName.create(TOP_CONTAINER_QNAME, "string"); + static final QName NAME_LEAF_QNAME = QName.create(TOP_CONTAINER_QNAME, "name"); + static final QName TEXT_LEAF_QNAME = QName.create(TOP_CONTAINER_QNAME, "text"); + static final QName NESTED_LIST_QNAME = QName.create(TOP_CONTAINER_QNAME, "nested-list"); + static final QName DEEP_LIST_QNAME = QName.create(TOP_CONTAINER_QNAME, "deep-list"); + + static final QName WITH_CHOICE_CONTAINER_QNAME = + QName.create("urn:opendaylight:params:xml:ns:yang:test:diff", "2015-01-05", "with-choice"); + static final QName CHOICE_QNAME = QName.create(WITH_CHOICE_CONTAINER_QNAME, "choice"); + static final QName IN_CASE1_LEAF_QNAME = QName.create(WITH_CHOICE_CONTAINER_QNAME, "in-case1"); + static final QName IN_CASE2_LEAF_QNAME = QName.create(WITH_CHOICE_CONTAINER_QNAME, "in-case2"); + + static final YangInstanceIdentifier TOP_CONTAINER_ID = YangInstanceIdentifier.of(TOP_CONTAINER_QNAME); + static final YangInstanceIdentifier NESTED_LIST_ID = TOP_CONTAINER_ID.node(new YangInstanceIdentifier.NodeIdentifier(NESTED_LIST_QNAME)); - private static final YangInstanceIdentifier TOP_CONTAINER_ID = YangInstanceIdentifier.of(TOP_CONTAINER_QNAME); @Test public void testInitialWrite() throws Exception { @@ -47,10 +58,33 @@ public class ModificationDiffTest { dataTreeModification.write(TOP_CONTAINER_ID, topContainer); final DataTreeCandidateTip prepare = prepareModification(dataTree, dataTreeModification); - final ModifiableDataTreeDelegator.ModificationDiff modificationDiff = getModificationDiff(prepare); + final ModificationDiff modificationDiff = getModificationDiff(prepare); - assertTrue(modificationDiff.getModificationsBefore().isEmpty()); - assertAfter(topContainer, TOP_CONTAINER_ID, modificationDiff); + assertThat(modificationDiff.getUpdates().size(), is(1)); + assertThat(modificationDiff.getUpdates().values().size(), is(1)); + assertUpdate(modificationDiff.getUpdates().values().iterator().next(), TOP_CONTAINER_ID, null, topContainer); + } + + @Test + public void testInitialWriteForContainerWithChoice() throws Exception { + final TipProducingDataTree dataTree = getDataTree(); + final DataTreeModification dataTreeModification = getModification(dataTree); + final ContainerNode containerWithChoice = Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(WITH_CHOICE_CONTAINER_QNAME)) + .withChild(Builders.choiceBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(CHOICE_QNAME)) + .withChild(ImmutableNodes.leafNode(IN_CASE1_LEAF_QNAME, "withinCase1")) + .build()) + .build(); + final YangInstanceIdentifier WITH_CHOICE_CONTAINER_ID = YangInstanceIdentifier.of(WITH_CHOICE_CONTAINER_QNAME); + dataTreeModification.write(WITH_CHOICE_CONTAINER_ID, containerWithChoice); + final DataTreeCandidateTip prepare = prepareModification(dataTree, dataTreeModification); + + final Map updates = getModificationDiff(prepare).getUpdates(); + + assertThat(updates.size(), is(1)); + assertUpdate(getNormalizedNodeUpdateForAfterType(updates, ContainerNode.class), + WITH_CHOICE_CONTAINER_ID, null, containerWithChoice); } private DataTreeModification getModification(final TipProducingDataTree dataTree) { @@ -66,10 +100,9 @@ public class ModificationDiffTest { dataTreeModification.write(TOP_CONTAINER_ID, topContainer); final DataTreeCandidateTip prepare = prepareModification(dataTree, dataTreeModification); - final ModifiableDataTreeDelegator.ModificationDiff modificationDiff = getModificationDiff(prepare); + final ModificationDiff modificationDiff = getModificationDiff(prepare); - assertTrue(modificationDiff.getModificationsBefore().isEmpty()); - assertTrue(modificationDiff.getModificationsAfter().isEmpty()); + assertThat(modificationDiff.getUpdates().size(), is(0)); } private DataTreeCandidateTip prepareModification(final TipProducingDataTree dataTree, @@ -83,60 +116,61 @@ public class ModificationDiffTest { @Test public void testUpdateWrite() throws Exception { final TipProducingDataTree dataTree = getDataTree(); - final NormalizedNode topContainerBefore = addTopContainer(dataTree); + final ContainerNode topContainer = getTopContainer("string1"); + addNodeToTree(dataTree, topContainer, TOP_CONTAINER_ID); final DataTreeModification dataTreeModification = getModification(dataTree); final NormalizedNode topContainerAfter = getTopContainer("string2"); dataTreeModification.write(TOP_CONTAINER_ID, topContainerAfter); final DataTreeCandidateTip prepare = prepareModification(dataTree, dataTreeModification); - final ModifiableDataTreeDelegator.ModificationDiff modificationDiff = getModificationDiff(prepare); + final Map updates = getModificationDiff(prepare).getUpdates(); - assertBefore(topContainerBefore, TOP_CONTAINER_ID, modificationDiff); - assertAfter(topContainerAfter, TOP_CONTAINER_ID, modificationDiff); + assertThat(updates.size(), is(1)); + assertThat(updates.values().size(), is(1)); + assertUpdate(updates.values().iterator().next(), TOP_CONTAINER_ID, topContainer, topContainerAfter); } - private ModifiableDataTreeDelegator.ModificationDiff getModificationDiff(final DataTreeCandidateTip prepare) { - return ModifiableDataTreeDelegator.ModificationDiff - .recursivelyFromCandidate(YangInstanceIdentifier.EMPTY, prepare.getRootNode()); + private ModificationDiff getModificationDiff(final DataTreeCandidateTip prepare) { + return ModificationDiff.recursivelyFromCandidate(YangInstanceIdentifier.EMPTY, prepare.getRootNode()); } @Test public void testUpdateMerge() throws Exception { final TipProducingDataTree dataTree = getDataTree(); - final NormalizedNode topContainerBefore = addTopContainer(dataTree); + final ContainerNode topContainer = getTopContainer("string1"); + addNodeToTree(dataTree, topContainer, TOP_CONTAINER_ID); final DataTreeModification dataTreeModification = getModification(dataTree); final NormalizedNode topContainerAfter = getTopContainer("string2"); dataTreeModification.merge(TOP_CONTAINER_ID, topContainerAfter); final DataTreeCandidateTip prepare = prepareModification(dataTree, dataTreeModification); - final ModifiableDataTreeDelegator.ModificationDiff modificationDiff = - getModificationDiff(prepare); - - assertBefore(topContainerBefore, TOP_CONTAINER_ID, modificationDiff); - assertAfter(topContainerAfter, TOP_CONTAINER_ID, modificationDiff); + final Map updates = getModificationDiff(prepare).getUpdates(); + assertThat(updates.size(), is(1)); + assertThat(updates.values().size(), is(1)); + assertUpdate(updates.values().iterator().next(), TOP_CONTAINER_ID, topContainer, topContainerAfter); } @Test public void testUpdateDelete() throws Exception { final TipProducingDataTree dataTree = getDataTree(); - final NormalizedNode topContainerBefore = addTopContainer(dataTree); + final ContainerNode topContainer = getTopContainer("string1"); + addNodeToTree(dataTree, topContainer, TOP_CONTAINER_ID); final DataTreeModification dataTreeModification = getModification(dataTree); dataTreeModification.delete(TOP_CONTAINER_ID); final DataTreeCandidateTip prepare = prepareModification(dataTree, dataTreeModification); - final ModifiableDataTreeDelegator.ModificationDiff modificationDiff = getModificationDiff(prepare); - - assertBefore(topContainerBefore, TOP_CONTAINER_ID, modificationDiff); - assertTrue(modificationDiff.getModificationsAfter().isEmpty()); + final Map updates = getModificationDiff(prepare).getUpdates(); + assertThat(updates.size(), is(1)); + assertThat(updates.values().size(), is(1)); + assertUpdate(updates.values().iterator().next(), TOP_CONTAINER_ID, topContainer, null); } @Test public void testWriteAndUpdateInnerList() throws Exception { final TipProducingDataTree dataTree = getDataTree(); - addTopContainer(dataTree); DataTreeSnapshot dataTreeSnapshot = dataTree.takeSnapshot(); DataTreeModification dataTreeModification = dataTreeSnapshot.newModification(); @@ -146,15 +180,17 @@ public class ModificationDiffTest { new YangInstanceIdentifier.NodeIdentifier(NESTED_LIST_QNAME)); final MapNode mapNode = getNestedList("name1", "text"); + final YangInstanceIdentifier listEntryId = listId.node(mapNode.getValue().iterator().next().getIdentifier()); dataTreeModification.write(listId, mapNode); dataTreeModification.ready(); dataTree.validate(dataTreeModification); DataTreeCandidateTip prepare = dataTree.prepare(dataTreeModification); - ModifiableDataTreeDelegator.ModificationDiff modificationDiff = getModificationDiff(prepare); + Map updates = getModificationDiff(prepare).getUpdates(); - assertTrue(modificationDiff.getModificationsBefore().isEmpty()); - assertAfter(mapNode, listId, modificationDiff); + assertThat(updates.size(), is(1)); + assertUpdate(getNormalizedNodeUpdateForAfterType(updates, MapEntryNode.class), + listEntryId, null, mapNode.getValue().iterator().next()); // Commit so that update can be tested next dataTree.commit(prepare); @@ -171,9 +207,17 @@ public class ModificationDiffTest { dataTree.validate(dataTreeModification); prepare = dataTree.prepare(dataTreeModification); - modificationDiff = getModificationDiff(prepare); - assertBefore(mapNode.getValue().iterator().next(), listItemId, modificationDiff); - assertAfter(mapEntryNode, listItemId, modificationDiff); + updates = getModificationDiff(prepare).getUpdates(); + assertThat(updates.size(), is(1 /*Actual list entry*/)); + } +// + private void assertUpdate(final ModificationDiff.NormalizedNodeUpdate update, + final YangInstanceIdentifier idExpected, + final NormalizedNode beforeExpected, + final NormalizedNode afterExpected) { + assertThat(update.getId(), is(idExpected)); + assertThat(update.getDataBefore(), is(beforeExpected)); + assertThat(update.getDataAfter(), is(afterExpected)); } @Test @@ -192,25 +236,44 @@ public class ModificationDiffTest { new YangInstanceIdentifier.NodeIdentifier(NESTED_LIST_QNAME)); final MapNode mapNode = getNestedList("name1", "text"); + final YangInstanceIdentifier listEntryId = listId.node(mapNode.getValue().iterator().next().getIdentifier()); + dataTreeModification.write(listId, mapNode); final DataTreeCandidateTip prepare = prepareModification(dataTree, dataTreeModification); - final ModifiableDataTreeDelegator.ModificationDiff modificationDiff = getModificationDiff(prepare); + final Map updates = getModificationDiff(prepare).getUpdates(); + + assertThat(updates.size(), is(2)); + assertThat(updates.values().size(), is(2)); + assertUpdate(getNormalizedNodeUpdateForAfterType(updates, ContainerNode.class), TOP_CONTAINER_ID, null, + Builders.containerBuilder(topContainer).withChild(mapNode).build()); + assertUpdate(getNormalizedNodeUpdateForAfterType(updates, MapEntryNode.class), listEntryId, null, mapNode.getValue().iterator().next()); + // Assert that keys of the updates map are not wildcarded YID + assertThat(updates.keySet(), hasItems( + TOP_CONTAINER_ID, + listEntryId)); + } - assertTrue(modificationDiff.getModificationsBefore().isEmpty()); + private ModificationDiff.NormalizedNodeUpdate getNormalizedNodeUpdateForAfterType( + final Map updates, + final Class> containerNodeClass) { + return updates.values().stream() + .filter(update -> containerNodeClass.isAssignableFrom(update.getDataAfter().getClass())) + .findFirst().get(); + } - // TODO HONEYCOMB-94 2 after modifications should appear, for top-container and nested-list entry - assertAfter(Builders.containerBuilder(topContainer) - .withChild(mapNode) - .build(), - TOP_CONTAINER_ID, modificationDiff); + private ModificationDiff.NormalizedNodeUpdate getNormalizedNodeUpdateForBeforeType( + final Map updates, + final Class> containerNodeClass) { + return updates.values().stream() + .filter(update -> containerNodeClass.isAssignableFrom(update.getDataBefore().getClass())) + .findFirst().get(); } @Test public void testWriteDeepList() throws Exception { final TipProducingDataTree dataTree = getDataTree(); - addTopContainer(dataTree); DataTreeSnapshot dataTreeSnapshot = dataTree.takeSnapshot(); DataTreeModification dataTreeModification = dataTreeSnapshot.newModification(); @@ -250,7 +313,8 @@ public class ModificationDiffTest { .withChild(ImmutableNodes.leafNode(NAME_LEAF_QNAME, "name1")).build()); dataTreeModification.merge( deepListId, - Builders.mapBuilder().withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(DEEP_LIST_QNAME)) + Builders.mapBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(DEEP_LIST_QNAME)) .build()); dataTreeModification.merge( deepListEntryId, @@ -261,16 +325,14 @@ public class ModificationDiffTest { prepare = dataTree.prepare(dataTreeModification); dataTree.commit(prepare); - final ModifiableDataTreeDelegator.ModificationDiff modificationDiff = getModificationDiff(prepare); - - assertTrue(modificationDiff.getModificationsBefore().isEmpty()); - assertAfter(getDeepList("name1"), deepListId, modificationDiff); + final Map updates = getModificationDiff(prepare).getUpdates(); + assertThat(updates.size(), is(1)); + assertUpdate(getNormalizedNodeUpdateForAfterType(updates, MapEntryNode.class), deepListEntryId, null, deepListEntry); } @Test public void testDeleteInnerListItem() throws Exception { final TipProducingDataTree dataTree = getDataTree(); - addTopContainer(dataTree); DataTreeSnapshot dataTreeSnapshot = dataTree.takeSnapshot(); DataTreeModification dataTreeModification = dataTreeSnapshot.newModification(); @@ -298,58 +360,37 @@ public class ModificationDiffTest { dataTree.validate(dataTreeModification); prepare = dataTree.prepare(dataTreeModification); - final ModifiableDataTreeDelegator.ModificationDiff modificationDiff = getModificationDiff(prepare); - - assertBefore(mapNode.getValue().iterator().next(), listItemId, modificationDiff); - assertTrue(modificationDiff.getModificationsAfter().isEmpty()); + final Map updates = getModificationDiff(prepare).getUpdates(); + assertThat(updates.size(), is(1)); + assertUpdate(getNormalizedNodeUpdateForBeforeType(updates, MapEntryNode.class), listItemId, mapNode.getValue().iterator().next(), null); } - private NormalizedNode addTopContainer(final TipProducingDataTree dataTree) + static void addNodeToTree(final DataTree dataTree, final NormalizedNode node, + final YangInstanceIdentifier id) throws DataValidationFailedException { DataTreeSnapshot dataTreeSnapshot = dataTree.takeSnapshot(); DataTreeModification dataTreeModification = dataTreeSnapshot.newModification(); - final NormalizedNode topContainerBefore = getTopContainer("string1"); - dataTreeModification.write(TOP_CONTAINER_ID, topContainerBefore); + dataTreeModification.write(id, node); dataTreeModification.ready(); dataTree.validate(dataTreeModification); - DataTreeCandidateTip prepare = dataTree.prepare(dataTreeModification); + DataTreeCandidate prepare = dataTree.prepare(dataTreeModification); dataTree.commit(prepare); - return topContainerBefore; - } - - private void assertAfter(final NormalizedNode topContainer, final YangInstanceIdentifier TOP_CONTAINER_ID, - final ModifiableDataTreeDelegator.ModificationDiff modificationDiff) { - assertModification(topContainer, TOP_CONTAINER_ID, modificationDiff.getModificationsAfter()); - } - - private void assertModification(final NormalizedNode topContainer, - final YangInstanceIdentifier TOP_CONTAINER_ID, - final Map> modificationMap) { - assertEquals(1, modificationMap.keySet().size()); - assertEquals(TOP_CONTAINER_ID, modificationMap.keySet().iterator().next()); - assertEquals(topContainer, modificationMap.values().iterator().next()); - } - - private void assertBefore(final NormalizedNode topContainerBefore, - final YangInstanceIdentifier TOP_CONTAINER_ID, - final ModifiableDataTreeDelegator.ModificationDiff modificationDiff) { - assertModification(topContainerBefore, TOP_CONTAINER_ID, modificationDiff.getModificationsBefore()); } - private TipProducingDataTree getDataTree() throws ReactorException { + static TipProducingDataTree getDataTree() throws ReactorException { final TipProducingDataTree dataTree = InMemoryDataTreeFactory.getInstance().create(TreeType.CONFIGURATION); dataTree.setSchemaContext(getSchemaCtx()); return dataTree; } - private ContainerNode getTopContainer(final String stringValue) { + static ContainerNode getTopContainer(final String stringValue) { return Builders.containerBuilder() .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) .withChild(ImmutableNodes.leafNode(STRING_LEAF_QNAME, stringValue)) .build(); } - private MapNode getNestedList(final String listItemName, final String text) { + static MapNode getNestedList(final String listItemName, final String text) { return Builders.mapBuilder() .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_LIST_QNAME)) .withChild( @@ -378,9 +419,9 @@ public class ModificationDiffTest { .build(); } - private SchemaContext getSchemaCtx() throws ReactorException { + private static SchemaContext getSchemaCtx() throws ReactorException { final CrossSourceStatementReactor.BuildAction buildAction = YangInferencePipeline.RFC6020_REACTOR.newBuild(); - buildAction.addSource(new YangStatementSourceImpl(getClass().getResourceAsStream("/test-diff.yang"))); + buildAction.addSource(new YangStatementSourceImpl(ModificationDiffTest.class.getResourceAsStream("/test-diff.yang"))); return buildAction.buildEffective(); } } \ No newline at end of file diff --git a/v3po/data-impl/src/test/resources/test-diff.yang b/v3po/data-impl/src/test/resources/test-diff.yang index 7e8721f00..5cccc8718 100644 --- a/v3po/data-impl/src/test/resources/test-diff.yang +++ b/v3po/data-impl/src/test/resources/test-diff.yang @@ -34,4 +34,21 @@ module test-diff { } } + container with-choice { + + choice choice { + case case1 { + leaf in-case1 { + type string; + } + } + + case case2 { + leaf in-case2 { + type string; + } + } + } + } + } -- cgit 1.2.3-korg