403e3dc57e22088fe3c2da6baf2a22394ade2b8c
[mdsal.git] / binding / mdsal-binding-dom-codec / src / main / java / org / opendaylight / mdsal / binding / dom / codec / impl / DataContainerCodecContext.java
1 /*
2  * Copyright (c) 2014 Cisco Systems, Inc. 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.codec.impl;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.collect.ImmutableCollection;
13 import com.google.common.collect.ImmutableSet;
14 import edu.umd.cs.findbugs.annotations.CheckReturnValue;
15 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
16 import java.io.IOException;
17 import java.lang.invoke.MethodHandles;
18 import java.lang.invoke.VarHandle;
19 import java.lang.reflect.Method;
20 import java.lang.reflect.Modifier;
21 import java.util.Arrays;
22 import java.util.HashSet;
23 import java.util.List;
24 import java.util.Optional;
25 import java.util.Set;
26 import org.eclipse.jdt.annotation.NonNull;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.opendaylight.mdsal.binding.dom.codec.api.BindingDataContainerCodecTreeNode;
29 import org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeCachingCodec;
30 import org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeCodec;
31 import org.opendaylight.mdsal.binding.dom.codec.api.IncorrectNestingException;
32 import org.opendaylight.mdsal.binding.dom.codec.api.MissingClassInLoadingStrategyException;
33 import org.opendaylight.mdsal.binding.dom.codec.api.MissingSchemaException;
34 import org.opendaylight.mdsal.binding.dom.codec.api.MissingSchemaForClassException;
35 import org.opendaylight.mdsal.binding.model.api.Type;
36 import org.opendaylight.mdsal.binding.runtime.api.BindingRuntimeContext;
37 import org.opendaylight.mdsal.binding.runtime.api.CompositeRuntimeType;
38 import org.opendaylight.yangtools.util.ClassLoaderUtils;
39 import org.opendaylight.yangtools.yang.binding.Augmentable;
40 import org.opendaylight.yangtools.yang.binding.Augmentation;
41 import org.opendaylight.yangtools.yang.binding.BindingObject;
42 import org.opendaylight.yangtools.yang.binding.DataContainer;
43 import org.opendaylight.yangtools.yang.binding.DataObject;
44 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier.PathArgument;
45 import org.opendaylight.yangtools.yang.common.QName;
46 import org.opendaylight.yangtools.yang.common.QNameModule;
47 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
48 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
49 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
50 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
51 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
52 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizationResultHolder;
53 import org.opendaylight.yangtools.yang.model.api.AnydataSchemaNode;
54 import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode;
55 import org.opendaylight.yangtools.yang.model.api.AugmentationSchemaNode;
56 import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode;
57 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
58 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
59 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
60 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
61 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
62 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
65
66 abstract sealed class DataContainerCodecContext<D extends BindingObject & DataContainer, T extends CompositeRuntimeType>
67         extends CodecContext implements BindingDataContainerCodecTreeNode<D>
68         permits CommonDataObjectCodecContext {
69     private static final Logger LOG = LoggerFactory.getLogger(DataContainerCodecContext.class);
70     private static final VarHandle EVENT_STREAM_SERIALIZER;
71
72     static {
73         try {
74             EVENT_STREAM_SERIALIZER = MethodHandles.lookup().findVarHandle(DataContainerCodecContext.class,
75                 "eventStreamSerializer", DataContainerSerializer.class);
76         } catch (NoSuchFieldException | IllegalAccessException e) {
77             throw new ExceptionInInitializerError(e);
78         }
79     }
80
81     private final @NonNull ChildAddressabilitySummary childAddressabilitySummary;
82
83     // Accessed via a VarHandle
84     @SuppressWarnings("unused")
85     @SuppressFBWarnings(value = "UUF_UNUSED_FIELD", justification = "https://github.com/spotbugs/spotbugs/issues/2749")
86     private volatile DataContainerSerializer eventStreamSerializer;
87
88     DataContainerCodecContext(final T type) {
89         childAddressabilitySummary = computeChildAddressabilitySummary(type.statement());
90     }
91
92     @Override
93     public final ChildAddressabilitySummary getChildAddressabilitySummary() {
94         return childAddressabilitySummary;
95     }
96
97     protected abstract @NonNull CodecContextFactory factory();
98
99     protected abstract @NonNull T type();
100
101     // Non-final for ChoiceCodecContext
102     @Override
103     public CodecContext yangPathArgumentChild(final YangInstanceIdentifier.PathArgument arg) {
104         CodecContextSupplier supplier;
105         if (arg instanceof NodeIdentifier nodeId) {
106             supplier = yangChildSupplier(nodeId);
107         } else if (arg instanceof NodeIdentifierWithPredicates nip) {
108             supplier = yangChildSupplier(new NodeIdentifier(nip.getNodeType()));
109         } else {
110             supplier = null;
111         }
112         return childNonNull(supplier, arg, "Argument %s is not valid child of %s", arg, getSchema()).get();
113     }
114
115     abstract @Nullable CodecContextSupplier yangChildSupplier(@NonNull NodeIdentifier arg);
116
117     @Override
118     public CommonDataObjectCodecContext<?, ?> bindingPathArgumentChild(final PathArgument arg,
119             final List<YangInstanceIdentifier.PathArgument> builder) {
120         final var child = getStreamChild(arg.getType());
121         child.addYangPathArgument(arg, builder);
122         return child;
123     }
124
125     /**
126      * Serializes supplied Binding Path Argument and adds all necessary YANG instance identifiers to supplied list.
127      *
128      * @param arg Binding Path Argument
129      * @param builder DOM Path argument.
130      */
131     final void addYangPathArgument(final PathArgument arg, final List<YangInstanceIdentifier.PathArgument> builder) {
132         if (builder != null) {
133             addYangPathArgument(builder, arg);
134         }
135     }
136
137     void addYangPathArgument(final @NonNull List<YangInstanceIdentifier.PathArgument> builder, final PathArgument arg) {
138         final var yangArg = getDomPathArgument();
139         if (yangArg != null) {
140             builder.add(yangArg);
141         }
142     }
143
144     @Override
145     public final <C extends DataObject> CommonDataObjectCodecContext<C, ?> getStreamChild(final Class<C> childClass) {
146         return childNonNull(streamChild(childClass), childClass,
147             "Child %s is not valid child of %s", getBindingClass(), childClass);
148     }
149
150     @SuppressWarnings("unchecked")
151     @Override
152     public final <C extends DataObject> CommonDataObjectCodecContext<C, ?> streamChild(final Class<C> childClass) {
153         final var childProto = streamChildPrototype(requireNonNull(childClass));
154         return childProto == null ? null : (CommonDataObjectCodecContext<C, ?>) childProto.get();
155     }
156
157     abstract @Nullable CommonDataObjectCodecPrototype<?> streamChildPrototype(@NonNull Class<?> childClass);
158
159     @Override
160     public String toString() {
161         return getClass().getSimpleName() + " [" + getBindingClass() + "]";
162     }
163
164     static final <T extends DataObject, C extends DataContainerCodecContext<T, ?> & BindingNormalizedNodeCodec<T>>
165             @NonNull BindingNormalizedNodeCachingCodec<T> createCachingCodec(final C context,
166                 final ImmutableCollection<Class<? extends BindingObject>> cacheSpecifier) {
167         return cacheSpecifier.isEmpty() ? new NonCachingCodec<>(context)
168             : new CachingNormalizedNodeCodec<>(context, ImmutableSet.copyOf(cacheSpecifier));
169     }
170
171     protected final <V> @NonNull V childNonNull(final @Nullable V nullable,
172             final YangInstanceIdentifier.PathArgument child, final String message, final Object... args) {
173         if (nullable == null) {
174             throw childNullException(child.getNodeType(), message, args);
175         }
176         return nullable;
177     }
178
179     protected final <V> @NonNull V childNonNull(final @Nullable V nullable, final QName child, final String message,
180             final Object... args) {
181         if (nullable == null) {
182             throw childNullException(child, message, args);
183         }
184         return nullable;
185     }
186
187     protected final <V> @NonNull V childNonNull(final @Nullable V nullable, final Class<?> childClass,
188             final String message, final Object... args) {
189         if (nullable == null) {
190             throw childNullException(childClass, message, args);
191         }
192         return nullable;
193     }
194
195     @CheckReturnValue
196     private IllegalArgumentException childNullException(final QName child, final String message, final Object... args) {
197         final QNameModule module = child.getModule();
198         if (!factory().getRuntimeContext().getEffectiveModelContext().findModule(module).isPresent()) {
199             return new MissingSchemaException("Module " + module + " is not present in current schema context.");
200         }
201         return new IncorrectNestingException(message, args);
202     }
203
204     @CheckReturnValue
205     private @NonNull IllegalArgumentException childNullException(final Class<?> childClass, final String message,
206             final Object... args) {
207         return childNullException(factory().getRuntimeContext(), childClass, message, args);
208     }
209
210     @CheckReturnValue
211     static @NonNull IllegalArgumentException childNullException(final BindingRuntimeContext runtimeContext,
212             final Class<?> childClass, final String message, final Object... args) {
213         final CompositeRuntimeType schema;
214         if (Augmentation.class.isAssignableFrom(childClass)) {
215             schema = runtimeContext.getAugmentationDefinition(childClass.asSubclass(Augmentation.class));
216         } else {
217             schema = runtimeContext.getSchemaDefinition(childClass);
218         }
219         if (schema == null) {
220             return new MissingSchemaForClassException(childClass);
221         }
222
223         try {
224             runtimeContext.loadClass(Type.of(childClass));
225         } catch (final ClassNotFoundException e) {
226             return new MissingClassInLoadingStrategyException(
227                 "User supplied class " + childClass.getName() + " is not available in " + runtimeContext, e);
228         }
229
230         return new IncorrectNestingException(message, args);
231     }
232
233     final DataContainerSerializer eventStreamSerializer() {
234         final DataContainerSerializer existing = (DataContainerSerializer) EVENT_STREAM_SERIALIZER.getAcquire(this);
235         return existing != null ? existing : loadEventStreamSerializer();
236     }
237
238     // Split out to aid inlining
239     private DataContainerSerializer loadEventStreamSerializer() {
240         final DataContainerSerializer loaded = factory().getEventStreamSerializer(getBindingClass());
241         final Object witness = EVENT_STREAM_SERIALIZER.compareAndExchangeRelease(this, null, loaded);
242         return witness == null ? loaded : (DataContainerSerializer) witness;
243     }
244
245     final @NonNull NormalizedNode serializeImpl(final @NonNull D data) {
246         final var result = new NormalizationResultHolder();
247         // We create DOM stream writer which produces normalized nodes
248         final var domWriter = ImmutableNormalizedNodeStreamWriter.from(result);
249         try {
250             eventStreamSerializer().serialize(data, new BindingToNormalizedStreamWriter(this, domWriter));
251         } catch (final IOException e) {
252             throw new IllegalStateException("Failed to serialize Binding DTO",e);
253         }
254         return result.getResult().data();
255     }
256
257     static final <T extends NormalizedNode> @NonNull T checkDataArgument(final @NonNull Class<T> expectedType,
258             final NormalizedNode data) {
259         try {
260             return expectedType.cast(requireNonNull(data));
261         } catch (ClassCastException e) {
262             throw new IllegalArgumentException("Expected " + expectedType.getSimpleName(), e);
263         }
264     }
265
266     /**
267      * Determines if two augmentation classes or case classes represents same data.
268      *
269      * <p>
270      * Two augmentations or cases could be substituted only if and if:
271      * <ul>
272      *   <li>Both implements same interfaces</li>
273      *   <li>Both have same children</li>
274      *   <li>If augmentations: Both have same augmentation target class. Target class was generated for data node in a
275      *       grouping.</li>
276      *   <li>If cases: Both are from same choice. Choice class was generated for data node in grouping.</li>
277      * </ul>
278      *
279      * <p>
280      * <b>Explanation:</b>
281      * Binding Specification reuses classes generated for groupings as part of normal data tree, this classes from
282      * grouping could be used at various locations and user may not be aware of it and may use incorrect case or
283      * augmentation in particular subtree (via copy constructors, etc).
284      *
285      * @param potential Class which is potential substitution
286      * @param target Class which should be used at particular subtree
287      * @return true if and only if classes represents same data.
288      * @throws NullPointerException if any argument is {@code null}
289      */
290     // FIXME: MDSAL-785: this really should live in BindingRuntimeTypes and should not be based on reflection. The only
291     //                   user is binding-dom-codec and the logic could easily be performed on GeneratedType instead. For
292     //                   a particular world this boils down to a matrix, which can be calculated either on-demand or
293     //                   when we create BindingRuntimeTypes. Achieving that will bring us one step closer to being able
294     //                   to have a pre-compiled Binding Runtime.
295     @SuppressWarnings({ "rawtypes", "unchecked" })
296     static final boolean isSubstitutionFor(final Class potential, final Class target) {
297         Set<Class> subImplemented = new HashSet<>(Arrays.asList(potential.getInterfaces()));
298         Set<Class> targetImplemented = new HashSet<>(Arrays.asList(target.getInterfaces()));
299         if (!subImplemented.equals(targetImplemented)) {
300             return false;
301         }
302         if (Augmentation.class.isAssignableFrom(potential)
303             && !findAugmentationTarget(potential).equals(findAugmentationTarget(target))) {
304             return false;
305         }
306         for (Method potentialMethod : potential.getMethods()) {
307             if (Modifier.isStatic(potentialMethod.getModifiers())) {
308                 // Skip any static methods, as we are not interested in those
309                 continue;
310             }
311
312             try {
313                 Method targetMethod = target.getMethod(potentialMethod.getName(), potentialMethod.getParameterTypes());
314                 if (!potentialMethod.getReturnType().equals(targetMethod.getReturnType())) {
315                     return false;
316                 }
317             } catch (NoSuchMethodException e) {
318                 // Counterpart method is missing, so classes could not be substituted.
319                 return false;
320             } catch (SecurityException e) {
321                 throw new IllegalStateException("Could not compare methods", e);
322             }
323         }
324         return true;
325     }
326
327     /**
328      * Find augmentation target class from concrete Augmentation class. This method uses first generic argument of
329      * implemented {@link Augmentation} interface.
330      *
331      * @param augmentation {@link Augmentation} subclass for which we want to determine augmentation target.
332      * @return Augmentation target - class which augmentation provides additional extensions.
333      */
334     static final Class<? extends Augmentable<?>> findAugmentationTarget(
335             final Class<? extends Augmentation<?>> augmentation) {
336         final Optional<Class<Augmentable<?>>> opt = ClassLoaderUtils.findFirstGenericArgument(augmentation,
337             Augmentation.class);
338         return opt.orElse(null);
339     }
340
341     private static @NonNull ChildAddressabilitySummary computeChildAddressabilitySummary(final Object nodeSchema) {
342         // FIXME: rework this to work on EffectiveStatements
343         if (nodeSchema instanceof DataNodeContainer contaner) {
344             boolean haveAddressable = false;
345             boolean haveUnaddressable = false;
346             for (DataSchemaNode child : contaner.getChildNodes()) {
347                 if (child instanceof ContainerSchemaNode || child instanceof AugmentationSchemaNode) {
348                     haveAddressable = true;
349                 } else if (child instanceof ListSchemaNode list) {
350                     if (list.getKeyDefinition().isEmpty()) {
351                         haveUnaddressable = true;
352                     } else {
353                         haveAddressable = true;
354                     }
355                 } else if (child instanceof AnydataSchemaNode || child instanceof AnyxmlSchemaNode
356                         || child instanceof TypedDataSchemaNode) {
357                     haveUnaddressable = true;
358                 } else if (child instanceof ChoiceSchemaNode choice) {
359                     switch (computeChildAddressabilitySummary(choice)) {
360                         case ADDRESSABLE -> haveAddressable = true;
361                         case UNADDRESSABLE -> haveUnaddressable = true;
362                         case MIXED -> {
363                             haveAddressable = true;
364                             haveUnaddressable = true;
365                         }
366                         default -> throw new IllegalStateException("Unhandled accessibility summary for " + child);
367                     }
368                 } else {
369                     LOG.warn("Unhandled child node {}", child);
370                 }
371             }
372
373             if (!haveAddressable) {
374                 // Empty or all are unaddressable
375                 return ChildAddressabilitySummary.UNADDRESSABLE;
376             }
377
378             return haveUnaddressable ? ChildAddressabilitySummary.MIXED : ChildAddressabilitySummary.ADDRESSABLE;
379         } else if (nodeSchema instanceof ChoiceSchemaNode choice) {
380             return computeChildAddressabilitySummary(choice);
381         }
382
383         // No child nodes possible: return unaddressable
384         return ChildAddressabilitySummary.UNADDRESSABLE;
385     }
386
387     private static @NonNull ChildAddressabilitySummary computeChildAddressabilitySummary(
388             final ChoiceSchemaNode choice) {
389         boolean haveAddressable = false;
390         boolean haveUnaddressable = false;
391         for (CaseSchemaNode child : choice.getCases()) {
392             switch (computeChildAddressabilitySummary(child)) {
393                 case ADDRESSABLE:
394                     haveAddressable = true;
395                     break;
396                 case UNADDRESSABLE:
397                     haveUnaddressable = true;
398                     break;
399                 case MIXED:
400                     // A child is mixed, which means we are mixed, too
401                     return ChildAddressabilitySummary.MIXED;
402                 default:
403                     throw new IllegalStateException("Unhandled accessibility summary for " + child);
404             }
405         }
406
407         if (!haveAddressable) {
408             // Empty or all are unaddressable
409             return ChildAddressabilitySummary.UNADDRESSABLE;
410         }
411
412         return haveUnaddressable ? ChildAddressabilitySummary.MIXED : ChildAddressabilitySummary.ADDRESSABLE;
413     }
414 }