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 --- .../fd/honeycomb/data/impl/ModificationDiff.java | 493 ++++++--------------- 1 file changed, 127 insertions(+), 366 deletions(-) (limited to 'infra/data-impl/src/main/java/io/fd/honeycomb/data/impl/ModificationDiff.java') 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); } } + } -- cgit 1.2.3-korg