Refactor PathArgument to DataObjectStep
[mdsal.git] / binding / mdsal-binding-dom-codec / src / main / java / org / opendaylight / mdsal / binding / dom / codec / impl / DataContainerCodecContext.java
index 1b1478d15023b9d7de1191558ef4bdcb66df01b0..2848884b277148ce1a9343210324234ea3e7b4f6 100644 (file)
@@ -12,24 +12,22 @@ import static java.util.Objects.requireNonNull;
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableSet;
 import edu.umd.cs.findbugs.annotations.CheckReturnValue;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.lang.invoke.VarHandle;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
-import java.lang.reflect.ParameterizedType;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.mdsal.binding.dom.codec.api.BindingDataContainerCodecTreeNode;
 import org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeCachingCodec;
 import org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeCodec;
-import org.opendaylight.mdsal.binding.dom.codec.api.BindingStreamEventWriter;
-import org.opendaylight.mdsal.binding.dom.codec.api.CommonDataObjectCodecTreeNode;
 import org.opendaylight.mdsal.binding.dom.codec.api.IncorrectNestingException;
 import org.opendaylight.mdsal.binding.dom.codec.api.MissingClassInLoadingStrategyException;
 import org.opendaylight.mdsal.binding.dom.codec.api.MissingSchemaException;
@@ -37,159 +35,151 @@ import org.opendaylight.mdsal.binding.dom.codec.api.MissingSchemaForClassExcepti
 import org.opendaylight.mdsal.binding.model.api.Type;
 import org.opendaylight.mdsal.binding.runtime.api.BindingRuntimeContext;
 import org.opendaylight.mdsal.binding.runtime.api.CompositeRuntimeType;
-import org.opendaylight.mdsal.binding.runtime.api.RuntimeTypeContainer;
-import org.opendaylight.mdsal.binding.spec.reflect.BindingReflections;
 import org.opendaylight.yangtools.util.ClassLoaderUtils;
+import org.opendaylight.yangtools.yang.binding.Augmentable;
 import org.opendaylight.yangtools.yang.binding.Augmentation;
 import org.opendaylight.yangtools.yang.binding.BindingObject;
 import org.opendaylight.yangtools.yang.binding.DataContainer;
 import org.opendaylight.yangtools.yang.binding.DataObject;
-import org.opendaylight.yangtools.yang.binding.InstanceIdentifier.PathArgument;
+import org.opendaylight.yangtools.yang.binding.DataObjectStep;
 import org.opendaylight.yangtools.yang.common.QName;
