/* * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html */ package org.opendaylight.mdsal.binding.dom.adapter; import static com.google.common.base.Verify.verifyNotNull; import static java.util.Objects.requireNonNull; import static org.opendaylight.yangtools.yang.data.tree.api.ModificationType.UNMODIFIED; import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects.ToStringHelper; import com.google.common.base.VerifyException; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.opendaylight.mdsal.binding.api.DataObjectModification; import org.opendaylight.mdsal.binding.dom.codec.api.BindingAugmentationCodecTreeNode; import org.opendaylight.mdsal.binding.dom.codec.api.BindingDataObjectCodecTreeNode; import org.opendaylight.mdsal.binding.dom.codec.api.CommonDataObjectCodecTreeNode; import org.opendaylight.yangtools.yang.binding.Augmentation; import org.opendaylight.yangtools.yang.binding.ChildOf; import org.opendaylight.yangtools.yang.binding.ChoiceIn; import org.opendaylight.yangtools.yang.binding.DataObject; import org.opendaylight.yangtools.yang.binding.InstanceIdentifier.IdentifiableItem; import org.opendaylight.yangtools.yang.binding.InstanceIdentifier.Item; import org.opendaylight.yangtools.yang.binding.InstanceIdentifier.PathArgument; import org.opendaylight.yangtools.yang.binding.Key; import org.opendaylight.yangtools.yang.binding.KeyAware; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; import org.opendaylight.yangtools.yang.data.tree.api.DataTreeCandidateNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Lazily translated {@link DataObjectModification} based on {@link DataTreeCandidateNode}. * {@link AbstractDataObjectModification} represents Data tree change event, but whole tree is not translated or * resolved eagerly, but only child nodes which are directly accessed by user of data object modification. * *

