Switch to Objects.requireNonNull
[mdsal.git] / binding2 / mdsal-binding2-dom-codec / src / main / java / org / opendaylight / mdsal / binding / javav2 / dom / codec / impl / context / SchemaRootCodecContext.java
1 /*
2  * Copyright (c) 2017 Pantheon Technologies s.r.o. 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.javav2.dom.codec.impl.context;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.annotations.Beta;
13 import com.google.common.base.Preconditions;
14 import com.google.common.base.Throwables;
15 import com.google.common.cache.CacheBuilder;
16 import com.google.common.cache.CacheLoader;
17 import com.google.common.cache.LoadingCache;
18 import com.google.common.util.concurrent.UncheckedExecutionException;
19 import java.util.Iterator;
20 import java.util.Optional;
21 import java.util.Set;
22 import javax.annotation.Nonnull;
23 import javax.annotation.Nullable;
24 import org.opendaylight.mdsal.binding.javav2.dom.codec.impl.OperationInputCodec;
25 import org.opendaylight.mdsal.binding.javav2.dom.codec.impl.UnmappedOperationInputCodec;
26 import org.opendaylight.mdsal.binding.javav2.dom.codec.impl.context.base.DataContainerCodecContext;
27 import org.opendaylight.mdsal.binding.javav2.dom.codec.impl.context.base.DataContainerCodecPrototype;
28 import org.opendaylight.mdsal.binding.javav2.generator.util.JavaIdentifier;
29 import org.opendaylight.mdsal.binding.javav2.generator.util.JavaIdentifierNormalizer;
30 import org.opendaylight.mdsal.binding.javav2.runtime.reflection.BindingReflections;
31 import org.opendaylight.mdsal.binding.javav2.spec.base.Instantiable;
32 import org.opendaylight.mdsal.binding.javav2.spec.base.Notification;
33 import org.opendaylight.mdsal.binding.javav2.spec.base.TreeArgument;
34 import org.opendaylight.mdsal.binding.javav2.spec.base.TreeNode;
35 import org.opendaylight.yangtools.yang.common.QName;
36 import org.opendaylight.yangtools.yang.common.QNameModule;
37 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
38 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
39 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
40 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
41 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
43 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.NotificationDefinition;
45 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
46 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
47 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
48 import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement;
49 import org.opendaylight.yangtools.yang.model.api.meta.StatementSource;
50 import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
51
52 /**
53  * Creates RootNode from supplied CodecContextFactory and make operations with.
54  *
55  * @param <D>
56  *            - tree node type
57  */
58 @Beta
59 public final class SchemaRootCodecContext<D extends TreeNode> extends DataContainerCodecContext<D, SchemaContext> {
60
61     private final LoadingCache<Class<?>, DataContainerCodecContext<?, ?>> childrenByClass =
62             CacheBuilder.newBuilder().build(new CacheLoader<Class<?>, DataContainerCodecContext<?, ?>>() {
63                 @Override
64                 public DataContainerCodecContext<?, ?> load(final Class<?> key) {
65                     return createDataTreeChildContext(key);
66                 }
67             });
68
69     private final LoadingCache<Class<?>, ContainerNodeCodecContext<?>> operationDataByClass =
70             CacheBuilder.newBuilder().build(new CacheLoader<Class<?>, ContainerNodeCodecContext<?>>() {
71                 @Override
72                 public ContainerNodeCodecContext<?> load(final Class<?> key) {
73                     return createOperationDataContext(key);
74                 }
75             });
76
77     private final LoadingCache<Class<?>, NotificationCodecContext<?>> notificationsByClass =
78             CacheBuilder.newBuilder().build(new CacheLoader<Class<?>, NotificationCodecContext<?>>() {
79                 @Override
80                 public NotificationCodecContext<?> load(final Class<?> key) {
81                     return createNotificationDataContext(key);
82                 }
83             });
84
85     private final LoadingCache<QName, DataContainerCodecContext<?, ?>> childrenByQName =
86             CacheBuilder.newBuilder().build(new CacheLoader<QName, DataContainerCodecContext<?, ?>>() {
87                 @SuppressWarnings("unchecked")
88                 @Override
89                 public DataContainerCodecContext<?, ?> load(final QName qname) {
90                     final DataSchemaNode childSchema = getSchema().getDataChildByName(qname);
91                     childNonNull(childSchema, qname, "Argument %s is not valid child of %s", qname, getSchema());
92                     if (childSchema instanceof DataNodeContainer || childSchema instanceof ChoiceSchemaNode) {
93                         @SuppressWarnings("rawtypes")
94                         final Class childCls = factory().getRuntimeContext().getClassForSchema(childSchema);
95                         return streamChild(childCls);
96                     }
97
98                     throw new UnsupportedOperationException("Unsupported child type " + childSchema.getClass());
99                 }
100             });
101
102     private final LoadingCache<SchemaPath, OperationInputCodec<?>> operationDataByPath =
103             CacheBuilder.newBuilder().build(new CacheLoader<SchemaPath, OperationInputCodec<?>>() {
104                 @SuppressWarnings({ "rawtypes", "unchecked" })
105                 @Override
106                 public OperationInputCodec load(final SchemaPath key) {
107                     final ContainerSchemaNode schema = getOperationDataSchema(getSchema(), key);
108                     if (schema instanceof EffectiveStatement && ((EffectiveStatement) schema).getDeclared()
109                             .getStatementSource() != StatementSource.DECLARATION) {
110                         // This is an implicitly-defined input or output statement. We do not have a
111                         // corresponding
112                         // data representation, so we hard-wire it to null.
113                         return UnmappedOperationInputCodec.getInstance();
114                     }
115
116                     final Class cls = factory().getRuntimeContext().getClassForSchema(schema);
117                     return getOperation(cls);
118                 }
119             });
120
121     /**
122      * Returns operation Input or Output Data container from operation definition.
123      *
124      * @param schema
125      *            - SchemaContext in which lookup should be performed
126      * @param path
127      *            - Schema path of operation input/output data container
128      * @return operation schema or null, if operation is not present in schema context.
129      */
130     private ContainerSchemaNode getOperationDataSchema(final SchemaContext schema, final SchemaPath path) {
131         requireNonNull(schema, "Schema context must not be null.");
132         requireNonNull(path, "Schema path must not be null.");
133         final Iterator<QName> it = path.getPathFromRoot().iterator();
134         Preconditions.checkArgument(it.hasNext(), "Operation must have QName.");
135         final QName operationName = it.next();
136         Preconditions.checkArgument(it.hasNext(), "input or output must be part of path.");
137         final QName inOrOut = it.next();
138         ContainerSchemaNode contSchemaNode = null;
139         if ((contSchemaNode = getOperationDataSchema(schema.getOperations(), operationName, inOrOut)) == null) {
140             contSchemaNode = getOperationDataSchema(schema.getActions(), operationName, inOrOut);
141         }
142         return contSchemaNode;
143     }
144
145     private ContainerSchemaNode getOperationDataSchema(final Set<? extends OperationDefinition> operations,
146             final QName operationName, final QName inOrOut) {
147         for (final OperationDefinition potential : operations) {
148             if (operationName.equals(potential.getQName())) {
149                 return getOperationDataSchema(potential, inOrOut);
150             }
151         }
152         return null;
153     }
154
155     private ContainerSchemaNode getOperationDataSchema(final OperationDefinition operation, final QName qname) {
156         requireNonNull(operation, "Operation Schema must not be null.");
157         requireNonNull(qname, "QName must not be null.");
158         switch (qname.getLocalName()) {
159             case "input":
160                 return operation.getInput();
161             case "output":
162                 return operation.getOutput();
163             default:
164                 throw new IllegalArgumentException(
165                     "Supplied qname " + qname + " does not represent operation input or output.");
166         }
167     }
168
169     private final LoadingCache<SchemaPath, NotificationCodecContext<?>> notificationsByPath =
170             CacheBuilder.newBuilder().build(new CacheLoader<SchemaPath, NotificationCodecContext<?>>() {
171
172                 @SuppressWarnings({ "rawtypes", "unchecked" })
173                 @Override
174                 public NotificationCodecContext load(final SchemaPath key) throws Exception {
175                     final NotificationDefinition schema = SchemaContextUtil.getNotificationSchema(getSchema(), key);
176                     final Class clz = factory().getRuntimeContext().getClassForSchema(schema);
177                     return getNotification(clz);
178                 }
179             });
180
181     private SchemaRootCodecContext(final DataContainerCodecPrototype<SchemaContext> dataPrototype) {
182         super(dataPrototype);
183     }
184
185     /**
186      * Creates RootNode from supplied CodecContextFactory.
187      *
188      * @param factory
189      *            - CodecContextFactory
190      * @return schema root node
191      */
192     public static SchemaRootCodecContext<?> create(final CodecContextFactory factory) {
193         final DataContainerCodecPrototype<SchemaContext> prototype = DataContainerCodecPrototype.rootPrototype(factory);
194         return new SchemaRootCodecContext<>(prototype);
195     }
196
197     @SuppressWarnings({ "unchecked", "rawtypes" })
198     @Nonnull
199     @Override
200     public <C extends TreeNode> DataContainerCodecContext<C, ?> streamChild(@Nonnull final Class<C> childClass)
201             throws IllegalArgumentException {
202         /*
203          * FIXME: This is still not solved for operations TODO: Probably performance wise operations, Data and
204          * Notification loading cache should be merge for performance resons. Needs microbenchmark to
205          * determine which is faster (keeping them separate or in same cache).
206          */
207         if (Notification.class.isAssignableFrom(childClass)) {
208             return (DataContainerCodecContext<C, ?>) getNotification((Class<? extends Notification>) childClass);
209         }
210         return (DataContainerCodecContext<C, ?>) getOrRethrow(childrenByClass, childClass);
211     }
212
213     @Override
214     public <E extends TreeNode> Optional<DataContainerCodecContext<E, ?>>
215             possibleStreamChild(@Nonnull final Class<E> childClass) {
216         throw new UnsupportedOperationException("Not supported");
217     }
218
219     @Nonnull
220     @Override
221     public DataContainerCodecContext<?, ?> yangPathArgumentChild(final PathArgument arg) {
222         return getOrRethrow(childrenByQName, arg.getNodeType());
223     }
224
225     @Nonnull
226     @Override
227     public D deserialize(@Nonnull final NormalizedNode<?, ?> normalizedNode) {
228         throw new UnsupportedOperationException("Could not create Binding data representation for root");
229     }
230
231     @Override
232     protected Object deserializeObject(final NormalizedNode<?, ?> normalizedNode) {
233         throw new UnsupportedOperationException("Unable to deserialize root");
234     }
235
236     @Nullable
237     @Override
238     public TreeArgument<?> deserializePathArgument(@Nullable final YangInstanceIdentifier.PathArgument arg) {
239         Preconditions.checkArgument(arg == null);
240         return null;
241     }
242
243     @Nullable
244     @Override
245     public YangInstanceIdentifier.PathArgument serializePathArgument(@Nullable final TreeArgument<?> arg) {
246         Preconditions.checkArgument(arg == null);
247         return null;
248     }
249
250     /**
251      * Get operation as binding object of binding class.
252      *
253      * @param operationInputOrOutput
254      *            - binding class
255      * @return container node codec context of operation
256      */
257     public ContainerNodeCodecContext<?> getOperation(final Class<? extends Instantiable<?>> operationInputOrOutput) {
258         return getOrRethrow(operationDataByClass, operationInputOrOutput);
259     }
260
261     /**
262      * Get operation input as binding object according to schema path of operation.
263      *
264      * @param operation
265      *            - schema path of operation
266      * @return operation input codec of operation
267      */
268     public OperationInputCodec<?> getOperation(final SchemaPath operation) {
269         return getOrRethrow(operationDataByPath, operation);
270     }
271
272     /**
273      * Get notification as binding object of binding class.
274      *
275      * @param notification
276      *            - binding class
277      * @return notification codec context of notification
278      */
279     @SuppressWarnings("rawtypes")
280     public NotificationCodecContext<?> getNotification(final Class<? extends Notification> notification) {
281         return getOrRethrow(notificationsByClass, notification);
282     }
283
284     /**
285      * Get notification as binding object according to schema path of notification.
286      *
287      * @param notification
288      *            - schema path of notification
289      * @return notification codec context of notification
290      */
291     public NotificationCodecContext<?> getNotification(final SchemaPath notification) {
292         return getOrRethrow(notificationsByPath, notification);
293     }
294
295
296
297     private DataContainerCodecContext<?, ?> createDataTreeChildContext(final Class<?> key) {
298         final QName qname = BindingReflections.findQName(key);
299         final DataSchemaNode childSchema =
300                 childNonNull(getSchema().getDataChildByName(qname), key, "%s is not top-level item.", key);
301         return DataContainerCodecPrototype.from(key, childSchema, factory()).get();
302     }
303
304     private ContainerNodeCodecContext<?> createOperationDataContext(final Class<?> key) {
305         Preconditions.checkArgument(Instantiable.class.isAssignableFrom(key));
306         final QName qname = BindingReflections.findQName(key);
307         final QNameModule module = qname.getModule();
308         OperationDefinition operation = null;
309         final SchemaContext schemaContext = getSchema();
310         if ((operation = findPotentialOperation(schemaContext.getOperations(), module, key, qname)) == null) {
311             operation = findPotentialOperation(schemaContext.getActions(), module, key, qname);
312         }
313         Preconditions.checkArgument(operation != null,
314             "Supplied class %s is not valid operation class.", key);
315         final ContainerSchemaNode schema = getOperationDataSchema(operation, qname);// SchemaNodeUtils
316         // .getRpcDataSchema(operation, qname);
317         Preconditions.checkArgument(schema != null, "Schema for %s does not define input / output.",
318                 operation.getQName());
319         return (ContainerNodeCodecContext<?>) DataContainerCodecPrototype.from(key, schema, factory()).get();
320     }
321
322     private OperationDefinition findPotentialOperation(final Set<? extends OperationDefinition> set,
323             final QNameModule module, final Class<?> key, final QName qname) {
324         OperationDefinition operation = null;
325         for (final OperationDefinition potential : getSchema().getOperations()) {
326             final QName potentialQName = potential.getQName();
327             /*
328              * Check if operation and class represents data from same module and then checks if operation
329              * local name produces same class name as class name appended with Input/Output based on QName
330              * associated with bidning class.
331              *
332              * FIXME: Rework this to have more precise logic regarding Binding Specification.
333              */
334             final String moduleClassName = JavaIdentifierNormalizer
335                     .normalizeSpecificIdentifier(potentialQName.getLocalName(), JavaIdentifier.CLASS);
336             final String keyClassName =
337                     JavaIdentifierNormalizer.normalizeSpecificIdentifier(qname.getLocalName(), JavaIdentifier.CLASS);
338             if (module.equals(potentialQName.getModule())
339                     && key.getSimpleName().equals(moduleClassName + keyClassName)) {
340                 operation = potential;
341                 break;
342             }
343         }
344         return operation;
345     }
346
347     private NotificationCodecContext<?> createNotificationDataContext(final Class<?> notificationType) {
348         Preconditions.checkArgument(Notification.class.isAssignableFrom(notificationType));
349         Preconditions.checkArgument(notificationType.isInterface(), "Supplied class must be interface.");
350         final QName qname = BindingReflections.findQName(notificationType);
351         /**
352          * FIXME: After Lithium cleanup of yang-model-api, use direct call on schema context to retrieve
353          * notification via index.
354          */
355         final NotificationDefinition schema =
356                 SchemaContextUtil.getNotificationSchema(getSchema(), SchemaPath.create(true, qname));
357         Preconditions.checkArgument(schema != null, "Supplied %s is not valid notification", notificationType);
358
359         return new NotificationCodecContext<>(notificationType, schema, factory());
360     }
361
362     private static <K, V> V getOrRethrow(final LoadingCache<K, V> cache, final K key) {
363         try {
364             return cache.getUnchecked(key);
365         } catch (final UncheckedExecutionException e) {
366             final Throwable cause = e.getCause();
367             if (cause != null) {
368                 Throwables.propagateIfPossible(cause);
369             }
370             throw e;
371         }
372     }
373 }