Mass-migrate to java.util.Optional
[mdsal.git] / binding2 / mdsal-binding2-runtime / src / main / java / org / opendaylight / mdsal / binding / javav2 / runtime / context / BindingRuntimeContext.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.runtime.context;
9
10 import com.google.common.annotations.Beta;
11 import com.google.common.base.Preconditions;
12 import com.google.common.cache.CacheBuilder;
13 import com.google.common.cache.CacheLoader;
14 import com.google.common.cache.LoadingCache;
15 import com.google.common.collect.BiMap;
16 import com.google.common.collect.FluentIterable;
17 import com.google.common.collect.HashBiMap;
18 import com.google.common.collect.HashMultimap;
19 import com.google.common.collect.ImmutableMap;
20 import com.google.common.collect.Multimap;
21 import java.lang.reflect.Method;
22 import java.util.AbstractMap.SimpleEntry;
23 import java.util.Collection;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.Map;
27 import java.util.Map.Entry;
28 import java.util.Optional;
29 import java.util.Set;
30 import javax.annotation.Nullable;
31 import org.opendaylight.mdsal.binding.javav2.generator.api.ClassLoadingStrategy;
32 import org.opendaylight.mdsal.binding.javav2.generator.context.ModuleContext;
33 import org.opendaylight.mdsal.binding.javav2.generator.impl.BindingGeneratorImpl;
34 import org.opendaylight.mdsal.binding.javav2.generator.util.JavaIdentifier;
35 import org.opendaylight.mdsal.binding.javav2.generator.util.JavaIdentifierNormalizer;
36 import org.opendaylight.mdsal.binding.javav2.generator.util.ReferencedTypeImpl;
37 import org.opendaylight.mdsal.binding.javav2.model.api.GeneratedType;
38 import org.opendaylight.mdsal.binding.javav2.model.api.MethodSignature;
39 import org.opendaylight.mdsal.binding.javav2.model.api.ParameterizedType;
40 import org.opendaylight.mdsal.binding.javav2.model.api.Type;
41 import org.opendaylight.mdsal.binding.javav2.model.api.type.builder.GeneratedTypeBuilder;
42 import org.opendaylight.mdsal.binding.javav2.runtime.context.util.BindingSchemaContextUtils;
43 import org.opendaylight.mdsal.binding.javav2.runtime.reflection.BindingReflections;
44 import org.opendaylight.mdsal.binding.javav2.spec.structural.Augmentation;
45 import org.opendaylight.yangtools.concepts.Immutable;
46 import org.opendaylight.yangtools.yang.common.QName;
47 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
48 import org.opendaylight.yangtools.yang.model.api.AugmentationSchemaNode;
49 import org.opendaylight.yangtools.yang.model.api.AugmentationTarget;
50 import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode;
51 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
52 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
53 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
54 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
55 import org.opendaylight.yangtools.yang.model.api.Module;
56 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
57 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
58 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
59 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
60 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
61 import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement;
62 import org.opendaylight.yangtools.yang.model.api.meta.StatementSource;
63 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition;
64 import org.opendaylight.yangtools.yang.model.util.EffectiveAugmentationSchema;
65 import org.opendaylight.yangtools.yang.model.util.SchemaNodeUtils;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
68
69 /**
70  * Runtime Context for Java YANG Binding classes
71  *
72  * <p>
73  * Runtime Context provides additional insight in Java YANG Binding, binding
74  * classes and underlying YANG schema, it contains runtime information, which
75  * could not be derived from generated classes alone using
76  * {@link BindingReflections}.
77  * <p>
78  * Some of this information are for example list of all available children for
79  * cases {@link #getChoiceCaseChildren(DataNodeContainer)}, since choices are
80  * augmentable and new choices may be introduced by additional models.
81  * <p>
82  * Same goes for all possible augmentations.
83  *
84  */
85 @Beta
86 public class BindingRuntimeContext implements Immutable {
87
88     private static final Logger LOG = LoggerFactory.getLogger(BindingRuntimeContext.class);
89     private static final char DOT = '.';
90     private final ClassLoadingStrategy strategy;
91     private final SchemaContext schemaContext;
92     private final Multimap<Type, AugmentationSchemaNode> augmentationToSchemas = HashMultimap.create();
93     private final BiMap<SchemaPath,Type> targetToAugmentation = HashBiMap.create();
94
95     private final BiMap<Type, Object> typeToDefiningSchema = HashBiMap.create();
96     private final Multimap<Type, Type> choiceToCases = HashMultimap.create();
97     private final Map<QName, Type> identities = new HashMap<>();
98
99     private final LoadingCache<QName, Class<?>> identityClasses = CacheBuilder.newBuilder().weakValues().build(
100         new CacheLoader<QName, Class<?>>() {
101             @Override
102             public Class<?> load(final QName key) {
103                 final Type identityType = BindingRuntimeContext.this.identities.get(key);
104                 Preconditions.checkArgument(identityType != null, "Supplied QName %s is not a valid identity", key);
105                 try {
106                     return BindingRuntimeContext.this.strategy.loadClass(identityType);
107                 } catch (final ClassNotFoundException e) {
108                     throw new IllegalArgumentException("Required class " + identityType + "was not found.", e);
109                 }
110             }
111         });
112
113     private BindingRuntimeContext(final ClassLoadingStrategy strategy, final SchemaContext schema) {
114         this.strategy = strategy;
115         this.schemaContext = schema;
116
117         final BindingGeneratorImpl generator = new BindingGeneratorImpl(false);
118         final Map<Module, ModuleContext> modules = generator.getModuleContexts(this.schemaContext);
119
120         for (final ModuleContext ctx : modules.values()) {
121             this.augmentationToSchemas.putAll(ctx.getTypeToAugmentations());
122             this.targetToAugmentation.putAll(ctx.getTargetToAugmentation());
123             this.typeToDefiningSchema.putAll(ctx.getTypeToSchema());
124
125             ctx.getTypedefs();
126             this.choiceToCases.putAll(ctx.getChoiceToCases());
127             this.identities.putAll(ctx.getIdentities());
128         }
129     }
130
131     /**
132      * Creates Binding Runtime Context from supplied class loading strategy and
133      * schema context.
134      *
135      * @param strategy
136      *            - class loading strategy to retrieve generated Binding classes
137      * @param ctx
138      *            - schema context which describes YANG model and to which
139      *            Binding classes should be mapped
140      * @return Instance of BindingRuntimeContext for supplied schema context.
141      */
142     public static final BindingRuntimeContext create(final ClassLoadingStrategy strategy, final SchemaContext ctx) {
143         return new BindingRuntimeContext(strategy, ctx);
144     }
145
146     /**
147      * Returns a class loading strategy associated with this binding runtime context
148      * which is used to load classes.
149      *
150      * @return Class loading strategy.
151      */
152     public ClassLoadingStrategy getStrategy() {
153         return this.strategy;
154     }
155
156     /**
157      * Returns an stable immutable view of schema context associated with this Binding runtime context.
158      *
159      * @return stable view of schema context
160      */
161     public SchemaContext getSchemaContext() {
162         return this.schemaContext;
163     }
164
165     /**
166      * Returns schema of augmentation
167      * <p>
168      * Returned schema is schema definition from which augmentation class was
169      * generated. This schema is isolated from other augmentations. This means
170      * it contains augmentation definition as was present in original YANG
171      * module.
172      * <p>
173      * Children of returned schema does not contain any additional
174      * augmentations, which may be present in runtime for them, thus returned
175      * schema is unsuitable for use for validation of data.
176      * <p>
177      * For retrieving {@link AugmentationSchemaNode}, which will contains full model
178      * for child nodes, you should use method
179      * {@link #getResolvedAugmentationSchema(DataNodeContainer, Class)} which
180      * will return augmentation schema derived from supplied augmentation target
181      * schema.
182      *
183      * @param augClass
184      *            - ugmentation class
185      * @return Schema of augmentation or null if augmentaiton is not known in
186      *         this context
187      * @throws IllegalArgumentException
188      *             - if supplied class is not an augmentation
189      */
190     public @Nullable Entry<Type, Collection<AugmentationSchemaNode>>
191     getAugmentationDefinition(final Class<?> augClass) {
192         Preconditions.checkArgument(Augmentation.class.isAssignableFrom(augClass),
193             "Class %s does not represent augmentation", augClass);
194
195         final Type refType = referencedType(augClass);
196         for (Entry<Type, Collection<AugmentationSchemaNode>> entry : this.augmentationToSchemas.asMap().entrySet()) {
197             if (refType.equals(entry.getKey())) {
198                 return entry;
199             }
200         }
201
202         throw new IllegalArgumentException("Supplied class is not an augmentation.");
203     }
204
205     /**
206      * Returns defining {@link DataSchemaNode} for supplied class.
207      *
208      * <p>
209      * Returned schema is schema definition from which class was generated. This
210      * schema may be isolated from augmentations, if supplied class represent
211      * node, which was child of grouping or augmentation.
212      * <p>
213      * For getting augmentation schema from augmentation class use
214      * {@link #getAugmentationDefinition(Class)} instead.
215      *
216      * @param cls
217      *            - class which represents list, container, choice or case.
218      * @return Schema node, from which class was generated.
219      */
220     public DataSchemaNode getSchemaDefinition(final Class<?> cls) {
221         Preconditions.checkArgument(!Augmentation.class.isAssignableFrom(cls),"Supplied class must not be augmentation (%s is)", cls);
222         return (DataSchemaNode) this.typeToDefiningSchema.get(referencedType(cls));
223     }
224
225     /**
226      * Returns defining {@link AugmentationSchemaNode} of target for supplied class.
227      *
228      * @param target
229      *            - {@link DataNodeContainer}
230      * @param aug
231      *            - supplied class
232      * @return entry of {@link AugmentationSchemaNode} according to its identifier
233      *         {@link AugmentationIdentifier}
234      */
235     public Entry<AugmentationIdentifier, AugmentationSchemaNode> getResolvedAugmentationSchema(
236             final DataNodeContainer target, final Class<? extends Augmentation<?>> aug) {
237         final Collection<AugmentationSchemaNode> origSchemas = getAugmentationDefinition(aug).getValue();
238         Preconditions.checkArgument(origSchemas != null, "Augmentation %s is not known in current schema context",aug);
239         /*
240          * FIXME: Validate augmentation schema lookup
241          *
242          * Currently this algorithm, does not verify if instantiated child nodes
243          * are real one derived from augmentation schema. The problem with full
244          * validation is, if user used copy builders, he may use augmentation
245          * which was generated for different place.
246          *
247          * If this augmentations have same definition, we emit same identifier
248          * with data and it is up to underlying user to validate data.
249          *
250          */
251         final Set<QName> childNames = new HashSet<>();
252         final Set<DataSchemaNode> realChilds = new HashSet<>();
253         for (final AugmentationSchemaNode origSchema : origSchemas) {
254             for (final DataSchemaNode child : origSchema.getChildNodes()) {
255                 final DataSchemaNode dataChildQNname = target.getDataChildByName(child.getQName());
256                 final String childLocalName = child.getQName().getLocalName();
257                 if (dataChildQNname == null) {
258                     for (final DataSchemaNode dataSchemaNode : target.getChildNodes()) {
259                         if (childLocalName.equals(dataSchemaNode.getQName().getLocalName())) {
260                             realChilds.add(dataSchemaNode);
261                             childNames.add(dataSchemaNode.getQName());
262                         }
263                     }
264                 } else {
265                     realChilds.add(dataChildQNname);
266                     childNames.add(child.getQName());
267                 }
268             }
269         }
270
271         final AugmentationIdentifier identifier = new AugmentationIdentifier(childNames);
272         final AugmentationSchemaNode proxy = new EffectiveAugmentationSchema(origSchemas.stream().findFirst().get(),
273             realChilds);
274         return new SimpleEntry<>(identifier, proxy);
275     }
276
277     /**
278      * Returns resolved case schema for supplied class
279      *
280      * @param schema
281      *            - resolved parent choice schema
282      * @param childClass
283      *            - class representing case.
284      * @return Optionally a resolved case schema,.empty if the choice is not
285      *         legal in the given context.
286      * @throws IllegalArgumentException
287      *             - if supplied class does not represent case
288      */
289     public Optional<CaseSchemaNode> getCaseSchemaDefinition(final ChoiceSchemaNode schema, final Class<?> childClass) throws IllegalArgumentException {
290         final DataSchemaNode origSchema = getSchemaDefinition(childClass);
291         Preconditions.checkArgument(origSchema instanceof CaseSchemaNode, "Supplied schema %s is not case.", origSchema);
292
293         /*
294          * FIXME: Make sure that if there are multiple augmentations of same
295          * named case, with same structure we treat it as equals this is due
296          * property of Binding specification and copy builders that user may be
297          * unaware that he is using incorrect case which was generated for
298          * choice inside grouping.
299          */
300         final Optional<CaseSchemaNode> found = BindingSchemaContextUtils.findInstantiatedCase(schema,
301                 (CaseSchemaNode) origSchema);
302         return found;
303     }
304
305     private static Type referencedType(final Class<?> type) {
306         return new ReferencedTypeImpl(type.getPackage().getName(), type.getSimpleName(), true, null);
307     }
308
309     static Type referencedType(final String type) {
310         final int packageClassSeparator = type.lastIndexOf(DOT);
311         return new ReferencedTypeImpl(type.substring(0, packageClassSeparator),
312             type.substring(packageClassSeparator + 1), true, null);
313     }
314
315     /**
316      * Returns schema ({@link DataSchemaNode}, {@link AugmentationSchemaNode} or {@link TypeDefinition})
317      * from which supplied class was generated. Returned schema may be augmented with
318      * additional information, which was not available at compile type
319      * (e.g. third party augmentations).
320      *
321      * @param type Binding Class for which schema should be retrieved.
322      * @return Instance of generated type (definition of Java API), along with
323      *     {@link DataSchemaNode}, {@link AugmentationSchemaNode} or {@link TypeDefinition}
324      *     which was used to generate supplied class.
325      */
326     public Entry<GeneratedType, Object> getTypeWithSchema(final Class<?> type) {
327         return getTypeWithSchema(referencedType(type));
328     }
329
330     public Entry<GeneratedType, Object> getTypeWithSchema(final String type) {
331         return getTypeWithSchema(referencedType(type));
332     }
333
334     private Entry<GeneratedType, Object> getTypeWithSchema(final Type referencedType) {
335         final Object schema = this.typeToDefiningSchema.get(referencedType);
336         Preconditions.checkNotNull(schema, "Failed to find schema for type %s", referencedType);
337
338         final Type definedType = this.typeToDefiningSchema.inverse().get(schema);
339         Preconditions.checkNotNull(definedType, "Failed to find defined type for %s schema %s", referencedType, schema);
340
341         if (definedType instanceof GeneratedTypeBuilder) {
342             return new SimpleEntry<>(((GeneratedTypeBuilder) definedType).toInstance(), schema);
343         }
344         Preconditions.checkArgument(definedType instanceof GeneratedType,"Type {} is not GeneratedType", referencedType);
345         return new SimpleEntry<>((GeneratedType) definedType,schema);
346     }
347
348     public ImmutableMap<Type, Entry<Type, Type>> getChoiceCaseChildren(final DataNodeContainer schema) {
349         final Map<Type,Entry<Type,Type>> childToCase = new HashMap<>();
350         for (final ChoiceSchemaNode choice :  FluentIterable.from(schema.getChildNodes()).filter(ChoiceSchemaNode.class)) {
351             final ChoiceSchemaNode originalChoice = getOriginalSchema(choice);
352             final Type choiceType = referencedType(this.typeToDefiningSchema.inverse().get(originalChoice));
353             final Collection<Type> cases = this.choiceToCases.get(choiceType);
354
355             for (Type caze : cases) {
356                 final Entry<Type,Type> caseIdentifier = new SimpleEntry<>(choiceType,caze);
357                 final HashSet<Type> caseChildren = new HashSet<>();
358                 if (caze instanceof GeneratedTypeBuilder) {
359                     caze = ((GeneratedTypeBuilder) caze).toInstance();
360                 }
361                 collectAllContainerTypes((GeneratedType) caze, caseChildren);
362                 for (final Type caseChild : caseChildren) {
363                     childToCase.put(caseChild, caseIdentifier);
364                 }
365             }
366         }
367         return ImmutableMap.copyOf(childToCase);
368     }
369
370     /**
371      * Map enum constants: yang - java
372      *
373      * @param enumClass enum generated class
374      * @return mapped enum constants from yang with their corresponding values in generated binding classes
375      */
376     public BiMap<String, String> getEnumMapping(final Class<?> enumClass) {
377         final Entry<GeneratedType, Object> typeWithSchema = getTypeWithSchema(enumClass);
378         return getEnumMapping(typeWithSchema);
379     }
380
381     /**
382      * See {@link #getEnumMapping(Class)}}
383      */
384     public BiMap<String, String> getEnumMapping(final String enumClass) {
385         final Entry<GeneratedType, Object> typeWithSchema = getTypeWithSchema(enumClass);
386         return getEnumMapping(typeWithSchema);
387     }
388
389     private static BiMap<String, String> getEnumMapping(final Entry<GeneratedType, Object> typeWithSchema) {
390         final TypeDefinition<?> typeDef = (TypeDefinition<?>) typeWithSchema.getValue();
391
392         Preconditions.checkArgument(typeDef instanceof EnumTypeDefinition);
393         final EnumTypeDefinition enumType = (EnumTypeDefinition) typeDef;
394
395         final HashBiMap<String, String> mappedEnums = HashBiMap.create();
396
397         for (final EnumTypeDefinition.EnumPair enumPair : enumType.getValues()) {
398             mappedEnums.put(enumPair.getName(),
399                     JavaIdentifierNormalizer.normalizeSpecificIdentifier(enumPair.getName(), JavaIdentifier.CLASS));
400         }
401
402         // TODO cache these maps for future use
403
404         return mappedEnums;
405     }
406
407     public Set<Class<?>> getCases(final Class<?> choice) {
408         final Collection<Type> cazes = this.choiceToCases.get(referencedType(choice));
409         final Set<Class<?>> ret = new HashSet<>(cazes.size());
410         for(final Type caze : cazes) {
411             try {
412                 final Class<?> c = this.strategy.loadClass(caze);
413                 ret.add(c);
414             } catch (final ClassNotFoundException e) {
415                 LOG.warn("Failed to load class for case {}, ignoring it", caze, e);
416             }
417         }
418         return ret;
419     }
420
421     public Class<?> getClassForSchema(final SchemaNode childSchema) {
422         final SchemaNode origSchema = getOriginalSchema(childSchema);
423         final Type clazzType = this.typeToDefiningSchema.inverse().get(origSchema);
424         try {
425             return this.strategy.loadClass(clazzType);
426         } catch (final ClassNotFoundException e) {
427             throw new IllegalStateException(e);
428         }
429     }
430
431     private static boolean isLocalAugment(final AugmentationTarget container, final AugmentationSchemaNode augment) {
432         Preconditions.checkState(container instanceof SchemaNode);
433         final QName root = ((SchemaNode) container).getPath().getPathFromRoot().iterator().next();
434         // findFirst makes no sense but just pick up one child to judge whether the target node is
435         // in the same module.
436         final java.util.Optional<DataSchemaNode> child = augment.getChildNodes().stream().findFirst();
437         if (child.isPresent()) {
438             return root.getModule().equals(child.get().getQName().getModule());
439         }
440         return false;
441     }
442
443     public ImmutableMap<AugmentationIdentifier,Type> getAvailableAugmentationTypes(final DataNodeContainer container) {
444         final Map<AugmentationIdentifier,Type> identifierToType = new HashMap<>();
445         if (container instanceof AugmentationTarget) {
446             for (final AugmentationSchemaNode augment : ((AugmentationTarget) container).getAvailableAugmentations()) {
447                 if (!isLocalAugment((AugmentationTarget) container, augment)) {
448                     // Augmentation must have child nodes if is to be used with Binding classes
449                     AugmentationSchemaNode augOrig = augment;
450                     while (augOrig.getOriginalDefinition().isPresent()) {
451                         augOrig = augOrig.getOriginalDefinition().get();
452                     }
453
454                     if (!augment.getChildNodes().isEmpty()) {
455                         final Type augType = this.targetToAugmentation.get(augOrig.getTargetPath());
456                         if (augType != null) {
457                             identifierToType.put(getAugmentationIdentifier(augment), augType);
458                         }
459                     }
460                 }
461             }
462         }
463
464         return ImmutableMap.copyOf(identifierToType);
465     }
466
467     private static AugmentationIdentifier getAugmentationIdentifier(final AugmentationSchemaNode augment) {
468         final Set<QName> childNames = new HashSet<>();
469         for (final DataSchemaNode child : augment.getChildNodes()) {
470             childNames.add(child.getQName());
471         }
472         return new AugmentationIdentifier(childNames);
473     }
474
475     private static Type referencedType(final Type type) {
476         if (type instanceof ReferencedTypeImpl) {
477             return type;
478         }
479         return new ReferencedTypeImpl(type.getPackageName(), type.getName(), true, null);
480     }
481
482     private static Set<Type> collectAllContainerTypes(final GeneratedType type, final Set<Type> collection) {
483         for (final MethodSignature definition : type.getMethodDefinitions()) {
484             Type childType = definition.getReturnType();
485             if (childType instanceof ParameterizedType) {
486                 childType = ((ParameterizedType) childType).getActualTypeArguments()[0];
487             }
488             if (childType instanceof GeneratedType || childType instanceof GeneratedTypeBuilder) {
489                 collection.add(referencedType(childType));
490             }
491         }
492         for (final Type parent : type.getImplements()) {
493             if (parent instanceof GeneratedType) {
494                 collectAllContainerTypes((GeneratedType) parent, collection);
495             }
496         }
497         return collection;
498     }
499
500     private static <T extends SchemaNode> T getOriginalSchema(final T choice) {
501         @SuppressWarnings("unchecked")
502         final T original = (T) SchemaNodeUtils.getRootOriginalIfPossible(choice);
503         if (original != null) {
504             return original;
505         }
506         return choice;
507     }
508
509     public Class<?> getIdentityClass(final QName input) {
510         return this.identityClasses.getUnchecked(input);
511     }
512
513     public Method findOperationMethod(final Class<?> key, final OperationDefinition operationDef)
514             throws NoSuchMethodException {
515         final String methodName =
516                 JavaIdentifierNormalizer.normalizeSpecificIdentifier(operationDef.getQName().getLocalName(),
517                 JavaIdentifier.METHOD);
518         if (operationDef.getInput() != null && isExplicitStatement(operationDef.getInput())) {
519             final Class<?> inputClz = this.getClassForSchema(operationDef.getInput());
520             return key.getMethod(methodName, inputClz);
521         }
522         return key.getMethod(methodName);
523     }
524
525     @SuppressWarnings("rawtypes")
526     private static boolean isExplicitStatement(final ContainerSchemaNode node) {
527         return node instanceof EffectiveStatement
528                 && ((EffectiveStatement) node).getDeclared().getStatementSource() == StatementSource.DECLARATION;
529     }
530 }