import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMap.Builder;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.MultimapBuilder.SetMultimapBuilder;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.SetMultimap;
+import java.util.ArrayList;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.opendaylight.yangtools.yang.binding.DataObject;
final class ChoiceNodeCodecContext<D extends DataObject> extends DataContainerCodecContext<D, ChoiceSchemaNode> {
private static final Logger LOG = LoggerFactory.getLogger(ChoiceNodeCodecContext.class);
private final ImmutableMap<YangInstanceIdentifier.PathArgument, DataContainerCodecPrototype<?>> byYangCaseChild;
+
+ /*
+ * This is a bit tricky. DataObject addressing does not take into account choice/case statements, and hence
+ * given:
+ *
+ * container foo {
+ * choice bar {
+ * leaf baz;
+ * }
+ * }
+ *
+ * we will see {@code Baz extends ChildOf<Foo>}, which is how the users would address it in InstanceIdentifier
+ * terms. The implicit assumption being made is that {@code Baz} identifies a particular instantiation and hence
+ * provides unambiguous reference to an effective schema statement.
+ *
+ * <p>
+ * Unfortunately this does not quite work with groupings, as their generation has changed: we do not have
+ * interfaces that would capture grouping instantiations, hence we do not have a proper addressing point and
+ * users need to specify the interfaces generated in the grouping's definition. These can be very much
+ * ambiguous, as a {@code grouping} can be used in multiple modules independently within an {@code augment}
+ * targeting {@code choice}, as each instantiation is guaranteed to have a unique namespace -- but we do not
+ * have the appropriate instantiations of those nodes.
+ *
+ * <p>
+ * To address this issue we have a two-class lookup mechanism, which relies on the interface generated for
+ * the {@code case} statement to act as the namespace anchor bridging the nodes inside the grouping to the
+ * namespace in which they are instantiated.
+ *
+ * <p>
+ * Furthermore downstream code relies on historical mechanics, which would guess what the instantiation is,
+ * silently assuming the ambiguity is theoretical and does not occur in practice.
+ *
+ * <p>
+ * This leads to three classes of addressing, in order descending performance requirements.
+ * <ul>
+ * <li>Direct DataObject, where we name an exact child</li>
+ * <li>Case DataObject + Grouping DataObject</li>
+ * <li>Grouping DataObject, which is ambiguous</li>
+ * </ul>
+ *
+ * {@code byCaseChildClass} supports direct DataObject mapping and contains only unambiguous children, while
+ * {@code byClass} supports indirect mapping and contains {@code case} sub-statements.
+ *
+ * ambiguousByCaseChildClass contains ambiguous mappings, for which we end up issuing warnings. We track each
+ * ambiguous reference and issue warnings when they are encountered.
+ */
private final ImmutableMap<Class<?>, DataContainerCodecPrototype<?>> byClass;
private final ImmutableMap<Class<?>, DataContainerCodecPrototype<?>> byCaseChildClass;
+ private final ImmutableListMultimap<Class<?>, DataContainerCodecPrototype<?>> ambiguousByCaseChildClass;
+ private final Set<Class<?>> ambiguousByCaseChildWarnings;
ChoiceNodeCodecContext(final DataContainerCodecPrototype<ChoiceSchemaNode> prototype) {
super(prototype);
final Map<YangInstanceIdentifier.PathArgument, DataContainerCodecPrototype<?>> byYangCaseChildBuilder =
new HashMap<>();
final Map<Class<?>, DataContainerCodecPrototype<?>> byClassBuilder = new HashMap<>();
- final Map<Class<?>, DataContainerCodecPrototype<?>> byCaseChildClassBuilder = new HashMap<>();
+ final SetMultimap<Class<?>, DataContainerCodecPrototype<?>> childToCase = SetMultimapBuilder.hashKeys()
+ .hashSetValues().build();
final Set<Class<?>> potentialSubstitutions = new HashSet<>();
// Walks all cases for supplied choice in current runtime context
for (final Class<?> caze : factory().getRuntimeContext().getCases(getBindingClass())) {
@SuppressWarnings("unchecked")
final Class<? extends DataObject> cazeCls = (Class<? extends DataObject>) caze;
for (final Class<? extends DataObject> cazeChild : BindingReflections.getChildrenClasses(cazeCls)) {
- byCaseChildClassBuilder.put(cazeChild, cazeDef);
+ childToCase.put(cazeChild, cazeDef);
}
// Updates collection of YANG instance identifier to case
for (final DataSchemaNode cazeChild : cazeDef.getSchema().getChildNodes()) {
potentialSubstitutions.add(caze);
}
}
+ byYangCaseChild = ImmutableMap.copyOf(byYangCaseChildBuilder);
+
+ // Move unambiguous child->case mappings to byCaseChildClass, removing them from childToCase
+ final ImmutableListMultimap.Builder<Class<?>, DataContainerCodecPrototype<?>> ambiguousByCaseBuilder =
+ ImmutableListMultimap.builder();
+ final Builder<Class<?>, DataContainerCodecPrototype<?>> unambiguousByCaseBuilder = ImmutableMap.builder();
+ for (Entry<Class<?>, Set<DataContainerCodecPrototype<?>>> e : Multimaps.asMap(childToCase).entrySet()) {
+ final Set<DataContainerCodecPrototype<?>> cases = e.getValue();
+ if (cases.size() != 1) {
+ // Sort all possibilities by their FQCN to retain semi-predictable results
+ final List<DataContainerCodecPrototype<?>> list = new ArrayList<>(e.getValue());
+ list.sort(Comparator.comparing(proto -> proto.getBindingClass().getCanonicalName()));
+ ambiguousByCaseBuilder.putAll(e.getKey(), list);
+ } else {
+ unambiguousByCaseBuilder.put(e.getKey(), cases.iterator().next());
+ }
+ }
+ byCaseChildClass = unambiguousByCaseBuilder.build();
+
+ // Setup ambiguous tracking, if needed
+ ambiguousByCaseChildClass = ambiguousByCaseBuilder.build();
+ ambiguousByCaseChildWarnings = ambiguousByCaseChildClass.isEmpty() ? ImmutableSet.of()
+ : ConcurrentHashMap.newKeySet();
final Map<Class<?>, DataContainerCodecPrototype<?>> bySubstitutionBuilder = new HashMap<>();
/*
}
}
byClassBuilder.putAll(bySubstitutionBuilder);
- byYangCaseChild = ImmutableMap.copyOf(byYangCaseChildBuilder);
byClass = ImmutableMap.copyOf(byClassBuilder);
- byCaseChildClass = ImmutableMap.copyOf(byCaseChildClassBuilder);
}
@SuppressWarnings("unchecked")
@Override
public <C extends DataObject> DataContainerCodecContext<C, ?> streamChild(final Class<C> childClass) {
final DataContainerCodecPrototype<?> child = byClass.get(childClass);
- return (DataContainerCodecContext<C, ?>) childNonNull(child, childClass, "Supplied class %s is not valid case",
- childClass).get();
+ return (DataContainerCodecContext<C, ?>) childNonNull(child, childClass,
+ "Supplied class %s is not valid case in %s", childClass, bindingArg()).get();
}
@SuppressWarnings("unchecked")
}
Iterable<Class<?>> getCaseChildrenClasses() {
- return byCaseChildClass.keySet();
+ return Iterables.concat(byCaseChildClass.keySet(), ambiguousByCaseChildClass.keySet());
}
protected DataContainerCodecPrototype<CaseSchemaNode> loadCase(final Class<?> childClass) {
return (D) caze.get().deserialize(data);
}
- DataContainerCodecContext<?, ?> getCazeByChildClass(final @Nonnull Class<? extends DataObject> type) {
- final DataContainerCodecPrototype<?> protoCtx =
- childNonNull(byCaseChildClass.get(type), type, "Class %s is not child of any cases for %s", type,
- bindingArg());
- return protoCtx.get();
- }
-
@Override
protected Object deserializeObject(final NormalizedNode<?, ?> normalizedNode) {
return deserialize(normalizedNode);
// FIXME: check for null, since binding container is null.
return getDomPathArgument();
}
+
+ DataContainerCodecContext<?, ?> getCaseByChildClass(final @Nonnull Class<? extends DataObject> type) {
+ DataContainerCodecPrototype<?> result = byCaseChildClass.get(type);
+ if (result == null) {
+ // We have not found an unambiguous result, try ambiguous ones
+ final List<DataContainerCodecPrototype<?>> inexact = ambiguousByCaseChildClass.get(type);
+ if (!inexact.isEmpty()) {
+ result = inexact.get(0);
+ // Issue a warning, but only once so as not to flood the logs
+ if (ambiguousByCaseChildWarnings.add(type)) {
+ LOG.warn("Ambiguous reference {} to child of {} resolved to {}, the first case in {} This mapping "
+ + "is not guaranteed to be stable and is subject to variations based on runtime "
+ + "circumstances. Please see the stack trace for hints about the source of ambiguity.",
+ type, bindingArg(), result.getBindingClass(),
+ Lists.transform(inexact, DataContainerCodecPrototype::getBindingClass), new Throwable());
+ }
+ }
+ }
+
+ return childNonNull(result, type, "Class %s is not child of any cases for %s", type, bindingArg()).get();
+ }
}
private final T schema;
private final QNameModule namespace;
private final CodecContextFactory factory;
- private final Class<?> bindingClass;
private final Item<?> bindingArg;
private final PathArgument yangArg;
private final ChildAddressabilitySummary childAddressabilitySummary;
@SuppressWarnings("unchecked")
private DataContainerCodecPrototype(final Class<?> cls, final PathArgument arg, final T nodeSchema,
final CodecContextFactory factory) {
- this.bindingClass = cls;
+ this(Item.of((Class<? extends DataObject>) cls), arg, nodeSchema, factory);
+ }
+
+ private DataContainerCodecPrototype(final Item<?> bindingArg, final PathArgument arg, final T nodeSchema,
+ final CodecContextFactory factory) {
+ this.bindingArg = bindingArg;
this.yangArg = arg;
this.schema = nodeSchema;
this.factory = factory;
- this.bindingArg = Item.of((Class<? extends DataObject>) bindingClass);
if (arg instanceof AugmentationIdentifier) {
this.namespace = Iterables.getFirst(((AugmentationIdentifier) arg).getPossibleChildNames(), null)
return new DataContainerCodecPrototype(cls, NodeIdentifier.create(schema.getQName()), schema, factory);
}
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ static <T extends DataSchemaNode> DataContainerCodecPrototype<T> from(final Item<?> bindingArg, final T schema,
+ final CodecContextFactory factory) {
+ return new DataContainerCodecPrototype(bindingArg, NodeIdentifier.create(schema.getQName()), schema, factory);
+ }
+
@SuppressWarnings({ "rawtypes", "unchecked" })
static DataContainerCodecPrototype<?> from(final Class<?> augClass, final AugmentationIdentifier arg,
final AugmentationSchemaNode schema, final CodecContextFactory factory) {
}
protected Class<?> getBindingClass() {
- return bindingClass;
+ return bindingArg.getType();
}
protected Item<?> getBindingArg() {
} else if (schema instanceof CaseSchemaNode) {
return new CaseNodeCodecContext(this);
}
- throw new IllegalArgumentException("Unsupported type " + bindingClass + " " + schema);
+ throw new IllegalArgumentException("Unsupported type " + getBindingClass() + " " + schema);
}
boolean isChoice() {
import org.opendaylight.yangtools.yang.binding.AugmentationHolder;
import org.opendaylight.yangtools.yang.binding.DataObject;
import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier.Item;
import org.opendaylight.yangtools.yang.binding.util.BindingReflections;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
childNonNull(ctxProto, argType, "Class %s is not valid child of %s", argType, getBindingClass()).get();
if (context instanceof ChoiceNodeCodecContext) {
final ChoiceNodeCodecContext<?> choice = (ChoiceNodeCodecContext<?>) context;
- final DataContainerCodecContext<?, ?> caze = choice.getCazeByChildClass(arg.getType());
choice.addYangPathArgument(arg, builder);
+
+ final java.util.Optional<? extends Class<? extends DataObject>> caseType = arg.getCaseType();
+ final Class<? extends DataObject> type = arg.getType();
+ final DataContainerCodecContext<?, ?> caze;
+ if (caseType.isPresent()) {
+ // Non-ambiguous addressing this should not pose any problems
+ caze = choice.streamChild(caseType.get());
+ } else {
+ caze = choice.getCaseByChildClass(type);
+ }
+
caze.addYangPathArgument(arg, builder);
return caze.bindingPathArgumentChild(arg, builder);
}
}
final DataSchemaNode nonNullChild =
childNonNull(childSchema, childClass, "Node %s does not have child named %s", getSchema(), childClass);
- return DataContainerCodecPrototype.from(childClass, nonNullChild, factory());
+ return DataContainerCodecPrototype.from(createBindingArg(childClass, nonNullChild), nonNullChild, factory());
+ }
+
+ @SuppressWarnings("unchecked")
+ Item<?> createBindingArg(final Class<?> childClass, final DataSchemaNode childSchema) {
+ return Item.of((Class<? extends DataObject>) childClass);
}
private DataContainerCodecPrototype<?> yangAugmentationChild(final AugmentationIdentifier arg) {
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.mdsal.test.binding.rev140701.Top;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.mdsal.test.binding.rev140701.two.level.list.TopLevelList;
import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.mdsal.test.binding.rev140701.two.level.list.TopLevelListKey;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal45.aug.norev.cont.cont.choice.ContAug;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal45.base.norev.Cont;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal45.base.norev.cont.ContChoice;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal45.base.norev.cont.cont.choice.ContBase;
+import org.opendaylight.yang.gen.v1.urn.test.opendaylight.mdsal45.base.norev.grp.GrpCont;
import org.opendaylight.yangtools.yang.binding.Identifier;
import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
import org.opendaylight.yangtools.yang.common.QName;
assertTrue(leafOnlyLastArg instanceof AugmentationIdentifier);
assertTrue(((AugmentationIdentifier) leafOnlyLastArg).getPossibleChildNames().contains(SIMPLE_VALUE_QNAME));
}
+
+ @Test
+ public void testChoiceCaseGroupingFromBinding() {
+ final YangInstanceIdentifier contBase = registry.toYangInstanceIdentifier(
+ InstanceIdentifier.builder(Cont.class).child(ContBase.class, GrpCont.class).build());
+ assertEquals(YangInstanceIdentifier.create(NodeIdentifier.create(Cont.QNAME),
+ NodeIdentifier.create(ContChoice.QNAME), NodeIdentifier.create(GrpCont.QNAME)), contBase);
+
+ final YangInstanceIdentifier contAug = registry.toYangInstanceIdentifier(
+ InstanceIdentifier.builder(Cont.class).child(ContAug.class, GrpCont.class).build());
+ assertEquals(YangInstanceIdentifier.create(NodeIdentifier.create(Cont.QNAME),
+ NodeIdentifier.create(ContChoice.QNAME),
+ NodeIdentifier.create(GrpCont.QNAME.withModule(ContAug.QNAME.getModule()))), contAug);
+
+ // Legacy: downcast the child to Class, losing type safety but still working. Faced with ambiguity, it will
+ // select the lexically-lower class
+ assertEquals(1, ContBase.class.getCanonicalName().compareTo(ContAug.class.getCanonicalName()));
+ final YangInstanceIdentifier contAugLegacy = registry.toYangInstanceIdentifier(
+ InstanceIdentifier.builder(Cont.class).child((Class) GrpCont.class).build());
+ assertEquals(YangInstanceIdentifier.create(NodeIdentifier.create(Cont.QNAME),
+ NodeIdentifier.create(ContChoice.QNAME),
+ NodeIdentifier.create(GrpCont.QNAME.withModule(ContAug.QNAME.getModule()))), contAugLegacy);
+
+ // FIXME: root choice handling is busted
+ // final YangInstanceIdentifier rootAugLegacy = registry.toYangInstanceIdentifier(
+ // InstanceIdentifier.create((Class) GrpCont.class));
+ }
+
+ @Test
+ public void testChoiceCaseGroupingToBinding() {
+ final InstanceIdentifier<?> contBase = registry.fromYangInstanceIdentifier(
+ YangInstanceIdentifier.create(NodeIdentifier.create(Cont.QNAME),
+ NodeIdentifier.create(ContChoice.QNAME), NodeIdentifier.create(GrpCont.QNAME)));
+ assertEquals(InstanceIdentifier.builder(Cont.class).child(ContBase.class, GrpCont.class).build(), contBase);
+
+ final InstanceIdentifier<?> contAug = registry.fromYangInstanceIdentifier(
+ YangInstanceIdentifier.create(NodeIdentifier.create(Cont.QNAME), NodeIdentifier.create(ContChoice.QNAME),
+ NodeIdentifier.create(GrpCont.QNAME.withModule(ContAug.QNAME.getModule()))));
+ assertEquals(InstanceIdentifier.builder(Cont.class).child(ContAug.class, GrpCont.class).build(), contAug);
+ }
}