2 * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.mdsal.binding.dom.codec.impl;
10 import static com.google.common.base.Preconditions.checkArgument;
12 import com.google.common.collect.ImmutableListMultimap;
13 import com.google.common.collect.ImmutableMap;
14 import com.google.common.collect.ImmutableSet;
15 import com.google.common.collect.Iterables;
16 import com.google.common.collect.Lists;
17 import com.google.common.collect.MultimapBuilder.SetMultimapBuilder;
18 import com.google.common.collect.Multimaps;
19 import java.util.ArrayList;
20 import java.util.Comparator;
21 import java.util.HashMap;
22 import java.util.HashSet;
23 import java.util.LinkedList;
24 import java.util.List;
26 import java.util.concurrent.ConcurrentHashMap;
27 import org.eclipse.jdt.annotation.NonNull;
28 import org.opendaylight.mdsal.binding.dom.codec.api.BindingChoiceCodecTreeNode;
29 import org.opendaylight.mdsal.binding.model.api.JavaTypeName;
30 import org.opendaylight.mdsal.binding.runtime.api.BindingRuntimeContext;
31 import org.opendaylight.mdsal.binding.runtime.api.CaseRuntimeType;
32 import org.opendaylight.mdsal.binding.runtime.api.ChoiceRuntimeType;
33 import org.opendaylight.yangtools.yang.binding.ChoiceIn;
34 import org.opendaylight.yangtools.yang.binding.DataContainer;
35 import org.opendaylight.yangtools.yang.binding.DataObject;
36 import org.opendaylight.yangtools.yang.binding.DataObjectStep;
37 import org.opendaylight.yangtools.yang.binding.contract.Naming;
38 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
39 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
40 import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode;
41 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
42 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.DocumentedNode.WithStatus;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
48 * This is a bit tricky. DataObject addressing does not take into account choice/case statements, and hence given:
59 * we will see {@code Baz extends ChildOf<Foo>}, which is how the users would address it in InstanceIdentifier terms.
60 * The implicit assumption being made is that {@code Baz} identifies a particular instantiation and hence provides
61 * unambiguous reference to an effective schema statement.
64 * Unfortunately this does not quite work with groupings, as their generation has changed: we do not have interfaces
65 * that would capture grouping instantiations, hence we do not have a proper addressing point and users need to specify
66 * the interfaces generated in the grouping's definition. These can be very much ambiguous, as a {@code grouping} can be
67 * used in multiple modules independently within an {@code augment} targeting {@code choice}, as each instantiation is
68 * guaranteed to have a unique namespace -- but we do not have the appropriate instantiations of those nodes.
71 * To address this issue we have a two-class lookup mechanism, which relies on the interface generated for the
72 * {@code case} statement to act as the namespace anchor bridging the nodes inside the grouping to the namespace in
73 * which they are instantiated.
76 * Furthermore downstream code relies on historical mechanics, which would guess what the instantiation is, silently
77 * assuming the ambiguity is theoretical and does not occur in practice.
80 * This leads to three classes of addressing, in order descending performance requirements.
82 * <li>Direct DataObject, where we name an exact child</li>
83 * <li>Case DataObject + Grouping DataObject</li>
84 * <li>Grouping DataObject, which is ambiguous</li>
87 * {@link #byCaseChildClass} supports direct DataObject mapping and contains only unambiguous children, while
88 * {@link #byClass} supports indirect mapping and contains {@code case} sub-statements.
90 * {@link #ambiguousByCaseChildClass} contains ambiguous mappings, for which we end up issuing warnings. We track each
91 * ambiguous reference and issue warn once when they are encountered -- tracking warning information in
92 * {@link #ambiguousByCaseChildWarnings}.
94 final class ChoiceCodecContext<T extends ChoiceIn<?>>
95 extends DataContainerCodecContext<T, ChoiceRuntimeType, ChoiceCodecPrototype<T>>
96 implements BindingChoiceCodecTreeNode<T> {
97 private static final Logger LOG = LoggerFactory.getLogger(ChoiceCodecContext.class);
99 private final ImmutableListMultimap<Class<?>, CommonDataObjectCodecPrototype<?>> ambiguousByCaseChildClass;
100 private final ImmutableMap<Class<?>, CommonDataObjectCodecPrototype<?>> byCaseChildClass;
101 private final ImmutableMap<NodeIdentifier, CaseCodecPrototype> byYangCaseChild;
102 private final ImmutableMap<Class<?>, CommonDataObjectCodecPrototype<?>> byClass;
103 private final Set<Class<?>> ambiguousByCaseChildWarnings;
105 ChoiceCodecContext(final Class<T> javaClass, final ChoiceRuntimeType runtimeType,
106 final CodecContextFactory contextFactory) {
107 this(new ChoiceCodecPrototype<>(contextFactory, runtimeType, javaClass));
110 ChoiceCodecContext(final ChoiceCodecPrototype<T> prototype) {
112 final var byYangCaseChildBuilder = new HashMap<NodeIdentifier, CaseCodecPrototype>();
113 final var byClassBuilder = new HashMap<Class<?>, CommonDataObjectCodecPrototype<?>>();
114 final var childToCase = SetMultimapBuilder.hashKeys().hashSetValues()
115 .<Class<?>, CommonDataObjectCodecPrototype<?>>build();
117 // Load case statements valid in this choice and keep track of their names
118 final var choiceType = prototype.runtimeType();
119 final var factory = prototype.contextFactory();
120 final var localCases = new HashSet<JavaTypeName>();
121 for (var caseType : choiceType.validCaseChildren()) {
122 @SuppressWarnings("unchecked")
123 final var caseClass = (Class<? extends DataObject>) loadCase(factory.getRuntimeContext(), caseType);
124 final var caseProto = new CaseCodecPrototype(caseClass, caseType, factory);
126 localCases.add(caseType.getIdentifier());
127 byClassBuilder.put(caseClass, caseProto);
129 // Updates collection of case children
130 for (var cazeChild : getChildrenClasses(caseClass)) {
131 childToCase.put(cazeChild, caseProto);
133 // Updates collection of YANG instance identifier to case
134 for (var stmt : caseType.statement().effectiveSubstatements()) {
135 if (stmt instanceof DataSchemaNode cazeChild) {
136 byYangCaseChildBuilder.put(NodeIdentifier.create(cazeChild.getQName()), caseProto);
140 byYangCaseChild = ImmutableMap.copyOf(byYangCaseChildBuilder);
142 // Move unambiguous child->case mappings to byCaseChildClass, removing them from childToCase
143 final var ambiguousByCaseBuilder = ImmutableListMultimap.<Class<?>, CommonDataObjectCodecPrototype<?>>builder();
144 final var unambiguousByCaseBuilder = ImmutableMap.<Class<?>, CommonDataObjectCodecPrototype<?>>builder();
145 for (var entry : Multimaps.asMap(childToCase).entrySet()) {
146 final var cases = entry.getValue();
147 if (cases.size() != 1) {
148 // Sort all possibilities by their FQCN to retain semi-predictable results
149 final var list = new ArrayList<>(entry.getValue());
150 list.sort(Comparator.comparing(proto -> proto.javaClass().getCanonicalName()));
151 ambiguousByCaseBuilder.putAll(entry.getKey(), list);
153 unambiguousByCaseBuilder.put(entry.getKey(), cases.iterator().next());
156 byCaseChildClass = unambiguousByCaseBuilder.build();
158 // Setup ambiguous tracking, if needed
159 ambiguousByCaseChildClass = ambiguousByCaseBuilder.build();
160 ambiguousByCaseChildWarnings = ambiguousByCaseChildClass.isEmpty() ? ImmutableSet.of()
161 : ConcurrentHashMap.newKeySet();
164 * Choice/Case mapping across groupings is compile-time unsafe and we therefore need to also track any
165 * CaseRuntimeTypes added to the choice in other contexts. This is necessary to discover when a case represents
166 * equivalent data in a different instantiation context.
168 * This is required due property of binding specification, that if choice is in grouping schema path location is
169 * lost, and users may use incorrect case class using copy builders.
171 final var bySubstitutionBuilder = new HashMap<Class<?>, CommonDataObjectCodecPrototype<?>>();
172 final var context = factory.getRuntimeContext();
173 for (var caseType : context.getTypes().allCaseChildren(choiceType)) {
174 final var caseName = caseType.getIdentifier();
175 if (!localCases.contains(caseName)) {
176 // FIXME: do not rely on class loading here, the check we are performing should be possible on
177 // GeneratedType only -- or it can be provided by BindingRuntimeTypes -- i.e. rather than
178 // 'allCaseChildren()' it would calculate additional mappings we can use off-the-bat.
179 final var substitution = loadCase(context, caseType);
181 search: for (var real : byClassBuilder.entrySet()) {
182 if (isSubstitutionFor(substitution, real.getKey())) {
183 bySubstitutionBuilder.put(substitution, real.getValue());
190 byClassBuilder.putAll(bySubstitutionBuilder);
191 byClass = ImmutableMap.copyOf(byClassBuilder);
194 private static Class<?> loadCase(final BindingRuntimeContext context, final CaseRuntimeType caseType) {
195 final var className = caseType.getIdentifier();
197 return context.loadClass(className);
198 } catch (ClassNotFoundException e) {
199 throw new LinkageError("Failed to load class for " + className, e);
204 public WithStatus getSchema() {
205 // FIXME: Bad cast, we should be returning an EffectiveStatement perhaps?
206 return (WithStatus) prototype().runtimeType().statement();
210 CommonDataObjectCodecPrototype<?> streamChildPrototype(final Class<?> childClass) {
211 return byClass.get(childClass);
214 Iterable<Class<?>> getCaseChildrenClasses() {
215 return Iterables.concat(byCaseChildClass.keySet(), ambiguousByCaseChildClass.keySet());
219 public CodecContext yangPathArgumentChild(final PathArgument arg) {
220 return ((CaseCodecContext<?>) super.yangPathArgumentChild(arg)).yangPathArgumentChild(arg);
224 CodecContextSupplier yangChildSupplier(final NodeIdentifier arg) {
225 return byYangCaseChild.get(arg);
229 T deserializeObject(final NormalizedNode normalizedNode) {
230 final var casted = checkDataArgument(ChoiceNode.class, normalizedNode);
231 final var it = casted.body().iterator();
233 // FIXME: can this reasonably happen? Empty choice nodes do not have semantics, or do they?
237 final var childName = it.next().name();
238 final var caze = childNonNull(byYangCaseChild.get(childName), childName, "%s is not a valid case child of %s",
240 return (T) caze.getCodecContext().deserializeObject(casted);
244 public CommonDataObjectCodecContext<?, ?> bindingPathArgumentChild(final DataObjectStep<?> step,
245 final List<PathArgument> builder) {
246 final var caseType = step.caseType();
247 // Prefer non-ambiguous addressing, which should not pose any problems. Otherwise fall back to checking for
249 final var caze = caseType != null ? getStreamChild(caseType) : getCaseByChildClass(step.type());
250 caze.addYangPathArgument(step, builder);
251 return caze.bindingPathArgumentChild(step, builder);
254 private DataContainerCodecContext<?, ?, ?> getCaseByChildClass(final @NonNull Class<? extends DataObject> type) {
255 var result = byCaseChildClass.get(type);
256 if (result == null) {
257 // We have not found an unambiguous result, try ambiguous ones
258 final var inexact = ambiguousByCaseChildClass.get(type);
259 if (!inexact.isEmpty()) {
260 result = inexact.get(0);
261 // Issue a warning, but only once so as not to flood the logs
262 if (ambiguousByCaseChildWarnings.add(type)) {
264 Ambiguous reference {} to child of {} resolved to {}, the first case in {} This mapping is \
265 not guaranteed to be stable and is subject to variations based on runtime circumstances. \
266 Please see the stack trace for hints about the source of ambiguity.""",
267 type, getBindingClass(), result.javaClass(),
268 Lists.transform(inexact, CommonDataObjectCodecPrototype::javaClass), new Throwable());
273 return childNonNull(result, type, "Class %s is not child of any cases for %s", type, getBindingClass())
278 * Scans supplied class and returns an iterable of all data children classes.
281 * YANG Modeled Entity derived from DataContainer
282 * @return Iterable of all data children, which have YANG modeled entity
284 // FIXME: MDSAL-780: replace use of this method
285 @SuppressWarnings("unchecked")
286 private static Iterable<Class<? extends DataObject>> getChildrenClasses(final Class<? extends DataContainer> type) {
287 checkArgument(type != null, "Target type must not be null");
288 checkArgument(DataContainer.class.isAssignableFrom(type), "Supplied type must be derived from DataContainer");
289 final var ret = new LinkedList<Class<? extends DataObject>>();
290 for (var method : type.getMethods()) {
291 DataContainerAnalysis.getYangModeledReturnType(method, Naming.GETTER_PREFIX)
292 .ifPresent(entity -> ret.add((Class<? extends DataObject>) entity));