X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=blobdiff_plain;f=binding%2Fmdsal-binding-dom-codec%2Fsrc%2Fmain%2Fjava%2Forg%2Fopendaylight%2Fmdsal%2Fbinding%2Fdom%2Fcodec%2Fimpl%2FDataContainerCodecContext.java;h=2848884b277148ce1a9343210324234ea3e7b4f6;hb=889d6606afceea88af3884ee340008c0f8810496;hp=6624e3ab950938a228ae23c3b6fbb284a1e604af;hpb=2922529e8704b9d1b506c99adb3617284384b5fe;p=mdsal.git diff --git a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataContainerCodecContext.java b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataContainerCodecContext.java index 6624e3ab95..2848884b27 100644 --- a/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataContainerCodecContext.java +++ b/binding/mdsal-binding-dom-codec/src/main/java/org/opendaylight/mdsal/binding/dom/codec/impl/DataContainerCodecContext.java @@ -11,206 +11,414 @@ 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.util.Arrays; +import java.util.HashSet; import java.util.List; 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.BindingDataObjectCodecTreeNode; +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.BindingStreamEventWriter; +import org.opendaylight.mdsal.binding.dom.codec.api.BindingNormalizedNodeCodec; +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; +import org.opendaylight.mdsal.binding.dom.codec.api.MissingSchemaForClassException; +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.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.api.schema.stream.NormalizedNodeStreamWriter; import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter; -import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult; -import org.opendaylight.yangtools.yang.model.api.DocumentedNode.WithStatus; +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 class DataContainerCodecContext extends NodeCodecContext - implements BindingDataObjectCodecTreeNode { +abstract sealed class DataContainerCodecContext> + extends CodecContext implements BindingDataContainerCodecTreeNode + 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); } } - private final @NonNull DataContainerCodecPrototype 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 prototype) { + DataContainerCodecContext(final P prototype) { this.prototype = requireNonNull(prototype); + childAddressabilitySummary = computeChildAddressabilitySummary(prototype.runtimeType().statement()); } - @Override - public final T getSchema() { - return prototype.getSchema(); + final @NonNull P prototype() { + return prototype; } @Override - public final ChildAddressabilitySummary getChildAddressabilitySummary() { - return prototype.getChildAddressabilitySummary(); + @SuppressWarnings("unchecked") + public final Class getBindingClass() { + return (Class) prototype().javaClass(); } - protected final QNameModule namespace() { - return prototype.getNamespace(); + // overridden in AugmentationCodecContext + @Override + protected NodeIdentifier getDomPathArgument() { + return prototype.yangArg(); } - protected final CodecContextFactory factory() { - return prototype.getFactory(); + @Override + public final ChildAddressabilitySummary getChildAddressabilitySummary() { + return childAddressabilitySummary; } + // Non-final for ChoiceCodecContext @Override - protected YangInstanceIdentifier.PathArgument getDomPathArgument() { - return prototype.getYangArg(); + 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 childNonNull(supplier, arg, "Argument %s is not valid child of %s", arg, getSchema()).getCodecContext(); } - /** - * 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. - */ + abstract @Nullable CodecContextSupplier yangChildSupplier(@NonNull NodeIdentifier arg); + @Override - public abstract NodeCodecContext yangPathArgumentChild(YangInstanceIdentifier.PathArgument arg); + public abstract CommonDataObjectCodecContext bindingPathArgumentChild(DataObjectStep step, + List builder); /** - * Returns nested node context using supplied Binding Instance Identifier - * and adds YANG instance identifiers to supplied list. + * Serializes supplied Binding Path Argument and adds all necessary 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. + * @param step Binding Path Argument + * @param builder DOM Path argument. */ - @Override - public DataContainerCodecContext bindingPathArgumentChild(final PathArgument arg, - final List builder) { - final DataContainerCodecContext child = streamChild(arg.getType()); + final void addYangPathArgument(final DataObjectStep step, final List builder) { if (builder != null) { - child.addYangPathArgument(arg,builder); + addYangPathArgument(builder, step); } - return child; } - /** - * Returns deserialized Binding Path Argument from YANG instance identifier. - */ - protected PathArgument getBindingPathArgument(final YangInstanceIdentifier.PathArgument domArg) { - return bindingArg(); + void addYangPathArgument(final @NonNull List builder, final DataObjectStep step) { + final var yangArg = getDomPathArgument(); + if (yangArg != null) { + builder.add(yangArg); + } } - protected final PathArgument bindingArg() { - return prototype.getBindingArg(); + @Override + public final DataContainerCodecContext getStreamChild(final Class childClass) { + return childNonNull(streamChild(childClass), childClass, + "Child %s is not valid child of %s", getBindingClass(), childClass); } @SuppressWarnings("unchecked") @Override - public final Class getBindingClass() { - return Class.class.cast(prototype.getBindingClass()); + public final DataContainerCodecContext streamChild(final Class childClass) { + final var childProto = streamChildPrototype(requireNonNull(childClass)); + return childProto == null ? null : (DataContainerCodecContext) childProto.getCodecContext(); } - @Override - public abstract DataContainerCodecContext streamChild(Class 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 Optional> possibleStreamChild( - Class childClass); + abstract @Nullable DataContainerPrototype streamChildPrototype(@NonNull Class childClass); @Override public String toString() { - return getClass().getSimpleName() + " [" + prototype.getBindingClass() + "]"; - } - - @Override - public BindingNormalizedNodeCachingCodec createCachingCodec( - final ImmutableCollection> cacheSpecifier) { - if (cacheSpecifier.isEmpty()) { - return new NonCachingCodec<>(this); - } - return new CachingNormalizedNodeCodec<>(this, ImmutableSet.copyOf(cacheSpecifier)); + return getClass().getSimpleName() + " [" + getBindingClass() + "]"; } - BindingStreamEventWriter createWriter(final NormalizedNodeStreamWriter domWriter) { - return BindingToNormalizedStreamWriter.create(this, domWriter); + static final & BindingNormalizedNodeCodec> + @NonNull BindingNormalizedNodeCachingCodec createCachingCodec(final C context, + final ImmutableCollection> cacheSpecifier) { + return cacheSpecifier.isEmpty() ? new NonCachingCodec<>(context) + : new CachingNormalizedNodeCodec<>(context, ImmutableSet.copyOf(cacheSpecifier)); } - protected final @NonNull V childNonNull(final @Nullable V nullable, - final YangInstanceIdentifier.PathArgument child, final String message, final Object... args) { - if (nullable != null) { - return nullable; + protected final @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); } - MissingSchemaException.checkModulePresent(factory().getRuntimeContext().getSchemaContext(), child); - throw IncorrectNestingException.create(message, args); + return nullable; } protected final @NonNull V childNonNull(final @Nullable V nullable, final QName child, final String message, final Object... args) { - if (nullable != null) { - return nullable; + if (nullable == null) { + throw childNullException(child, message, args); } - MissingSchemaException.checkModulePresent(factory().getRuntimeContext().getSchemaContext(), child); - throw IncorrectNestingException.create(message, args); + return nullable; } protected final @NonNull V childNonNull(final @Nullable V nullable, final Class childClass, final String message, final Object... args) { - if (nullable != null) { - return nullable; + if (nullable == null) { + throw childNullException(childClass, message, args); + } + return nullable; + } + + @CheckReturnValue + private IllegalArgumentException childNullException(final QName child, final String message, final Object... args) { + 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."); } - MissingSchemaForClassException.check(factory().getRuntimeContext(), childClass); - MissingClassInLoadingStrategyException.check(factory().getRuntimeContext().getStrategy(), childClass); - throw IncorrectNestingException.create(message, args); + return new IncorrectNestingException(message, args); + } + + @CheckReturnValue + private @NonNull IllegalArgumentException childNullException(final Class childClass, final String message, + final Object... args) { + return childNullException(prototype().contextFactory().getRuntimeContext(), childClass, message, args); } - final DataObjectSerializer eventStreamSerializer() { - final DataObjectSerializer existing = (DataObjectSerializer) EVENT_STREAM_SERIALIZER.getAcquire(this); + @CheckReturnValue + static @NonNull IllegalArgumentException childNullException(final BindingRuntimeContext runtimeContext, + final Class childClass, final String message, final Object... args) { + final CompositeRuntimeType schema; + if (Augmentation.class.isAssignableFrom(childClass)) { + schema = runtimeContext.getAugmentationDefinition(childClass.asSubclass(Augmentation.class)); + } else { + schema = runtimeContext.getSchemaDefinition(childClass); + } + if (schema == null) { + return new MissingSchemaForClassException(childClass); + } + + try { + runtimeContext.loadClass(Type.of(childClass)); + } catch (final ClassNotFoundException e) { + return new MissingClassInLoadingStrategyException( + "User supplied class " + childClass.getName() + " is not available in " + runtimeContext, e); + } + + return new IncorrectNestingException(message, args); + } + + 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; } - @Override - public NormalizedNode serialize(final D data) { - final NormalizedNodeResult result = new NormalizedNodeResult(); + final @NonNull NormalizedNode serializeImpl(final @NonNull D data) { + final var result = new NormalizationResultHolder(); // We create DOM stream writer which produces normalized nodes - final NormalizedNodeStreamWriter domWriter = ImmutableNormalizedNodeStreamWriter.from(result); - writeAsNormalizedNode(data, domWriter); - return result.getResult(); - } - - @Override - public void writeAsNormalizedNode(final D data, final NormalizedNodeStreamWriter writer) { + final var domWriter = ImmutableNormalizedNodeStreamWriter.from(result); try { - eventStreamSerializer().serialize(data, createWriter(writer)); + eventStreamSerializer().serialize(data, new BindingToNormalizedStreamWriter(this, domWriter)); } catch (final IOException e) { throw new IllegalStateException("Failed to serialize Binding DTO",e); } + return result.getResult().data(); + } + + static final @NonNull T checkDataArgument(final @NonNull Class expectedType, + final NormalizedNode data) { + try { + return expectedType.cast(requireNonNull(data)); + } catch (ClassCastException e) { + throw new IllegalArgumentException("Expected " + expectedType.getSimpleName(), e); + } + } + + /** + * Determines if two augmentation classes or case classes represents same data. + * + *

+ * Two augmentations or cases could be substituted only if and if: + *

    + *
  • Both implements same interfaces
  • + *
  • Both have same children
  • + *
  • If augmentations: Both have same augmentation target class. Target class was generated for data node in a + * grouping.
  • + *
  • If cases: Both are from same choice. Choice class was generated for data node in grouping.
  • + *
+ * + *

+ * Explanation: + * Binding Specification reuses classes generated for groupings as part of normal data tree, this classes from + * grouping could be used at various locations and user may not be aware of it and may use incorrect case or + * augmentation in particular subtree (via copy constructors, etc). + * + * @param potential Class which is potential substitution + * @param target Class which should be used at particular subtree + * @return true if and only if classes represents same data. + * @throws NullPointerException if any argument is {@code null} + */ + // FIXME: MDSAL-785: this really should live in BindingRuntimeTypes and should not be based on reflection. The only + // user is binding-dom-codec and the logic could easily be performed on GeneratedType instead. For + // a particular world this boils down to a matrix, which can be calculated either on-demand or + // when we create BindingRuntimeTypes. Achieving that will bring us one step closer to being able + // to have a pre-compiled Binding Runtime. + @SuppressWarnings({ "rawtypes", "unchecked" }) + static final boolean isSubstitutionFor(final Class potential, final Class target) { + Set subImplemented = new HashSet<>(Arrays.asList(potential.getInterfaces())); + Set targetImplemented = new HashSet<>(Arrays.asList(target.getInterfaces())); + if (!subImplemented.equals(targetImplemented)) { + return false; + } + if (Augmentation.class.isAssignableFrom(potential) + && !findAugmentationTarget(potential).equals(findAugmentationTarget(target))) { + return false; + } + for (Method potentialMethod : potential.getMethods()) { + if (Modifier.isStatic(potentialMethod.getModifiers())) { + // Skip any static methods, as we are not interested in those + continue; + } + + try { + Method targetMethod = target.getMethod(potentialMethod.getName(), potentialMethod.getParameterTypes()); + if (!potentialMethod.getReturnType().equals(targetMethod.getReturnType())) { + return false; + } + } catch (NoSuchMethodException e) { + // Counterpart method is missing, so classes could not be substituted. + return false; + } catch (SecurityException e) { + throw new IllegalStateException("Could not compare methods", e); + } + } + 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> findAugmentationTarget( + final Class> augmentation) { + final Optional>> 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; } }