From be05d84deebf8bd030bb6564d5cd49094f6da961 Mon Sep 17 00:00:00 2001 From: Jan Srnicek Date: Tue, 9 May 2017 15:28:14 +0200 Subject: HONEYCOMB-350 - APPEAR/DISAPPEAR modification handling Allows these types of modifications to check in depth, to see if some of their children nodes were not modified Change-Id: Ice2f988732c2d9ecad8e960c4f10d01863fb0cfd Signed-off-by: Jan Srnicek --- .../impl/AugmentationRewriteDeleteProducer.java | 42 ++ .../data/impl/CaseRewriteDeleteProducer.java | 42 ++ .../data/impl/ChoiceRewriteDeleteProducer.java | 42 ++ .../data/impl/ContainerRewriteDeleteProducer.java | 83 ++++ .../data/impl/DelegatingRewriteDeleteProducer.java | 68 +++ .../data/impl/LeafListRewriteDeleteProducer.java | 40 ++ .../data/impl/LeafRewriteDeleteProducer.java | 40 ++ .../data/impl/ListRewriteDeleteProducer.java | 44 ++ .../data/impl/ModifiableDataTreeDelegator.java | 14 +- .../io/fd/honeycomb/data/impl/Modification.java | 243 ++++++++++ .../fd/honeycomb/data/impl/ModificationDiff.java | 493 ++++++--------------- .../impl/NormalizedNodeRewriteDeleteRegistry.java | 101 +++++ .../honeycomb/data/impl/NormalizedNodeUpdate.java | 105 +++++ .../honeycomb/data/impl/RewriteDeleteProducer.java | 30 ++ .../data/impl/ModifiableDataTreeDelegatorTest.java | 38 +- .../honeycomb/data/impl/ModificationBaseTest.java | 367 +++++++++++++++ .../impl/ModificationDiffAugRewriteDeleteTest.java | 120 +++++ ...ModificationDiffNestedAugRewriteDeleteTest.java | 120 +++++ .../impl/ModificationDiffRewriteDeleteTest.java | 173 ++++++++ .../honeycomb/data/impl/ModificationDiffTest.java | 234 +++------- .../honeycomb/data/impl/ModificationMetadata.java | 68 +++ infra/data-impl/src/test/resources/test-diff.yang | 72 +++ 22 files changed, 2026 insertions(+), 553 deletions(-) create mode 100644 infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/AugmentationRewriteDeleteProducer.java create mode 100644 infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/CaseRewriteDeleteProducer.java create mode 100644 infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ChoiceRewriteDeleteProducer.java create mode 100644 infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ContainerRewriteDeleteProducer.java create mode 100644 infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/DelegatingRewriteDeleteProducer.java create mode 100644 infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/LeafListRewriteDeleteProducer.java create mode 100644 infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/LeafRewriteDeleteProducer.java create mode 100644 infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ListRewriteDeleteProducer.java create mode 100644 infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/Modification.java create mode 100644 infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/NormalizedNodeRewriteDeleteRegistry.java create mode 100644 infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/NormalizedNodeUpdate.java create mode 100644 infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/RewriteDeleteProducer.java create mode 100644 infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationBaseTest.java create mode 100644 infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationDiffAugRewriteDeleteTest.java create mode 100644 infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationDiffNestedAugRewriteDeleteTest.java create mode 100644 infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationDiffRewriteDeleteTest.java create mode 100644 infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationMetadata.java diff --git a/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/AugmentationRewriteDeleteProducer.java b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/AugmentationRewriteDeleteProducer.java new file mode 100644 index 000000000..1a9397eb4 --- /dev/null +++ b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/AugmentationRewriteDeleteProducer.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2017 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.data.impl; + +import java.util.Collection; +import java.util.Map; +import javax.annotation.Nonnull; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class AugmentationRewriteDeleteProducer extends DelegatingRewriteDeleteProducer implements RewriteDeleteProducer { + + private static final Logger LOG = LoggerFactory.getLogger(AugmentationRewriteDeleteProducer.class); + + AugmentationRewriteDeleteProducer( + @Nonnull final NormalizedNodeRewriteDeleteRegistry baseRewriteProducer) { + super(baseRewriteProducer); + } + + @Override + public Collection normalizedUpdates(@Nonnull final YangInstanceIdentifier topLevelIdentifier, + @Nonnull final Map.Entry> entry) { + LOG.debug("Processing {} as augmentation", topLevelIdentifier); + return super.normalizedUpdates(topLevelIdentifier, entry); + } +} diff --git a/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/CaseRewriteDeleteProducer.java b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/CaseRewriteDeleteProducer.java new file mode 100644 index 000000000..37fdef0e4 --- /dev/null +++ b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/CaseRewriteDeleteProducer.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2017 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.data.impl; + +import java.util.Collection; +import java.util.Map; +import javax.annotation.Nonnull; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class CaseRewriteDeleteProducer extends DelegatingRewriteDeleteProducer implements RewriteDeleteProducer { + + private static final Logger LOG = LoggerFactory.getLogger(CaseRewriteDeleteProducer.class); + + CaseRewriteDeleteProducer(@Nonnull final NormalizedNodeRewriteDeleteRegistry baseRewriteProducer) { + super(baseRewriteProducer); + } + + @Override + public Collection normalizedUpdates(@Nonnull final YangInstanceIdentifier topLevelIdentifier, + @Nonnull final Map.Entry> entry) { + // just delegates to lower level, there is nothing to detect on case level + LOG.debug("Processing {} as case", topLevelIdentifier); + return super.normalizedUpdates(topLevelIdentifier, entry); + } +} diff --git a/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ChoiceRewriteDeleteProducer.java b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ChoiceRewriteDeleteProducer.java new file mode 100644 index 000000000..4c249e084 --- /dev/null +++ b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ChoiceRewriteDeleteProducer.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2017 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.data.impl; + +import java.util.Collection; +import java.util.Map; +import javax.annotation.Nonnull; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class ChoiceRewriteDeleteProducer extends DelegatingRewriteDeleteProducer implements RewriteDeleteProducer { + + private static final Logger LOG = LoggerFactory.getLogger(ChoiceRewriteDeleteProducer.class); + + ChoiceRewriteDeleteProducer(@Nonnull final NormalizedNodeRewriteDeleteRegistry baseRewriteProducer) { + super(baseRewriteProducer); + } + + @Override + public Collection normalizedUpdates(@Nonnull final YangInstanceIdentifier topLevelIdentifier, + @Nonnull final Map.Entry> entry) { + // just delegates to lower level, there is nothing to detect on choice level + LOG.debug("Processing {} as choice", topLevelIdentifier); + return super.normalizedUpdates(topLevelIdentifier, entry); + } +} diff --git a/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ContainerRewriteDeleteProducer.java b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ContainerRewriteDeleteProducer.java new file mode 100644 index 000000000..7a2b31eea --- /dev/null +++ b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ContainerRewriteDeleteProducer.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2017 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.data.impl; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +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.model.api.ContainerSchemaNode; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.opendaylight.yangtools.yang.model.api.SchemaPath; +import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class ContainerRewriteDeleteProducer extends DelegatingRewriteDeleteProducer implements RewriteDeleteProducer { + + private static final Logger LOG = LoggerFactory.getLogger(ContainerRewriteDeleteProducer.class); + + private final SchemaContext ctx; + + ContainerRewriteDeleteProducer(@Nonnull final NormalizedNodeRewriteDeleteRegistry baseRewriteProducer, + @Nonnull final SchemaContext ctx) { + super(baseRewriteProducer); + this.ctx = ctx; + } + + @Override + public Collection normalizedUpdates(@Nonnull final YangInstanceIdentifier topLevelIdentifier, + @Nonnull final Map.Entry> entry) { + final ContainerSchemaNode containerSchemaNode = + (ContainerSchemaNode) SchemaContextUtil + .findDataSchemaNode(ctx, getSchemaPath(topLevelIdentifier, entry)); + + if (containerSchemaNode.isPresenceContainer()) { + LOG.debug("Processing {} as presence container", topLevelIdentifier); + // if presence container - create delete right away + return ((ContainerNode) entry.getValue()).getValue().stream() + .map(containerNode -> new NormalizedNodeUpdate( + YangInstanceIdentifier.builder(topLevelIdentifier) + .node(containerNode.getIdentifier()).build(), containerNode, null)) + .collect(Collectors.toList()); + } else { + LOG.debug("Processing {} as non-presence container", topLevelIdentifier); + // if non-presence - goes deep with base logic + return super.normalizedUpdates(topLevelIdentifier, entry); + } + } + + private static SchemaPath getSchemaPath(final YangInstanceIdentifier topLevelIdentifier, + final Map.Entry> entry) { + return SchemaPath.create(extractSchemaPathQNames(topLevelIdentifier, entry), true); + } + + private static List extractSchemaPathQNames(final YangInstanceIdentifier topLevelIdentifier, + final Map.Entry> entry) { + // must be filtered out of augmentation and keyed NodeIdentifiers + return Stream.concat(topLevelIdentifier.getPathArguments().stream(), Stream.of(entry.getKey())) + .filter(pathArgument -> pathArgument instanceof YangInstanceIdentifier.NodeIdentifier) + .map(YangInstanceIdentifier.PathArgument::getNodeType) + .collect(Collectors.toList()); + } +} diff --git a/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/DelegatingRewriteDeleteProducer.java b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/DelegatingRewriteDeleteProducer.java new file mode 100644 index 000000000..e3b3c74dc --- /dev/null +++ b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/DelegatingRewriteDeleteProducer.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2017 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.data.impl; + +import static com.google.common.base.Preconditions.checkState; + +import java.util.AbstractMap; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode; + +/** + * Basic implementation for nodes that are not considered modifications but their children could be + */ +abstract class DelegatingRewriteDeleteProducer implements RewriteDeleteProducer { + + private final NormalizedNodeRewriteDeleteRegistry baseRewriteProducer; + + DelegatingRewriteDeleteProducer(@Nonnull final NormalizedNodeRewriteDeleteRegistry baseRewriteProducer) { + this.baseRewriteProducer = baseRewriteProducer; + } + + @Override + public Collection normalizedUpdates(@Nonnull final YangInstanceIdentifier topLevelIdentifier, + @Nonnull final Map.Entry> entry) { + // just delegates to lower level + checkState(entry.getValue() instanceof DataContainerNode, "Unable to extract children"); + checkState(entry.getValue() instanceof DataContainerChild, "Unable to extract identifier"); + final Collection> value = + DataContainerNode.class.cast(entry.getValue()).getValue(); + return value.stream() + .map(DataContainerChild.class::cast) + .flatMap(node -> baseRewriteProducer + .normalizedUpdates(childYangId(topLevelIdentifier, entry), childMapEntry(node)).stream()) + .collect(Collectors.toList()); + } + + private static AbstractMap.SimpleEntry> childMapEntry( + final DataContainerChild node) { + return new HashMap.SimpleEntry>( + node.getIdentifier(), node); + } + + private static YangInstanceIdentifier childYangId(final @Nonnull YangInstanceIdentifier topLevelIdentifier, + final @Nonnull Map.Entry> entry) { + return YangInstanceIdentifier.builder(topLevelIdentifier) + .node(entry.getKey()).build(); + } +} diff --git a/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/LeafListRewriteDeleteProducer.java b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/LeafListRewriteDeleteProducer.java new file mode 100644 index 000000000..ca7899694 --- /dev/null +++ b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/LeafListRewriteDeleteProducer.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2017 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.data.impl; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import javax.annotation.Nonnull; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class LeafListRewriteDeleteProducer implements RewriteDeleteProducer{ + + private static final Logger LOG = LoggerFactory.getLogger(LeafListRewriteDeleteProducer.class); + + @Override + public Collection normalizedUpdates(@Nonnull final YangInstanceIdentifier topLevelIdentifier, + @Nonnull final Map.Entry> entry) { + // if leaf-list,just builds delete right away + LOG.debug("Processing {} as leaf-list", topLevelIdentifier); + return Collections.singletonList(new NormalizedNodeUpdate(YangInstanceIdentifier.builder(topLevelIdentifier) + .node(entry.getKey()).build(), entry.getValue(), null)); + } +} diff --git a/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/LeafRewriteDeleteProducer.java b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/LeafRewriteDeleteProducer.java new file mode 100644 index 000000000..ddf0d64f7 --- /dev/null +++ b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/LeafRewriteDeleteProducer.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2017 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.data.impl; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import javax.annotation.Nonnull; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class LeafRewriteDeleteProducer implements RewriteDeleteProducer { + + private static final Logger LOG = LoggerFactory.getLogger(LeafRewriteDeleteProducer.class); + + @Override + public Collection normalizedUpdates(@Nonnull final YangInstanceIdentifier topLevelIdentifier, + @Nonnull final Map.Entry> entry) { + // if leaf,just builds delete right away + LOG.debug("Processing {} as leaf", topLevelIdentifier); + return Collections.singletonList(new NormalizedNodeUpdate(YangInstanceIdentifier.builder(topLevelIdentifier) + .node(entry.getKey()).build(), entry.getValue(), null)); + } +} diff --git a/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ListRewriteDeleteProducer.java b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ListRewriteDeleteProducer.java new file mode 100644 index 000000000..793cab8fa --- /dev/null +++ b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ListRewriteDeleteProducer.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2017 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.data.impl; + +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; +import org.opendaylight.yangtools.yang.data.api.schema.MapNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class ListRewriteDeleteProducer implements RewriteDeleteProducer { + + private static final Logger LOG = LoggerFactory.getLogger(ListRewriteDeleteProducer.class); + + @Override + public Collection normalizedUpdates(@Nonnull final YangInstanceIdentifier topLevelIdentifier, + @Nonnull final Map.Entry> entry) { + // identifier has different format : parent_node/list_node/list_node_with_key + LOG.debug("Processing {} as list", topLevelIdentifier); + return ((MapNode) entry.getValue()).getValue().stream() + .map(mapEntryNode -> new NormalizedNodeUpdate(YangInstanceIdentifier.builder(topLevelIdentifier) + .node(mapEntryNode.getNodeType()) + .node(mapEntryNode.getIdentifier()).build(), mapEntryNode, null)) + .collect(Collectors.toList()); + } +} diff --git a/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ModifiableDataTreeDelegator.java b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ModifiableDataTreeDelegator.java index f4989aa37..ccc40576a 100644 --- a/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ModifiableDataTreeDelegator.java +++ b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ModifiableDataTreeDelegator.java @@ -119,8 +119,8 @@ public final class ModifiableDataTreeDelegator extends ModifiableDataTreeManager LOG.trace("ConfigDataTree.modify() rootPath={}, rootNode={}, dataBefore={}, dataAfter={}", rootPath, rootNode, rootNode.getDataBefore(), rootNode.getDataAfter()); - final ModificationDiff modificationDiff = - ModificationDiff.recursivelyFromCandidateRoot(rootNode, schema); + final ModificationDiff modificationDiff = new ModificationDiff.ModificationDiffBuilder() + .setCtx(schema).build(rootNode); LOG.debug("ConfigDataTree.modify() diff: {}", modificationDiff); // Distinguish between updates (create + update) and deletes @@ -194,25 +194,25 @@ public final class ModifiableDataTreeDelegator extends ModifiableDataTreeManager } private WriterRegistry.DataObjectUpdates toBindingAware( - final Map biNodes) { + final Map biNodes) { return ModifiableDataTreeDelegator.toBindingAware(biNodes, serializer); } } @VisibleForTesting static WriterRegistry.DataObjectUpdates toBindingAware( - final Map biNodes, + 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()) { + for (Map.Entry biEntry : biNodes.entrySet()) { final InstanceIdentifier unkeyedIid = RWUtils.makeIidWildcarded(serializer.fromYangInstanceIdentifier(biEntry.getKey())); - ModificationDiff.NormalizedNodeUpdate normalizedNodeUpdate = biEntry.getValue(); + NormalizedNodeUpdate normalizedNodeUpdate = biEntry.getValue(); final DataObjectUpdate dataObjectUpdate = toDataObjectUpdate(normalizedNodeUpdate, serializer); if (dataObjectUpdate != null) { if (dataObjectUpdate instanceof DataObjectUpdate.DataObjectDelete) { @@ -227,7 +227,7 @@ public final class ModifiableDataTreeDelegator extends ModifiableDataTreeManager @Nullable private static DataObjectUpdate toDataObjectUpdate( - final ModificationDiff.NormalizedNodeUpdate normalizedNodeUpdate, + final NormalizedNodeUpdate normalizedNodeUpdate, final BindingNormalizedNodeSerializer serializer) { InstanceIdentifier baId = serializer.fromYangInstanceIdentifier(normalizedNodeUpdate.getId()); diff --git a/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/Modification.java b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/Modification.java new file mode 100644 index 000000000..88823c53d --- /dev/null +++ b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/Modification.java @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2017 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.data.impl; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +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; +import org.opendaylight.yangtools.yang.model.api.AugmentationSchema; +import org.opendaylight.yangtools.yang.model.api.AugmentationTarget; +import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode; +import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; +import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.SchemaNode; + +/** + * Intermediate representation of a modification + its schema. + */ +final class Modification { + private final YangInstanceIdentifier id; + private final DataTreeCandidateNode dataCandidate; + // Using Object as type for schema node since it's the only type that's a parent to all schema node types from + // yangtools. The hierarchy does not use e.g. SchemaNode class for all types + private final Object parentNode; + private final Object schemaNode; + private final boolean updateParentNode; + + private Modification(final YangInstanceIdentifier id, + final DataTreeCandidateNode dataCandidate, + final Object parentNode, + final Object schemaNode, + final boolean updateParentNode) { + this.id = id; + this.dataCandidate = dataCandidate; + this.parentNode = parentNode; + this.schemaNode = schemaNode; + // controls process of updating parent node while moving down the schema tree: + this.updateParentNode = updateParentNode; + } + + Modification(final YangInstanceIdentifier id, + final DataTreeCandidateNode dataCandidate, + final Object parentNode, + final Object schemaNode) { + this(id, dataCandidate, parentNode, schemaNode, true); + } + + Modification(final YangInstanceIdentifier id, + final DataTreeCandidateNode dataCandidate, + final Object schemaNode) { + this(id, dataCandidate, schemaNode, schemaNode); + } + + List getChildNodes() { + return streamChildren().collect(Collectors.toList()); + } + + YangInstanceIdentifier getId() { + return id; + } + + ModificationType getModificationType() { + return dataCandidate.getModificationType(); + } + + com.google.common.base.Optional> getDataBefore() { + return dataCandidate.getDataBefore(); + } + + com.google.common.base.Optional> getDataAfter() { + return dataCandidate.getDataAfter(); + } + + Object getSchemaNode() { + return schemaNode; + } + + boolean is(final Class schemaType) { + return schemaType.isAssignableFrom(schemaNode.getClass()); + } + + boolean isMixin() { + // Checking whether node is a mixin is not performed on schema, but on data since mixin is + // only a NormalizedNode concept, not a schema concept + return dataCandidate.getDataBefore().orNull() instanceof MixinNode || + dataCandidate.getDataAfter().orNull() instanceof MixinNode; + } + + boolean isBeforeAndAfterDifferent() { + if (dataCandidate.getDataBefore().isPresent()) { + return !dataCandidate.getDataBefore().get().equals(dataCandidate.getDataAfter().orNull()); + } + + // considering not a modification if data after is also null + return dataCandidate.getDataAfter().isPresent(); + } + + private AugmentationSchema findAugmentation(Object currentNode, + final YangInstanceIdentifier.AugmentationIdentifier identifier) { + if (currentNode != null) { + // check if identifier points to some augmentation of currentNode + if (currentNode instanceof AugmentationTarget) { + Optional augmentationSchema = + ((AugmentationTarget) currentNode).getAvailableAugmentations().stream() + .filter(aug -> identifier.equals(new YangInstanceIdentifier.AugmentationIdentifier( + aug.getChildNodes().stream() + .map(SchemaNode::getQName) + .collect(Collectors.toSet())))) + .findFirst(); + if (augmentationSchema.isPresent()) { + return augmentationSchema.get(); + } + } + + // continue search: + Collection childNodes = Collections.emptyList(); + if (currentNode instanceof DataNodeContainer) { + childNodes = ((DataNodeContainer) currentNode).getChildNodes(); + } else if (currentNode instanceof ChoiceSchemaNode) { + childNodes = ((ChoiceSchemaNode) currentNode).getCases().stream() + .flatMap(cas -> cas.getChildNodes().stream()).collect(Collectors.toList()); + } + return childNodes.stream().map(n -> findAugmentation(n, identifier)).filter(n -> n != null).findFirst() + .orElse(null); + } else { + return null; + } + } + + Stream streamChildren() { + return dataCandidate.getChildNodes().stream() + .map(child -> { + final YangInstanceIdentifier childId = id.node(child.getIdentifier()); + final Object schemaChild = schemaChild(schemaNode, child.getIdentifier()); + + // An augment cannot change other augment, so we do not update parent node if we are streaming + // children of AugmentationSchema (otherwise we would fail to find schema for nested augmentations): + if (updateParentNode) { + if (schemaNode instanceof AugmentationSchema) { + // child nodes would not have nested augmentations, so we stop moving parentNode: + return new Modification(childId, child, parentNode, schemaChild, false); + } else { + // update parent node: + return new Modification(childId, child, schemaNode, schemaChild, true); + } + } + return new Modification(childId, child, parentNode, schemaChild, updateParentNode); + }); + } + + /** + * Find next schema node in hierarchy. + */ + private Object schemaChild(final Object schemaNode, final YangInstanceIdentifier.PathArgument identifier) { + Object found = null; + + if (identifier instanceof YangInstanceIdentifier.AugmentationIdentifier) { + if (schemaNode instanceof AugmentationTarget) { + // Find matching augmentation + found = ((AugmentationTarget) schemaNode).getAvailableAugmentations().stream() + .filter(aug -> identifier.equals(new YangInstanceIdentifier.AugmentationIdentifier( + aug.getChildNodes().stream() + .map(SchemaNode::getQName) + .collect(Collectors.toSet())))) + .findFirst() + .orElse(null); + + if (found == null) { + // An augment cannot change other augment, but all augments only change their targets (data nodes). + // + // As a consequence, if nested augmentations are present, + // AugmentationSchema might reference child schema node instances that do not include changes + // from nested augments. + // + // But schemaNode, as mentioned earlier, contains all the changes introduced by augments. + // + // On the other hand, in case of augments which introduce leaves, + // we need to address AugmentationSchema node directly so we can't simply do + // found = schemaNode; + // + found = + findAugmentation(parentNode, (YangInstanceIdentifier.AugmentationIdentifier) identifier); + } + } + } else if (schemaNode instanceof DataNodeContainer) { + // Special handling for list aggregator nodes. If we are at list aggregator node e.g. MapNode and + // we are searching for schema for a list entry e.g. MapEntryNode just return the same schema + if (schemaNode instanceof ListSchemaNode && + ((SchemaNode) schemaNode).getQName().equals(identifier.getNodeType())) { + found = schemaNode; + } else { + found = ((DataNodeContainer) schemaNode).getDataChildByName(identifier.getNodeType()); + } + } else if (schemaNode instanceof ChoiceSchemaNode) { + // For choices, iterate through all the cases + final Optional maybeChild = ((ChoiceSchemaNode) schemaNode).getCases().stream() + .flatMap(cas -> cas.getChildNodes().stream()) + .filter(child -> child.getQName().equals(identifier.getNodeType())) + .findFirst(); + if (maybeChild.isPresent()) { + found = maybeChild.get(); + } + // Special handling for leaf-list nodes. Basically the same as is for list mixin nodes + } else if (schemaNode instanceof LeafListSchemaNode && + ((SchemaNode) schemaNode).getQName().equals(identifier.getNodeType())) { + found = schemaNode; + } + + return checkNotNull(found, "Unable to find child node in: %s identifiable by: %s", schemaNode, identifier); + } + + @Override + public String toString() { + return "Modification{" + + "id=" + id + + '}'; + } +} diff --git a/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ModificationDiff.java b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ModificationDiff.java index 863f8abb2..74e21dfa1 100644 --- a/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ModificationDiff.java +++ b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ModificationDiff.java @@ -16,41 +16,28 @@ package io.fd.honeycomb.data.impl; -import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import static org.opendaylight.yangtools.yang.data.api.schema.tree.ModificationType.APPEARED; import static org.opendaylight.yangtools.yang.data.api.schema.tree.ModificationType.DELETE; -import static org.opendaylight.yangtools.yang.data.api.schema.tree.ModificationType.DISAPPEARED; import static org.opendaylight.yangtools.yang.data.api.schema.tree.ModificationType.WRITE; import com.google.common.collect.ImmutableMap; -import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.annotation.Nonnull; -import javax.annotation.Nullable; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; 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; +import org.opendaylight.yangtools.yang.data.impl.schema.nodes.AbstractImmutableDataContainerNode; import org.opendaylight.yangtools.yang.model.api.AugmentationSchema; -import org.opendaylight.yangtools.yang.model.api.AugmentationTarget; 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.LeafListSchemaNode; import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode; -import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; import org.opendaylight.yangtools.yang.model.api.SchemaContext; -import org.opendaylight.yangtools.yang.model.api.SchemaNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,7 +50,6 @@ final class ModificationDiff { private static final ModificationDiff EMPTY_DIFF = new ModificationDiff(Collections.emptyMap()); private static final EnumSet VALID_MODIFICATIONS = EnumSet.of(WRITE, DELETE); - private static final EnumSet IGNORED_MODIFICATIONS = EnumSet.of(APPEARED, DISAPPEARED); private final Map updates; @@ -105,391 +91,166 @@ final class ModificationDiff { return new ModificationDiff(ImmutableMap.of(modification.getId(), NormalizedNodeUpdate.create(modification))); } - /** - * 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 Modification modification) { - // recursively process child nodes for exact modifications - return recursivelyChildrenFromCandidate(modification) - // also add modification on current level, if elligible - .merge(isModification(modification) - ? ModificationDiff.create(modification) - : EMPTY_DIFF); - } - - /** - * Same as {@link #recursivelyFromCandidate(Modification)} but does - * not process the root node for modifications, since it's the artificial data root, that has no child leaves but - * always is marked as SUBTREE_MODIFIED. - */ - @Nonnull - static ModificationDiff recursivelyFromCandidateRoot(@Nonnull final DataTreeCandidateNode currentCandidate, - @Nonnull final SchemaContext ctx) { - return recursivelyChildrenFromCandidate(new Modification(YangInstanceIdentifier.EMPTY, currentCandidate, ctx)); - } - - /** - * 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 Modification modification) { - // Disappear is not a modification - if (IGNORED_MODIFICATIONS.contains(modification.getModificationType())) { - return false; - // Mixin nodes are not considered modifications - } else if (modification.isMixin() && !modification.is(AugmentationSchema.class)) { - return false; - } else { - return isCurrentModified(modification); - } - } - - private static Boolean isCurrentModified(@Nonnull final Modification modification) { - // First check if it's an empty presence node - final boolean emptyPresenceNode = isEmptyPresenceNode(modification); - - // Check if there are any modified leaves and if so, consider current node as modified - final Boolean directLeavesModified = emptyPresenceNode - || modification.streamChildren() - // Checking leaf or leaf-lists children for direct modification, which means that leafs of leaf lists - // trigger a modification on parent node - .filter(child -> child.is(LeafSchemaNode.class) || child.is(LeafListSchemaNode.class)) - // For some reason, we get modifications on unmodified list keys - // and that messes up our modifications collection here, so we need to skip - .filter(Modification::isBeforeAndAfterDifferent) - .filter(child -> VALID_MODIFICATIONS.contains(child.getModificationType())) - .findFirst() - .isPresent(); - - // Also as fallback check choices (choices do not exist in BA world and if anything within a choice was modified, - // consider its parent as being modified) - final boolean modified = directLeavesModified - || modification.streamChildren() - .filter(child -> child.is(ChoiceSchemaNode.class)) - // Recursively check each choice if there was any change to it - .filter(ModificationDiff::isCurrentModified) - .findFirst() - .isPresent(); - - if (modified) { - LOG.debug("Modification detected as {} at {}", - modification.getModificationType(), modification.getId()); - } - - return modified; - } - - /** - * Check if new data are empty but still to be considered as a modification, meaning it's presence has a meaning - * e.g. containers with presence statement. - */ - private static boolean isEmptyPresenceNode(@Nonnull final Modification modification) { - return modification.is(ContainerSchemaNode.class) - && ((ContainerSchemaNode) modification.getSchemaNode()).isPresenceContainer() - && modification.getChildNodes().isEmpty() - && VALID_MODIFICATIONS.contains(modification.getModificationType()); - } - - /** - * Process all non-leaf child nodes recursively, creating aggregated {@link ModificationDiff}. - */ - private static ModificationDiff recursivelyChildrenFromCandidate(@Nonnull final Modification modification) { - // recursively process child nodes for specific modifications - return modification.streamChildren() - .filter(child -> !child.is(LeafSchemaNode.class)) - .map(ModificationDiff::recursivelyFromCandidate) - .reduce(ModificationDiff::merge) - .orElse(EMPTY_DIFF); - } - @Override public String toString() { return "ModificationDiff{updates=" + updates + '}'; } - /** - * Update to a normalized node identifiable by its {@link YangInstanceIdentifier}. - */ - static final class NormalizedNodeUpdate { + static final class ModificationDiffBuilder { + private NormalizedNodeRewriteDeleteRegistry registry; + private SchemaContext ctx; - @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; + ModificationDiffBuilder setCtx(final SchemaContext ctx) { + this.ctx = ctx; + registry = new NormalizedNodeRewriteDeleteRegistry(ctx); + return this; } - @Nullable - public NormalizedNode getDataBefore() { - return dataBefore; - } + ModificationDiff build(@Nonnull final DataTreeCandidateNode currentCandidate) { + checkNotNull(currentCandidate, "Data tree candidate cannot be null"); + checkNotNull(ctx, "Schema ctx cannot be null"); - @Nullable - public NormalizedNode getDataAfter() { - return dataAfter; + return recursivelyFromCandidateRoot(currentCandidate, ctx); } + /** + * 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 - public YangInstanceIdentifier getId() { - return id; - } - - static NormalizedNodeUpdate create(@Nonnull final Modification modification) { - final com.google.common.base.Optional> beforeData = - modification.getDataBefore(); - final com.google.common.base.Optional> afterData = - modification.getDataAfter(); - checkArgument(beforeData.isPresent() || afterData.isPresent(), - "Both before and after data are null for %s", modification.getId()); - return NormalizedNodeUpdate.create(modification.getId(), beforeData.orNull(), afterData.orNull()); - } - - static NormalizedNodeUpdate create(@Nonnull final YangInstanceIdentifier id, - @Nullable final NormalizedNode dataBefore, - @Nullable final NormalizedNode dataAfter) { - 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 - + '}'; - } - } - - /** - * Intermediate representation of a modification + its schema. - */ - private static final class Modification { - private final YangInstanceIdentifier id; - private final DataTreeCandidateNode dataCandidate; - // Using Object as type for schema node since it's the only type that's a parent to all schema node types from - // yangtools. The hierarchy does not use e.g. SchemaNode class for all types - private final Object parentNode; - private final Object schemaNode; - private final boolean updateParentNode; - - private Modification(final YangInstanceIdentifier id, - final DataTreeCandidateNode dataCandidate, - final Object parentNode, - final Object schemaNode, - final boolean updateParentNode) { - this.id = id; - this.dataCandidate = dataCandidate; - this.parentNode = parentNode; - this.schemaNode = schemaNode; - // controls process of updating parent node while moving down the schema tree: - this.updateParentNode = updateParentNode; + ModificationDiff recursivelyFromCandidate(@Nonnull final Modification modification) { + // recursively process child nodes for exact modifications + return recursivelyChildrenFromCandidate(modification) + // also add modification on current level, if elligible + .merge(isModification(modification) + ? ModificationDiff.create(modification) + // Modification that writes only non-presence container to override nested nodes wont have + // child nodes(in data tree candidate) so logic before will not detected such change, so checking directly + : isNonPresenceOverride(modification) + ? detectUnderDisappearedNonPresenceContainer(modification) + : EMPTY_DIFF); } - Modification(final YangInstanceIdentifier id, - final DataTreeCandidateNode dataCandidate, - final Object parentNode, - final Object schemaNode) { - this(id, dataCandidate, parentNode, schemaNode, true); - } + private ModificationDiff detectUnderDisappearedNonPresenceContainer( + @Nonnull final Modification modification) { + final com.google.common.base.Optional> dataBefore = modification.getDataBefore(); - Modification(final YangInstanceIdentifier id, - final DataTreeCandidateNode dataCandidate, - final Object schemaNode) { - this(id, dataCandidate, schemaNode, schemaNode); - } + // is disappear case + if (dataBefore.isPresent()) { + final NormalizedNode parentData = dataBefore.get(); - List getChildNodes() { - return streamChildren().collect(Collectors.toList()); - } + // have potential to extract children + if (parentData instanceof AbstractImmutableDataContainerNode) { + final AbstractImmutableDataContainerNode parentContainerNode = + (AbstractImmutableDataContainerNode) parentData; - YangInstanceIdentifier getId() { - return id; - } + final Map updates = + parentContainerNode.getChildren().entrySet().stream() + .flatMap(entry -> registry.normalizedUpdates(modification.getId(), entry).stream()) + .collect(Collectors.toMap(NormalizedNodeUpdate::getId, update -> update)); - ModificationType getModificationType() { - return dataCandidate.getModificationType(); - } - - com.google.common.base.Optional> getDataBefore() { - return dataCandidate.getDataBefore(); - } - - com.google.common.base.Optional> getDataAfter() { - return dataCandidate.getDataAfter(); - } - - Object getSchemaNode() { - return schemaNode; - } - - boolean is(final Class schemaType) { - return schemaType.isAssignableFrom(schemaNode.getClass()); + return new ModificationDiff(updates); + } + } + return EMPTY_DIFF; } - boolean isMixin() { - // Checking whether node is a mixin is not performed on schema, but on data since mixin is - // only a NormalizedNode concept, not a schema concept - return dataCandidate.getDataBefore().orNull() instanceof MixinNode || - dataCandidate.getDataAfter().orNull() instanceof MixinNode; + /** + * Same as {@link #recursivelyFromCandidate(Modification)} but does not process the root node for modifications, + * since it's the artificial data root, that has no child leaves but always is marked as SUBTREE_MODIFIED. + */ + @Nonnull + ModificationDiff recursivelyFromCandidateRoot(@Nonnull final DataTreeCandidateNode currentCandidate, + @Nonnull final SchemaContext ctx) { + return recursivelyChildrenFromCandidate( + new Modification(YangInstanceIdentifier.EMPTY, currentCandidate, ctx)); } - private boolean isBeforeAndAfterDifferent() { - if (dataCandidate.getDataBefore().isPresent()) { - return !dataCandidate.getDataBefore().get().equals(dataCandidate.getDataAfter().orNull()); + /** + * 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 Boolean isModification(@Nonnull final Modification modification) { + // APPEAR/DISAPPEAR are not valid modifications, but some of the children can be modified + // aka. list entry added to nested list under non-presence container, which would be resolved as APPEAR for + // that container, but MERGE for nested list + if (modification.isMixin() && !modification.is(AugmentationSchema.class)) { + return false; + } else { + return isCurrentModified(modification); } - - // considering not a modification if data after is also null - return dataCandidate.getDataAfter().isPresent(); } - private AugmentationSchema findAugmentation(Object currentNode, - final YangInstanceIdentifier.AugmentationIdentifier identifier) { - if (currentNode != null) { - // check if identifier points to some augmentation of currentNode - if (currentNode instanceof AugmentationTarget) { - Optional augmentationSchema = - ((AugmentationTarget) currentNode).getAvailableAugmentations().stream() - .filter(aug -> identifier.equals(new YangInstanceIdentifier.AugmentationIdentifier( - aug.getChildNodes().stream() - .map(SchemaNode::getQName) - .collect(Collectors.toSet())))) - .findFirst(); - if (augmentationSchema.isPresent()) { - return augmentationSchema.get(); - } - } - - // continue search: - Collection childNodes = Collections.emptyList(); - if (currentNode instanceof DataNodeContainer) { - childNodes = ((DataNodeContainer) currentNode).getChildNodes(); - } else if (currentNode instanceof ChoiceSchemaNode) { - childNodes = ((ChoiceSchemaNode) currentNode).getCases().stream() - .flatMap(cas -> cas.getChildNodes().stream()).collect(Collectors.toList()); - } - return childNodes.stream().map(n -> findAugmentation(n, identifier)).filter(n -> n != null).findFirst() - .orElse(null); - } else { - return null; + private Boolean isCurrentModified(@Nonnull final Modification modification) { + // First check if it's an empty presence node + final boolean emptyPresenceNode = isEmptyPresenceNode(modification); + + // Check if there are any modified leaves and if so, consider current node as modified + final Boolean directLeavesModified = emptyPresenceNode + || modification.streamChildren() + // Checking leaf or leaf-lists children for direct modification, which means that leafs of leaf lists + // trigger a modification on parent node + .filter(child -> child.is(LeafSchemaNode.class) || child.is(LeafListSchemaNode.class)) + // For some reason, we get modifications on unmodified list keys + // and that messes up our modifications collection here, so we need to skip + .filter(Modification::isBeforeAndAfterDifferent) + .filter(child -> VALID_MODIFICATIONS.contains(child.getModificationType())) + .findFirst() + .isPresent(); + + // Also as fallback check choices (choices do not exist in BA world and if anything within a choice was modified, + // consider its parent as being modified) + final boolean modified = directLeavesModified + || modification.streamChildren() + .filter(child -> child.is(ChoiceSchemaNode.class)) + // Recursively check each choice if there was any change to it + .filter(this::isCurrentModified) + .findFirst() + .isPresent(); + + if (modified) { + LOG.debug("Modification detected as {} at {}", + modification.getModificationType(), modification.getId()); } - } - Stream streamChildren() { - return dataCandidate.getChildNodes().stream() - .map(child -> { - final YangInstanceIdentifier childId = id.node(child.getIdentifier()); - final Object schemaChild = schemaChild(schemaNode, child.getIdentifier()); - - // An augment cannot change other augment, so we do not update parent node if we are streaming - // children of AugmentationSchema (otherwise we would fail to find schema for nested augmentations): - if (updateParentNode) { - if (schemaNode instanceof AugmentationSchema) { - // child nodes would not have nested augmentations, so we stop moving parentNode: - return new Modification(childId, child, parentNode, schemaChild, false); - } else { - // update parent node: - return new Modification(childId, child, schemaNode, schemaChild, true); - } - } - return new Modification(childId, child, parentNode, schemaChild, updateParentNode); - }); + return modified; } /** - * Find next schema node in hierarchy. + * Check if new data are empty but still to be considered as a modification, meaning it's presence has a meaning + * e.g. containers with presence statement. */ - private Object schemaChild(final Object schemaNode, final YangInstanceIdentifier.PathArgument identifier) { - Object found = null; - - if (identifier instanceof YangInstanceIdentifier.AugmentationIdentifier) { - if (schemaNode instanceof AugmentationTarget) { - // Find matching augmentation - found = ((AugmentationTarget) schemaNode).getAvailableAugmentations().stream() - .filter(aug -> identifier.equals(new YangInstanceIdentifier.AugmentationIdentifier( - aug.getChildNodes().stream() - .map(SchemaNode::getQName) - .collect(Collectors.toSet())))) - .findFirst() - .orElse(null); - - if (found == null) { - // An augment cannot change other augment, but all augments only change their targets (data nodes). - // - // As a consequence, if nested augmentations are present, - // AugmentationSchema might reference child schema node instances that do not include changes - // from nested augments. - // - // But schemaNode, as mentioned earlier, contains all the changes introduced by augments. - // - // On the other hand, in case of augments which introduce leaves, - // we need to address AugmentationSchema node directly so we can't simply do - // found = schemaNode; - // - found = - findAugmentation(parentNode, (YangInstanceIdentifier.AugmentationIdentifier) identifier); - } - } - } else if (schemaNode instanceof DataNodeContainer) { - // Special handling for list aggregator nodes. If we are at list aggregator node e.g. MapNode and - // we are searching for schema for a list entry e.g. MapEntryNode just return the same schema - if (schemaNode instanceof ListSchemaNode && - ((SchemaNode) schemaNode).getQName().equals(identifier.getNodeType())) { - found = schemaNode; - } else { - found = ((DataNodeContainer) schemaNode).getDataChildByName(identifier.getNodeType()); - } - } else if (schemaNode instanceof ChoiceSchemaNode) { - // For choices, iterate through all the cases - final Optional maybeChild = ((ChoiceSchemaNode) schemaNode).getCases().stream() - .flatMap(cas -> cas.getChildNodes().stream()) - .filter(child -> child.getQName().equals(identifier.getNodeType())) - .findFirst(); - if (maybeChild.isPresent()) { - found = maybeChild.get(); - } - // Special handling for leaf-list nodes. Basically the same as is for list mixin nodes - } else if (schemaNode instanceof LeafListSchemaNode && - ((SchemaNode) schemaNode).getQName().equals(identifier.getNodeType())) { - found = schemaNode; - } + private static boolean isEmptyPresenceNode(@Nonnull final Modification modification) { + return modification.is(ContainerSchemaNode.class) + && ((ContainerSchemaNode) modification.getSchemaNode()).isPresenceContainer() + && modification.getChildNodes().isEmpty() + && VALID_MODIFICATIONS.contains(modification.getModificationType()); + } - return checkNotNull(found, "Unable to find child node in: %s identifiable by: %s", schemaNode, identifier); + /** + * Checks whether node is non-presence container but with changed nested data + */ + private static boolean isNonPresenceOverride(@Nonnull final Modification modification) { + return modification.is(ContainerSchemaNode.class)// must be container + && !((ContainerSchemaNode) modification.getSchemaNode()).isPresenceContainer() + // must be non-presence + && modification.getChildNodes().isEmpty() // is override to empty + && modification.isBeforeAndAfterDifferent()// to detect that it is modification + && + modification.getDataBefore().isPresent(); // to ensure the case when overriding previously existing } - @Override - public String toString() { - return "Modification{" + - "id=" + id + - '}'; + /** + * Process all non-leaf child nodes recursively, creating aggregated {@link ModificationDiff}. + */ + private ModificationDiff recursivelyChildrenFromCandidate(@Nonnull final Modification modification) { + // recursively process child nodes for specific modifications + return modification.streamChildren() + .filter(child -> !child.is(LeafSchemaNode.class)) + .map(this::recursivelyFromCandidate) + .reduce(ModificationDiff::merge) + .orElse(EMPTY_DIFF); } } + } diff --git a/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/NormalizedNodeRewriteDeleteRegistry.java b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/NormalizedNodeRewriteDeleteRegistry.java new file mode 100644 index 000000000..c7832ac0e --- /dev/null +++ b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/NormalizedNodeRewriteDeleteRegistry.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2017 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.data.impl; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import javax.annotation.Nonnull; +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.ContainerNode; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; +import org.opendaylight.yangtools.yang.data.api.schema.LeafNode; +import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode; +import org.opendaylight.yangtools.yang.data.api.schema.MapNode; +import org.opendaylight.yangtools.yang.model.api.ChoiceCaseNode; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Basic logic for creating {@link NormalizedNodeUpdate} for changed that delete by rewrite + */ +final class NormalizedNodeRewriteDeleteRegistry implements RewriteDeleteProducer { + + private static final Logger LOG = LoggerFactory.getLogger(NormalizedNodeRewriteDeleteRegistry.class); + + private final RewriteDeleteProducer leafDeleteProducer; + private final RewriteDeleteProducer leafListDeleteProducer; + private final RewriteDeleteProducer listDeleteProducer; + private final RewriteDeleteProducer containerDeleteProducer; + private final RewriteDeleteProducer choiceDeleteProducer; + private final RewriteDeleteProducer caseDeleteProducer; + private final RewriteDeleteProducer augmentationDeleteProducer; + + NormalizedNodeRewriteDeleteRegistry(@Nonnull final SchemaContext ctx) { + leafDeleteProducer = new LeafRewriteDeleteProducer(); + leafListDeleteProducer = new LeafListRewriteDeleteProducer(); + listDeleteProducer = new ListRewriteDeleteProducer(); + containerDeleteProducer = new ContainerRewriteDeleteProducer(this, ctx); + choiceDeleteProducer = new ChoiceRewriteDeleteProducer(this); + caseDeleteProducer = new CaseRewriteDeleteProducer(this); + augmentationDeleteProducer = new AugmentationRewriteDeleteProducer(this); + } + + @Override + public Collection normalizedUpdates(@Nonnull final YangInstanceIdentifier topLevelIdentifier, + @Nonnull final Map.Entry> entry) { + if (entry.getValue() instanceof LeafNode) { + LOG.debug("Processing leaf node {}", topLevelIdentifier); + return leafDeleteProducer.normalizedUpdates(topLevelIdentifier, entry); + } + + if (entry.getValue() instanceof LeafSetNode) { + LOG.debug("Processing leaf-list node {}", topLevelIdentifier); + return leafListDeleteProducer.normalizedUpdates(topLevelIdentifier, entry); + } + + if (entry.getValue() instanceof MapNode) { + LOG.debug("Processing list {}", topLevelIdentifier); + return listDeleteProducer.normalizedUpdates(topLevelIdentifier, entry); + } + + if (entry.getValue() instanceof ContainerNode) { + LOG.debug("Processing container {}", topLevelIdentifier); + return containerDeleteProducer.normalizedUpdates(topLevelIdentifier, entry); + } + + if (entry.getValue() instanceof ChoiceNode) { + LOG.debug("Processing choice {}", topLevelIdentifier); + return choiceDeleteProducer.normalizedUpdates(topLevelIdentifier, entry); + } + + if (entry.getValue() instanceof ChoiceCaseNode) { + LOG.debug("Processing case {}", topLevelIdentifier); + return caseDeleteProducer.normalizedUpdates(topLevelIdentifier, entry); + } + + if (entry.getValue() instanceof AugmentationNode) { + LOG.debug("Processing augmentation {}", topLevelIdentifier); + return augmentationDeleteProducer.normalizedUpdates(topLevelIdentifier, entry); + } + + return Collections.emptyList(); + } +} diff --git a/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/NormalizedNodeUpdate.java b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/NormalizedNodeUpdate.java new file mode 100644 index 000000000..bfc8a1e2c --- /dev/null +++ b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/NormalizedNodeUpdate.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2017 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.data.impl; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; + +/** + * Update to a normalized node identifiable by its {@link YangInstanceIdentifier}. + */ +final class NormalizedNodeUpdate { + + @Nonnull + private final YangInstanceIdentifier id; + @Nullable + private final NormalizedNode dataBefore; + @Nullable + private final NormalizedNode dataAfter; + + 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 Modification modification) { + final com.google.common.base.Optional> beforeData = + modification.getDataBefore(); + final com.google.common.base.Optional> afterData = + modification.getDataAfter(); + checkArgument(beforeData.isPresent() || afterData.isPresent(), + "Both before and after data are null for %s", modification.getId()); + return NormalizedNodeUpdate.create(modification.getId(), beforeData.orNull(), afterData.orNull()); + } + + static NormalizedNodeUpdate create(@Nonnull final YangInstanceIdentifier id, + @Nullable final NormalizedNode dataBefore, + @Nullable final NormalizedNode dataAfter) { + 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/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/RewriteDeleteProducer.java b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/RewriteDeleteProducer.java new file mode 100644 index 000000000..e2c5f4ae0 --- /dev/null +++ b/infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/RewriteDeleteProducer.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2017 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.data.impl; + + +import java.util.Collection; +import java.util.Map; +import javax.annotation.Nonnull; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; + +interface RewriteDeleteProducer { + + Collection normalizedUpdates(@Nonnull final YangInstanceIdentifier topLevelIdentifier, + @Nonnull final Map.Entry> entry); +} \ No newline at end of file diff --git a/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModifiableDataTreeDelegatorTest.java b/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModifiableDataTreeDelegatorTest.java index 26a936f23..432833be3 100644 --- a/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModifiableDataTreeDelegatorTest.java +++ b/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModifiableDataTreeDelegatorTest.java @@ -64,7 +64,7 @@ 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; -public class ModifiableDataTreeDelegatorTest { +public class ModifiableDataTreeDelegatorTest extends ModificationBaseTest { @Mock private WriterRegistry writer; @@ -90,7 +90,7 @@ public class ModifiableDataTreeDelegatorTest { @Before public void setUp() throws Exception { initMocks(this); - dataTree = ModificationDiffTest.getDataTree(); + dataTree = getDataTree(); when(contextBroker.newReadWriteTransaction()).thenReturn(tx); when(tx.submit()).thenReturn(Futures.immediateCheckedFuture(null)); @@ -98,39 +98,39 @@ public class ModifiableDataTreeDelegatorTest { final Map.Entry, DataObject> parsed = new AbstractMap.SimpleEntry<>(DEFAULT_ID, DEFAULT_DATA_OBJECT); when(serializer.fromNormalizedNode(any(YangInstanceIdentifier.class), any(NormalizedNode.class))).thenReturn(parsed); - configDataTree = new ModifiableDataTreeDelegator(serializer, dataTree, ModificationDiffTest.getSchemaCtx(), writer, contextBroker); + configDataTree = new ModifiableDataTreeDelegator(serializer, dataTree, getSchemaCtx(), writer, contextBroker); } @Test public void testRead() throws Exception { - final ContainerNode topContainer = ModificationDiffTest.getTopContainer("topContainer"); - ModificationDiffTest.addNodeToTree(dataTree, topContainer, ModificationDiffTest.TOP_CONTAINER_ID); + final ContainerNode topContainer = getTopContainer("topContainer"); + addNodeToTree(dataTree, topContainer, TOP_CONTAINER_ID); final CheckedFuture>, ReadFailedException> read = - configDataTree.read(ModificationDiffTest.TOP_CONTAINER_ID); + configDataTree.read(TOP_CONTAINER_ID); final CheckedFuture>, ReadFailedException> read2 = - configDataTree.newModification().read(ModificationDiffTest.TOP_CONTAINER_ID); + configDataTree.newModification().read(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); + assertEquals(dataTree.takeSnapshot().readNode(TOP_CONTAINER_ID), normalizedNodeOptional); } @Test public void testCommitSuccessful() throws Exception { - final MapNode nestedList = ModificationDiffTest.getNestedList("listEntry", "listValue"); + final MapNode nestedList = getNestedList("listEntry", "listValue"); final DataModification dataModification = configDataTree.newModification(); - dataModification.write(ModificationDiffTest.NESTED_LIST_ID, nestedList); + dataModification.write(NESTED_LIST_ID, nestedList); dataModification.validate(); dataModification.commit(); 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()); + assertEquals(nestedList, dataTree.takeSnapshot().readNode(NESTED_LIST_ID).get()); } private static DataObject mockDataObject(final String name, final Class classToMock) { @@ -141,7 +141,7 @@ public class ModifiableDataTreeDelegatorTest { @Test public void testCommitUndoSuccessful() throws Exception { - final MapNode nestedList = ModificationDiffTest.getNestedList("listEntry", "listValue"); + final MapNode nestedList = getNestedList("listEntry", "listValue"); // Fail on update: final WriterRegistry.Reverter reverter = mock(WriterRegistry.Reverter.class); @@ -152,7 +152,7 @@ public class ModifiableDataTreeDelegatorTest { try { // Run the test final DataModification dataModification = configDataTree.newModification(); - dataModification.write(ModificationDiffTest.NESTED_LIST_ID, nestedList); + dataModification.write(NESTED_LIST_ID, nestedList); dataModification.validate(); dataModification.commit(); fail("WriterRegistry.RevertSuccessException was expected"); @@ -165,7 +165,7 @@ public class ModifiableDataTreeDelegatorTest { @Test public void testCommitUndoFailed() throws Exception { - final MapNode nestedList = ModificationDiffTest.getNestedList("listEntry", "listValue"); + final MapNode nestedList = getNestedList("listEntry", "listValue"); // Fail on update: final WriterRegistry.Reverter reverter = mock(WriterRegistry.Reverter.class); @@ -182,7 +182,7 @@ public class ModifiableDataTreeDelegatorTest { try { // Run the test final DataModification dataModification = configDataTree.newModification(); - dataModification.write(ModificationDiffTest.NESTED_LIST_ID, nestedList); + dataModification.write(NESTED_LIST_ID, nestedList); dataModification.validate(); dataModification.commit(); fail("WriterRegistry.Reverter.RevertFailedException was expected"); @@ -201,14 +201,14 @@ public class ModifiableDataTreeDelegatorTest { public void testToBindingAware() throws Exception { when(serializer.fromNormalizedNode(any(YangInstanceIdentifier.class), eq(null))).thenReturn(null); - final Map biNodes = new HashMap<>(); + 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)); + biNodes.put(yid1, NormalizedNodeUpdate.create(yid1, nn1B, null)); // create final QName nn2 = QName.create("namespace", "nn1"); @@ -216,7 +216,7 @@ public class ModifiableDataTreeDelegatorTest { 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)); + biNodes.put(yid2, NormalizedNodeUpdate.create(yid2, null, nn2A)); // update final QName nn3 = QName.create("namespace", "nn1"); @@ -226,7 +226,7 @@ public class ModifiableDataTreeDelegatorTest { 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)); + biNodes.put(yid3, NormalizedNodeUpdate.create(yid3, nn3B, nn3A)); final WriterRegistry.DataObjectUpdates dataObjectUpdates = ModifiableDataTreeDelegator.toBindingAware(biNodes, serializer); diff --git a/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationBaseTest.java b/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationBaseTest.java new file mode 100644 index 000000000..c7d78faf6 --- /dev/null +++ b/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationBaseTest.java @@ -0,0 +1,367 @@ +/* + * Copyright (c) 2017 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.data.impl; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +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.LeafSetEntryNode; +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; +import org.opendaylight.yangtools.yang.data.api.schema.tree.DataValidationFailedException; +import org.opendaylight.yangtools.yang.data.api.schema.tree.TipProducingDataTree; +import org.opendaylight.yangtools.yang.data.api.schema.tree.TreeType; +import org.opendaylight.yangtools.yang.data.impl.schema.Builders; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes; +import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.ListNodeBuilder; +import org.opendaylight.yangtools.yang.data.impl.schema.tree.InMemoryDataTreeFactory; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; +import org.opendaylight.yangtools.yang.parser.spi.meta.ReactorException; +import org.opendaylight.yangtools.yang.parser.stmt.reactor.CrossSourceStatementReactor; +import org.opendaylight.yangtools.yang.parser.stmt.rfc6020.YangInferencePipeline; +import org.opendaylight.yangtools.yang.parser.stmt.rfc6020.YangStatementSourceImpl; + +abstract class ModificationBaseTest extends ModificationMetadata { + + void addNodeToTree(final DataTree dataTree, final NormalizedNode node, + final YangInstanceIdentifier id) + throws DataValidationFailedException { + DataTreeSnapshot dataTreeSnapshot = dataTree.takeSnapshot(); + DataTreeModification dataTreeModification = dataTreeSnapshot.newModification(); + dataTreeModification.write(id, node); + dataTreeModification.ready(); + dataTree.validate(dataTreeModification); + DataTreeCandidate prepare = dataTree.prepare(dataTreeModification); + dataTree.commit(prepare); + } + + protected TipProducingDataTree getDataTree() throws ReactorException { + final TipProducingDataTree dataTree = InMemoryDataTreeFactory.getInstance().create(TreeType.CONFIGURATION); + dataTree.setSchemaContext(getSchemaCtx()); + return dataTree; + } + + ContainerNode getTopContainer(final String stringValue) { + return Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + .withChild(ImmutableNodes.leafNode(STRING_LEAF_QNAME, stringValue)) + .build(); + } + + ContainerNode getTopContainerWithLeafList(final String... stringValue) { + final ListNodeBuilder> leafSetBuilder = Builders.leafSetBuilder(); + for (final String value : stringValue) { + leafSetBuilder.withChild(Builders.leafSetEntryBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeWithValue<>(NESTED_LEAF_LIST_QNAME, value)) + .withValue(value) + .build()); + } + + return Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + .withChild(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(FOR_LEAF_LIST_QNAME)) + .withChild(leafSetBuilder + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_LEAF_LIST_QNAME)) + .build()) + .build()) + .build(); + } + + MapNode getNestedList(final String listItemName, final String text) { + return Builders.mapBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_LIST_QNAME)) + .withChild( + Builders.mapEntryBuilder() + .withNodeIdentifier( + new YangInstanceIdentifier.NodeIdentifierWithPredicates(NESTED_LIST_QNAME, + NAME_LEAF_QNAME, listItemName)) + .withChild(ImmutableNodes.leafNode(NAME_LEAF_QNAME, listItemName)) + .withChild(ImmutableNodes.leafNode(TEXT_LEAF_QNAME, text)) + .build() + ) + .build(); + } + + MapNode getDeepList(final String listItemName) { + return Builders.mapBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(DEEP_LIST_QNAME)) + .withChild( + Builders.mapEntryBuilder() + .withNodeIdentifier( + new YangInstanceIdentifier.NodeIdentifierWithPredicates(DEEP_LIST_QNAME, + NAME_LEAF_QNAME, listItemName)) + .withChild(ImmutableNodes.leafNode(NAME_LEAF_QNAME, listItemName)) + .build() + ) + .build(); + } + + SchemaContext getSchemaCtx() throws ReactorException { + final CrossSourceStatementReactor.BuildAction buildAction = YangInferencePipeline.RFC6020_REACTOR.newBuild(); + buildAction.addSource( + new YangStatementSourceImpl(ModificationDiffTest.class.getResourceAsStream("/test-diff.yang"))); + return buildAction.buildEffective(); + } + + DataTreeModification getModification(final TipProducingDataTree dataTree) { + final DataTreeSnapshot dataTreeSnapshot = dataTree.takeSnapshot(); + return dataTreeSnapshot.newModification(); + } + + + DataTreeCandidateTip prepareModification(final TipProducingDataTree dataTree, + final DataTreeModification dataTreeModification) + throws DataValidationFailedException { + dataTreeModification.ready(); + dataTree.validate(dataTreeModification); + return dataTree.prepare(dataTreeModification); + } + + ModificationDiff getModificationDiff(final DataTreeCandidateTip prepare) throws ReactorException { + return new ModificationDiff.ModificationDiffBuilder() + .setCtx(getSchemaCtx()) + .build(prepare.getRootNode()); + } + + NormalizedNodeUpdate getNormalizedNodeUpdateForAfterType( + final Map updates, + final Class> containerNodeClass) { + return updates.values().stream() + .filter(update -> containerNodeClass.isAssignableFrom(update.getDataAfter().getClass())) + .findFirst().get(); + } + + NormalizedNodeUpdate getNormalizedNodeUpdateForBeforeType( + final Map updates, + final Class> containerNodeClass) { + return updates.values().stream() + .filter(update -> containerNodeClass.isAssignableFrom(update.getDataBefore().getClass())) + .findFirst().get(); + } + + void assertUpdate(final 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)); + } + + + ContainerNode getNestedContainerWithLeafList(final String... stringValue) { + final ListNodeBuilder> leafSetBuilder = Builders.leafSetBuilder(); + for (final String value : stringValue) { + leafSetBuilder.withChild(Builders.leafSetEntryBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeWithValue<>(NESTED_CONTAINER_LEAF_LIST, value)) + .withValue(value) + .build()); + } + + return Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_CONTAINER_QNAME)) + .withChild(leafSetBuilder + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_CONTAINER_LEAF_LIST)) + .build()) + .build(); + } + + ContainerNode getNestedContainerWithChoice(final String caseLeafValue) { + return Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_CONTAINER_QNAME)) + .withChild(Builders.choiceBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_CHOICE)) + .withChild(Builders.leafBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(UNDER_NESTED_CASE)) + .withValue(caseLeafValue) + .build()).build()) + .build(); + } + + MapEntryNode getNestedListEntry(final String listItemName, final String text) { + return Builders.mapEntryBuilder() + .withNodeIdentifier( + new YangInstanceIdentifier.NodeIdentifierWithPredicates(NESTED_LIST_QNAME, + NAME_LEAF_QNAME, listItemName)) + .withChild(ImmutableNodes.leafNode(NAME_LEAF_QNAME, listItemName)) + .withChild(ImmutableNodes.leafNode(TEXT_LEAF_QNAME, text)) + .build(); + } + + MapEntryNode getNestedListInContainerEntry(final String listItemName) { + return Builders.mapEntryBuilder() + .withNodeIdentifier( + new YangInstanceIdentifier.NodeIdentifierWithPredicates(NESTED_LIST_IN_CONTAINER_QNAME, + IN_CONTAINER_NAME_LEAF_QNAME, listItemName)) + .withChild(ImmutableNodes.leafNode(IN_CONTAINER_NAME_LEAF_QNAME, listItemName)) + .build(); + } + + MapNode getNestedList(MapEntryNode... entries) { + return Builders.mapBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_LIST_QNAME)) + .withValue(Arrays.asList(entries)) + .build(); + } + + MapNode getNestedListInContainer(MapEntryNode... entries) { + return Builders.mapBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_LIST_IN_CONTAINER_QNAME)) + .withValue(Arrays.asList(entries)) + .build(); + } + + ContainerNode getNestedContWithLeafUnderAug(String value) { + return Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_CONTAINER_QNAME)) + .withChild(Builders.leafBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(AUG_LEAF)) + .withValue(value).build()).build(); + } + + ContainerNode getNestedContWithListUnderAug(String value) { + return Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_CONTAINER_QNAME)) + .withChild(Builders.mapBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(AUG_LIST)) + .withChild(Builders.mapEntryBuilder() + .withNodeIdentifier( + new YangInstanceIdentifier.NodeIdentifierWithPredicates(AUG_LIST, AUG_LIST_KEY, + value)) + .build()) + .build()).build(); + } + + ContainerNode getNestedContWithContUnderAug(String value) { + return Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_CONTAINER_QNAME)) + .withChild(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(AUG_CONTAINER)) + .withChild(Builders.leafBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(AUG_CONTAINER_LEAF)) + .withValue(value) + .build()) + .build()).build(); + } + + ContainerNode getNestedContWithLeafListUnderAug(String value) { + return Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_CONTAINER_QNAME)) + .withChild(Builders.leafSetBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(AUG_LEAFLIST)) + .withChildValue(value) + .build()).build(); + } + + ContainerNode getNestedContWithContainerUnderNestedAug(String value) { + return Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_CONTAINER_QNAME)) + .withChild(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(AUG_CONTAINER)) + .withChild(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_AUG_CONTAINER)) + .withChild(Builders.leafBuilder() + .withNodeIdentifier( + new YangInstanceIdentifier.NodeIdentifier(NESTED_AUG_CONTAINER_LEAF)) + .withValue(value) + .build()).build()).build()).build(); + } + + ContainerNode getNestedContWithLeafListUnderNestedAug(String value) { + return Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_CONTAINER_QNAME)) + .withChild(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(AUG_CONTAINER)) + .withChild(Builders.leafSetBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_AUG_LEAF_LIST)) + .withChildValue(value) + .build()).build()).build(); + } + + ContainerNode getNestedContWithLeafUnderNestedAug(String value) { + return Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_CONTAINER_QNAME)) + .withChild(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(AUG_CONTAINER)) + .withChild(Builders.leafBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_AUG_LEAF)) + .withValue(value) + .build()).build()).build(); + } + + ContainerNode getNestedContWithListUnderNestedAug(String value) { + return Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_CONTAINER_QNAME)) + .withChild(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(AUG_CONTAINER)) + .withChild(Builders.mapBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_AUG_LIST)) + .withChild(Builders.mapEntryBuilder() + .withNodeIdentifier( + new YangInstanceIdentifier.NodeIdentifierWithPredicates(NESTED_AUG_LIST, + NESTED_AUG_LIST_KEY, value)).build()) + .build()).build()).build(); + } + + TipProducingDataTree prepareStateBeforeWithTopContainer(final NormalizedNode topContainerData) + throws ReactorException, DataValidationFailedException { + final TipProducingDataTree dataTree = getDataTree(); + final DataTreeModification dataTreeModificationOriginal = getModification(dataTree); + // non presence, but with valid child list + dataTreeModificationOriginal.write(TOP_CONTAINER_ID, topContainerData); + final DataTreeCandidateTip prepare = prepareModification(dataTree, dataTreeModificationOriginal); + dataTree.commit(prepare); + return dataTree; + } + + DataTreeCandidateTip prepareStateAfterEmpty(final TipProducingDataTree dataTree) + throws DataValidationFailedException { + final NormalizedNode topContainerModified = Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + .build(); + final DataTreeModification dataTreeModificationModified = getModification(dataTree); + dataTreeModificationModified.write(TOP_CONTAINER_ID, topContainerModified); + return prepareModification(dataTree, dataTreeModificationModified); + } + + void assertCollectionContainsOnlyDeletes(final ModificationDiff modificationDiff) { + assertTrue(modificationDiff.getUpdates().entrySet().stream().map(Map.Entry::getValue) + .map(NormalizedNodeUpdate::getDataAfter) + .filter(Objects::nonNull).collect(Collectors.toList()).isEmpty()); + } + + void assertNodeModificationPresent(final ModificationDiff modificationDiff, final Set expected) { + assertTrue(modificationDiff.getUpdates().values().stream() + .allMatch(update -> expected.contains(update.getId().getLastPathArgument().getNodeType()))); + } +} diff --git a/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationDiffAugRewriteDeleteTest.java b/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationDiffAugRewriteDeleteTest.java new file mode 100644 index 000000000..c2b959809 --- /dev/null +++ b/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationDiffAugRewriteDeleteTest.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2017 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.data.impl; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import com.google.common.collect.ImmutableSet; +import org.junit.Test; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateTip; +import org.opendaylight.yangtools.yang.data.api.schema.tree.DataValidationFailedException; +import org.opendaylight.yangtools.yang.data.api.schema.tree.TipProducingDataTree; +import org.opendaylight.yangtools.yang.data.impl.schema.Builders; +import org.opendaylight.yangtools.yang.parser.spi.meta.ReactorException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ModificationDiffAugRewriteDeleteTest extends ModificationBaseTest { + + private static final Logger LOG = LoggerFactory.getLogger(ModificationDiffAugRewriteDeleteTest.class); + + @Test + public void testWriteNonPresenceNonEmptyContainerAugWithLeaf() + throws ReactorException, DataValidationFailedException { + final TipProducingDataTree dataTree = prepareStateBeforeWithTopContainer(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + .withChild(getNestedContWithLeafUnderAug("val")) + .build()); + // now we have state with some data + + // just empty non presence container + final DataTreeCandidateTip prepareModified = prepareStateAfterEmpty(dataTree); + + final ModificationDiff modificationDiff = getModificationDiff(prepareModified); + // should detect one new entry + LOG.debug(modificationDiff.toString()); + assertThat(modificationDiff.getUpdates().size(), is(1)); + // is delete + assertCollectionContainsOnlyDeletes(modificationDiff); + assertNodeModificationPresent(modificationDiff, ImmutableSet.of(AUG_LEAF)); + } + + @Test + public void testWriteNonPresenceNonEmptyContainerAugWithList() + throws ReactorException, DataValidationFailedException { + final TipProducingDataTree dataTree = prepareStateBeforeWithTopContainer(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + .withChild(getNestedContWithListUnderAug("val")) + .build()); + // now we have state with some data + + // just empty non presence container + final DataTreeCandidateTip prepareModified = prepareStateAfterEmpty(dataTree); + + final ModificationDiff modificationDiff = getModificationDiff(prepareModified); + // should detect one new entry + LOG.debug(modificationDiff.toString()); + assertThat(modificationDiff.getUpdates().size(), is(1)); + // is delete + assertCollectionContainsOnlyDeletes(modificationDiff); + assertNodeModificationPresent(modificationDiff, ImmutableSet.of(AUG_LIST)); + } + + @Test + public void testWriteNonPresenceNonEmptyContainerAugWithNonPresenceContainer() + throws ReactorException, DataValidationFailedException { + final TipProducingDataTree dataTree = prepareStateBeforeWithTopContainer(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + .withChild(getNestedContWithContUnderAug("val")) + .build()); + // now we have state with some data + + // just empty non presence container + final DataTreeCandidateTip prepareModified = prepareStateAfterEmpty(dataTree); + + final ModificationDiff modificationDiff = getModificationDiff(prepareModified); + // should detect one new entry + LOG.debug(modificationDiff.toString()); + assertThat(modificationDiff.getUpdates().size(), is(1)); + // is delete + assertCollectionContainsOnlyDeletes(modificationDiff); + assertNodeModificationPresent(modificationDiff, ImmutableSet.of(AUG_CONTAINER_LEAF)); + } + + @Test + public void testWriteNonPresenceNonEmptyContainerAugWithLeafList() + throws ReactorException, DataValidationFailedException { + final TipProducingDataTree dataTree = prepareStateBeforeWithTopContainer(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + .withChild(getNestedContWithLeafListUnderAug("val")) + .build()); + // now we have state with some data + + // just empty non presence container + final DataTreeCandidateTip prepareModified = prepareStateAfterEmpty(dataTree); + + final ModificationDiff modificationDiff = getModificationDiff(prepareModified); + // should detect one new entry + LOG.debug(modificationDiff.toString()); + assertThat(modificationDiff.getUpdates().size(), is(1)); + // is delete + assertCollectionContainsOnlyDeletes(modificationDiff); + assertNodeModificationPresent(modificationDiff, ImmutableSet.of(AUG_LEAFLIST)); + } +} diff --git a/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationDiffNestedAugRewriteDeleteTest.java b/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationDiffNestedAugRewriteDeleteTest.java new file mode 100644 index 000000000..9c92b4f85 --- /dev/null +++ b/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationDiffNestedAugRewriteDeleteTest.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2017 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.data.impl; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import com.google.common.collect.ImmutableSet; +import org.junit.Test; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateTip; +import org.opendaylight.yangtools.yang.data.api.schema.tree.DataValidationFailedException; +import org.opendaylight.yangtools.yang.data.api.schema.tree.TipProducingDataTree; +import org.opendaylight.yangtools.yang.data.impl.schema.Builders; +import org.opendaylight.yangtools.yang.parser.spi.meta.ReactorException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ModificationDiffNestedAugRewriteDeleteTest extends ModificationBaseTest { + + private static final Logger LOG = LoggerFactory.getLogger(ModificationDiffNestedAugRewriteDeleteTest.class); + + @Test + public void testWriteNonPresenceNonEmptyContainerNestedAugWithContainer() + throws DataValidationFailedException, ReactorException { + final TipProducingDataTree dataTree = prepareStateBeforeWithTopContainer(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + .withChild(getNestedContWithContainerUnderNestedAug("val")) + .build()); + // now we have state with some data + + // just empty non presence container + final DataTreeCandidateTip prepareModified = prepareStateAfterEmpty(dataTree); + + final ModificationDiff modificationDiff = getModificationDiff(prepareModified); + // should detect one new entry + LOG.debug(modificationDiff.toString()); + assertThat(modificationDiff.getUpdates().size(), is(1)); + // is delete + assertCollectionContainsOnlyDeletes(modificationDiff); + assertNodeModificationPresent(modificationDiff, ImmutableSet.of(NESTED_AUG_CONTAINER_LEAF)); + } + + @Test + public void testWriteNonPresenceNonEmptyContainerNestedAugWithLeafList() + throws DataValidationFailedException, ReactorException { + final TipProducingDataTree dataTree = prepareStateBeforeWithTopContainer(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + .withChild(getNestedContWithLeafListUnderNestedAug("val")) + .build()); + // now we have state with some data + + // just empty non presence container + final DataTreeCandidateTip prepareModified = prepareStateAfterEmpty(dataTree); + + final ModificationDiff modificationDiff = getModificationDiff(prepareModified); + // should detect one new entry + LOG.debug(modificationDiff.toString()); + assertThat(modificationDiff.getUpdates().size(), is(1)); + // is delete + assertCollectionContainsOnlyDeletes(modificationDiff); + assertNodeModificationPresent(modificationDiff, ImmutableSet.of(NESTED_AUG_LEAF_LIST)); + } + + @Test + public void testWriteNonPresenceNonEmptyContainerNestedAugWithList() + throws DataValidationFailedException, ReactorException { + final TipProducingDataTree dataTree = prepareStateBeforeWithTopContainer(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + .withChild(getNestedContWithListUnderNestedAug("val")) + .build()); + // now we have state with some data + + // just empty non presence container + final DataTreeCandidateTip prepareModified = prepareStateAfterEmpty(dataTree); + + final ModificationDiff modificationDiff = getModificationDiff(prepareModified); + // should detect one new entry + LOG.debug(modificationDiff.toString()); + assertThat(modificationDiff.getUpdates().size(), is(1)); + // is delete + assertCollectionContainsOnlyDeletes(modificationDiff); + assertNodeModificationPresent(modificationDiff, ImmutableSet.of(NESTED_AUG_LIST)); + } + + @Test + public void testWriteNonPresenceNonEmptyContainerNestedAugWithLeaf() + throws DataValidationFailedException, ReactorException { + final TipProducingDataTree dataTree = prepareStateBeforeWithTopContainer(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + .withChild(getNestedContWithLeafUnderNestedAug("val")) + .build()); + // now we have state with some data + + // just empty non presence container + final DataTreeCandidateTip prepareModified = prepareStateAfterEmpty(dataTree); + + final ModificationDiff modificationDiff = getModificationDiff(prepareModified); + // should detect one new entry + LOG.debug(modificationDiff.toString()); + assertThat(modificationDiff.getUpdates().size(), is(1)); + // is delete + assertCollectionContainsOnlyDeletes(modificationDiff); + assertNodeModificationPresent(modificationDiff, ImmutableSet.of(NESTED_AUG_LEAF)); + } +} diff --git a/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationDiffRewriteDeleteTest.java b/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationDiffRewriteDeleteTest.java new file mode 100644 index 000000000..a1ede9691 --- /dev/null +++ b/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationDiffRewriteDeleteTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2017 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.data.impl; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import com.google.common.collect.ImmutableSet; +import org.junit.Test; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode; +import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidateTip; +import org.opendaylight.yangtools.yang.data.api.schema.tree.TipProducingDataTree; +import org.opendaylight.yangtools.yang.data.impl.schema.Builders; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ModificationDiffRewriteDeleteTest extends ModificationBaseTest { + + private static final Logger LOG = LoggerFactory.getLogger(ModificationDiffRewriteDeleteTest.class); + + /** + * Covers case when non-presence container was already filled with some data, + * and modification removes data by overriding by empty list + */ + @Test + public void testWriteNonPresenceNonEmptyContainerPreviousDataOverrideByEmpty() throws Exception { + final MapEntryNode alreadyPresent = getNestedListEntry("value", "txt"); + final TipProducingDataTree dataTree = prepareStateBeforeWithTopContainer(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + .withChild(getNestedList(alreadyPresent)) + .build()); + // now we have state with some data + + // just empty non presence container + final DataTreeCandidateTip prepareModified = prepareStateAfterEmpty(dataTree); + + final ModificationDiff modificationDiff = getModificationDiff(prepareModified); + // should detect one new entry + LOG.debug(modificationDiff.toString()); + assertThat(modificationDiff.getUpdates().size(), is(1)); + // is delete + assertCollectionContainsOnlyDeletes(modificationDiff); + assertNodeModificationPresent(modificationDiff, ImmutableSet.of(NESTED_LIST_QNAME)); + } + + /** + * Covers case when non-presence container was already filled with some data, + * and modification removes data by overriding by empty list. This case tests + * when there are multiple nested non-presence containers + */ + @Test + public void testWriteNonPresenceMultipleNonEmptyContainerPreviousDataOverrideByEmpty() throws Exception { + final MapEntryNode alreadyPresent = getNestedListInContainerEntry("key"); + final TipProducingDataTree dataTree = prepareStateBeforeWithTopContainer(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + // another non-presence container with list entry + .withChild(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_CONTAINER_QNAME)) + .withChild(getNestedListInContainer(alreadyPresent)) + .build()) + // direct list entry + .withChild(getNestedList(alreadyPresent)) + .build()); + // now we have state with some data + + // just empty non presence container + final DataTreeCandidateTip prepareModified = prepareStateAfterEmpty(dataTree); + + final ModificationDiff modificationDiff = getModificationDiff(prepareModified); + // should detect two new entries + LOG.debug(modificationDiff.toString()); + assertThat(modificationDiff.getUpdates().size(), is(2)); + // is delete + assertCollectionContainsOnlyDeletes(modificationDiff); + assertNodeModificationPresent(modificationDiff, + ImmutableSet.of(NESTED_LIST_IN_CONTAINER_QNAME, NESTED_LIST_QNAME)); + } + + /** + * Covers case when non-presence container was already filled with some data, + * and modification removes data by overriding. Tests case with leaf + */ + @Test + public void testWriteNonPresenceNonEmptyContainerLeaf() throws Exception { + final TipProducingDataTree dataTree = prepareStateBeforeWithTopContainer(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + // another non-presence container with leaf + .withChild(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_CONTAINER_QNAME)) + .withChild(Builders.leafBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_CONTAINER_VAL)) + .withValue("val").build()) + .build()) + .build()); + // now we have state with some data + + // just empty non presence container + final DataTreeCandidateTip prepareModified = prepareStateAfterEmpty(dataTree); + + final ModificationDiff modificationDiff = getModificationDiff(prepareModified); + // should detect one new entry + LOG.debug(modificationDiff.toString()); + assertThat(modificationDiff.getUpdates().size(), is(1)); + // is delete + assertCollectionContainsOnlyDeletes(modificationDiff); + assertNodeModificationPresent(modificationDiff, ImmutableSet.of(NESTED_CONTAINER_VAL)); + } + + /** + * Covers case when non-presence container was already filled with some data, + * and modification removes data by overriding. Tests case with leaf-list + */ + @Test + public void testWriteNonPresenceNonEmptyContainerLeafList() throws Exception { + final TipProducingDataTree dataTree = prepareStateBeforeWithTopContainer(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + // another non-presence container with leaf + .withChild(getNestedContainerWithLeafList()) + .build()); + // now we have state with some data + + // just empty non presence container + final DataTreeCandidateTip prepareModified = prepareStateAfterEmpty(dataTree); + + final ModificationDiff modificationDiff = getModificationDiff(prepareModified); + // should detect one new entry + LOG.debug(modificationDiff.toString()); + assertThat(modificationDiff.getUpdates().size(), is(1)); + // is delete + assertCollectionContainsOnlyDeletes(modificationDiff); + assertNodeModificationPresent(modificationDiff, ImmutableSet.of(NESTED_CONTAINER_LEAF_LIST)); + } + + /** + * Covers case when non-presence container was already filled with some data, + * and modification removes data by overriding. Tests case with choice/case + */ + @Test + public void testWriteNonPresenceNonEmptyContainerWithChoice() throws Exception { + final TipProducingDataTree dataTree = prepareStateBeforeWithTopContainer(Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + // another non-presence container with leaf + .withChild(getNestedContainerWithChoice("val")) + .build()); + // now we have state with some data + + // just empty non presence container + final DataTreeCandidateTip prepareModified = prepareStateAfterEmpty(dataTree); + + final ModificationDiff modificationDiff = getModificationDiff(prepareModified); + // should detect one new entry + LOG.debug(modificationDiff.toString()); + assertThat(modificationDiff.getUpdates().size(), is(1)); + // is delete + assertCollectionContainsOnlyDeletes(modificationDiff); + assertNodeModificationPresent(modificationDiff, ImmutableSet.of(UNDER_NESTED_CASE)); + } +} diff --git a/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationDiffTest.java b/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationDiffTest.java index 664b57378..cd980dd1e 100644 --- a/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationDiffTest.java +++ b/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationDiffTest.java @@ -23,59 +23,26 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import com.google.common.base.Optional; +import com.google.common.collect.ImmutableSet; import java.util.Map; import org.junit.Test; -import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; -import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode; 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; -import org.opendaylight.yangtools.yang.data.api.schema.tree.DataValidationFailedException; import org.opendaylight.yangtools.yang.data.api.schema.tree.TipProducingDataTree; -import org.opendaylight.yangtools.yang.data.api.schema.tree.TreeType; import org.opendaylight.yangtools.yang.data.impl.schema.Builders; import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes; -import org.opendaylight.yangtools.yang.data.impl.schema.builder.api.ListNodeBuilder; -import org.opendaylight.yangtools.yang.data.impl.schema.tree.InMemoryDataTreeFactory; -import org.opendaylight.yangtools.yang.model.api.SchemaContext; -import org.opendaylight.yangtools.yang.parser.spi.meta.ReactorException; -import org.opendaylight.yangtools.yang.parser.stmt.reactor.CrossSourceStatementReactor; -import org.opendaylight.yangtools.yang.parser.stmt.rfc6020.YangInferencePipeline; -import org.opendaylight.yangtools.yang.parser.stmt.rfc6020.YangStatementSourceImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -public class ModificationDiffTest { +public class ModificationDiffTest extends ModificationBaseTest { - static final QName TOP_CONTAINER_QNAME = - QName.create("urn:opendaylight:params:xml:ns:yang:test:diff", "2015-01-05", "top-container"); - 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 EMPTY_QNAME = QName.create(TOP_CONTAINER_QNAME, "empty"); - static final QName IN_EMPTY_QNAME = QName.create(TOP_CONTAINER_QNAME, "in-empty"); - - static final QName FOR_LEAF_LIST_QNAME = QName.create(TOP_CONTAINER_QNAME, "for-leaf-list"); - static final QName NESTED_LEAF_LIST_QNAME = QName.create(TOP_CONTAINER_QNAME, "nested-leaf-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 QName PRESENCE_CONTAINER_QNAME = QName.create(TOP_CONTAINER_QNAME, "presence"); - - 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 Logger LOG = LoggerFactory.getLogger(ModificationDiffTest.class); @Test public void testInitialWrite() throws Exception { @@ -150,17 +117,14 @@ public class ModificationDiffTest { dataTreeModification.write(WITH_CHOICE_CONTAINER_ID, containerWithChoice); final DataTreeCandidateTip prepare = prepareModification(dataTree, dataTreeModification); - final Map updates = getModificationDiff(prepare).getUpdates(); + 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) { - final DataTreeSnapshot dataTreeSnapshot = dataTree.takeSnapshot(); - return dataTreeSnapshot.newModification(); - } + @Test public void testWriteNonPresenceEmptyContainer() throws Exception { @@ -175,6 +139,65 @@ public class ModificationDiffTest { assertThat(modificationDiff.getUpdates().size(), is(0)); } + /** + * Covers case when both non-presence and nested is not present + */ + @Test + public void testWriteNonPresenceNonEmptyContainer() throws Exception { + final TipProducingDataTree dataTree = getDataTree(); + final DataTreeModification dataTreeModification = getModification(dataTree); + // non presence ,but with valid child list + final NormalizedNode topContainer = Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + .withChild(getNestedList("value","txt")) + .build(); + + dataTreeModification.write(TOP_CONTAINER_ID, topContainer); + final DataTreeCandidateTip prepare = prepareModification(dataTree, dataTreeModification); + + final ModificationDiff modificationDiff = getModificationDiff(prepare); + + assertThat(modificationDiff.getUpdates().size(), is(1)); + assertNodeModificationPresent(modificationDiff, ImmutableSet.of(NESTED_LIST_QNAME)); + } + + /** + * Covers case when non-presence container was already filled with some data, + * and modification just ads some other nested data + */ + @Test + public void testWriteNonPresenceNonEmptyContainerPreviousData() throws Exception { + final TipProducingDataTree dataTree = getDataTree(); + final DataTreeModification dataTreeModificationOriginal = getModification(dataTree); + // non presence, but with valid child list + final MapEntryNode alreadyPresent = getNestedListEntry("value", "txt"); + final NormalizedNode topContainerOriginal = Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + .withChild(getNestedList(alreadyPresent)) + .build(); + + dataTreeModificationOriginal.write(TOP_CONTAINER_ID, topContainerOriginal); + final DataTreeCandidateTip prepare = prepareModification(dataTree, dataTreeModificationOriginal); + dataTree.commit(prepare); + // now we have state with some data + + final MapEntryNode newEntry = getNestedListEntry("value2", "txt2"); + final NormalizedNode topContainerModified = Builders.containerBuilder() + .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) + .withChild(getNestedList(alreadyPresent, newEntry)) + .build(); + final DataTreeModification dataTreeModificationModified = getModification(dataTree); + dataTreeModificationModified.write(TOP_CONTAINER_ID, topContainerModified); + final DataTreeCandidateTip prepareModified = prepareModification(dataTree, dataTreeModificationModified); + + final ModificationDiff modificationDiff = getModificationDiff(prepareModified); + // should detect one new entry + LOG.debug(modificationDiff.toString()); + assertThat(modificationDiff.getUpdates().size(), is(1)); + assertNodeModificationPresent(modificationDiff, ImmutableSet.of(NESTED_LIST_QNAME)); + } + + @Test public void testWriteNonPresenceEmptyNestedContainer() throws Exception { final TipProducingDataTree dataTree = getDataTree(); @@ -195,14 +218,6 @@ public class ModificationDiffTest { assertThat(modificationDiff.getUpdates().size(), is(1)); } - private DataTreeCandidateTip prepareModification(final TipProducingDataTree dataTree, - final DataTreeModification dataTreeModification) - throws DataValidationFailedException { - dataTreeModification.ready(); - dataTree.validate(dataTreeModification); - return dataTree.prepare(dataTreeModification); - } - @Test public void testUpdateWrite() throws Exception { final TipProducingDataTree dataTree = getDataTree(); @@ -214,16 +229,13 @@ public class ModificationDiffTest { dataTreeModification.write(TOP_CONTAINER_ID, topContainerAfter); final DataTreeCandidateTip prepare = prepareModification(dataTree, dataTreeModification); - final Map updates = getModificationDiff(prepare).getUpdates(); + 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); } - private ModificationDiff getModificationDiff(final DataTreeCandidateTip prepare) throws ReactorException { - return ModificationDiff.recursivelyFromCandidateRoot(prepare.getRootNode(), getSchemaCtx()); - } @Test public void testUpdateMerge() throws Exception { @@ -236,7 +248,7 @@ public class ModificationDiffTest { dataTreeModification.merge(TOP_CONTAINER_ID, topContainerAfter); final DataTreeCandidateTip prepare = prepareModification(dataTree, dataTreeModification); - final Map updates = getModificationDiff(prepare).getUpdates(); + 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); @@ -252,7 +264,7 @@ public class ModificationDiffTest { dataTreeModification.delete(TOP_CONTAINER_ID); final DataTreeCandidateTip prepare = prepareModification(dataTree, dataTreeModification); - final Map updates = getModificationDiff(prepare).getUpdates(); + 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); @@ -276,7 +288,7 @@ public class ModificationDiffTest { dataTree.validate(dataTreeModification); DataTreeCandidateTip prepare = dataTree.prepare(dataTreeModification); - Map updates = getModificationDiff(prepare).getUpdates(); + Map updates = getModificationDiff(prepare).getUpdates(); assertThat(updates.size(), is(1)); assertUpdate(getNormalizedNodeUpdateForAfterType(updates, MapEntryNode.class), @@ -301,14 +313,6 @@ public class ModificationDiffTest { 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 public void testWriteTopContainerAndInnerList() throws Exception { @@ -332,7 +336,7 @@ public class ModificationDiffTest { final DataTreeCandidateTip prepare = prepareModification(dataTree, dataTreeModification); - final Map updates = getModificationDiff(prepare).getUpdates(); + final Map updates = getModificationDiff(prepare).getUpdates(); assertThat(updates.size(), is(2)); assertThat(updates.values().size(), is(2)); @@ -345,21 +349,7 @@ public class ModificationDiffTest { listEntryId)); } - private ModificationDiff.NormalizedNodeUpdate getNormalizedNodeUpdateForAfterType( - final Map updates, - final Class> containerNodeClass) { - return updates.values().stream() - .filter(update -> containerNodeClass.isAssignableFrom(update.getDataAfter().getClass())) - .findFirst().get(); - } - 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 { @@ -415,7 +405,7 @@ public class ModificationDiffTest { prepare = dataTree.prepare(dataTreeModification); dataTree.commit(prepare); - final Map updates = getModificationDiff(prepare).getUpdates(); + final Map updates = getModificationDiff(prepare).getUpdates(); assertThat(updates.size(), is(1)); assertUpdate(getNormalizedNodeUpdateForAfterType(updates, MapEntryNode.class), deepListEntryId, null, deepListEntry); } @@ -450,88 +440,10 @@ public class ModificationDiffTest { dataTree.validate(dataTreeModification); prepare = dataTree.prepare(dataTreeModification); - final Map updates = getModificationDiff(prepare).getUpdates(); + final Map updates = getModificationDiff(prepare).getUpdates(); assertThat(updates.size(), is(1)); assertUpdate(getNormalizedNodeUpdateForBeforeType(updates, MapEntryNode.class), listItemId, mapNode.getValue().iterator().next(), null); } - static void addNodeToTree(final DataTree dataTree, final NormalizedNode node, - final YangInstanceIdentifier id) - throws DataValidationFailedException { - DataTreeSnapshot dataTreeSnapshot = dataTree.takeSnapshot(); - DataTreeModification dataTreeModification = dataTreeSnapshot.newModification(); - dataTreeModification.write(id, node); - dataTreeModification.ready(); - dataTree.validate(dataTreeModification); - DataTreeCandidate prepare = dataTree.prepare(dataTreeModification); - dataTree.commit(prepare); - } - - static TipProducingDataTree getDataTree() throws ReactorException { - final TipProducingDataTree dataTree = InMemoryDataTreeFactory.getInstance().create(TreeType.CONFIGURATION); - dataTree.setSchemaContext(getSchemaCtx()); - return dataTree; - } - - static ContainerNode getTopContainer(final String stringValue) { - return Builders.containerBuilder() - .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) - .withChild(ImmutableNodes.leafNode(STRING_LEAF_QNAME, stringValue)) - .build(); - } - - static ContainerNode getTopContainerWithLeafList(final String... stringValue) { - final ListNodeBuilder> leafSetBuilder = Builders.leafSetBuilder(); - for (final String value : stringValue) { - leafSetBuilder.withChild(Builders.leafSetEntryBuilder() - .withNodeIdentifier(new YangInstanceIdentifier.NodeWithValue<>(NESTED_LEAF_LIST_QNAME, value)) - .withValue(value) - .build()); - } - return Builders.containerBuilder() - .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(TOP_CONTAINER_QNAME)) - .withChild(Builders.containerBuilder() - .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(FOR_LEAF_LIST_QNAME)) - .withChild(leafSetBuilder - .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_LEAF_LIST_QNAME)) - .build()) - .build()) - .build(); - } - - static MapNode getNestedList(final String listItemName, final String text) { - return Builders.mapBuilder() - .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NESTED_LIST_QNAME)) - .withChild( - Builders.mapEntryBuilder() - .withNodeIdentifier( - new YangInstanceIdentifier.NodeIdentifierWithPredicates(NESTED_LIST_QNAME, - NAME_LEAF_QNAME, listItemName)) - .withChild(ImmutableNodes.leafNode(NAME_LEAF_QNAME, listItemName)) - .withChild(ImmutableNodes.leafNode(TEXT_LEAF_QNAME, text)) - .build() - ) - .build(); - } - - private MapNode getDeepList(final String listItemName) { - return Builders.mapBuilder() - .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(DEEP_LIST_QNAME)) - .withChild( - Builders.mapEntryBuilder() - .withNodeIdentifier( - new YangInstanceIdentifier.NodeIdentifierWithPredicates(DEEP_LIST_QNAME, - NAME_LEAF_QNAME, listItemName)) - .withChild(ImmutableNodes.leafNode(NAME_LEAF_QNAME, listItemName)) - .build() - ) - .build(); - } - - static SchemaContext getSchemaCtx() throws ReactorException { - final CrossSourceStatementReactor.BuildAction buildAction = YangInferencePipeline.RFC6020_REACTOR.newBuild(); - buildAction.addSource(new YangStatementSourceImpl(ModificationDiffTest.class.getResourceAsStream("/test-diff.yang"))); - return buildAction.buildEffective(); - } } \ No newline at end of file diff --git a/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationMetadata.java b/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationMetadata.java new file mode 100644 index 000000000..27d16307a --- /dev/null +++ b/infra/data-impl/src/test/java/io/fd/honeycomb/data/impl/ModificationMetadata.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2017 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.data.impl; + +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; + +abstract class ModificationMetadata { + + private static final String YANG_NAMESPACE = "urn:opendaylight:params:xml:ns:yang:test:diff"; + final QName TOP_CONTAINER_QNAME = QName.create(YANG_NAMESPACE, "2015-01-05", "top-container"); + final QName STRING_LEAF_QNAME = QName.create(TOP_CONTAINER_QNAME, "string"); + final QName NAME_LEAF_QNAME = QName.create(TOP_CONTAINER_QNAME, "name"); + final QName TEXT_LEAF_QNAME = QName.create(TOP_CONTAINER_QNAME, "text"); + final QName NESTED_LIST_QNAME = QName.create(TOP_CONTAINER_QNAME, "nested-list"); + final QName DEEP_LIST_QNAME = QName.create(TOP_CONTAINER_QNAME, "deep-list"); + final QName EMPTY_QNAME = QName.create(TOP_CONTAINER_QNAME, "empty"); + + final QName FOR_LEAF_LIST_QNAME = QName.create(TOP_CONTAINER_QNAME, "for-leaf-list"); + final QName NESTED_LEAF_LIST_QNAME = QName.create(TOP_CONTAINER_QNAME, "nested-leaf-list"); + + final QName WITH_CHOICE_CONTAINER_QNAME = QName.create(YANG_NAMESPACE, "2015-01-05", "with-choice"); + final QName CHOICE_QNAME = QName.create(WITH_CHOICE_CONTAINER_QNAME, "choice"); + final QName IN_CASE1_LEAF_QNAME = QName.create(WITH_CHOICE_CONTAINER_QNAME, "in-case1"); + + final QName PRESENCE_CONTAINER_QNAME = QName.create(TOP_CONTAINER_QNAME, "presence"); + + final QName NESTED_CONTAINER_QNAME = QName.create(TOP_CONTAINER_QNAME, "nested-container"); + final QName NESTED_LIST_IN_CONTAINER_QNAME = QName.create(NESTED_CONTAINER_QNAME, "nested-list-in-container"); + final QName IN_CONTAINER_NAME_LEAF_QNAME = QName.create(NESTED_LIST_IN_CONTAINER_QNAME, "name"); + final QName NESTED_CONTAINER_VAL = QName.create(NESTED_CONTAINER_QNAME, "nested-container-val"); + final QName NESTED_CONTAINER_LEAF_LIST = QName.create(NESTED_CONTAINER_QNAME, "nested-container-leaf-list"); + + final QName NESTED_CHOICE = QName.create(NESTED_CONTAINER_QNAME,"nested-choice"); + final QName NESTED_CASE=QName.create(NESTED_CHOICE,"nested-case"); + final QName UNDER_NESTED_CASE=QName.create(NESTED_CASE,"under-nested-case"); + + final QName AUG_LEAF = QName.create(NESTED_CONTAINER_QNAME,"under-aug-leaf"); + final QName AUG_LEAFLIST = QName.create(NESTED_CONTAINER_QNAME,"under-aug-leaflist"); + final QName AUG_CONTAINER = QName.create(NESTED_CONTAINER_QNAME,"under-aug-container"); + final QName AUG_CONTAINER_LEAF = QName.create(NESTED_CONTAINER_QNAME,"under-aug-cont-leaf"); + final QName AUG_LIST = QName.create(NESTED_CONTAINER_QNAME,"under-aug-list"); + final QName AUG_LIST_KEY = QName.create(NESTED_CONTAINER_QNAME,"under-aug-list-key"); + + final QName NESTED_AUG_CONTAINER = QName.create(AUG_CONTAINER, "nested-under-aug-container"); + final QName NESTED_AUG_CONTAINER_LEAF = QName.create(AUG_CONTAINER, "nested-under-aug-container-leaf"); + final QName NESTED_AUG_LEAF = QName.create(AUG_CONTAINER, "nested-under-aug-leaf"); + final QName NESTED_AUG_LIST = QName.create(AUG_CONTAINER, "nested-under-aug-list"); + final QName NESTED_AUG_LIST_KEY = QName.create(AUG_CONTAINER, "nested-under-aug-list-key"); + final QName NESTED_AUG_LEAF_LIST = QName.create(AUG_CONTAINER, "nested-under-aug-leaf-list"); + + final YangInstanceIdentifier TOP_CONTAINER_ID = YangInstanceIdentifier.of(TOP_CONTAINER_QNAME); + final YangInstanceIdentifier NESTED_LIST_ID = TOP_CONTAINER_ID.node(new YangInstanceIdentifier.NodeIdentifier(NESTED_LIST_QNAME)); +} diff --git a/infra/data-impl/src/test/resources/test-diff.yang b/infra/data-impl/src/test/resources/test-diff.yang index b7a0c7e7f..f86ad32a7 100644 --- a/infra/data-impl/src/test/resources/test-diff.yang +++ b/infra/data-impl/src/test/resources/test-diff.yang @@ -28,6 +28,32 @@ module test-diff { } } + container nested-container { + list nested-list-in-container { + key "name"; + + leaf name { + type string; + } + } + + leaf nested-container-val { + type string; + } + + leaf-list nested-container-leaf-list { + type string; + } + + choice nested-choice { + case nested-case{ + leaf under-nested-case{ + type string; + } + } + } + } + list nested-list { key "name"; @@ -50,6 +76,52 @@ module test-diff { } } + augment /top-container/nested-container { + container under-aug-container{ + leaf under-aug-cont-leaf{ + type string; + } + } + + leaf under-aug-leaf { + type string; + } + + list under-aug-list { + key under-aug-list-key; + leaf under-aug-list-key{ + type string; + } + } + + leaf-list under-aug-leaflist{ + type string; + } + } + + augment /top-container/nested-container/under-aug-container { + container nested-under-aug-container { + leaf nested-under-aug-container-leaf { + type string; + } + } + + leaf nested-under-aug-leaf{ + type string; + } + + list nested-under-aug-list{ + key nested-under-aug-list-key; + leaf nested-under-aug-list-key{ + type string; + } + } + + leaf-list nested-under-aug-leaf-list { + type string; + } + } + container with-choice { choice choice { -- cgit 1.2.3-korg