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