/* * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html */ package org.opendaylight.mdsal.binding.dom.codec.impl; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Verify.verify; import static com.google.common.base.Verify.verifyNotNull; import com.google.common.base.VerifyException; import com.google.common.collect.ImmutableMap; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Method; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.jdt.annotation.NonNull; import org.opendaylight.mdsal.binding.dom.codec.impl.NodeCodecContext.CodecContextFactory; import org.opendaylight.mdsal.binding.model.api.JavaTypeName; import org.opendaylight.mdsal.binding.runtime.api.AugmentRuntimeType; import org.opendaylight.mdsal.binding.runtime.api.AugmentableRuntimeType; import org.opendaylight.mdsal.binding.runtime.api.ChoiceRuntimeType; import org.opendaylight.mdsal.binding.runtime.api.CompositeRuntimeType; import org.opendaylight.yangtools.yang.binding.Augmentable; import org.opendaylight.yangtools.yang.binding.DataContainer; import org.opendaylight.yangtools.yang.binding.DataObject; import org.opendaylight.yangtools.yang.binding.OpaqueObject; import org.opendaylight.yangtools.yang.binding.contract.Naming; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; import org.opendaylight.yangtools.yang.data.api.schema.DistinctNodeContainer; /** * Analysis of a {@link DataObject} specialization class. The primary point of this class is to separate out creation * indices needed for {@link #proxyConstructor}. Since we want to perform as much indexing as possible in a single pass, * we also end up indexing things that are not strictly required to arrive at that constructor. */ final class CodecDataObjectAnalysis { private static final MethodType CONSTRUCTOR_TYPE = MethodType.methodType(void.class, DataObjectCodecContext.class, DistinctNodeContainer.class); private static final MethodType DATAOBJECT_TYPE = MethodType.methodType(DataObject.class, DataObjectCodecContext.class, DistinctNodeContainer.class); final @NonNull ImmutableMap, DataContainerCodecPrototype> byStreamClass; final @NonNull ImmutableMap, DataContainerCodecPrototype> byBindingArgClass; final @NonNull ImmutableMap byYang; final @NonNull ImmutableMap leafNodes; final @NonNull Class> generatedClass; final @NonNull List possibleAugmentations; final @NonNull MethodHandle proxyConstructor; CodecDataObjectAnalysis(final DataContainerCodecPrototype prototype, final CodecItemFactory itemFactory, final Method keyMethod) { // Preliminaries from prototype @SuppressWarnings("unchecked") final Class bindingClass = Class.class.cast(prototype.getBindingClass()); final var runtimeType = prototype.getType(); final var factory = prototype.getFactory(); final var leafContexts = factory.getLeafNodes(bindingClass, runtimeType.statement()); // Reflection-based on the passed class final var clsToMethod = getChildrenClassToMethod(bindingClass); // Indexing part: be very careful around what gets done when final var byYangBuilder = new HashMap(); // Step 1: add leaf children var leafBuilder = ImmutableMap.builderWithExpectedSize(leafContexts.size()); for (var leaf : leafContexts.values()) { leafBuilder.put(leaf.getSchema().getQName().getLocalName(), leaf); byYangBuilder.put(leaf.getDomPathArgument(), leaf); } leafNodes = leafBuilder.build(); final var byBindingArgClassBuilder = new HashMap, DataContainerCodecPrototype>(); final var byStreamClassBuilder = new HashMap, DataContainerCodecPrototype>(); final var daoProperties = new HashMap, PropertyInfo>(); for (var childDataObj : clsToMethod.entrySet()) { final var method = childDataObj.getValue(); verify(!method.isDefault(), "Unexpected default method %s in %s", method, bindingClass); final var retClass = childDataObj.getKey(); if (OpaqueObject.class.isAssignableFrom(retClass)) { // Filter OpaqueObjects, they are not containers continue; } // Record getter method daoProperties.put(retClass, new PropertyInfo.Getter(method)); final var childProto = getChildPrototype(runtimeType, factory, itemFactory, retClass); byStreamClassBuilder.put(childProto.getBindingClass(), childProto); byYangBuilder.put(childProto.getYangArg(), childProto); // FIXME: It really feels like we should be specializing DataContainerCodecPrototype so as to ditch // createInstance() and then we could do an instanceof check instead. if (childProto.getType() instanceof ChoiceRuntimeType) { final var choice = (ChoiceNodeCodecContext) childProto.get(); for (var cazeChild : choice.getCaseChildrenClasses()) { byBindingArgClassBuilder.put(cazeChild, childProto); } } } // Snapshot before below processing byStreamClass = ImmutableMap.copyOf(byStreamClassBuilder); // Slight footprint optimization: we do not want to copy byStreamClass, as that would force its entrySet view // to be instantiated. Furthermore the two maps can easily end up being equal -- hence we can reuse // byStreamClass for the purposes of both. byBindingArgClassBuilder.putAll(byStreamClassBuilder); byBindingArgClass = byStreamClassBuilder.equals(byBindingArgClassBuilder) ? byStreamClass : ImmutableMap.copyOf(byBindingArgClassBuilder); // Find all non-default nonnullFoo() methods and update the corresponding property info for (var entry : getChildrenClassToNonnullMethod(bindingClass).entrySet()) { final var method = entry.getValue(); if (!method.isDefault()) { daoProperties.compute(entry.getKey(), (key, value) -> new PropertyInfo.GetterAndNonnull( verifyNotNull(value, "No getter for %s", key).getterMethod(), method)); } } // At this point all indexing is done: byYangBuilder should not be referenced byYang = ImmutableMap.copyOf(byYangBuilder); // Final bits: generate the appropriate class, As a side effect we identify what Augmentations are possible if (Augmentable.class.isAssignableFrom(bindingClass)) { // Verify we have the appropriate backing runtimeType if (!(runtimeType instanceof AugmentableRuntimeType augmentableRuntimeType)) { throw new VerifyException( "Unexpected type %s backing augmenable %s".formatted(runtimeType, bindingClass)); } possibleAugmentations = augmentableRuntimeType.augments(); generatedClass = CodecDataObjectGenerator.generateAugmentable(prototype.getFactory().getLoader(), bindingClass, leafContexts, daoProperties, keyMethod); } else { possibleAugmentations = List.of(); generatedClass = CodecDataObjectGenerator.generate(prototype.getFactory().getLoader(), bindingClass, leafContexts, daoProperties, keyMethod); } // All done: acquire the constructor: it is supposed to be public final MethodHandle ctor; try { ctor = MethodHandles.publicLookup().findConstructor(generatedClass, CONSTRUCTOR_TYPE); } catch (NoSuchMethodException | IllegalAccessException e) { throw new LinkageError("Failed to find contructor for class " + generatedClass, e); } proxyConstructor = ctor.asType(DATAOBJECT_TYPE); } private static @NonNull DataContainerCodecPrototype getChildPrototype(final CompositeRuntimeType type, final CodecContextFactory factory, final CodecItemFactory itemFactory, final Class childClass) { final var child = type.bindingChild(JavaTypeName.create(childClass)); if (child == null) { throw DataContainerCodecContext.childNullException(factory.getRuntimeContext(), childClass, "Node %s does not have child named %s", type, childClass); } return DataContainerCodecPrototype.from(itemFactory.createItem(childClass, child.statement()), (CompositeRuntimeType) child, factory); } // FIXME: MDSAL-780: these methods perform analytics using java.lang.reflect to acquire the basic shape of the // class. This is not exactly AOT friendly, as most of the information should be provided by // CompositeRuntimeType. // // As as first cut, CompositeRuntimeType should provide the mapping between YANG children and the // corresponding GETTER_PREFIX/NONNULL_PREFIX method names, If we have that, the use in this // class should be fine. // // The second cut is binding the actual Method invocations, which is fine here, as this class is // all about having a run-time generated class. AOT would be providing an alternative, where the // equivalent class would be generated at compile-time and hence would bind to the methods // directly -- and AOT equivalent of this class would really be a compile-time generated registry // to those classes' entry points. /** * Scans supplied class and returns an iterable of all data children classes. * * @param type YANG Modeled Entity derived from DataContainer * @return Iterable of all data children, which have YANG modeled entity */ private static Map, Method> getChildrenClassToMethod(final Class type) { return getChildClassToMethod(type, Naming.GETTER_PREFIX); } private static Map, Method> getChildrenClassToNonnullMethod(final Class type) { return getChildClassToMethod(type, Naming.NONNULL_PREFIX); } private static Map, Method> getChildClassToMethod(final Class type, final String prefix) { checkArgument(type != null, "Target type must not be null"); checkArgument(DataContainer.class.isAssignableFrom(type), "Supplied type %s must be derived from DataContainer", type); final var ret = new HashMap, Method>(); for (Method method : type.getMethods()) { DataContainerCodecContext.getYangModeledReturnType(method, prefix) .ifPresent(entity -> ret.put(entity, method)); } return ret; } }