Separate out notification handling
[mdsal.git] / binding / mdsal-binding-dom-codec / src / main / java / org / opendaylight / mdsal / binding / dom / codec / impl / SchemaRootCodecContext.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 import static com.google.common.base.Preconditions.checkState;
12 import static com.google.common.base.Verify.verify;
13 import static java.util.Objects.requireNonNull;
14
15 import com.google.common.base.Throwables;
16 import com.google.common.base.Verify;
17 import com.google.common.cache.CacheBuilder;
18 import com.google.common.cache.CacheLoader;
19 import com.google.common.cache.LoadingCache;
20 import com.google.common.util.concurrent.UncheckedExecutionException;
21 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
22 import java.lang.reflect.ParameterizedType;
23 import java.lang.reflect.Type;
24 import java.util.Iterator;
25 import java.util.List;
26 import java.util.Optional;
27 import org.eclipse.jdt.annotation.NonNull;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.opendaylight.mdsal.binding.spec.naming.BindingMapping;
30 import org.opendaylight.mdsal.binding.spec.reflect.BindingReflections;
31 import org.opendaylight.yangtools.util.ClassLoaderUtils;
32 import org.opendaylight.yangtools.yang.binding.Action;
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;
37 import org.opendaylight.yangtools.yang.binding.KeyedListAction;
38 import org.opendaylight.yangtools.yang.binding.Notification;
39 import org.opendaylight.yangtools.yang.binding.RpcInput;
40 import org.opendaylight.yangtools.yang.binding.RpcOutput;
41 import org.opendaylight.yangtools.yang.common.QName;
42 import org.opendaylight.yangtools.yang.common.QNameModule;
43 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
44 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
45 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
46 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
47 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
48 import org.opendaylight.yangtools.yang.model.api.ContainerLike;
49 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
50 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
51 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
52 import org.opendaylight.yangtools.yang.model.api.Module;
53 import org.opendaylight.yangtools.yang.model.api.NotificationDefinition;
54 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
55 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
56 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaTreeEffectiveStatement;
57
58 final class SchemaRootCodecContext<D extends DataObject> extends DataContainerCodecContext<D, EffectiveModelContext> {
59
60     private final LoadingCache<Class<? extends DataObject>, DataContainerCodecContext<?, ?>> childrenByClass =
61         CacheBuilder.newBuilder().build(new CacheLoader<>() {
62             @Override
63             public DataContainerCodecContext<?, ?> load(final Class<? extends DataObject> key) {
64                 return createDataTreeChildContext(key);
65             }
66         });
67
68     private final LoadingCache<Class<? extends Action<?, ?, ?>>, ActionCodecContext> actionsByClass =
69         CacheBuilder.newBuilder().build(new CacheLoader<>() {
70             @Override
71             public ActionCodecContext load(final Class<? extends Action<?, ?, ?>> key) {
72                 return createActionContext(key);
73             }
74         });
75
76     private final LoadingCache<Class<? extends DataObject>, ChoiceNodeCodecContext<?>> choicesByClass =
77         CacheBuilder.newBuilder().build(new CacheLoader<>() {
78             @Override
79             public ChoiceNodeCodecContext<?> load(final Class<? extends DataObject> key) {
80                 return createChoiceDataContext(key);
81             }
82         });
83
84     private final LoadingCache<Class<?>, NotificationCodecContext<?>> notificationsByClass = CacheBuilder.newBuilder()
85         .build(new CacheLoader<Class<?>, NotificationCodecContext<?>>() {
86             @Override
87             public NotificationCodecContext<?> load(final Class<?> key) {
88                 checkArgument(key.isInterface(), "Supplied class must be interface.");
89                 final QName qname = BindingReflections.findQName(key);
90                 final NotificationDefinition schema = getSchema().findNotification(qname).orElseThrow(
91                     () -> new IllegalArgumentException("Supplied " + key + " is not valid notification"));
92                 return new NotificationCodecContext<>(key, schema, factory());
93             }
94         });
95
96     private final LoadingCache<Class<?>, ContainerNodeCodecContext<?>> rpcDataByClass = CacheBuilder.newBuilder()
97         .build(new CacheLoader<Class<?>, ContainerNodeCodecContext<?>>() {
98             @Override
99             public ContainerNodeCodecContext<?> load(final Class<?> key) {
100                 final QName qname = BindingReflections.findQName(key);
101                 final QNameModule qnameModule = qname.getModule();
102                 final Module module = getSchema().findModule(qnameModule).orElseThrow(
103                     () -> new IllegalArgumentException("Failed to find module for " + qnameModule));
104                 final String className = BindingMapping.getClassName(qname);
105
106                 for (final RpcDefinition potential : module.getRpcs()) {
107                     final QName potentialQName = potential.getQName();
108                     /*
109                      * Check if rpc and class represents data from same module and then checks if rpc local name
110                      * produces same class name as class name appended with Input/Output based on QName associated
111                      * with binding class.
112                      *
113                      * FIXME: Rework this to have more precise logic regarding Binding Specification.
114                      */
115                     if (key.getSimpleName().equals(BindingMapping.getClassName(potentialQName) + className)) {
116                         final ContainerLike schema = getRpcDataSchema(potential, qname);
117                         checkArgument(schema != null, "Schema for %s does not define input / output.", potentialQName);
118                         return (ContainerNodeCodecContext<?>) DataContainerCodecPrototype.from(key, schema, factory())
119                             .get();
120                     }
121                 }
122
123                 throw new IllegalArgumentException("Supplied class " + key + " is not valid RPC class.");
124             }
125         });
126
127     private final LoadingCache<QName, DataContainerCodecContext<?,?>> childrenByQName =
128         CacheBuilder.newBuilder().build(new CacheLoader<>() {
129             @Override
130             public DataContainerCodecContext<?, ?> load(final QName qname) {
131                 final DataSchemaNode childSchema = getSchema().dataChildByName(qname);
132                 childNonNull(childSchema, qname, "Argument %s is not valid child of %s", qname, getSchema());
133                 if (childSchema instanceof DataNodeContainer || childSchema instanceof ChoiceSchemaNode) {
134                     @SuppressWarnings("unchecked")
135                     final Class<? extends DataObject> childCls = (Class<? extends DataObject>)
136                         factory().getRuntimeContext().getClassForSchema(childSchema);
137                     return streamChild(childCls);
138                 }
139
140                 throw new UnsupportedOperationException("Unsupported child type " + childSchema.getClass());
141             }
142         });
143
144     private final LoadingCache<Absolute, RpcInputCodec<?>> rpcDataByPath =
145         CacheBuilder.newBuilder().build(new CacheLoader<>() {
146             @Override
147             public RpcInputCodec<?> load(final Absolute key) {
148                 final ContainerLike schema = getRpcDataSchema(getSchema(), key);
149                 @SuppressWarnings("unchecked")
150                 final Class<? extends DataContainer> cls = (Class<? extends DataContainer>)
151                     factory().getRuntimeContext().getClassForSchema(schema);
152                 return getRpc(cls);
153             }
154         });
155
156     private final LoadingCache<Absolute, NotificationCodecContext<?>> notificationsByPath =
157         CacheBuilder.newBuilder().build(new CacheLoader<>() {
158             @Override
159             public NotificationCodecContext<?> load(final Absolute key) {
160                 final SchemaTreeEffectiveStatement<?> stmt = getSchema().findSchemaTreeNode(key)
161                     .orElseThrow(() -> new IllegalArgumentException("Cannot find statement at " + key));
162                 checkArgument(stmt instanceof NotificationDefinition, "Statement %s is not a notification", stmt);
163
164                 @SuppressWarnings("unchecked")
165                 final Class<? extends Notification<?>> clz = (Class<? extends Notification<?>>)
166                     factory().getRuntimeContext().getClassForSchema((NotificationDefinition) stmt);
167                 return getNotification(clz);
168             }
169         });
170
171     private SchemaRootCodecContext(final DataContainerCodecPrototype<EffectiveModelContext> dataPrototype) {
172         super(dataPrototype);
173     }
174
175     /**
176      * Creates RootNode from supplied CodecContextFactory.
177      *
178      * @param factory
179      *            CodecContextFactory
180      * @return A new root node
181      */
182     static SchemaRootCodecContext<?> create(final CodecContextFactory factory) {
183         return new SchemaRootCodecContext<>(DataContainerCodecPrototype.rootPrototype(factory));
184     }
185
186     @SuppressWarnings("unchecked")
187     @Override
188     public <C extends DataObject> DataContainerCodecContext<C, ?> streamChild(final Class<C> childClass) {
189         return (DataContainerCodecContext<C, ?>) getOrRethrow(childrenByClass, childClass);
190     }
191
192     @Override
193     public <C extends DataObject> Optional<DataContainerCodecContext<C, ?>> possibleStreamChild(
194             final Class<C> childClass) {
195         throw new UnsupportedOperationException("Not supported");
196     }
197
198     @Override
199     public DataContainerCodecContext<?,?> yangPathArgumentChild(final PathArgument arg) {
200         return getOrRethrow(childrenByQName, arg.getNodeType());
201     }
202
203     @Override
204     public D deserialize(final NormalizedNode normalizedNode) {
205         throw new UnsupportedOperationException("Could not create Binding data representation for root");
206     }
207
208     ActionCodecContext getAction(final Class<? extends Action<?, ?, ?>> action) {
209         return getOrRethrow(actionsByClass, action);
210     }
211
212     NotificationCodecContext<?> getNotification(final Class<? extends Notification<?>> notification) {
213         return getOrRethrow(notificationsByClass, notification);
214     }
215
216     NotificationCodecContext<?> getNotification(final Absolute notification) {
217         return getOrRethrow(notificationsByPath, notification);
218     }
219
220     ContainerNodeCodecContext<?> getRpc(final Class<? extends DataContainer> rpcInputOrOutput) {
221         return getOrRethrow(rpcDataByClass, rpcInputOrOutput);
222     }
223
224     RpcInputCodec<?> getRpc(final Absolute containerPath) {
225         return getOrRethrow(rpcDataByPath, containerPath);
226     }
227
228     DataContainerCodecContext<?, ?> createDataTreeChildContext(final Class<? extends DataObject> key) {
229         final QName qname = BindingReflections.findQName(key);
230         final DataSchemaNode childSchema = childNonNull(getSchema().dataChildByName(qname), key,
231             "%s is not top-level item.", key);
232         return DataContainerCodecPrototype.from(key, childSchema, factory()).get();
233     }
234
235     ActionCodecContext createActionContext(final Class<? extends Action<?, ?, ?>> action) {
236         if (KeyedListAction.class.isAssignableFrom(action)) {
237             return prepareActionContext(2, 3, 4, action, KeyedListAction.class);
238         } else if (Action.class.isAssignableFrom(action)) {
239             return prepareActionContext(1, 2, 3, action, Action.class);
240         }
241         throw new IllegalArgumentException("The specific action type does not exist for action " + action.getName());
242     }
243
244     private ActionCodecContext prepareActionContext(final int inputOffset, final int outputOffset,
245             final int expectedArgsLength, final Class<? extends Action<?, ?, ?>> action, final Class<?> actionType) {
246         final Optional<ParameterizedType> optParamType = ClassLoaderUtils.findParameterizedType(action, actionType);
247         checkState(optParamType.isPresent(), "%s does not specialize %s", action, actionType);
248
249         final ParameterizedType paramType = optParamType.get();
250         final Type[] args = paramType.getActualTypeArguments();
251         checkArgument(args.length == expectedArgsLength, "Unexpected (%s) Action generatic arguments", args.length);
252         final ActionDefinition schema = factory().getRuntimeContext().getActionDefinition(action);
253         return new ActionCodecContext(
254                 DataContainerCodecPrototype.from(asClass(args[inputOffset], RpcInput.class), schema.getInput(),
255                         factory()).get(),
256                 DataContainerCodecPrototype.from(asClass(args[outputOffset], RpcOutput.class), schema.getOutput(),
257                         factory()).get());
258     }
259
260     private static <T extends DataObject> Class<? extends T> asClass(final Type type, final Class<T> target) {
261         verify(type instanceof Class, "Type %s is not a class", type);
262         return ((Class<?>) type).asSubclass(target);
263     }
264
265     /**
266      * Returns RPC input or output schema based on supplied QName.
267      *
268      * @param rpc RPC Definition
269      * @param qname input or output QName with namespace same as RPC
270      * @return input or output schema. Returns null if RPC does not have input/output specified.
271      */
272     private static @Nullable ContainerLike getRpcDataSchema(final @NonNull RpcDefinition rpc,
273             final @NonNull QName qname) {
274         requireNonNull(rpc, "Rpc Schema must not be null");
275         switch (requireNonNull(qname, "QName must not be null").getLocalName()) {
276             case "input":
277                 return rpc.getInput();
278             case "output":
279                 return rpc.getOutput();
280             default:
281                 throw new IllegalArgumentException("Supplied qname " + qname
282                         + " does not represent rpc input or output.");
283         }
284     }
285
286     /**
287      * Returns RPC Input or Output Data container from RPC definition.
288      *
289      * @param schema SchemaContext in which lookup should be performed.
290      * @param path Schema path of RPC input/output data container
291      * @return Notification schema or null, if notification is not present in schema context.
292      */
293     @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD",
294         justification = "https://github.com/spotbugs/spotbugs/issues/811")
295     private static @Nullable ContainerLike getRpcDataSchema(final @NonNull EffectiveModelContext schema,
296             final @NonNull Absolute path) {
297         requireNonNull(schema, "Schema context must not be null.");
298         requireNonNull(path, "Schema path must not be null.");
299         final Iterator<QName> it = path.getNodeIdentifiers().iterator();
300         checkArgument(it.hasNext(), "Rpc must have QName.");
301         final QName rpcName = it.next();
302         checkArgument(it.hasNext(), "input or output must be part of path.");
303         final QName inOrOut = it.next();
304         for (final RpcDefinition potential : schema.getOperations()) {
305             if (rpcName.equals(potential.getQName())) {
306                 return getRpcDataSchema(potential, inOrOut);
307             }
308         }
309         return null;
310     }
311
312     ChoiceNodeCodecContext<?> createChoiceDataContext(final Class<? extends DataObject> caseType) {
313         final Class<?> choiceClass = findCaseChoice(caseType);
314         checkArgument(choiceClass != null, "Class %s is not a valid case representation", caseType);
315         final DataSchemaNode schema = factory().getRuntimeContext().getSchemaDefinition(choiceClass);
316         checkArgument(schema instanceof ChoiceSchemaNode, "Class %s does not refer to a choice", caseType);
317
318         final DataContainerCodecContext<?, ChoiceSchemaNode> choice = DataContainerCodecPrototype.from(choiceClass,
319             (ChoiceSchemaNode)schema, factory()).get();
320         Verify.verify(choice instanceof ChoiceNodeCodecContext);
321         return (ChoiceNodeCodecContext<?>) choice;
322     }
323
324     @Override
325     protected Object deserializeObject(final NormalizedNode normalizedNode) {
326         throw new UnsupportedOperationException("Unable to deserialize root");
327     }
328
329     @Override
330     public InstanceIdentifier.PathArgument deserializePathArgument(final YangInstanceIdentifier.PathArgument arg) {
331         checkArgument(arg == null);
332         return null;
333     }
334
335     @Override
336     public YangInstanceIdentifier.PathArgument serializePathArgument(final InstanceIdentifier.PathArgument arg) {
337         checkArgument(arg == null);
338         return null;
339     }
340
341     @Override
342     public DataContainerCodecContext<?, ?> bindingPathArgumentChild(final InstanceIdentifier.PathArgument arg,
343             final List<PathArgument> builder) {
344         final Optional<? extends Class<? extends DataObject>> caseType = arg.getCaseType();
345         if (caseType.isPresent()) {
346             final @NonNull Class<? extends DataObject> type = caseType.orElseThrow();
347             final ChoiceNodeCodecContext<?> choice = choicesByClass.getUnchecked(type);
348             choice.addYangPathArgument(arg, builder);
349             final DataContainerCodecContext<?, ?> caze = choice.streamChild(type);
350             caze.addYangPathArgument(arg, builder);
351             return caze.bindingPathArgumentChild(arg, builder);
352         }
353
354         return super.bindingPathArgumentChild(arg, builder);
355     }
356
357     private static Class<?> findCaseChoice(final Class<? extends DataObject> caseClass) {
358         for (Type type : caseClass.getGenericInterfaces()) {
359             if (type instanceof Class) {
360                 final Class<?> typeClass = (Class<?>) type;
361                 if (ChoiceIn.class.isAssignableFrom(typeClass)) {
362                     return typeClass.asSubclass(ChoiceIn.class);
363                 }
364             }
365         }
366
367         return null;
368     }
369
370     private static <K,V> V getOrRethrow(final LoadingCache<K, V> cache, final K key) {
371         try {
372             return cache.getUnchecked(key);
373         } catch (final UncheckedExecutionException e) {
374             final Throwable cause = e.getCause();
375             if (cause != null) {
376                 Throwables.throwIfUnchecked(cause);
377             }
378             throw e;
379         }
380     }
381 }