Reparent ChoiceCodecContext
[mdsal.git] / binding / mdsal-binding-dom-codec / src / main / java / org / opendaylight / mdsal / binding / dom / codec / impl / DataContainerAnalysis.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.collect.ImmutableMap;
15 import java.lang.reflect.Method;
16 import java.lang.reflect.ParameterizedType;
17 import java.util.HashMap;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Optional;
21 import org.eclipse.jdt.annotation.NonNull;
22 import org.opendaylight.mdsal.binding.model.api.JavaTypeName;
23 import org.opendaylight.mdsal.binding.runtime.api.ChoiceRuntimeType;
24 import org.opendaylight.mdsal.binding.runtime.api.CompositeRuntimeType;
25 import org.opendaylight.mdsal.binding.runtime.api.ContainerLikeRuntimeType;
26 import org.opendaylight.mdsal.binding.runtime.api.ContainerRuntimeType;
27 import org.opendaylight.mdsal.binding.runtime.api.ListRuntimeType;
28 import org.opendaylight.yangtools.util.ClassLoaderUtils;
29 import org.opendaylight.yangtools.yang.binding.ChoiceIn;
30 import org.opendaylight.yangtools.yang.binding.DataContainer;
31 import org.opendaylight.yangtools.yang.binding.OpaqueObject;
32 import org.opendaylight.yangtools.yang.binding.contract.Naming;
33 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
34 import org.opendaylight.yangtools.yang.model.api.stmt.PresenceEffectiveStatement;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 /**
39  * Analysis of a {@link DataContainer} specialization class. This includes things needed for
40  * {@link DataContainerCodecContext}'s methods as well as the appropriate run-time generated class.
41  */
42 final class DataContainerAnalysis<R extends CompositeRuntimeType> {
43     private static final Logger LOG = LoggerFactory.getLogger(DataContainerAnalysis.class);
44
45     // Needed for DataContainerCodecContext
46     final @NonNull ImmutableMap<Class<?>, DataContainerPrototype<?, ?>> byStreamClass;
47     final @NonNull ImmutableMap<Class<?>, DataContainerPrototype<?, ?>> byBindingArgClass;
48     final @NonNull ImmutableMap<NodeIdentifier, CodecContextSupplier> byYang;
49     final @NonNull ImmutableMap<String, ValueNodeCodecContext> leafNodes;
50
51     // Needed for generated classes
52     final @NonNull ImmutableMap<Method, ValueNodeCodecContext> leafContexts;
53     final @NonNull ImmutableMap<Class<?>, PropertyInfo> daoProperties;
54
55     DataContainerAnalysis(final CommonDataObjectCodecPrototype<R> prototype, final CodecItemFactory itemFactory) {
56         this(prototype.javaClass(), prototype.runtimeType(), prototype.contextFactory(), itemFactory);
57     }
58
59     DataContainerAnalysis(final Class<?> bindingClass, final R runtimeType, final CodecContextFactory factory,
60             final CodecItemFactory itemFactory) {
61         leafContexts = factory.getLeafNodes(bindingClass, runtimeType.statement());
62
63         // Reflection-based on the passed class
64         final var clsToMethod = getChildrenClassToMethod(bindingClass);
65
66         // Indexing part: be very careful around what gets done when
67         final var byYangBuilder = new HashMap<NodeIdentifier, CodecContextSupplier>();
68
69         // Step 1: add leaf children
70         var leafBuilder = ImmutableMap.<String, ValueNodeCodecContext>builderWithExpectedSize(leafContexts.size());
71         for (var leaf : leafContexts.values()) {
72             leafBuilder.put(leaf.getSchema().getQName().getLocalName(), leaf);
73             byYangBuilder.put(leaf.getDomPathArgument(), leaf);
74         }
75         leafNodes = leafBuilder.build();
76
77         final var byBindingArgClassBuilder = new HashMap<Class<?>, DataContainerPrototype<?, ?>>();
78         final var byStreamClassBuilder = new HashMap<Class<?>, DataContainerPrototype<?, ?>>();
79         final var daoPropertiesBuilder = new HashMap<Class<?>, PropertyInfo>();
80         for (var childDataObj : clsToMethod.entrySet()) {
81             final var method = childDataObj.getValue();
82             verify(!method.isDefault(), "Unexpected default method %s in %s", method, bindingClass);
83
84             final var retClass = childDataObj.getKey();
85             if (OpaqueObject.class.isAssignableFrom(retClass)) {
86                 // Filter OpaqueObjects, they are not containers
87                 continue;
88             }
89
90             // Record getter method
91             daoPropertiesBuilder.put(retClass, new PropertyInfo.Getter(method));
92
93             final var childProto = getChildPrototype(runtimeType, factory, itemFactory, retClass);
94             byStreamClassBuilder.put(childProto.javaClass(), childProto);
95             byYangBuilder.put(childProto.yangArg(), childProto);
96
97             if (childProto instanceof ChoiceCodecPrototype<?> choiceProto) {
98                 for (var cazeChild : choiceProto.getCodecContext().getCaseChildrenClasses()) {
99                     byBindingArgClassBuilder.put(cazeChild, choiceProto);
100                 }
101             }
102         }
103
104         // Snapshot before below processing
105         byStreamClass = ImmutableMap.copyOf(byStreamClassBuilder);
106
107         // Slight footprint optimization: we do not want to copy byStreamClass, as that would force its entrySet view
108         // to be instantiated. Furthermore the two maps can easily end up being equal -- hence we can reuse
109         // byStreamClass for the purposes of both.
110         byBindingArgClassBuilder.putAll(byStreamClassBuilder);
111         byBindingArgClass = byStreamClassBuilder.equals(byBindingArgClassBuilder) ? byStreamClass
112             : ImmutableMap.copyOf(byBindingArgClassBuilder);
113
114         // Find all non-default nonnullFoo() methods and update the corresponding property info
115         for (var entry : getChildrenClassToNonnullMethod(bindingClass).entrySet()) {
116             final var method = entry.getValue();
117             if (!method.isDefault()) {
118                 daoPropertiesBuilder.compute(entry.getKey(), (key, value) -> new PropertyInfo.GetterAndNonnull(
119                     verifyNotNull(value, "No getter for %s", key).getterMethod(), method));
120             }
121         }
122
123         // At this point all indexing is done: byYangBuilder should not be referenced
124         byYang = ImmutableMap.copyOf(byYangBuilder);
125         daoProperties = ImmutableMap.copyOf(daoPropertiesBuilder);
126     }
127
128     private static @NonNull DataContainerPrototype<?, ?> getChildPrototype(final CompositeRuntimeType type,
129             final CodecContextFactory factory, final CodecItemFactory itemFactory,
130             final Class<? extends DataContainer> childClass) {
131         final var child = type.bindingChild(JavaTypeName.create(childClass));
132         if (child == null) {
133             throw DataContainerCodecContext.childNullException(factory.getRuntimeContext(), childClass,
134                 "Node %s does not have child named %s", type, childClass);
135         }
136
137         if (child instanceof ChoiceRuntimeType choice) {
138             return new ChoiceCodecPrototype<>(factory, choice, childClass.asSubclass(ChoiceIn.class));
139         }
140
141         final var item = itemFactory.createItem(childClass, child.statement());
142         if (child instanceof ContainerLikeRuntimeType containerLike) {
143             if (child instanceof ContainerRuntimeType container
144                 && container.statement().findFirstEffectiveSubstatement(PresenceEffectiveStatement.class).isEmpty()) {
145                 return new StructuralContainerCodecPrototype(item, container, factory);
146             }
147             return new ContainerLikeCodecPrototype(item, containerLike, factory);
148         } else if (child instanceof ListRuntimeType list) {
149             return list.keyType() != null ? new MapCodecPrototype(item, list, factory)
150                 : new ListCodecPrototype(item, list, factory);
151         } else {
152             throw new UnsupportedOperationException("Unhandled type " + child);
153         }
154     }
155
156     // FIXME: MDSAL-780: these methods perform analytics using java.lang.reflect to acquire the basic shape of the
157     //                   class. This is not exactly AOT friendly, as most of the information should be provided by
158     //                   CompositeRuntimeType.
159     //
160     //                   As as first cut, CompositeRuntimeType should provide the mapping between YANG children and the
161     //                   corresponding GETTER_PREFIX/NONNULL_PREFIX method names, If we have that, the use in this
162     //                   class should be fine.
163     //
164     //                   The second cut is binding the actual Method invocations, which is fine here, as this class is
165     //                   all about having a run-time generated class. AOT would be providing an alternative, where the
166     //                   equivalent class would be generated at compile-time and hence would bind to the methods
167     //                   directly -- and AOT equivalent of this class would really be a compile-time generated registry
168     //                   to those classes' entry points.
169
170     /**
171      * Scans supplied class and returns an iterable of all data children classes.
172      *
173      * @param type YANG Modeled Entity derived from DataContainer
174      * @return Iterable of all data children, which have YANG modeled entity
175      */
176     private static Map<Class<? extends DataContainer>, Method> getChildrenClassToMethod(final Class<?> type) {
177         return getChildClassToMethod(type, Naming.GETTER_PREFIX);
178     }
179
180     private static Map<Class<? extends DataContainer>, Method> getChildrenClassToNonnullMethod(final Class<?> type) {
181         return getChildClassToMethod(type, Naming.NONNULL_PREFIX);
182     }
183
184     private static Map<Class<? extends DataContainer>, Method> getChildClassToMethod(final Class<?> type,
185             final String prefix) {
186         checkArgument(type != null, "Target type must not be null");
187         checkArgument(DataContainer.class.isAssignableFrom(type), "Supplied type %s must be derived from DataContainer",
188             type);
189         final var ret = new HashMap<Class<? extends DataContainer>, Method>();
190         for (Method method : type.getMethods()) {
191             getYangModeledReturnType(method, prefix).ifPresent(entity -> ret.put(entity, method));
192         }
193         return ret;
194     }
195
196     static Optional<Class<? extends DataContainer>> getYangModeledReturnType(final Method method,
197             final String prefix) {
198         final String methodName = method.getName();
199         if ("getClass".equals(methodName) || !methodName.startsWith(prefix) || method.getParameterCount() > 0) {
200             return Optional.empty();
201         }
202
203         final Class<?> returnType = method.getReturnType();
204         if (DataContainer.class.isAssignableFrom(returnType)) {
205             return optionalDataContainer(returnType);
206         } else if (List.class.isAssignableFrom(returnType)) {
207             return getYangModeledReturnType(method, 0);
208         } else if (Map.class.isAssignableFrom(returnType)) {
209             return getYangModeledReturnType(method, 1);
210         }
211         return Optional.empty();
212     }
213
214     @SuppressWarnings("checkstyle:illegalCatch")
215     private static Optional<Class<? extends DataContainer>> getYangModeledReturnType(final Method method,
216             final int parameterOffset) {
217         try {
218             return ClassLoaderUtils.callWithClassLoader(method.getDeclaringClass().getClassLoader(),
219                 () -> genericParameter(method.getGenericReturnType(), parameterOffset)
220                     .flatMap(result -> result instanceof Class ? optionalCast((Class<?>) result) : Optional.empty()));
221         } catch (Exception e) {
222             /*
223              * It is safe to log this this exception on debug, since this
224              * method should not fail. Only failures are possible if the
225              * runtime / backing.
226              */
227             LOG.debug("Unable to find YANG modeled return type for {}", method, e);
228         }
229         return Optional.empty();
230     }
231
232     private static Optional<java.lang.reflect.Type> genericParameter(final java.lang.reflect.Type type,
233             final int offset) {
234         if (type instanceof ParameterizedType parameterized) {
235             final var parameters = parameterized.getActualTypeArguments();
236             if (parameters.length > offset) {
237                 return Optional.of(parameters[offset]);
238             }
239         }
240         return Optional.empty();
241     }
242
243     private static Optional<Class<? extends DataContainer>> optionalCast(final Class<?> type) {
244         return DataContainer.class.isAssignableFrom(type) ? optionalDataContainer(type) : Optional.empty();
245     }
246
247     private static Optional<Class<? extends DataContainer>> optionalDataContainer(final Class<?> type) {
248         return Optional.of(type.asSubclass(DataContainer.class));
249     }
250 }