* This class is further specialized as {@link LazyAugmentationModification} and {@link LazyDataObjectModification}, as * both use different serialization methods. * * @param Type of Binding {@link DataObject} * @param Type of underlying {@link CommonDataObjectCodecTreeNode} */ abstract sealed class AbstractDataObjectModification> implements DataObjectModification permits LazyAugmentationModification, LazyDataObjectModification { private static final Logger LOG = LoggerFactory.getLogger(AbstractDataObjectModification.class); private static final @NonNull Object NULL_DATA_OBJECT = new Object(); private static final VarHandle MODIFICATION_TYPE; private static final VarHandle MODIFIED_CHILDREN; private static final VarHandle DATA_BEFORE; private static final VarHandle DATA_AFTER; static { final var lookup = MethodHandles.lookup(); try { MODIFICATION_TYPE = lookup.findVarHandle(AbstractDataObjectModification.class, "modificationType", ModificationType.class); MODIFIED_CHILDREN = lookup.findVarHandle(AbstractDataObjectModification.class, "modifiedChildren", ImmutableList.class); DATA_BEFORE = lookup.findVarHandle(AbstractDataObjectModification.class, "dataBefore", Object.class); DATA_AFTER = lookup.findVarHandle(AbstractDataObjectModification.class, "dataAfter", Object.class); } catch (NoSuchFieldException | IllegalAccessException e) { throw new ExceptionInInitializerError(e); } } final @NonNull DataTreeCandidateNode domData; final @NonNull PathArgument identifier; final @NonNull N codec; @SuppressWarnings("unused") @SuppressFBWarnings(value = "UUF_UNUSED_FIELD", justification = "https://github.com/spotbugs/spotbugs/issues/2749") private volatile ImmutableList> modifiedChildren; @SuppressWarnings("unused") @SuppressFBWarnings(value = "UUF_UNUSED_FIELD", justification = "https://github.com/spotbugs/spotbugs/issues/2749") private volatile ModificationType modificationType; @SuppressWarnings("unused") @SuppressFBWarnings(value = "UUF_UNUSED_FIELD", justification = "https://github.com/spotbugs/spotbugs/issues/2749") private volatile Object dataBefore; @SuppressWarnings("unused") @SuppressFBWarnings(value = "UUF_UNUSED_FIELD", justification = "https://github.com/spotbugs/spotbugs/issues/2749") private volatile Object dataAfter; AbstractDataObjectModification(final DataTreeCandidateNode domData, final N codec, final PathArgument identifier) { this.domData = requireNonNull(domData); this.identifier = requireNonNull(identifier); this.codec = requireNonNull(codec); } static @Nullable AbstractDataObjectModification from(final CommonDataObjectCodecTreeNode codec, final @NonNull DataTreeCandidateNode current) { if (codec instanceof BindingDataObjectCodecTreeNode childDataObjectCodec) { return new LazyDataObjectModification<>(childDataObjectCodec, current); } else if (codec instanceof BindingAugmentationCodecTreeNode childAugmentationCodec) { return LazyAugmentationModification.forParent(childAugmentationCodec, current); } else { throw new VerifyException("Unhandled codec " + codec); } } @Override public final Class getDataType() { return codec.getBindingClass(); } @Override public final PathArgument getIdentifier() { return identifier; } @Override public final ModificationType getModificationType() { final var local = (ModificationType) MODIFICATION_TYPE.getAcquire(this); return local != null ? local : loadModificationType(); } private @NonNull ModificationType loadModificationType() { final var domModificationType = domModificationType(); final var computed = switch (domModificationType) { case APPEARED, WRITE -> ModificationType.WRITE; case DISAPPEARED, DELETE -> ModificationType.DELETE; case SUBTREE_MODIFIED -> resolveSubtreeModificationType(); default -> // TODO: Should we lie about modification type instead of exception? throw new IllegalStateException("Unsupported DOM Modification type " + domModificationType); }; MODIFICATION_TYPE.setRelease(this, computed); return computed; } @Override public final T getDataBefore() { final var local = DATA_BEFORE.getAcquire(this); return local != null ? unmask(local) : loadDataBefore(); } private @Nullable T loadDataBefore() { final var computed = deserializeNullable(domData.dataBefore()); final var witness = DATA_BEFORE.compareAndExchangeRelease(this, null, mask(computed)); return witness == null ? computed : unmask(witness); } @Override public final T getDataAfter() { final var local = DATA_AFTER.getAcquire(this); return local != null ? unmask(local) : loadDataAfter(); } private @Nullable T loadDataAfter() { final var computed = deserializeNullable(domData.dataAfter()); final var witness = DATA_AFTER.compareAndExchangeRelease(this, null, mask(computed)); return witness == null ? computed : unmask(witness); } private static @NonNull Object mask(final @Nullable T obj) { return obj != null ? obj : NULL_DATA_OBJECT; } @SuppressWarnings("unchecked") private @Nullable T unmask(final @NonNull Object obj) { return obj == NULL_DATA_OBJECT ? null : (T) verifyNotNull(obj); } private @Nullable T deserializeNullable(final @Nullable NormalizedNode normalized) { return normalized == null ? null : deserialize(normalized); } abstract @Nullable T deserialize(@NonNull NormalizedNode normalized); @Override public final DataObjectModification getModifiedChild(final PathArgument arg) { final var domArgumentList = new ArrayList(); final var childCodec = codec.bindingPathArgumentChild(arg, domArgumentList); final var toEnter = domArgumentList.iterator(); // Careful now: we need to validated the first item against subclass var current = toEnter.hasNext() ? firstModifiedChild(toEnter.next()) : domData; // ... and for everything else we can just go wild while (toEnter.hasNext() && current != null) { current = current.modifiedChild(toEnter.next()); } if (current == null || current.modificationType() == UNMODIFIED) { return null; } return from(childCodec, current); } abstract @Nullable DataTreeCandidateNode firstModifiedChild(YangInstanceIdentifier.PathArgument arg); @Override public final ImmutableList> getModifiedChildren() { final var local = (ImmutableList>) MODIFIED_CHILDREN.getAcquire(this); return local != null ? local : loadModifiedChilden(); } @Override public final > List> getModifiedChildren( final Class childType) { return streamModifiedChildren(childType).collect(Collectors.toList()); } @Override public final & DataObject, C extends ChildOf> List> getModifiedChildren(final Class caseType, final Class childType) { return streamModifiedChildren(childType) .filter(child -> caseType.equals(child.identifier.getCaseType().orElse(null))) .collect(Collectors.toList()); } @SuppressWarnings("unchecked") private @NonNull ImmutableList> loadModifiedChilden() { final var builder = ImmutableList.>builder(); populateList(builder, codec, domData, domChildNodes()); final var computed = builder.build(); // Non-trivial return: use CAS to ensure we reuse concurrent loads final var witness = MODIFIED_CHILDREN.compareAndExchangeRelease(this, null, computed); return witness == null ? computed : (ImmutableList>) witness; } @SuppressWarnings("unchecked") private Stream> streamModifiedChildren( final Class childType) { return getModifiedChildren().stream() .filter(child -> childType.isAssignableFrom(child.getDataType())) .map(child -> (LazyDataObjectModification) child); } @Override @SuppressWarnings("unchecked") public final & ChildOf, K extends Key> DataObjectModification getModifiedChildListItem(final Class listItem, final K listKey) { return (DataObjectModification) getModifiedChild(IdentifiableItem.of(listItem, listKey)); } @Override @SuppressWarnings("unchecked") public final & DataObject, C extends KeyAware & ChildOf, K extends Key> DataObjectModification getModifiedChildListItem(final Class caseType, final Class listItem, final K listKey) { return (DataObjectModification) getModifiedChild(IdentifiableItem.of(caseType, listItem, listKey)); } @Override @SuppressWarnings("unchecked") public final > DataObjectModification getModifiedChildContainer( final Class child) { return (DataObjectModification) getModifiedChild(Item.of(child)); } @Override @SuppressWarnings("unchecked") public final & DataObject, C extends ChildOf> DataObjectModification getModifiedChildContainer(final Class caseType, final Class child) { return (DataObjectModification) getModifiedChild(Item.of(caseType, child)); } @Override @SuppressWarnings("unchecked") public final & DataObject> DataObjectModification getModifiedAugmentation( final Class augmentation) { return (DataObjectModification) getModifiedChild(Item.of(augmentation)); } @Override public final String toString() { return addToStringAttributes(MoreObjects.toStringHelper(this)).toString(); } ToStringHelper addToStringAttributes(final ToStringHelper helper) { return helper.add("identifier", identifier).add("domData", domData); } abstract @NonNull Collection domChildNodes(); abstract org.opendaylight.yangtools.yang.data.tree.api.@NonNull ModificationType domModificationType(); private @NonNull ModificationType resolveSubtreeModificationType() { return switch (codec.getChildAddressabilitySummary()) { case ADDRESSABLE -> // All children are addressable, it is safe to report SUBTREE_MODIFIED ModificationType.SUBTREE_MODIFIED; case UNADDRESSABLE -> // All children are non-addressable, report WRITE ModificationType.WRITE; case MIXED -> { // This case is not completely trivial, as we may have NOT_ADDRESSABLE nodes underneath us. If that // is the case, we need to turn this modification into a WRITE operation, so that the user is able // to observe those nodes being introduced. This is not efficient, but unfortunately unavoidable, // as we cannot accurately represent such changes. for (var child : domChildNodes()) { if (BindingStructuralType.recursiveFrom(child) == BindingStructuralType.NOT_ADDRESSABLE) { // We have a non-addressable child, turn this modification into a write yield ModificationType.WRITE; } } // No unaddressable children found, proceed in addressed mode yield ModificationType.SUBTREE_MODIFIED; } }; } private static void populateList(final ImmutableList.Builder> result, final CommonDataObjectCodecTreeNode parentCodec, final DataTreeCandidateNode parent, final Collection children) { final var augmentChildren = ArrayListMultimap., DataTreeCandidateNode>create(); for (var domChildNode : parent.childNodes()) { if (domChildNode.modificationType() != UNMODIFIED) { final var type = BindingStructuralType.from(domChildNode); if (type != BindingStructuralType.NOT_ADDRESSABLE) { /* * Even if type is UNKNOWN, from perspective of BindingStructuralType we try to load codec for it. * We will use that type to further specify debug log. */ try { final var childCodec = parentCodec.yangPathArgumentChild(domChildNode.name()); if (childCodec instanceof BindingDataObjectCodecTreeNode childDataObjectCodec) { populateList(result, type, childDataObjectCodec, domChildNode); } else if (childCodec instanceof BindingAugmentationCodecTreeNode childAugmentationCodec) { // Defer creation once we have collected all modified children augmentChildren.put(childAugmentationCodec, domChildNode); } else { throw new VerifyException("Unhandled codec %s for type %s".formatted(childCodec, type)); } } catch (final IllegalArgumentException e) { if (type == BindingStructuralType.UNKNOWN) { LOG.debug("Unable to deserialize unknown DOM node {}", domChildNode, e); } else { LOG.debug("Binding representation for DOM node {} was not found", domChildNode, e); } } } } } for (var entry : augmentChildren.asMap().entrySet()) { final var modification = LazyAugmentationModification.forModifications(entry.getKey(), parent, entry.getValue()); if (modification != null) { result.add(modification); } } } private static void populateList(final ImmutableList.Builder> result, final BindingStructuralType type, final BindingDataObjectCodecTreeNode childCodec, final DataTreeCandidateNode domChildNode) { switch (type) { case INVISIBLE_LIST: // We use parent codec intentionally. populateListWithSingleCodec(result, childCodec, domChildNode.childNodes()); break; case INVISIBLE_CONTAINER: populateList(result, childCodec, domChildNode, domChildNode.childNodes()); break; case UNKNOWN: case VISIBLE_CONTAINER: result.add(new LazyDataObjectModification<>(childCodec, domChildNode)); break; default: } } private static void populateListWithSingleCodec( final ImmutableList.Builder> result, final BindingDataObjectCodecTreeNode codec, final Collection childNodes) { for (var child : childNodes) { if (child.modificationType() != UNMODIFIED) { result.add(new LazyDataObjectModification<>(codec, child)); } } } }