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