Relax AbstractDataObjectModification.childNodesCache
[mdsal.git] / binding / mdsal-binding-dom-adapter / src / main / java / org / opendaylight / mdsal / binding / dom / adapter / AbstractDataObjectModification.java
1 /*
2  * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.mdsal.binding.dom.adapter;
9
10 import static java.util.Objects.requireNonNull;
11 import static org.opendaylight.yangtools.yang.data.tree.api.ModificationType.UNMODIFIED;
12
13 import com.google.common.base.MoreObjects;
14 import com.google.common.base.MoreObjects.ToStringHelper;
15 import com.google.common.base.VerifyException;
16 import com.google.common.collect.ArrayListMultimap;
17 import com.google.common.collect.ImmutableList;
18 import java.lang.invoke.MethodHandles;
19 import java.lang.invoke.VarHandle;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.List;
23 import java.util.Optional;
24 import java.util.stream.Collectors;
25 import java.util.stream.Stream;
26 import org.eclipse.jdt.annotation.NonNull;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.opendaylight.mdsal.binding.api.DataObjectModification;
29 import org.opendaylight.mdsal.binding.dom.codec.api.BindingAugmentationCodecTreeNode;
30 import org.opendaylight.mdsal.binding.dom.codec.api.BindingDataObjectCodecTreeNode;
31 import org.opendaylight.mdsal.binding.dom.codec.api.CommonDataObjectCodecTreeNode;
32 import org.opendaylight.yangtools.yang.binding.Augmentation;
33 import org.opendaylight.yangtools.yang.binding.ChildOf;
34 import org.opendaylight.yangtools.yang.binding.ChoiceIn;
35 import org.opendaylight.yangtools.yang.binding.DataObject;
36 import org.opendaylight.yangtools.yang.binding.Identifiable;
37 import org.opendaylight.yangtools.yang.binding.Identifier;
38 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier.IdentifiableItem;
39 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier.Item;
40 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier.PathArgument;
41 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
42 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
43 import org.opendaylight.yangtools.yang.data.tree.api.DataTreeCandidateNode;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 /**
48  * Lazily translated {@link DataObjectModification} based on {@link DataTreeCandidateNode}.
49  * {@link AbstractDataObjectModification} represents Data tree change event, but whole tree is not translated or
50  * resolved eagerly, but only child nodes which are directly accessed by user of data object modification.
51  *
52  * <p>
53  * This class is further specialized as {@link LazyAugmentationModification} and {@link LazyDataObjectModification}, as
54  * both use different serialization methods.
55  *
56  * @param <T> Type of Binding {@link DataObject}
57  * @param <N> Type of underlying {@link CommonDataObjectCodecTreeNode}
58  */
59 abstract sealed class AbstractDataObjectModification<T extends DataObject, N extends CommonDataObjectCodecTreeNode<T>>
60         implements DataObjectModification<T>
61         permits LazyAugmentationModification, LazyDataObjectModification {
62     private static final Logger LOG = LoggerFactory.getLogger(AbstractDataObjectModification.class);
63     private static final VarHandle MODIFICATION_TYPE;
64     private static final VarHandle MODIFIED_CHILDREN;
65
66     static {
67         final var lookup = MethodHandles.lookup();
68
69         try {
70             MODIFICATION_TYPE = lookup.findVarHandle(AbstractDataObjectModification.class, "modificationType",
71                 ModificationType.class);
72             MODIFIED_CHILDREN = lookup.findVarHandle(AbstractDataObjectModification.class, "modifiedChildren",
73                 ImmutableList.class);
74         } catch (NoSuchFieldException | IllegalAccessException e) {
75             throw new ExceptionInInitializerError(e);
76         }
77     }
78
79     final @NonNull DataTreeCandidateNode domData;
80     final @NonNull PathArgument identifier;
81     final @NonNull N codec;
82
83     @SuppressWarnings("unused")
84     private volatile ImmutableList<AbstractDataObjectModification<?, ?>> modifiedChildren;
85     @SuppressWarnings("unused")
86     private volatile ModificationType modificationType;
87     private volatile @Nullable T dataBefore;
88     private volatile @Nullable T dataAfter;
89
90     AbstractDataObjectModification(final DataTreeCandidateNode domData, final N codec, final PathArgument identifier) {
91         this.domData = requireNonNull(domData);
92         this.identifier = requireNonNull(identifier);
93         this.codec = requireNonNull(codec);
94     }
95
96     static @Nullable AbstractDataObjectModification<?, ?> from(final CommonDataObjectCodecTreeNode<?> codec,
97             final @NonNull DataTreeCandidateNode current) {
98         if (codec instanceof BindingDataObjectCodecTreeNode<?> childDataObjectCodec) {
99             return new LazyDataObjectModification<>(childDataObjectCodec, current);
100         } else if (codec instanceof BindingAugmentationCodecTreeNode<?> childAugmentationCodec) {
101             return LazyAugmentationModification.forParent(childAugmentationCodec, current);
102         } else {
103             throw new VerifyException("Unhandled codec " + codec);
104         }
105     }
106
107     @Override
108     public final Class<T> getDataType() {
109         return codec.getBindingClass();
110     }
111
112     @Override
113     public final PathArgument getIdentifier() {
114         return identifier;
115     }
116
117     @Override
118     public final ModificationType getModificationType() {
119         final var local = (ModificationType) MODIFICATION_TYPE.getAcquire(this);
120         return local != null ? local : loadModificationType();
121     }
122
123     private @NonNull ModificationType loadModificationType() {
124         final var domModificationType = domModificationType();
125         final var computed = switch (domModificationType) {
126             case APPEARED, WRITE -> ModificationType.WRITE;
127             case DISAPPEARED, DELETE -> ModificationType.DELETE;
128             case SUBTREE_MODIFIED -> resolveSubtreeModificationType();
129             default ->
130                 // TODO: Should we lie about modification type instead of exception?
131                 throw new IllegalStateException("Unsupported DOM Modification type " + domModificationType);
132         };
133
134         MODIFICATION_TYPE.setRelease(this, computed);
135         return computed;
136     }
137
138     @Override
139     public final T getDataBefore() {
140         var local = dataBefore;
141         if (local == null) {
142             dataBefore = local = deserialize(domData.getDataBefore());
143         }
144         return local;
145     }
146
147     @Override
148     public final T getDataAfter() {
149         var local = dataAfter;
150         if (local == null) {
151             dataAfter = local = deserialize(domData.getDataAfter());
152         }
153         return local;
154     }
155
156     private @Nullable T deserialize(final Optional<NormalizedNode> normalized) {
157         return normalized.isEmpty() ? null : deserialize(normalized.orElseThrow());
158     }
159
160     abstract @Nullable T deserialize(@NonNull NormalizedNode normalized);
161
162     @Override
163     public final DataObjectModification<?> getModifiedChild(final PathArgument arg) {
164         final var domArgumentList = new ArrayList<YangInstanceIdentifier.PathArgument>();
165         final var childCodec = codec.bindingPathArgumentChild(arg, domArgumentList);
166         final var toEnter = domArgumentList.iterator();
167
168         // Careful now: we need to validated the first item against subclass
169         var current = toEnter.hasNext() ? firstModifiedChild(toEnter.next()) : domData;
170         // ... and for everything else we can just go wild
171         while (toEnter.hasNext() && current != null) {
172             current = current.getModifiedChild(toEnter.next()).orElse(null);
173         }
174
175         if (current == null || current.getModificationType() == UNMODIFIED) {
176             return null;
177         }
178         return from(childCodec, current);
179     }
180
181     abstract @Nullable DataTreeCandidateNode firstModifiedChild(YangInstanceIdentifier.PathArgument arg);
182
183     @Override
184     public final ImmutableList<AbstractDataObjectModification<?, ?>> getModifiedChildren() {
185         final var local = (ImmutableList<AbstractDataObjectModification<?, ?>>) MODIFIED_CHILDREN.getAcquire(this);
186         return local != null ? local : loadModifiedChilden();
187     }
188
189     @Override
190     public final <C extends ChildOf<? super T>> List<DataObjectModification<C>> getModifiedChildren(
191             final Class<C> childType) {
192         return streamModifiedChildren(childType).collect(Collectors.toList());
193     }
194
195     @Override
196     public final <H extends ChoiceIn<? super T> & DataObject, C extends ChildOf<? super H>>
197             List<DataObjectModification<C>> getModifiedChildren(final Class<H> caseType, final Class<C> childType) {
198         return streamModifiedChildren(childType)
199             .filter(child -> caseType.equals(child.identifier.getCaseType().orElse(null)))
200             .collect(Collectors.toList());
201     }
202
203     @SuppressWarnings("unchecked")
204     private @NonNull ImmutableList<AbstractDataObjectModification<?, ?>> loadModifiedChilden() {
205         final var builder = ImmutableList.<AbstractDataObjectModification<?, ?>>builder();
206         populateList(builder, codec, domData, domChildNodes());
207         final var computed = builder.build();
208         // Non-trivial return: use CAS to ensure we reuse concurrent loads
209         final var witness = MODIFIED_CHILDREN.compareAndExchangeRelease(this, null, computed);
210         return witness == null ? computed : (ImmutableList<AbstractDataObjectModification<?, ?>>) witness;
211     }
212
213     @SuppressWarnings("unchecked")
214     private <C extends DataObject> Stream<LazyDataObjectModification<C>> streamModifiedChildren(
215             final Class<C> childType) {
216         return getModifiedChildren().stream()
217             .filter(child -> childType.isAssignableFrom(child.getDataType()))
218             .map(child -> (LazyDataObjectModification<C>) child);
219     }
220
221     @Override
222     @SuppressWarnings("unchecked")
223     public final <C extends Identifiable<K> & ChildOf<? super T>, K extends Identifier<C>> DataObjectModification<C>
224             getModifiedChildListItem(final Class<C> listItem, final K listKey) {
225         return (DataObjectModification<C>) getModifiedChild(IdentifiableItem.of(listItem, listKey));
226     }
227
228     @Override
229     @SuppressWarnings("unchecked")
230     public final <H extends ChoiceIn<? super T> & DataObject, C extends Identifiable<K> & ChildOf<? super H>,
231             K extends Identifier<C>> DataObjectModification<C> getModifiedChildListItem(final Class<H> caseType,
232                     final Class<C> listItem, final K listKey) {
233         return (DataObjectModification<C>) getModifiedChild(IdentifiableItem.of(caseType, listItem, listKey));
234     }
235
236     @Override
237     @SuppressWarnings("unchecked")
238     public final <C extends ChildOf<? super T>> DataObjectModification<C> getModifiedChildContainer(
239             final Class<C> child) {
240         return (DataObjectModification<C>) getModifiedChild(Item.of(child));
241     }
242
243     @Override
244     @SuppressWarnings("unchecked")
245     public final <H extends ChoiceIn<? super T> & DataObject, C extends ChildOf<? super H>> DataObjectModification<C>
246             getModifiedChildContainer(final Class<H> caseType, final Class<C> child) {
247         return (DataObjectModification<C>) getModifiedChild(Item.of(caseType, child));
248     }
249
250     @Override
251     @SuppressWarnings("unchecked")
252     public final <C extends Augmentation<T> & DataObject> DataObjectModification<C> getModifiedAugmentation(
253             final Class<C> augmentation) {
254         return (DataObjectModification<C>) getModifiedChild(Item.of(augmentation));
255     }
256
257     @Override
258     public final String toString() {
259         return addToStringAttributes(MoreObjects.toStringHelper(this)).toString();
260     }
261
262     ToStringHelper addToStringAttributes(final ToStringHelper helper) {
263         return helper.add("identifier", identifier).add("domData", domData);
264     }
265
266     abstract @NonNull Collection<DataTreeCandidateNode> domChildNodes();
267
268     abstract org.opendaylight.yangtools.yang.data.tree.api.@NonNull ModificationType domModificationType();
269
270     private @NonNull ModificationType resolveSubtreeModificationType() {
271         return switch (codec.getChildAddressabilitySummary()) {
272             case ADDRESSABLE ->
273                 // All children are addressable, it is safe to report SUBTREE_MODIFIED
274                 ModificationType.SUBTREE_MODIFIED;
275             case UNADDRESSABLE ->
276                 // All children are non-addressable, report WRITE
277                 ModificationType.WRITE;
278             case MIXED -> {
279                 // This case is not completely trivial, as we may have NOT_ADDRESSABLE nodes underneath us. If that
280                 // is the case, we need to turn this modification into a WRITE operation, so that the user is able
281                 // to observe those nodes being introduced. This is not efficient, but unfortunately unavoidable,
282                 // as we cannot accurately represent such changes.
283                 for (var child : domChildNodes()) {
284                     if (BindingStructuralType.recursiveFrom(child) == BindingStructuralType.NOT_ADDRESSABLE) {
285                         // We have a non-addressable child, turn this modification into a write
286                         yield ModificationType.WRITE;
287                     }
288                 }
289
290                 // No unaddressable children found, proceed in addressed mode
291                 yield ModificationType.SUBTREE_MODIFIED;
292             }
293         };
294     }
295
296     private static void populateList(final ImmutableList.Builder<AbstractDataObjectModification<?, ?>> result,
297             final CommonDataObjectCodecTreeNode<?> parentCodec, final DataTreeCandidateNode parent,
298             final Collection<DataTreeCandidateNode> children) {
299         final var augmentChildren =
300             ArrayListMultimap.<BindingAugmentationCodecTreeNode<?>, DataTreeCandidateNode>create();
301
302         for (var domChildNode : parent.getChildNodes()) {
303             if (domChildNode.getModificationType() != UNMODIFIED) {
304                 final var type = BindingStructuralType.from(domChildNode);
305                 if (type != BindingStructuralType.NOT_ADDRESSABLE) {
306                     /*
307                      * Even if type is UNKNOWN, from perspective of BindingStructuralType we try to load codec for it.
308                      * We will use that type to further specify debug log.
309                      */
310                     try {
311                         final var childCodec = parentCodec.yangPathArgumentChild(domChildNode.getIdentifier());
312                         if (childCodec instanceof BindingDataObjectCodecTreeNode<?> childDataObjectCodec) {
313                             populateList(result, type, childDataObjectCodec, domChildNode);
314                         } else if (childCodec instanceof BindingAugmentationCodecTreeNode<?> childAugmentationCodec) {
315                             // Defer creation once we have collected all modified children
316                             augmentChildren.put(childAugmentationCodec, domChildNode);
317                         } else {
318                             throw new VerifyException("Unhandled codec %s for type %s".formatted(childCodec, type));
319                         }
320                     } catch (final IllegalArgumentException e) {
321                         if (type == BindingStructuralType.UNKNOWN) {
322                             LOG.debug("Unable to deserialize unknown DOM node {}", domChildNode, e);
323                         } else {
324                             LOG.debug("Binding representation for DOM node {} was not found", domChildNode, e);
325                         }
326                     }
327                 }
328             }
329         }
330
331         for (var entry : augmentChildren.asMap().entrySet()) {
332             final var modification = LazyAugmentationModification.forModifications(entry.getKey(), parent,
333                 entry.getValue());
334             if (modification != null) {
335                 result.add(modification);
336             }
337         }
338     }
339
340     private static void populateList(final ImmutableList.Builder<AbstractDataObjectModification<?, ?>> result,
341             final BindingStructuralType type, final BindingDataObjectCodecTreeNode<?> childCodec,
342             final DataTreeCandidateNode domChildNode) {
343         switch (type) {
344             case INVISIBLE_LIST:
345                 // We use parent codec intentionally.
346                 populateListWithSingleCodec(result, childCodec, domChildNode.getChildNodes());
347                 break;
348             case INVISIBLE_CONTAINER:
349                 populateList(result, childCodec, domChildNode, domChildNode.getChildNodes());
350                 break;
351             case UNKNOWN:
352             case VISIBLE_CONTAINER:
353                 result.add(new LazyDataObjectModification<>(childCodec, domChildNode));
354                 break;
355             default:
356         }
357     }
358
359     private static void populateListWithSingleCodec(
360             final ImmutableList.Builder<AbstractDataObjectModification<?, ?>> result,
361             final BindingDataObjectCodecTreeNode<?> codec, final Collection<DataTreeCandidateNode> childNodes) {
362         for (var child : childNodes) {
363             if (child.getModificationType() != UNMODIFIED) {
364                 result.add(new LazyDataObjectModification<>(codec, child));
365             }
366         }
367     }
368 }