2 * Copyright (c) 2017 Pantheon Technologies s.r.o. and others. All rights reserved.
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
8 package org.opendaylight.mdsal.binding.javav2.runtime.reflection;
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Preconditions.checkState;
13 import com.google.common.annotations.Beta;
14 import com.google.common.base.Optional;
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.collect.ImmutableSet;
19 import com.google.common.collect.ImmutableSet.Builder;
20 import com.google.common.collect.Sets;
21 import java.lang.reflect.Field;
22 import java.lang.reflect.Method;
23 import java.lang.reflect.Type;
25 import java.util.HashMap;
26 import java.util.HashSet;
27 import java.util.LinkedList;
28 import java.util.List;
30 import java.util.ServiceLoader;
31 import java.util.concurrent.Callable;
32 import java.util.concurrent.Future;
33 import java.util.concurrent.TimeUnit;
34 import java.util.regex.Matcher;
35 import java.util.regex.Pattern;
36 import javax.annotation.Nonnull;
37 import org.opendaylight.mdsal.binding.javav2.spec.base.BaseIdentity;
38 import org.opendaylight.mdsal.binding.javav2.spec.base.Instantiable;
39 import org.opendaylight.mdsal.binding.javav2.spec.base.Notification;
40 import org.opendaylight.mdsal.binding.javav2.spec.base.Operation;
41 import org.opendaylight.mdsal.binding.javav2.spec.base.TreeNode;
42 import org.opendaylight.mdsal.binding.javav2.spec.runtime.YangModelBindingProvider;
43 import org.opendaylight.mdsal.binding.javav2.spec.runtime.YangModuleInfo;
44 import org.opendaylight.mdsal.binding.javav2.spec.structural.Augmentable;
45 import org.opendaylight.mdsal.binding.javav2.spec.structural.Augmentation;
46 import org.opendaylight.mdsal.binding.javav2.spec.structural.TreeChildNode;
47 import org.opendaylight.yangtools.util.ClassLoaderUtils;
48 import org.opendaylight.yangtools.yang.common.QName;
49 import org.opendaylight.yangtools.yang.common.QNameModule;
50 import org.opendaylight.yangtools.yang.common.Revision;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
55 public final class BindingReflections {
57 private static final Logger LOG = LoggerFactory.getLogger(BindingReflections.class);
59 private static final long EXPIRATION_TIME = 60;
60 private static final String QNAME_STATIC_FIELD_NAME = "QNAME";
61 private static final String OPERATION_ACTION_OUTPUT_SUFFIX = "Output";
62 private static final String MODULE_INFO_CLASS_NAME = "$YangModuleInfoImpl";
63 private static final String PACKAGE_PREFIX = "org.opendaylight.mdsal.gen.javav2";
64 private static final String ROOT_PACKAGE_PATTERN_STRING =
65 "(org.opendaylight.mdsal.gen.javav2.[a-z0-9_\\.]*\\.rev[0-9][0-9][0-1][0-9][0-3][0-9])";
67 private static final Pattern ROOT_PACKAGE_PATTERN = Pattern.compile(ROOT_PACKAGE_PATTERN_STRING);
69 private static final LoadingCache<Class<?>, Optional<QName>> CLASS_TO_QNAME = CacheBuilder.newBuilder().weakKeys()
70 .expireAfterAccess(EXPIRATION_TIME, TimeUnit.SECONDS).build(new ClassToQNameLoader());
72 private BindingReflections() {
73 throw new UnsupportedOperationException("Utility class.");
77 * Find augmentation target class from concrete Augmentation class
79 * This method uses first generic argument of implemented
80 * {@link Augmentation} interface.
83 * {@link Augmentation} subclass for which we want to determine
84 * augmentation target.
85 * @return Augmentation target - class which augmentation provides
86 * additional extensions.
88 public static Class<? extends Augmentable<?>> findAugmentationTarget(
89 final Class<? extends Augmentation<?>> augmentation) {
90 return ClassLoaderUtils.findFirstGenericArgument(augmentation, Augmentation.class);
94 * Find data hierarchy parent from concrete Tree Node class
96 * This method uses first generic argument of implemented
97 * {@link TreeChildNode} interface.
100 * - child class for which we want to find the parent class
101 * @return Parent class, e.g. class of which the childClass is ChildOf
103 static Class<?> findHierarchicalParent(final Class<? extends TreeChildNode<?, ?>> childClass) {
104 return ClassLoaderUtils.findFirstGenericArgument(childClass, TreeChildNode.class);
108 * Returns a QName associated to supplied type
112 * @return QName associated to supplied dataType. If dataType is
113 * Augmentation method does not return canonical QName, but QName
114 * with correct namespace revision, but virtual local name, since
115 * augmentations do not have name.
117 * May return null if QName is not present.
119 public static QName findQName(final Class<?> dataType) {
120 return CLASS_TO_QNAME.getUnchecked(dataType).orNull();
124 * Checks if method is RPC or Action invocation
126 * @param possibleMethod
128 * @return true if method is RPC or Action invocation, false otherwise.
130 public static boolean isOperationMethod(final Method possibleMethod) {
131 return possibleMethod != null && Operation.class.isAssignableFrom(possibleMethod.getDeclaringClass())
132 && Future.class.isAssignableFrom(possibleMethod.getReturnType())
133 // length <= 2: it seemed to be impossible to get correct OperationMethodInvoker because of
134 // resolveOperationInputClass() check.While OperationMethodInvoker counts with one argument
136 // non input type and two arguments for input type, resolveOperationInputClass() counting
137 // with zero for non input and one for input type
138 && possibleMethod.getParameterTypes().length <= 2;
142 * Extracts Output class for RPC method
144 * @param targetMethod
146 * @return Optional.absent() if result type could not be get, or return type
149 @SuppressWarnings("rawtypes")
150 public static Optional<Class<?>> resolveOperationOutputClass(final Method targetMethod) {
151 checkState(isOperationMethod(targetMethod), "Supplied method is not a RPC or Action invocation method");
152 final Type futureType = targetMethod.getGenericReturnType();
153 final Type operationResultType = ClassLoaderUtils.getFirstGenericParameter(futureType);
154 final Type operationResultArgument = ClassLoaderUtils.getFirstGenericParameter(operationResultType);
155 if (operationResultArgument instanceof Class && !Void.class.equals(operationResultArgument)) {
156 return Optional.of((Class) operationResultArgument);
158 return Optional.absent();
162 * Extracts input class for RPC or Action
164 * @param targetMethod
166 * @return Optional.absent() if RPC or Action has no input, RPC input type
169 @SuppressWarnings({ "rawtypes", "unchecked" })
170 public static Optional<Class<? extends Instantiable<?>>> resolveOperationInputClass(final Method targetMethod) {
171 for (final Class clazz : targetMethod.getParameterTypes()) {
172 if (Instantiable.class.isAssignableFrom(clazz)) {
173 return Optional.of(clazz);
176 return Optional.absent();
180 * Find qname of base identity context.
183 * - base identity type context
184 * @return QName of base identity context
186 public static QName getQName(final Class<? extends BaseIdentity> context) {
187 return findQName(context);
191 * Checks if class is child of augmentation.
195 * @return true if is augmentation, false otherwise
197 public static boolean isAugmentationChild(final Class<?> clazz) {
198 // FIXME: Current resolver could be still confused when child node was
200 checkArgument(clazz != null);
202 @SuppressWarnings({ "rawtypes", "unchecked" })
203 final Class<?> parent = findHierarchicalParent((Class) clazz);
204 if (parent == null) {
205 LOG.debug("Did not find a parent for class {}", clazz);
209 final String clazzModelPackage = getModelRootPackageName(clazz.getPackage());
210 final String parentModelPackage = getModelRootPackageName(parent.getPackage());
212 return !clazzModelPackage.equals(parentModelPackage);
216 * Returns root package name for supplied package.
219 * Package for which find model root package.
220 * @return Package of model root.
222 public static String getModelRootPackageName(final Package pkg) {
223 return getModelRootPackageName(pkg.getName());
227 * Returns root package name for supplied package name.
230 * - package for which find model root package
231 * @return Package of model root
233 public static String getModelRootPackageName(final String name) {
234 checkArgument(name != null, "Package name should not be null.");
235 checkArgument(name.startsWith(PACKAGE_PREFIX), "Package name not starting with %s, is: %s", PACKAGE_PREFIX,
237 final Matcher match = ROOT_PACKAGE_PATTERN.matcher(name);
238 checkArgument(match.find(), "Package name '%s' does not match required pattern '%s'", name,
239 ROOT_PACKAGE_PATTERN_STRING);
240 return match.group(0);
244 * Get QName module of specific Binding object class.
247 * - class of binding object
248 * @return QName module of binding object
250 public static final QNameModule getQNameModule(final Class<?> clz) {
251 if (Instantiable.class.isAssignableFrom(clz) || BaseIdentity.class.isAssignableFrom(clz)) {
252 return findQName(clz).getModule();
255 final YangModuleInfo modInfo = BindingReflections.getModuleInfo(clz);
256 return getQNameModule(modInfo);
257 } catch (final Exception e) {
258 throw new IllegalStateException("Unable to get QName of defining model.", e);
263 * Returns module QName.
267 * @return {@link QNameModule} from module info
269 public static final QNameModule getQNameModule(final YangModuleInfo modInfo) {
270 return QNameModule.create(URI.create(modInfo.getNamespace()), Revision.ofNullable(modInfo.getRevision()));
274 * Returns instance of {@link YangModuleInfo} of declaring model for
278 * - class for getting info
279 * @return Instance of {@link YangModuleInfo} associated with model, from
280 * which this class was derived.
283 public static YangModuleInfo getModuleInfo(final Class<?> cls) throws Exception {
284 checkArgument(cls != null);
285 final String packageName = getModelRootPackageName(cls.getPackage());
286 final String potentialClassName = getModuleInfoClassName(packageName);
287 return ClassLoaderUtils.withClassLoader(cls.getClassLoader(), (Callable<YangModuleInfo>) () -> {
288 final Class<?> moduleInfoClass = Thread.currentThread().getContextClassLoader().loadClass(potentialClassName);
289 return (YangModuleInfo) moduleInfoClass.getMethod("getInstance").invoke(null);
294 * Returns name of module info class.
298 * @return name of module info class
300 public static String getModuleInfoClassName(final String packageName) {
301 return packageName + "." + MODULE_INFO_CLASS_NAME;
305 * Check if supplied class is derived from YANG model.
309 * @return true if class is derived from YANG model.
311 public static boolean isBindingClass(final Class<?> cls) {
312 if (Instantiable.class.isAssignableFrom(cls) || Augmentation.class.isAssignableFrom(cls)
313 || TreeNode.class.isAssignableFrom(cls)) {
316 return cls.getName().startsWith(PACKAGE_PREFIX);
320 * Checks if supplied method is callback for notifications.
324 * @return true if method is notification callback
326 public static boolean isNotificationCallback(final Method method) {
327 checkArgument(method != null);
328 if (method.getName().startsWith("on") && method.getParameterTypes().length == 1) {
329 final Class<?> potentialNotification = method.getParameterTypes()[0];
330 if (isNotification(potentialNotification)
331 && method.getName().equals("on" + potentialNotification.getSimpleName())) {
339 * Check if supplied class is Notification.
341 * @param potentialNotification
343 * @return true if class is notification, false otherwise
345 public static boolean isNotification(final Class<?> potentialNotification) {
346 checkArgument(potentialNotification != null, "potentialNotification must not be null.");
347 return Notification.class.isAssignableFrom(potentialNotification);
351 * Loads {@link YangModuleInfo} info available on current classloader.
353 * This method is shorthand for {@link #loadModuleInfos(ClassLoader)} with
354 * {@link Thread#getContextClassLoader()} for current thread.
356 * @return Set of {@link YangModuleInfo} available for current classloader.
358 public static ImmutableSet<YangModuleInfo> loadModuleInfos() {
359 return loadModuleInfos(Thread.currentThread().getContextClassLoader());
363 * Loads {@link YangModuleInfo} info available on supplied classloader.
365 * {@link YangModuleInfo} are discovered using {@link ServiceLoader} for
366 * {@link YangModelBindingProvider}. {@link YangModelBindingProvider} are
367 * simple classes which holds only pointers to actual instance
368 * {@link YangModuleInfo}.
370 * When {@link YangModuleInfo} is available, all dependencies are
371 * recursively collected into returning set by collecting results of
372 * {@link YangModuleInfo#getImportedModules()}.
375 * - classloader for which {@link YangModuleInfo} should be
377 * @return Set of {@link YangModuleInfo} available for supplied classloader.
379 public static ImmutableSet<YangModuleInfo> loadModuleInfos(final ClassLoader loader) {
380 final Builder<YangModuleInfo> moduleInfoSet = ImmutableSet.builder();
381 final ServiceLoader<YangModelBindingProvider> serviceLoader = ServiceLoader.load(YangModelBindingProvider.class,
383 for (final YangModelBindingProvider bindingProvider : serviceLoader) {
384 final YangModuleInfo moduleInfo = bindingProvider.getModuleInfo();
385 checkState(moduleInfo != null, "Module Info for %s is not available.", bindingProvider.getClass());
386 collectYangModuleInfo(bindingProvider.getModuleInfo(), moduleInfoSet);
388 return moduleInfoSet.build();
391 private static void collectYangModuleInfo(final YangModuleInfo moduleInfo,
392 final Builder<YangModuleInfo> moduleInfoSet) {
393 moduleInfoSet.add(moduleInfo);
394 for (final YangModuleInfo dependency : moduleInfo.getImportedModules()) {
395 collectYangModuleInfo(dependency, moduleInfoSet);
400 * Checks if supplied class represents RPC or Action input/output.
403 * - class to be checked
404 * @return true if class represents RPC or Action input/output class
406 public static boolean isOperationType(final Class<? extends TreeNode> targetType) {
407 return Instantiable.class.isAssignableFrom(targetType) && !TreeChildNode.class.isAssignableFrom(targetType)
408 && !Notification.class.isAssignableFrom(targetType)
409 && (targetType.getName().endsWith("Input") || targetType.getName().endsWith("Output"));
413 * Scans supplied class and returns an iterable of all data children
417 * - YANG Modeled Entity derived from DataContainer
418 * @return Iterable of all data children, which have YANG modeled entity
420 @SuppressWarnings("unchecked")
421 public static Iterable<Class<? extends TreeNode>> getChildrenClasses(final Class<? extends Instantiable<?>> type) {
422 checkArgument(type != null, "Target type must not be null");
423 checkArgument(Instantiable.class.isAssignableFrom(type), "Supplied type must be derived from Instantiable");
424 final List<Class<? extends TreeNode>> ret = new LinkedList<>();
425 for (final Method method : type.getMethods()) {
426 final Optional<Class<? extends Instantiable<?>>> entity = getYangModeledReturnType(method);
427 if (entity.isPresent()) {
428 ret.add((Class<? extends TreeNode>) entity.get());
435 * Scans supplied class and returns an iterable of all data children
439 * - YANG Modeled Entity derived from DataContainer
440 * @return Iterable of all data children, which have YANG modeled entity
442 public static Map<Class<?>, Method> getChildrenClassToMethod(final Class<?> type) {
443 checkArgument(type != null, "Target type must not be null");
444 checkArgument(Instantiable.class.isAssignableFrom(type), "Supplied type must be derived from Instantiable");
445 final Map<Class<?>, Method> ret = new HashMap<>();
446 for (final Method method : type.getMethods()) {
447 final Optional<Class<? extends Instantiable<?>>> entity = getYangModeledReturnType(method);
448 if (entity.isPresent()) {
449 ret.put(entity.get(), method);
455 @SuppressWarnings({ "unchecked", "rawtypes" })
456 private static Optional<Class<? extends Instantiable<?>>> getYangModeledReturnType(final Method method) {
457 if ("getClass".equals(method.getName()) || !method.getName().startsWith("get")
458 || method.getParameterTypes().length > 0) {
459 return Optional.absent();
462 final Class returnType = method.getReturnType();
463 if (Instantiable.class.isAssignableFrom(returnType)) {
464 return Optional.of(returnType);
465 } else if (List.class.isAssignableFrom(returnType)) {
467 return ClassLoaderUtils.withClassLoader(method.getDeclaringClass().getClassLoader(),
468 (Callable<Optional<Class<? extends Instantiable<?>>>>) () -> {
469 final Type listResult = ClassLoaderUtils.getFirstGenericParameter(method.getGenericReturnType());
470 if (listResult instanceof Class
471 && Instantiable.class.isAssignableFrom((Class) listResult)) {
472 return Optional.of((Class) listResult);
474 return Optional.absent();
476 } catch (final Exception e) {
479 * It is safe to log this this exception on debug, since this
480 * method should not fail. Only failures are possible if the
483 LOG.debug("Unable to find YANG modeled return type for {}", method, e);
486 return Optional.absent();
489 private static class ClassToQNameLoader extends CacheLoader<Class<?>, Optional<QName>> {
492 public Optional<QName> load(@Nonnull final Class<?> key) throws Exception {
493 return resolveQNameNoCache(key);
497 * Tries to resolve QName for supplied class.
499 * Looks up for static field with name from constant {@link #QNAME_STATIC_FIELD_NAME} and returns
502 * If field is not present uses {@link #computeQName(Class)} to compute QName for missing types.
505 * - class for resolving QName
506 * @return resolved QName
508 private static Optional<QName> resolveQNameNoCache(final Class<?> key) {
510 final Field field = key.getField(QNAME_STATIC_FIELD_NAME);
511 final Object obj = field.get(null);
512 if (obj instanceof QName) {
513 return Optional.of((QName) obj);
516 } catch (final NoSuchFieldException e) {
517 return Optional.of(computeQName(key));
518 } catch (SecurityException | IllegalArgumentException | IllegalAccessException e) {
520 * It is safe to log this this exception on debug, since this method
521 * should not fail. Only failures are possible if the runtime /
524 LOG.debug("Unexpected exception during extracting QName for {}", key, e);
526 return Optional.absent();
530 * Computes QName for supplied class
532 * Namespace and revision are same as {@link YangModuleInfo} associated
533 * with supplied class.
537 * <li>rpc/action input: local name is "input".
538 * <li>rpc/action output: local name is "output".
539 * <li>augmentation: local name is "module name".
542 * There is also fallback, if it is not possible to compute QName using
543 * following algorithm returns module QName.
545 * FIXME: Extend this algorithm to also provide QName for YANG modeled
548 * @throws IllegalStateException
549 * - if YangModuleInfo could not be resolved
550 * @throws IllegalArgumentException
551 * - if supplied class was not derived from YANG model
554 @SuppressWarnings({ "rawtypes", "unchecked" })
555 private static QName computeQName(final Class key) {
556 if (isBindingClass(key)) {
557 YangModuleInfo moduleInfo;
559 moduleInfo = getModuleInfo(key);
560 } catch (final Exception e) {
561 throw new IllegalStateException("Unable to get QName for " + key
562 + ". YangModuleInfo was not found.", e);
564 final QName module = getModuleQName(moduleInfo).intern();
565 if (Augmentation.class.isAssignableFrom(key)) {
567 } else if (isOperationType(key)) {
568 final String className = key.getSimpleName();
569 if (className.endsWith(OPERATION_ACTION_OUTPUT_SUFFIX)) {
570 return QName.create(module, "output").intern();
572 return QName.create(module, "input").intern();
576 * Fallback for Binding types which do not have QNAME field
580 throw new IllegalArgumentException("Supplied class " + key + "is not derived from YANG.");
587 * Given a {@link YangModuleInfo}, create a QName representing it. The QName
588 * is formed by reusing the module's namespace and revision using the
589 * module's name as the QName's local name.
593 * @return QName representing the module
595 public static QName getModuleQName(final YangModuleInfo moduleInfo) {
596 checkArgument(moduleInfo != null, "moduleInfo must not be null.");
597 return QName.create(moduleInfo.getNamespace(), moduleInfo.getRevision(), moduleInfo.getName());
601 * Extracts augmentation from Binding DTO field using reflection
604 * Instance of DataObject which is augmentable and may contain
606 * @return Map of augmentations if read was successful, otherwise empty map.
608 public static Map<Class<? extends Augmentation<?>>, Augmentation<?>> getAugmentations(final Augmentable<?> input) {
609 return AugmentationFieldGetter.getGetter(input.getClass()).getAugmentations(input);
613 * Determines if two augmentation classes or case classes represents same
616 * Two augmentations or cases could be substituted only if and if:
618 * <li>Both implements same interfaces</li>
619 * <li>Both have same children</li>
620 * <li>If augmentations: Both have same augmentation target class. Target
621 * class was generated for data node in grouping.</li>
622 * <li>If cases: Both are from same choice. Choice class was generated for
623 * data node in grouping.</li>
626 * <b>Explanation:</b> Binding Specification reuses classes generated for
627 * groupings as part of normal data tree, this classes from grouping could
628 * be used at various locations and user may not be aware of it and may use
629 * incorrect case or augmentation in particular subtree (via copy
630 * constructors, etc).
633 * - class which is potential substitution
635 * - class which should be used at particular subtree
636 * @return true if and only if classes represents same data.
638 @SuppressWarnings({ "rawtypes", "unchecked" })
639 public static boolean isSubstitutionFor(final Class potential, final Class target) {
640 final HashSet<Class> subImplemented = Sets.newHashSet(potential.getInterfaces());
641 final HashSet<Class> targetImplemented = Sets.newHashSet(target.getInterfaces());
642 if (!subImplemented.equals(targetImplemented)) {
645 if (Augmentation.class.isAssignableFrom(potential)
646 && !BindingReflections.findAugmentationTarget(potential).equals(
647 BindingReflections.findAugmentationTarget(target))) {
650 for (final Method potentialMethod : potential.getMethods()) {
652 final Method targetMethod = target.getMethod(potentialMethod.getName(), potentialMethod.getParameterTypes());
653 if (!potentialMethod.getReturnType().equals(targetMethod.getReturnType())) {
656 } catch (final NoSuchMethodException e) {
657 // Counterpart method is missing, so classes could not be
660 } catch (final SecurityException e) {
661 throw new IllegalStateException("Could not compare methods", e);