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