98bdbaf81f54478d3883d11ecb005ae5c3bc4d8e
[mdsal.git] / binding / mdsal-binding-dom-codec / src / main / java / org / opendaylight / mdsal / binding / dom / codec / impl / CodecDataObjectAnalysis.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.codec.impl;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Verify.verify;
12 import static com.google.common.base.Verify.verifyNotNull;
13
14 import com.google.common.base.VerifyException;
15 import com.google.common.collect.ImmutableMap;
16 import java.lang.invoke.MethodHandle;
17 import java.lang.invoke.MethodHandles;
18 import java.lang.invoke.MethodType;
19 import java.lang.reflect.Method;
20 import java.util.HashMap;
21 import java.util.List;
22 import java.util.Map;
23 import org.eclipse.jdt.annotation.NonNull;
24 import org.opendaylight.mdsal.binding.model.api.JavaTypeName;
25 import org.opendaylight.mdsal.binding.runtime.api.AugmentRuntimeType;
26 import org.opendaylight.mdsal.binding.runtime.api.AugmentableRuntimeType;
27 import org.opendaylight.mdsal.binding.runtime.api.ChoiceRuntimeType;
28 import org.opendaylight.mdsal.binding.runtime.api.CompositeRuntimeType;
29 import org.opendaylight.mdsal.binding.runtime.api.ContainerLikeRuntimeType;
30 import org.opendaylight.mdsal.binding.runtime.api.ContainerRuntimeType;
31 import org.opendaylight.mdsal.binding.runtime.api.ListRuntimeType;
32 import org.opendaylight.yangtools.yang.binding.Augmentable;
33 import org.opendaylight.yangtools.yang.binding.DataContainer;
34 import org.opendaylight.yangtools.yang.binding.DataObject;
35 import org.opendaylight.yangtools.yang.binding.OpaqueObject;
36 import org.opendaylight.yangtools.yang.binding.contract.Naming;
37 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
38 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode;
39 import org.opendaylight.yangtools.yang.model.api.stmt.PresenceEffectiveStatement;
40
41 /**
42  * Analysis of a {@link DataObject} specialization class. The primary point of this class is to separate out creation
43  * indices needed for {@link #proxyConstructor}. Since we want to perform as much indexing as possible in a single pass,
44  * we also end up indexing things that are not strictly required to arrive at that constructor.
45  */
46 final class CodecDataObjectAnalysis<R extends CompositeRuntimeType> {
47     private static final MethodType CONSTRUCTOR_TYPE = MethodType.methodType(void.class,
48         AbstractDataObjectCodecContext.class, DataContainerNode.class);
49     private static final MethodType DATAOBJECT_TYPE = MethodType.methodType(DataObject.class,
50         AbstractDataObjectCodecContext.class, DataContainerNode.class);
51
52     final @NonNull ImmutableMap<Class<?>, CommonDataObjectCodecPrototype<?>> byStreamClass;
53     final @NonNull ImmutableMap<Class<?>, CommonDataObjectCodecPrototype<?>> byBindingArgClass;
54     final @NonNull ImmutableMap<NodeIdentifier, CodecContextSupplier> byYang;
55     final @NonNull ImmutableMap<String, ValueNodeCodecContext> leafNodes;
56     final @NonNull Class<? extends CodecDataObject<?>> generatedClass;
57     final @NonNull List<AugmentRuntimeType> possibleAugmentations;
58     final @NonNull MethodHandle proxyConstructor;
59
60     CodecDataObjectAnalysis(final CommonDataObjectCodecPrototype<R> prototype, final CodecItemFactory itemFactory,
61             final Method keyMethod) {
62         // Preliminaries from prototype
63         @SuppressWarnings("unchecked")
64         final Class<DataObject> bindingClass = Class.class.cast(prototype.getBindingClass());
65         final var runtimeType = prototype.getType();
66         final var factory = prototype.getFactory();
67         final var leafContexts = factory.getLeafNodes(bindingClass, runtimeType.statement());
68
69         // Reflection-based on the passed class
70         final var clsToMethod = getChildrenClassToMethod(bindingClass);
71
72         // Indexing part: be very careful around what gets done when
73         final var byYangBuilder = new HashMap<NodeIdentifier, CodecContextSupplier>();
74
75         // Step 1: add leaf children
76         var leafBuilder = ImmutableMap.<String, ValueNodeCodecContext>builderWithExpectedSize(leafContexts.size());
77         for (var leaf : leafContexts.values()) {
78             leafBuilder.put(leaf.getSchema().getQName().getLocalName(), leaf);
79             byYangBuilder.put(leaf.getDomPathArgument(), leaf);
80         }
81         leafNodes = leafBuilder.build();
82
83         final var byBindingArgClassBuilder = new HashMap<Class<?>, CommonDataObjectCodecPrototype<?>>();
84         final var byStreamClassBuilder = new HashMap<Class<?>, CommonDataObjectCodecPrototype<?>>();
85         final var daoProperties = new HashMap<Class<?>, PropertyInfo>();
86         for (var childDataObj : clsToMethod.entrySet()) {
87             final var method = childDataObj.getValue();
88             verify(!method.isDefault(), "Unexpected default method %s in %s", method, bindingClass);
89
90             final var retClass = childDataObj.getKey();
91             if (OpaqueObject.class.isAssignableFrom(retClass)) {
92                 // Filter OpaqueObjects, they are not containers
93                 continue;
94             }
95
96             // Record getter method
97             daoProperties.put(retClass, new PropertyInfo.Getter(method));
98
99             final var childProto = getChildPrototype(runtimeType, factory, itemFactory, retClass);
100             byStreamClassBuilder.put(childProto.getBindingClass(), childProto);
101             byYangBuilder.put(childProto.getYangArg(), childProto);
102
103             // FIXME: It really feels like we should be specializing DataContainerCodecPrototype so as to ditch
104             //        createInstance() and then we could do an instanceof check instead.
105             if (childProto.getType() instanceof ChoiceRuntimeType) {
106                 final var choice = (ChoiceCodecContext<?>) childProto.get();
107                 for (var cazeChild : choice.getCaseChildrenClasses()) {
108                     byBindingArgClassBuilder.put(cazeChild, childProto);
109                 }
110             }
111         }
112
113         // Snapshot before below processing
114         byStreamClass = ImmutableMap.copyOf(byStreamClassBuilder);
115
116         // Slight footprint optimization: we do not want to copy byStreamClass, as that would force its entrySet view
117         // to be instantiated. Furthermore the two maps can easily end up being equal -- hence we can reuse
118         // byStreamClass for the purposes of both.
119         byBindingArgClassBuilder.putAll(byStreamClassBuilder);
120         byBindingArgClass = byStreamClassBuilder.equals(byBindingArgClassBuilder) ? byStreamClass
121             : ImmutableMap.copyOf(byBindingArgClassBuilder);
122
123         // Find all non-default nonnullFoo() methods and update the corresponding property info
124         for (var entry : getChildrenClassToNonnullMethod(bindingClass).entrySet()) {
125             final var method = entry.getValue();
126             if (!method.isDefault()) {
127                 daoProperties.compute(entry.getKey(), (key, value) -> new PropertyInfo.GetterAndNonnull(
128                     verifyNotNull(value, "No getter for %s", key).getterMethod(), method));
129             }
130         }
131         // At this point all indexing is done: byYangBuilder should not be referenced
132         byYang = ImmutableMap.copyOf(byYangBuilder);
133
134         // Final bits: generate the appropriate class, As a side effect we identify what Augmentations are possible
135         if (Augmentable.class.isAssignableFrom(bindingClass)) {
136             // Verify we have the appropriate backing runtimeType
137             if (!(runtimeType instanceof AugmentableRuntimeType augmentableRuntimeType)) {
138                 throw new VerifyException(
139                     "Unexpected type %s backing augmenable %s".formatted(runtimeType, bindingClass));
140             }
141
142             possibleAugmentations = augmentableRuntimeType.augments();
143             generatedClass = CodecDataObjectGenerator.generateAugmentable(factory.getLoader(), bindingClass,
144                 leafContexts, daoProperties, keyMethod);
145         } else {
146             possibleAugmentations = List.of();
147             generatedClass = CodecDataObjectGenerator.generate(factory.getLoader(), bindingClass, leafContexts,
148                 daoProperties, keyMethod);
149         }
150
151         // All done: acquire the constructor: it is supposed to be public
152         final MethodHandle ctor;
153         try {
154             ctor = MethodHandles.publicLookup().findConstructor(generatedClass, CONSTRUCTOR_TYPE);
155         } catch (NoSuchMethodException | IllegalAccessException e) {
156             throw new LinkageError("Failed to find contructor for class " + generatedClass, e);
157         }
158
159         proxyConstructor = ctor.asType(DATAOBJECT_TYPE);
160     }
161
162     private static @NonNull CommonDataObjectCodecPrototype<?> getChildPrototype(final CompositeRuntimeType type,
163             final CodecContextFactory factory, final CodecItemFactory itemFactory,
164             final Class<? extends DataContainer> childClass) {
165         final var child = type.bindingChild(JavaTypeName.create(childClass));
166         if (child == null) {
167             throw DataContainerCodecContext.childNullException(factory.getRuntimeContext(), childClass,
168                 "Node %s does not have child named %s", type, childClass);
169         }
170         final var item = itemFactory.createItem(childClass, child.statement());
171         if (child instanceof ContainerLikeRuntimeType containerLike) {
172             if (child instanceof ContainerRuntimeType container
173                 && container.statement().findFirstEffectiveSubstatement(PresenceEffectiveStatement.class).isEmpty()) {
174                 return new StructuralContainerCodecPrototype(item, container, factory);
175             }
176             return new ContainerLikeCodecPrototype(item, containerLike, factory);
177         } else if (child instanceof ListRuntimeType list) {
178             return list.keyType() != null ? new MapCodecPrototype(item, list, factory)
179                 : new ListCodecPrototype(item, list, factory);
180         } else if (child instanceof ChoiceRuntimeType choice) {
181             return new ChoiceCodecPrototype(item, choice, factory);
182         } else {
183             throw new UnsupportedOperationException("Unhandled type " + child);
184         }
185     }
186
187
188     // FIXME: MDSAL-780: these methods perform analytics using java.lang.reflect to acquire the basic shape of the
189     //                   class. This is not exactly AOT friendly, as most of the information should be provided by
190     //                   CompositeRuntimeType.
191     //
192     //                   As as first cut, CompositeRuntimeType should provide the mapping between YANG children and the
193     //                   corresponding GETTER_PREFIX/NONNULL_PREFIX method names, If we have that, the use in this
194     //                   class should be fine.
195     //
196     //                   The second cut is binding the actual Method invocations, which is fine here, as this class is
197     //                   all about having a run-time generated class. AOT would be providing an alternative, where the
198     //                   equivalent class would be generated at compile-time and hence would bind to the methods
199     //                   directly -- and AOT equivalent of this class would really be a compile-time generated registry
200     //                   to those classes' entry points.
201
202     /**
203      * Scans supplied class and returns an iterable of all data children classes.
204      *
205      * @param type YANG Modeled Entity derived from DataContainer
206      * @return Iterable of all data children, which have YANG modeled entity
207      */
208     private static Map<Class<? extends DataContainer>, Method> getChildrenClassToMethod(final Class<?> type) {
209         return getChildClassToMethod(type, Naming.GETTER_PREFIX);
210     }
211
212     private static Map<Class<? extends DataContainer>, Method> getChildrenClassToNonnullMethod(final Class<?> type) {
213         return getChildClassToMethod(type, Naming.NONNULL_PREFIX);
214     }
215
216     private static Map<Class<? extends DataContainer>, Method> getChildClassToMethod(final Class<?> type,
217             final String prefix) {
218         checkArgument(type != null, "Target type must not be null");
219         checkArgument(DataContainer.class.isAssignableFrom(type), "Supplied type %s must be derived from DataContainer",
220             type);
221         final var ret = new HashMap<Class<? extends DataContainer>, Method>();
222         for (Method method : type.getMethods()) {
223             DataContainerCodecContext.getYangModeledReturnType(method, prefix)
224                 .ifPresent(entity -> ret.put(entity, method));
225         }
226         return ret;
227     }
228 }