8454de4f25a283c49e07ded78f501b23ad5e5429
[mdsal.git] / binding / mdsal-binding-dom-codec / src / main / java / org / opendaylight / mdsal / binding / dom / codec / impl / ChoiceCodecContext.java
1 /*
2  * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.mdsal.binding.dom.codec.impl;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11
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;
25 import java.util.Set;
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.InstanceIdentifier.PathArgument;
37 import org.opendaylight.yangtools.yang.binding.contract.Naming;
38 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
39 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
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;
46
47 /**
48  * This is a bit tricky. DataObject addressing does not take into account choice/case statements, and hence given:
49  *
50  * <pre>
51  *   <code>
52  *     container foo {
53  *       choice bar {
54  *         leaf baz;
55  *       }
56  *     }
57  *   </code>
58  * </pre>
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.
62  *
63  * <p>
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.
69  *
70  * <p>
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.
74  *
75  * <p>
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.
78  *
79  * <p>
80  * This leads to three classes of addressing, in order descending performance requirements.
81  * <ul>
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>
85  * </ul>
86  *
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.
89  *
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}.
93  */
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);
98
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;
104
105     ChoiceCodecContext(final Class<T> javaClass, final ChoiceRuntimeType runtimeType,
106             final CodecContextFactory contextFactory) {
107         this(new ChoiceCodecPrototype<>(contextFactory, runtimeType, javaClass));
108     }
109
110     ChoiceCodecContext(final ChoiceCodecPrototype<T> prototype) {
111         super(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();
116
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);
125
126             localCases.add(caseType.getIdentifier());
127             byClassBuilder.put(caseClass, caseProto);
128
129             // Updates collection of case children
130             for (var cazeChild : getChildrenClasses(caseClass)) {
131                 childToCase.put(cazeChild, caseProto);
132             }
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);
137                 }
138             }
139         }
140         byYangCaseChild = ImmutableMap.copyOf(byYangCaseChildBuilder);
141
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);
152             } else {
153                 unambiguousByCaseBuilder.put(entry.getKey(), cases.iterator().next());
154             }
155         }
156         byCaseChildClass = unambiguousByCaseBuilder.build();
157
158         // Setup ambiguous tracking, if needed
159         ambiguousByCaseChildClass = ambiguousByCaseBuilder.build();
160         ambiguousByCaseChildWarnings = ambiguousByCaseChildClass.isEmpty() ? ImmutableSet.of()
161                 : ConcurrentHashMap.newKeySet();
162
163         /*
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.
167          *
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.
170          */
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);
180
181                 search: for (var real : byClassBuilder.entrySet()) {
182                     if (isSubstitutionFor(substitution, real.getKey())) {
183                         bySubstitutionBuilder.put(substitution, real.getValue());
184                         break search;
185                     }
186                 }
187             }
188         }
189
190         byClassBuilder.putAll(bySubstitutionBuilder);
191         byClass = ImmutableMap.copyOf(byClassBuilder);
192     }
193
194     private static Class<?> loadCase(final BindingRuntimeContext context, final CaseRuntimeType caseType) {
195         final var className = caseType.getIdentifier();
196         try {
197             return context.loadClass(className);
198         } catch (ClassNotFoundException e) {
199             throw new LinkageError("Failed to load class for " + className, e);
200         }
201     }
202
203     @Override
204     public WithStatus getSchema() {
205         // FIXME: Bad cast, we should be returning an EffectiveStatement perhaps?
206         return (WithStatus) prototype().runtimeType().statement();
207     }
208
209     @Override
210     CommonDataObjectCodecPrototype<?> streamChildPrototype(final Class<?> childClass) {
211         return byClass.get(childClass);
212     }
213
214     Iterable<Class<?>> getCaseChildrenClasses() {
215         return Iterables.concat(byCaseChildClass.keySet(), ambiguousByCaseChildClass.keySet());
216     }
217
218     @Override
219     public CodecContext yangPathArgumentChild(final YangInstanceIdentifier.PathArgument arg) {
220         return ((CaseCodecContext<?>) super.yangPathArgumentChild(arg)).yangPathArgumentChild(arg);
221     }
222
223     @Override
224     CodecContextSupplier yangChildSupplier(final NodeIdentifier arg) {
225         return byYangCaseChild.get(arg);
226     }
227
228     @Override
229     protected T deserializeObject(final NormalizedNode normalizedNode) {
230         final var casted = checkDataArgument(ChoiceNode.class, normalizedNode);
231         final var it = casted.body().iterator();
232         if (!it.hasNext()) {
233             // FIXME: can this reasonably happen? Empty choice nodes do not have semantics, or do they?
234             return null;
235         }
236
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",
239             childName, this);
240         return (T) caze.getCodecContext().deserializeObject(casted);
241     }
242
243     @Override
244     public CommonDataObjectCodecContext<?, ?> bindingPathArgumentChild(final PathArgument arg,
245             final List<YangInstanceIdentifier.PathArgument> builder) {
246         final var caseType = arg.getCaseType();
247         final var type = arg.getType();
248         final DataContainerCodecContext<?, ?, ?> caze;
249         if (caseType.isPresent()) {
250             // Non-ambiguous addressing this should not pose any problems
251             caze = getStreamChild(caseType.orElseThrow());
252         } else {
253             caze = getCaseByChildClass(type);
254         }
255
256         caze.addYangPathArgument(arg, builder);
257         return caze.bindingPathArgumentChild(arg, builder);
258     }
259
260     private DataContainerCodecContext<?, ?, ?> getCaseByChildClass(final @NonNull Class<? extends DataObject> type) {
261         var result = byCaseChildClass.get(type);
262         if (result == null) {
263             // We have not found an unambiguous result, try ambiguous ones
264             final var inexact = ambiguousByCaseChildClass.get(type);
265             if (!inexact.isEmpty()) {
266                 result = inexact.get(0);
267                 // Issue a warning, but only once so as not to flood the logs
268                 if (ambiguousByCaseChildWarnings.add(type)) {
269                     LOG.warn("""
270                         Ambiguous reference {} to child of {} resolved to {}, the first case in {} This mapping is \
271                         not guaranteed to be stable and is subject to variations based on runtime circumstances. \
272                         Please see the stack trace for hints about the source of ambiguity.""",
273                         type, getBindingClass(), result.javaClass(),
274                         Lists.transform(inexact, CommonDataObjectCodecPrototype::javaClass), new Throwable());
275                 }
276             }
277         }
278
279         return childNonNull(result, type, "Class %s is not child of any cases for %s", type, getBindingClass())
280             .getCodecContext();
281     }
282
283     /**
284      * Scans supplied class and returns an iterable of all data children classes.
285      *
286      * @param type
287      *            YANG Modeled Entity derived from DataContainer
288      * @return Iterable of all data children, which have YANG modeled entity
289      */
290     // FIXME: MDSAL-780: replace use of this method
291     @SuppressWarnings("unchecked")
292     private static Iterable<Class<? extends DataObject>> getChildrenClasses(final Class<? extends DataContainer> type) {
293         checkArgument(type != null, "Target type must not be null");
294         checkArgument(DataContainer.class.isAssignableFrom(type), "Supplied type must be derived from DataContainer");
295         final var ret = new LinkedList<Class<? extends DataObject>>();
296         for (var method : type.getMethods()) {
297             DataContainerAnalysis.getYangModeledReturnType(method, Naming.GETTER_PREFIX)
298                 .ifPresent(entity -> ret.add((Class<? extends DataObject>) entity));
299         }
300         return ret;
301     }
302 }