-import org.opendaylight.yangtools.yang.common.QNameModule;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizationResultHolder;
+import org.opendaylight.yangtools.yang.model.api.AnydataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.AugmentationSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
+import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-abstract sealed class DataContainerCodecContext<D extends DataObject, T extends RuntimeTypeContainer>
-        extends CodecContext implements CommonDataObjectCodecTreeNode<D>
-        permits AbstractDataObjectCodecContext, ChoiceNodeCodecContext, RootCodecContext {
+abstract sealed class DataContainerCodecContext<D extends DataContainer, R extends CompositeRuntimeType,
+        P extends DataContainerPrototype<?, R>>
+        extends CodecContext implements BindingDataContainerCodecTreeNode<D>
+        permits ChoiceCodecContext, CommonDataObjectCodecContext {
     private static final Logger LOG = LoggerFactory.getLogger(DataContainerCodecContext.class);
     private static final VarHandle EVENT_STREAM_SERIALIZER;
 
     static {
         try {
             EVENT_STREAM_SERIALIZER = MethodHandles.lookup().findVarHandle(DataContainerCodecContext.class,
-                "eventStreamSerializer", DataObjectSerializer.class);
+                "eventStreamSerializer", DataContainerSerializer.class);
         } catch (NoSuchFieldException | IllegalAccessException e) {
             throw new ExceptionInInitializerError(e);
         }
     }
 
-    final @NonNull DataContainerCodecPrototype<T> prototype;
+    private final @NonNull P prototype;
+    private final @NonNull ChildAddressabilitySummary childAddressabilitySummary;
 
     // Accessed via a VarHandle
     @SuppressWarnings("unused")
-    private volatile DataObjectSerializer eventStreamSerializer;
+    @SuppressFBWarnings(value = "UUF_UNUSED_FIELD", justification = "https://github.com/spotbugs/spotbugs/issues/2749")
+    private volatile DataContainerSerializer eventStreamSerializer;
 
-    DataContainerCodecContext(final DataContainerCodecPrototype<T> prototype) {
+    DataContainerCodecContext(final P prototype) {
         this.prototype = requireNonNull(prototype);
+        childAddressabilitySummary = computeChildAddressabilitySummary(prototype.runtimeType().statement());
     }
 
-    public final @NonNull T getType() {
-        return prototype.getType();
+    final @NonNull P prototype() {
+        return prototype;
     }
 
     @Override
-    public final ChildAddressabilitySummary getChildAddressabilitySummary() {
-        return prototype.getChildAddressabilitySummary();
-    }
-
-    protected final QNameModule namespace() {
-        return prototype.getNamespace();
-    }
-
-    protected final CodecContextFactory factory() {
-        return prototype.getFactory();
+    @SuppressWarnings("unchecked")
+    public final Class<D> getBindingClass() {
+        return (Class<D>) prototype().javaClass();
     }
 
+    // overridden in AugmentationCodecContext
     @Override
     protected NodeIdentifier getDomPathArgument() {
-        return prototype.getYangArg();
+        return prototype.yangArg();
     }
 
-    /**
-     * Returns nested node context using supplied YANG Instance Identifier.
-     *
-     * @param arg Yang Instance Identifier Argument
-     * @return Context of child
-     * @throws IllegalArgumentException If supplied argument does not represent valid child.
-     */
     @Override
-    public abstract CodecContext yangPathArgumentChild(YangInstanceIdentifier.PathArgument arg);
+    public final ChildAddressabilitySummary getChildAddressabilitySummary() {
+        return childAddressabilitySummary;
+    }
 
-    /**
-     * Returns nested node context using supplied Binding Instance Identifier
-     * and adds YANG instance identifiers to supplied list.
-     *
-     * @param arg Binding Instance Identifier Argument
-     * @return Context of child or null if supplied {@code arg} does not represent valid child.
-     * @throws IllegalArgumentException If supplied argument does not represent valid child.
-     */
+    // Non-final for ChoiceCodecContext
     @Override
-    public DataContainerCodecContext<?, ?> bindingPathArgumentChild(final PathArgument arg,
-            final List<YangInstanceIdentifier.PathArgument> builder) {
-        final DataContainerCodecContext<?, ?> child = streamChild(arg.getType());
-        if (builder != null) {
-            child.addYangPathArgument(arg, builder);
+    public CodecContext yangPathArgumentChild(final PathArgument arg) {
+        CodecContextSupplier supplier;
+        if (arg instanceof NodeIdentifier nodeId) {
+            supplier = yangChildSupplier(nodeId);
+        } else if (arg instanceof NodeIdentifierWithPredicates nip) {
+            supplier = yangChildSupplier(new NodeIdentifier(nip.getNodeType()));
+        } else {
+            supplier = null;
         }
-        return child;
+        return childNonNull(supplier, arg, "Argument %s is not valid child of %s", arg, getSchema()).getCodecContext();
     }
 
+    abstract @Nullable CodecContextSupplier yangChildSupplier(@NonNull NodeIdentifier arg);
+
+    @Override
+    public abstract CommonDataObjectCodecContext<?, ?> bindingPathArgumentChild(DataObjectStep<?> step,
+        List<PathArgument> builder);
+
     /**
      * Serializes supplied Binding Path Argument and adds all necessary YANG instance identifiers to supplied list.
      *
-     * @param arg Binding Path Argument
+     * @param step Binding Path Argument
      * @param builder DOM Path argument.
      */
-    void addYangPathArgument(final PathArgument arg, final List<YangInstanceIdentifier.PathArgument> builder) {
+    final void addYangPathArgument(final DataObjectStep<?> step, final List<PathArgument> builder) {
         if (builder != null) {
-            final var yangArg = getDomPathArgument();
-            if (yangArg != null) {
-                builder.add(yangArg);
-            }
+            addYangPathArgument(builder, step);
         }
     }
 
-    /**
-     * Returns deserialized Binding Path Argument from YANG instance identifier.
-     */
-    protected PathArgument getBindingPathArgument(final YangInstanceIdentifier.PathArgument domArg) {
-        return bindingArg();
+    void addYangPathArgument(final @NonNull List<PathArgument> builder, final DataObjectStep<?> step) {
+        final var yangArg = getDomPathArgument();
+        if (yangArg != null) {
+            builder.add(yangArg);
+        }
     }
 
-    protected final PathArgument bindingArg() {
-        return prototype.getBindingArg();
+    @Override
+    public final <C extends DataObject> DataContainerCodecContext<C, ?, ?> getStreamChild(final Class<C> childClass) {
+        return childNonNull(streamChild(childClass), childClass,
+            "Child %s is not valid child of %s", getBindingClass(), childClass);
     }
 
     @SuppressWarnings("unchecked")
     @Override
-    public final Class<D> getBindingClass() {
-        return Class.class.cast(prototype.getBindingClass());
+    public final <C extends DataObject> DataContainerCodecContext<C, ?, ?> streamChild(final Class<C> childClass) {
+        final var childProto = streamChildPrototype(requireNonNull(childClass));
+        return childProto == null ? null : (DataContainerCodecContext<C, ?, ?>) childProto.getCodecContext();
     }
 
-    @Override
-    public abstract <C extends DataObject> DataContainerCodecContext<C, ?> streamChild(Class<C> childClass);
-
-    /**
-     * Returns child context as if it was walked by {@link BindingStreamEventWriter}. This means that to enter case, one
-     * must issue getChild(ChoiceClass).getChild(CaseClass).
-     *
-     * @param childClass child class
-     * @return Context of child or Optional.empty is supplied class is not applicable in context.
-     */
-    @Override
-    public abstract <C extends DataObject> Optional<DataContainerCodecContext<C,?>> possibleStreamChild(
-            Class<C> childClass);
+    abstract @Nullable DataContainerPrototype<?, ?> streamChildPrototype(@NonNull Class<?> childClass);
 
     @Override
     public String toString() {
-        return getClass().getSimpleName() + " [" + prototype.getBindingClass() + "]";
+        return getClass().getSimpleName() + " [" + getBindingClass() + "]";
     }
 
-    static final <T extends DataObject, C extends DataContainerCodecContext<T, ?> & BindingNormalizedNodeCodec<T>>
+    static final <T extends DataObject, C extends DataContainerCodecContext<T, ?, ?> & BindingNormalizedNodeCodec<T>>
             @NonNull BindingNormalizedNodeCachingCodec<T> createCachingCodec(final C context,
                 final ImmutableCollection<Class<? extends BindingObject>> cacheSpecifier) {
         return cacheSpecifier.isEmpty() ? new NonCachingCodec<>(context)
             : new CachingNormalizedNodeCodec<>(context, ImmutableSet.copyOf(cacheSpecifier));
     }
 
-    protected final <V> @NonNull V childNonNull(final @Nullable V nullable,
-            final YangInstanceIdentifier.PathArgument child, final String message, final Object... args) {
+    protected final <V> @NonNull V childNonNull(final @Nullable V nullable, final PathArgument child,
+            final String message, final Object... args) {
         if (nullable == null) {
             throw childNullException(child.getNodeType(), message, args);
         }
@@ -214,8 +204,8 @@ abstract sealed class DataContainerCodecContext<D extends DataObject, T extends
 
     @CheckReturnValue
     private IllegalArgumentException childNullException(final QName child, final String message, final Object... args) {
-        final QNameModule module = child.getModule();
-        if (!factory().getRuntimeContext().getEffectiveModelContext().findModule(module).isPresent()) {
+        final var module = child.getModule();
+        if (!prototype().contextFactory().getRuntimeContext().modelContext().findModule(module).isPresent()) {
             return new MissingSchemaException("Module " + module + " is not present in current schema context.");
         }
         return new IncorrectNestingException(message, args);
@@ -224,7 +214,7 @@ abstract sealed class DataContainerCodecContext<D extends DataObject, T extends
     @CheckReturnValue
     private @NonNull IllegalArgumentException childNullException(final Class<?> childClass, final String message,
             final Object... args) {
-        return childNullException(factory().getRuntimeContext(), childClass, message, args);
+        return childNullException(prototype().contextFactory().getRuntimeContext(), childClass, message, args);
     }
 
     @CheckReturnValue
@@ -250,16 +240,16 @@ abstract sealed class DataContainerCodecContext<D extends DataObject, T extends
         return new IncorrectNestingException(message, args);
     }
 
-    final DataObjectSerializer eventStreamSerializer() {
-        final DataObjectSerializer existing = (DataObjectSerializer) EVENT_STREAM_SERIALIZER.getAcquire(this);
+    final DataContainerSerializer eventStreamSerializer() {
+        final DataContainerSerializer existing = (DataContainerSerializer) EVENT_STREAM_SERIALIZER.getAcquire(this);
         return existing != null ? existing : loadEventStreamSerializer();
     }
 
     // Split out to aid inlining
-    private DataObjectSerializer loadEventStreamSerializer() {
-        final DataObjectSerializer loaded = factory().getEventStreamSerializer(getBindingClass());
+    private DataContainerSerializer loadEventStreamSerializer() {
+        final DataContainerSerializer loaded = prototype().contextFactory().getEventStreamSerializer(getBindingClass());
         final Object witness = EVENT_STREAM_SERIALIZER.compareAndExchangeRelease(this, null, loaded);
-        return witness == null ? loaded : (DataObjectSerializer) witness;
+        return witness == null ? loaded : (DataContainerSerializer) witness;
     }
 
     final @NonNull NormalizedNode serializeImpl(final @NonNull D data) {
@@ -283,63 +273,6 @@ abstract sealed class DataContainerCodecContext<D extends DataObject, T extends
         }
     }
 
-    // FIXME: MDSAL-780 replace this method with BindingRuntimeTypes-driven logic
-    static final Optional<Class<? extends DataContainer>> getYangModeledReturnType(final Method method,
-            final String prefix) {
-        final String methodName = method.getName();
-        if ("getClass".equals(methodName) || !methodName.startsWith(prefix) || method.getParameterCount() > 0) {
-            return Optional.empty();
-        }
-
-        final Class<?> returnType = method.getReturnType();
-        if (DataContainer.class.isAssignableFrom(returnType)) {
-            return optionalDataContainer(returnType);
-        } else if (List.class.isAssignableFrom(returnType)) {
-            return getYangModeledReturnType(method, 0);
-        } else if (Map.class.isAssignableFrom(returnType)) {
-            return getYangModeledReturnType(method, 1);
-        }
-        return Optional.empty();
-    }
-
-    @SuppressWarnings("checkstyle:illegalCatch")
-    private static Optional<Class<? extends DataContainer>> getYangModeledReturnType(final Method method,
-            final int parameterOffset) {
-        try {
-            return ClassLoaderUtils.callWithClassLoader(method.getDeclaringClass().getClassLoader(),
-                () -> genericParameter(method.getGenericReturnType(), parameterOffset)
-                    .flatMap(result -> result instanceof Class ? optionalCast((Class<?>) result) : Optional.empty()));
-        } catch (Exception e) {
-            /*
-             * It is safe to log this this exception on debug, since this
-             * method should not fail. Only failures are possible if the
-             * runtime / backing.
-             */
-            LOG.debug("Unable to find YANG modeled return type for {}", method, e);
-        }
-        return Optional.empty();
-    }
-
-    private static Optional<java.lang.reflect.Type> genericParameter(final java.lang.reflect.Type type,
-            final int offset) {
-        if (type instanceof ParameterizedType parameterized) {
-            final var parameters = parameterized.getActualTypeArguments();
-            if (parameters.length > offset) {
-                return Optional.of(parameters[offset]);
-            }
-        }
-        return Optional.empty();
-    }
-
-    private static Optional<Class<? extends DataContainer>> optionalCast(final Class<?> type) {
-        return DataContainer.class.isAssignableFrom(type) ? optionalDataContainer(type) : Optional.empty();
-    }
-
-    // FIXME: MDSAL-780: remove this method
-    static final Optional<Class<? extends DataContainer>> optionalDataContainer(final Class<?> type) {
-        return Optional.of(type.asSubclass(DataContainer.class));
-    }
-
     /**
      * Determines if two augmentation classes or case classes represents same data.
      *
@@ -377,8 +310,7 @@ abstract sealed class DataContainerCodecContext<D extends DataObject, T extends
             return false;
         }
         if (Augmentation.class.isAssignableFrom(potential)
-                && !BindingReflections.findAugmentationTarget(potential).equals(
-                        BindingReflections.findAugmentationTarget(target))) {
+            && !findAugmentationTarget(potential).equals(findAugmentationTarget(target))) {
             return false;
         }
         for (Method potentialMethod : potential.getMethods()) {
@@ -401,4 +333,92 @@ abstract sealed class DataContainerCodecContext<D extends DataObject, T extends
         }
         return true;
     }
+
+    /**
+     * Find augmentation target class from concrete Augmentation class. This method uses first generic argument of
+     * implemented {@link Augmentation} interface.
+     *
+     * @param augmentation {@link Augmentation} subclass for which we want to determine augmentation target.
+     * @return Augmentation target - class which augmentation provides additional extensions.
+     */
+    static final Class<? extends Augmentable<?>> findAugmentationTarget(
+            final Class<? extends Augmentation<?>> augmentation) {
+        final Optional<Class<Augmentable<?>>> opt = ClassLoaderUtils.findFirstGenericArgument(augmentation,
+            Augmentation.class);
+        return opt.orElse(null);
+    }
+
+    private static @NonNull ChildAddressabilitySummary computeChildAddressabilitySummary(final Object nodeSchema) {
+        // FIXME: rework this to work on EffectiveStatements
+        if (nodeSchema instanceof DataNodeContainer contaner) {
+            boolean haveAddressable = false;
+            boolean haveUnaddressable = false;
+            for (DataSchemaNode child : contaner.getChildNodes()) {
+                if (child instanceof ContainerSchemaNode || child instanceof AugmentationSchemaNode) {
+                    haveAddressable = true;
+                } else if (child instanceof ListSchemaNode list) {
+                    if (list.getKeyDefinition().isEmpty()) {
+                        haveUnaddressable = true;
+                    } else {
+                        haveAddressable = true;
+                    }
+                } else if (child instanceof AnydataSchemaNode || child instanceof AnyxmlSchemaNode
+                        || child instanceof TypedDataSchemaNode) {
+                    haveUnaddressable = true;
+                } else if (child instanceof ChoiceSchemaNode choice) {
+                    switch (computeChildAddressabilitySummary(choice)) {
+                        case ADDRESSABLE -> haveAddressable = true;
+                        case UNADDRESSABLE -> haveUnaddressable = true;
+                        case MIXED -> {
+                            haveAddressable = true;
+                            haveUnaddressable = true;
+                        }
+                        default -> throw new IllegalStateException("Unhandled accessibility summary for " + child);
+                    }
+                } else {
+                    LOG.warn("Unhandled child node {}", child);
+                }
+            }
+
+            if (!haveAddressable) {
+                // Empty or all are unaddressable
+                return ChildAddressabilitySummary.UNADDRESSABLE;
+            }
+
+            return haveUnaddressable ? ChildAddressabilitySummary.MIXED : ChildAddressabilitySummary.ADDRESSABLE;
+        } else if (nodeSchema instanceof ChoiceSchemaNode choice) {
+            return computeChildAddressabilitySummary(choice);
+        }
+
+        // No child nodes possible: return unaddressable
+        return ChildAddressabilitySummary.UNADDRESSABLE;
+    }
+
+    private static @NonNull ChildAddressabilitySummary computeChildAddressabilitySummary(
+            final ChoiceSchemaNode choice) {
+        boolean haveAddressable = false;
+        boolean haveUnaddressable = false;
+        for (CaseSchemaNode child : choice.getCases()) {
+            switch (computeChildAddressabilitySummary(child)) {
+                case ADDRESSABLE:
+                    haveAddressable = true;
+                    break;
+                case UNADDRESSABLE:
+                    haveUnaddressable = true;
+                    break;
+                case MIXED:
+                    // A child is mixed, which means we are mixed, too
+                    return ChildAddressabilitySummary.MIXED;
+                default:
+                    throw new IllegalStateException("Unhandled accessibility summary for " + child);
+            }
+        }
+
+        if (!haveAddressable) {
+            // Empty or all are unaddressable
+            return ChildAddressabilitySummary.UNADDRESSABLE;
+        }
+
+        return haveUnaddressable ? ChildAddressabilitySummary.MIXED : ChildAddressabilitySummary.ADDRESSABLE;
+    }
 }