2 * Copyright (c) 2016 Cisco Systems, Inc. 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.yangtools.yang.binding.util;
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Preconditions.checkState;
12 import com.google.common.base.Optional;
13 import com.google.common.cache.CacheBuilder;
14 import com.google.common.cache.CacheLoader;
15 import com.google.common.cache.LoadingCache;
16 import com.google.common.collect.ImmutableSet;
17 import com.google.common.collect.ImmutableSet.Builder;
18 import com.google.common.collect.Sets;
19 import java.lang.reflect.Field;
20 import java.lang.reflect.InvocationTargetException;
21 import java.lang.reflect.Method;
22 import java.lang.reflect.Type;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.LinkedList;
27 import java.util.List;
29 import java.util.ServiceLoader;
30 import java.util.concurrent.Callable;
31 import java.util.concurrent.Future;
32 import java.util.concurrent.TimeUnit;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
35 import org.opendaylight.yangtools.util.ClassLoaderUtils;
36 import org.opendaylight.yangtools.yang.binding.Augmentable;
37 import org.opendaylight.yangtools.yang.binding.Augmentation;
38 import org.opendaylight.yangtools.yang.binding.BaseIdentity;
39 import org.opendaylight.yangtools.yang.binding.BindingMapping;
40 import org.opendaylight.yangtools.yang.binding.ChildTreeNode;
41 import org.opendaylight.yangtools.yang.binding.InterfaceTyped;
42 import org.opendaylight.yangtools.yang.binding.TreeNode;
43 import org.opendaylight.yangtools.yang.binding.Notification;
44 import org.opendaylight.yangtools.yang.binding.RpcService;
45 import org.opendaylight.yangtools.yang.binding.YangModelBindingProvider;
46 import org.opendaylight.yangtools.yang.binding.YangModuleInfo;
47 import org.opendaylight.yangtools.yang.common.QName;
48 import org.opendaylight.yangtools.yang.common.QNameModule;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
52 public class BindingReflections {
54 private static final long EXPIRATION_TIME = 60;
55 private static final String ROOT_PACKAGE_PATTERN_STRING = "(org.opendaylight.yang.gen.v1.[a-z0-9_\\.]*\\.rev[0-9][0-9][0-1][0-9][0-3][0-9])";
56 private static final Pattern ROOT_PACKAGE_PATTERN = Pattern.compile(ROOT_PACKAGE_PATTERN_STRING);
57 private static final Logger LOG = LoggerFactory.getLogger(BindingReflections.class);
59 private static final LoadingCache<Class<?>, Optional<QName>> CLASS_TO_QNAME = CacheBuilder.newBuilder() //
61 .expireAfterAccess(EXPIRATION_TIME, TimeUnit.SECONDS) //
62 .build(new ClassToQNameLoader());
64 private BindingReflections() {
65 throw new UnsupportedOperationException("Utility class.");
69 * Find augmentation target class from concrete Augmentation class
71 * This method uses first generic argument of implemented
72 * {@link Augmentation} interface.
75 * {@link Augmentation} subclass for which we want to determine
76 * augmentation target.
77 * @return Augmentation target - class which augmentation provides
78 * additional extensions.
80 public static Class<? extends Augmentable<?>> findAugmentationTarget(
81 final Class<? extends Augmentation<?>> augmentation) {
82 return ClassLoaderUtils.findFirstGenericArgument(augmentation, Augmentation.class);
86 * Find data hierarchy parent from concrete Data class
88 * This method uses first generic argument of implemented {@link ChildTreeNode}
92 * child class for which we want to find the parent class.
93 * @return Parent class, e.g. class of which the childClass is ChildOf.
95 public static Class<?> findHierarchicalParent(final Class<? extends ChildTreeNode<?>> childClass) {
96 return ClassLoaderUtils.findFirstGenericArgument(childClass, ChildTreeNode.class);
100 * Find data hierarchy parent from concrete Data class
102 * This method is shorthand which gets DataObject class by invoking
103 * {@link TreeNode#implementedInterface()} and uses
104 * {@link #findHierarchicalParent(Class)}.
107 * Child object for which the parent needs to be located.
108 * @return Parent class, or null if a parent is not found.
110 public static Class<?> findHierarchicalParent(final TreeNode child) {
111 if (child instanceof ChildTreeNode) {
112 return ClassLoaderUtils.findFirstGenericArgument(child.implementedInterface(), ChildTreeNode.class);
118 * Returns a QName associated to supplied type
121 * @return QName associated to supplied dataType. If dataType is
122 * Augmentation method does not return canonical QName, but QName
123 * with correct namespace revision, but virtual local name, since
124 * augmentations do not have name.
126 * May return null if QName is not present.
128 public static final QName findQName(final Class<?> dataType) {
129 return CLASS_TO_QNAME.getUnchecked(dataType).orNull();
133 * Checks if method is RPC invocation
137 * @param possibleMethod
139 * @return true if method is RPC invocation, false otherwise.
141 public static boolean isRpcMethod(final Method possibleMethod) {
142 return possibleMethod != null && RpcService.class.isAssignableFrom(possibleMethod.getDeclaringClass())
143 && Future.class.isAssignableFrom(possibleMethod.getReturnType())
144 && possibleMethod.getParameterTypes().length <= 1;
149 * Extracts Output class for RPC method
151 * @param targetMethod
153 * @return Optional.absent() if result type could not be get, or return type
156 @SuppressWarnings("rawtypes")
157 public static Optional<Class<?>> resolveRpcOutputClass(final Method targetMethod) {
158 checkState(isRpcMethod(targetMethod), "Supplied method is not Rpc invocation method");
159 Type futureType = targetMethod.getGenericReturnType();
160 Type rpcResultType = ClassLoaderUtils.getFirstGenericParameter(futureType);
161 Type rpcResultArgument = ClassLoaderUtils.getFirstGenericParameter(rpcResultType);
162 if (rpcResultArgument instanceof Class && !Void.class.equals(rpcResultArgument)) {
163 return Optional.<Class<?>> of((Class) rpcResultArgument);
165 return Optional.absent();
170 * Extracts input class for RPC method
172 * @param targetMethod
174 * @return Optional.absent() if rpc has no input, Rpc input type otherwise.
176 @SuppressWarnings("unchecked")
177 public static Optional<Class<? extends InterfaceTyped>> resolveRpcInputClass(final Method targetMethod) {
178 @SuppressWarnings("rawtypes")
179 Class[] types = targetMethod.getParameterTypes();
180 if (types.length == 0) {
181 return Optional.absent();
183 if (types.length == 1) {
184 return Optional.<Class<? extends InterfaceTyped>> of(types[0]);
186 throw new IllegalArgumentException("Method has 2 or more arguments.");
189 public static QName getQName(final Class<? extends BaseIdentity> context) {
190 return findQName(context);
195 * Checks if class is child of augmentation.
201 public static boolean isAugmentationChild(final Class<?> clazz) {
202 // FIXME: Current resolver could be still confused when
203 // child node was added by grouping
204 checkArgument(clazz != null);
206 @SuppressWarnings({ "rawtypes", "unchecked" })
207 Class<?> parent = findHierarchicalParent((Class) clazz);
208 if (parent == null) {
209 LOG.debug("Did not find a parent for class {}", clazz);
213 String clazzModelPackage = getModelRootPackageName(clazz.getPackage());
214 String parentModelPackage = getModelRootPackageName(parent.getPackage());
216 return !clazzModelPackage.equals(parentModelPackage);
220 * Returns root package name for suplied package.
223 * Package for which find model root package.
224 * @return Package of model root.
226 public static String getModelRootPackageName(final Package pkg) {
227 return getModelRootPackageName(pkg.getName());
231 * Returns root package name for supplied package name.
234 * Package for which find model root package.
235 * @return Package of model root.
237 public static String getModelRootPackageName(final String name) {
238 checkArgument(name != null, "Package name should not be null.");
239 checkArgument(name.startsWith(BindingMapping.PACKAGE_PREFIX), "Package name not starting with %s, is: %s",
240 BindingMapping.PACKAGE_PREFIX, name);
241 Matcher match = ROOT_PACKAGE_PATTERN.matcher(name);
242 checkArgument(match.find(), "Package name '%s' does not match required pattern '%s'", name,
243 ROOT_PACKAGE_PATTERN_STRING);
244 return match.group(0);
247 public static final QNameModule getQNameModule(final Class<?> clz) {
248 if(InterfaceTyped.class.isAssignableFrom(clz) || BaseIdentity.class.isAssignableFrom(clz)) {
249 return findQName(clz).getModule();
252 YangModuleInfo modInfo = BindingReflections.getModuleInfo(clz);
253 return getQNameModule(modInfo);
254 } catch (Exception e) {
255 throw new IllegalStateException("Unable to get QName of defining model.", e);
259 public static final QNameModule getQNameModule(final YangModuleInfo modInfo) {
260 return QNameModule.create(URI.create(modInfo.getNamespace()), QName.parseRevision(modInfo.getRevision()));
265 * Returns instance of {@link YangModuleInfo} of declaring model for
269 * @return Instance of {@link YangModuleInfo} associated with model, from
270 * which this class was derived.
273 public static YangModuleInfo getModuleInfo(final Class<?> cls) throws Exception {
274 checkArgument(cls != null);
275 String packageName = getModelRootPackageName(cls.getPackage());
276 final String potentialClassName = getModuleInfoClassName(packageName);
277 return ClassLoaderUtils.withClassLoader(cls.getClassLoader(), new Callable<YangModuleInfo>() {
279 public YangModuleInfo call() throws ClassNotFoundException, IllegalAccessException,
280 IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
281 Class<?> moduleInfoClass = Thread.currentThread().getContextClassLoader().loadClass(potentialClassName);
282 return (YangModuleInfo) moduleInfoClass.getMethod("getInstance").invoke(null);
287 public static String getModuleInfoClassName(final String packageName) {
288 return packageName + "." + BindingMapping.MODULE_INFO_CLASS_NAME;
293 * Check if supplied class is derived from YANG model.
297 * @return true if class is derived from YANG model.
299 public static boolean isBindingClass(final Class<?> cls) {
300 if (InterfaceTyped.class.isAssignableFrom(cls) || Augmentation.class.isAssignableFrom(cls)) {
303 return (cls.getName().startsWith(BindingMapping.PACKAGE_PREFIX));
308 * Checks if supplied method is callback for notifications.
311 * @return true if method is notification callback.
313 public static boolean isNotificationCallback(final Method method) {
314 checkArgument(method != null);
315 if (method.getName().startsWith("on") && method.getParameterTypes().length == 1) {
316 Class<?> potentialNotification = method.getParameterTypes()[0];
317 if (isNotification(potentialNotification)
318 && method.getName().equals("on" + potentialNotification.getSimpleName())) {
327 * Checks is supplied class is Notification.
329 * @param potentialNotification
332 public static boolean isNotification(final Class<?> potentialNotification) {
333 checkArgument(potentialNotification != null, "potentialNotification must not be null.");
334 return Notification.class.isAssignableFrom(potentialNotification);
339 * Loads {@link YangModuleInfo} infos available on current classloader.
341 * This method is shorthand for {@link #loadModuleInfos(ClassLoader)} with
342 * {@link Thread#getContextClassLoader()} for current thread.
344 * @return Set of {@link YangModuleInfo} available for current classloader.
346 public static ImmutableSet<YangModuleInfo> loadModuleInfos() {
347 return loadModuleInfos(Thread.currentThread().getContextClassLoader());
352 * Loads {@link YangModuleInfo} infos available on supplied classloader.
354 * {@link YangModuleInfo} are discovered using {@link ServiceLoader} for
355 * {@link YangModelBindingProvider}. {@link YangModelBindingProvider} are
356 * simple classes which holds only pointers to actual instance
357 * {@link YangModuleInfo}.
359 * When {@link YangModuleInfo} is available, all dependencies are
360 * recursivelly collected into returning set by collecting results of
361 * {@link YangModuleInfo#getImportedModules()}.
365 * Classloader for which {@link YangModuleInfo} should be
367 * @return Set of {@link YangModuleInfo} available for supplied classloader.
369 public static ImmutableSet<YangModuleInfo> loadModuleInfos(final ClassLoader loader) {
370 Builder<YangModuleInfo> moduleInfoSet = ImmutableSet.<YangModuleInfo> builder();
371 ServiceLoader<YangModelBindingProvider> serviceLoader = ServiceLoader.load(YangModelBindingProvider.class,
373 for (YangModelBindingProvider bindingProvider : serviceLoader) {
374 YangModuleInfo moduleInfo = bindingProvider.getModuleInfo();
375 checkState(moduleInfo != null, "Module Info for %s is not available.", bindingProvider.getClass());
376 collectYangModuleInfo(bindingProvider.getModuleInfo(), moduleInfoSet);
378 return moduleInfoSet.build();
381 private static void collectYangModuleInfo(final YangModuleInfo moduleInfo,
382 final Builder<YangModuleInfo> moduleInfoSet) {
383 moduleInfoSet.add(moduleInfo);
384 for (YangModuleInfo dependency : moduleInfo.getImportedModules()) {
385 collectYangModuleInfo(dependency, moduleInfoSet);
391 * Checks if supplied class represents RPC Input / RPC Output.
394 * Class to be checked
395 * @return true if class represents RPC Input or RPC Output class.
397 public static boolean isRpcType(final Class<? extends TreeNode> targetType) {
398 return InterfaceTyped.class.isAssignableFrom(targetType) //
399 && !ChildTreeNode.class.isAssignableFrom(targetType) //
400 && !Notification.class.isAssignableFrom(targetType) //
401 && (targetType.getName().endsWith("Input") || targetType.getName().endsWith("Output"));
406 * Scans supplied class and returns an iterable of all data children
410 * YANG Modeled Entity derived from DataContainer
411 * @return Iterable of all data children, which have YANG modeled entity
413 @SuppressWarnings("unchecked")
414 public static Iterable<Class<? extends TreeNode>> getChildrenClasses(final Class<? extends InterfaceTyped> type) {
415 checkArgument(type != null, "Target type must not be null");
416 checkArgument(InterfaceTyped.class.isAssignableFrom(type), "Supplied type must be derived from DataContainer");
417 List<Class<? extends TreeNode>> ret = new LinkedList<>();
418 for (Method method : type.getMethods()) {
419 Optional<Class<? extends InterfaceTyped>> entity = getYangModeledReturnType(method);
420 if (entity.isPresent()) {
421 ret.add((Class<? extends TreeNode>) entity.get());
429 * Scans supplied class and returns an iterable of all data children
433 * YANG Modeled Entity derived from DataContainer
434 * @return Iterable of all data children, which have YANG modeled entity
436 public static Map<Class<?>, Method> getChildrenClassToMethod(final Class<?> type) {
437 checkArgument(type != null, "Target type must not be null");
438 checkArgument(InterfaceTyped.class.isAssignableFrom(type), "Supplied type must be derived from DataContainer");
439 Map<Class<?>, Method> ret = new HashMap<>();
440 for (Method method : type.getMethods()) {
441 Optional<Class<? extends InterfaceTyped>> entity = getYangModeledReturnType(method);
442 if (entity.isPresent()) {
443 ret.put(entity.get(), method);
449 @SuppressWarnings("unchecked")
450 private static Optional<Class<? extends InterfaceTyped>> getYangModeledReturnType(final Method method) {
451 if (method.getName().equals("getClass") || !method.getName().startsWith("get")
452 || method.getParameterTypes().length > 0) {
453 return Optional.absent();
456 @SuppressWarnings("rawtypes")
457 Class returnType = method.getReturnType();
458 if (InterfaceTyped.class.isAssignableFrom(returnType)) {
459 return Optional.<Class<? extends InterfaceTyped>> of(returnType);
460 } else if (List.class.isAssignableFrom(returnType)) {
462 return ClassLoaderUtils.withClassLoader(method.getDeclaringClass().getClassLoader(),
463 new Callable<Optional<Class<? extends InterfaceTyped>>>() {
464 @SuppressWarnings("rawtypes")
466 public Optional<Class<? extends InterfaceTyped>> call() {
467 Type listResult = ClassLoaderUtils.getFirstGenericParameter(method
468 .getGenericReturnType());
469 if (listResult instanceof Class
470 && InterfaceTyped.class.isAssignableFrom((Class) listResult)) {
471 return Optional.<Class<? extends InterfaceTyped>> of((Class) listResult);
473 return Optional.absent();
477 } catch (Exception e) {
480 * It is safe to log this this exception on debug, since this
481 * method should not fail. Only failures are possible if the
484 LOG.debug("Unable to find YANG modeled return type for {}", method, e);
487 return Optional.absent();
490 private static class ClassToQNameLoader extends CacheLoader<Class<?>, Optional<QName>> {
493 public Optional<QName> load(final Class<?> key) throws Exception {
494 return resolveQNameNoCache(key);
500 * Tries to resolve QName for supplied class.
502 * Looks up for static field with name from constant
503 * {@link BindingMapping#QNAME_STATIC_FIELD_NAME} and returns value if
506 * If field is not present uses {@link #computeQName(Class)} to compute
507 * QName for missing types.
512 private static Optional<QName> resolveQNameNoCache(final Class<?> key) {
514 Field field = key.getField(BindingMapping.QNAME_STATIC_FIELD_NAME);
515 Object obj = field.get(null);
516 if (obj instanceof QName) {
517 return Optional.of((QName) obj);
520 } catch (NoSuchFieldException e) {
521 return Optional.of(computeQName(key));
523 } catch (SecurityException | IllegalArgumentException | IllegalAccessException e) {
526 * It is safe to log this this exception on debug, since this method
527 * should not fail. Only failures are possible if the runtime /
530 LOG.debug("Unexpected exception during extracting QName for {}", key, e);
532 return Optional.absent();
536 * Computes QName for supplied class
538 * Namespace and revision are same as {@link YangModuleInfo} associated with
543 * <li>rpc input: local name is "input".
544 * <li>rpc output: local name is "output".
545 * <li>augmentation: local name is "module name".
548 * There is also fallback, if it is not possible to compute QName using
549 * following algorithm returns module QName.
551 * FIXME: Extend this algorithm to also provide QName for YANG modeled
554 * @throws IllegalStateException
555 * If YangModuleInfo could not be resolved
556 * @throws IllegalArgumentException
557 * If supplied class was not derived from YANG model.
560 @SuppressWarnings({ "rawtypes", "unchecked" })
561 private static QName computeQName(final Class key) {
562 if (isBindingClass(key)) {
563 YangModuleInfo moduleInfo;
565 moduleInfo = getModuleInfo(key);
566 } catch (Exception e) {
567 throw new IllegalStateException("Unable to get QName for " + key + ". YangModuleInfo was not found.", e);
569 final QName module = getModuleQName(moduleInfo).intern();
570 if (Augmentation.class.isAssignableFrom(key)) {
572 } else if (isRpcType(key)) {
573 final String className = key.getSimpleName();
574 if (className.endsWith(BindingMapping.RPC_OUTPUT_SUFFIX)) {
575 return QName.create(module, "output").intern();
577 return QName.create(module, "input").intern();
581 * Fallback for Binding types which do not have QNAME field
585 throw new IllegalArgumentException("Supplied class " + key + "is not derived from YANG.");
590 * Given a {@link YangModuleInfo}, create a QName representing it. The QName
591 * is formed by reusing the module's namespace and revision using the
592 * module's name as the QName's local name.
596 * @return QName representing the module
598 public static QName getModuleQName(final YangModuleInfo moduleInfo) {
599 checkArgument(moduleInfo != null, "moduleInfo must not be null.");
600 return QName.create(moduleInfo.getNamespace(), moduleInfo.getRevision(), moduleInfo.getName());
604 * Extracts augmentation from Binding DTO field using reflection
607 * Instance of DataObject which is augmentable and may contain
609 * @return Map of augmentations if read was successful, otherwise empty map.
611 public static Map<Class<? extends Augmentation<?>>, Augmentation<?>> getAugmentations(final Augmentable<?> input) {
612 return AugmentationFieldGetter.getGetter(input.getClass()).getAugmentations(input);
617 * Determines if two augmentation classes or case classes represents same
620 * Two augmentations or cases could be substituted only if and if:
622 * <li>Both implements same interfaces</li>
623 * <li>Both have same children</li>
624 * <li>If augmentations: Both have same augmentation target class. Target
625 * class was generated for data node in grouping.</li>
626 * <li>If cases: Both are from same choice. Choice class was generated for
627 * data node in grouping.</li>
630 * <b>Explanation:</b> Binding Specification reuses classes generated for
631 * groupings as part of normal data tree, this classes from grouping could
632 * be used at various locations and user may not be aware of it and may use
633 * incorrect case or augmentation in particular subtree (via copy
634 * constructors, etc).
637 * Class which is potential substition
639 * Class which should be used at particular subtree
640 * @return true if and only if classes represents same data.
642 @SuppressWarnings({ "rawtypes", "unchecked" })
643 public static boolean isSubstitutionFor(final Class potential, final Class target) {
644 HashSet<Class> subImplemented = Sets.newHashSet(potential.getInterfaces());
645 HashSet<Class> targetImplemented = Sets.newHashSet(target.getInterfaces());
646 if (!subImplemented.equals(targetImplemented)) {
649 if (Augmentation.class.isAssignableFrom(potential)
650 && !BindingReflections.findAugmentationTarget(potential).equals(
651 BindingReflections.findAugmentationTarget(target))) {
654 for (Method potentialMethod : potential.getMethods()) {
656 Method targetMethod = target.getMethod(potentialMethod.getName(), potentialMethod.getParameterTypes());
657 if (!potentialMethod.getReturnType().equals(targetMethod.getReturnType())) {
660 } catch (NoSuchMethodException e) {
661 // Counterpart method is missing, so classes could not be
664 } catch (SecurityException e) {
665 throw new IllegalStateException("Could not compare methods", e);