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