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