1fa8ffa023e3f4de9d802283763fce30097eec03
[mdsal.git] / binding / mdsal-binding-spec-util / src / main / java / org / opendaylight / mdsal / binding / spec / reflect / BindingReflections.java
1 /*
2  * Copyright (c) 2013 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.spec.reflect;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Preconditions.checkState;
12
13 import com.google.common.annotations.Beta;
14 import com.google.common.annotations.VisibleForTesting;
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.util.concurrent.ListenableFuture;
21 import java.lang.reflect.Field;
22 import java.lang.reflect.InvocationTargetException;
23 import java.lang.reflect.Method;
24 import java.util.Optional;
25 import java.util.ServiceLoader;
26 import java.util.concurrent.TimeUnit;
27 import org.eclipse.jdt.annotation.NonNull;
28 import org.opendaylight.yangtools.yang.binding.Action;
29 import org.opendaylight.yangtools.yang.binding.Augmentation;
30 import org.opendaylight.yangtools.yang.binding.BaseIdentity;
31 import org.opendaylight.yangtools.yang.binding.BindingContract;
32 import org.opendaylight.yangtools.yang.binding.ChildOf;
33 import org.opendaylight.yangtools.yang.binding.DataContainer;
34 import org.opendaylight.yangtools.yang.binding.DataObject;
35 import org.opendaylight.yangtools.yang.binding.Notification;
36 import org.opendaylight.yangtools.yang.binding.Rpc;
37 import org.opendaylight.yangtools.yang.binding.RpcService;
38 import org.opendaylight.yangtools.yang.binding.YangModelBindingProvider;
39 import org.opendaylight.yangtools.yang.binding.YangModuleInfo;
40 import org.opendaylight.yangtools.yang.binding.contract.Naming;
41 import org.opendaylight.yangtools.yang.common.QName;
42 import org.opendaylight.yangtools.yang.common.QNameModule;
43 import org.opendaylight.yangtools.yang.common.YangConstants;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 public final class BindingReflections {
48     private static final Logger LOG = LoggerFactory.getLogger(BindingReflections.class);
49     private static final LoadingCache<Class<?>, Optional<QName>> CLASS_TO_QNAME = CacheBuilder.newBuilder()
50             .weakKeys()
51             .expireAfterAccess(60, TimeUnit.SECONDS)
52             .build(new ClassToQNameLoader());
53     private static final LoadingCache<ClassLoader, ImmutableSet<YangModuleInfo>> MODULE_INFO_CACHE =
54             CacheBuilder.newBuilder().weakKeys().weakValues().build(
55                 new CacheLoader<ClassLoader, ImmutableSet<YangModuleInfo>>() {
56                     @Override
57                     public ImmutableSet<YangModuleInfo> load(final ClassLoader key) {
58                         return loadModuleInfos(key);
59                     }
60                 });
61
62     private BindingReflections() {
63         // Hidden on purpose
64     }
65
66     /**
67      * Returns a QName associated to supplied type.
68      *
69      * @param dataType Data type class
70      * @return QName associated to supplied dataType. If dataType is Augmentation method does not return canonical
71      *         QName, but QName with correct namespace revision, but virtual local name, since augmentations do not
72      *         have name. May return null if QName is not present.
73      */
74     public static QName findQName(final Class<?> dataType) {
75         return CLASS_TO_QNAME.getUnchecked(dataType).orElse(null);
76     }
77
78     /**
79      * Checks if method is RPC invocation.
80      *
81      * @param possibleMethod
82      *            Method to check
83      * @return true if method is RPC invocation, false otherwise.
84      */
85     public static boolean isRpcMethod(final Method possibleMethod) {
86         return possibleMethod != null && RpcService.class.isAssignableFrom(possibleMethod.getDeclaringClass())
87                 && ListenableFuture.class.isAssignableFrom(possibleMethod.getReturnType())
88                 // length <= 2: it seemed to be impossible to get correct RpcMethodInvoker because of
89                 // resolveRpcInputClass() check.While RpcMethodInvoker counts with one argument for
90                 // non input type and two arguments for input type, resolveRpcInputClass() counting
91                 // with zero for non input and one for input type
92                 && possibleMethod.getParameterCount() <= 2;
93     }
94
95     public static @NonNull QName getQName(final BaseIdentity identity) {
96         return getContractQName(identity);
97     }
98
99     public static @NonNull QName getQName(final Rpc<?, ?> rpc) {
100         return getContractQName(rpc);
101     }
102
103     private static @NonNull QName getContractQName(final BindingContract<?> contract) {
104         return CLASS_TO_QNAME.getUnchecked(contract.implementedInterface())
105             .orElseThrow(() -> new IllegalStateException("Failed to resolve QName of " + contract));
106     }
107
108     public static QNameModule getQNameModule(final Class<?> clz) {
109         if (DataContainer.class.isAssignableFrom(clz) || BaseIdentity.class.isAssignableFrom(clz)
110                 || Action.class.isAssignableFrom(clz)) {
111             return findQName(clz).getModule();
112         }
113
114         return getModuleInfo(clz).getName().getModule();
115     }
116
117     /**
118      * Returns instance of {@link YangModuleInfo} of declaring model for specific class.
119      *
120      * @param cls data object class
121      * @return Instance of {@link YangModuleInfo} associated with model, from which this class was derived.
122      */
123     private static @NonNull YangModuleInfo getModuleInfo(final Class<?> cls) {
124         final String packageName = Naming.getModelRootPackageName(cls.getPackage().getName());
125         final String potentialClassName = getModuleInfoClassName(packageName);
126         final Class<?> moduleInfoClass;
127         try {
128             moduleInfoClass = cls.getClassLoader().loadClass(potentialClassName);
129         } catch (ClassNotFoundException e) {
130             throw new IllegalStateException("Failed to load " + potentialClassName, e);
131         }
132
133         final Object infoInstance;
134         try {
135             infoInstance = moduleInfoClass.getMethod("getInstance").invoke(null);
136         } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
137             throw new IllegalStateException("Failed to get instance of " + moduleInfoClass, e);
138         }
139
140         checkState(infoInstance instanceof YangModuleInfo, "Unexpected instance %s", infoInstance);
141         return (YangModuleInfo) infoInstance;
142     }
143
144     public static @NonNull String getModuleInfoClassName(final String packageName) {
145         return packageName + "." + Naming.MODULE_INFO_CLASS_NAME;
146     }
147
148     /**
149      * Check if supplied class is derived from YANG model.
150      *
151      * @param cls
152      *            Class to check
153      * @return true if class is derived from YANG model.
154      */
155     public static boolean isBindingClass(final Class<?> cls) {
156         if (DataContainer.class.isAssignableFrom(cls) || Augmentation.class.isAssignableFrom(cls)) {
157             return true;
158         }
159         return cls.getName().startsWith(Naming.PACKAGE_PREFIX);
160     }
161
162     /**
163      * Checks if supplied method is callback for notifications.
164      *
165      * @param method method to check
166      * @return true if method is notification callback.
167      */
168     public static boolean isNotificationCallback(final Method method) {
169         checkArgument(method != null);
170         if (method.getName().startsWith("on") && method.getParameterCount() == 1) {
171             Class<?> potentialNotification = method.getParameterTypes()[0];
172             if (isNotification(potentialNotification)
173                     && method.getName().equals("on" + potentialNotification.getSimpleName())) {
174                 return true;
175             }
176         }
177         return false;
178     }
179
180     /**
181      * Checks is supplied class is a {@link Notification}.
182      *
183      * @param potentialNotification class to examine
184      * @return True if the class represents a Notification.
185      */
186     @VisibleForTesting
187     static boolean isNotification(final Class<?> potentialNotification) {
188         checkArgument(potentialNotification != null, "potentialNotification must not be null.");
189         return Notification.class.isAssignableFrom(potentialNotification);
190     }
191
192     /**
193      * Loads {@link YangModuleInfo} infos available on supplied classloader.
194      *
195      * <p>
196      * {@link YangModuleInfo} are discovered using {@link ServiceLoader} for {@link YangModelBindingProvider}.
197      * {@link YangModelBindingProvider} are simple classes which holds only pointers to actual instance
198      * {@link YangModuleInfo}.
199      *
200      * <p>
201      * When {@link YangModuleInfo} is available, all dependencies are recursively collected into returning set by
202      * collecting results of {@link YangModuleInfo#getImportedModules()}.
203      *
204      * <p>
205      * Consider using {@link #cacheModuleInfos(ClassLoader)} if the classloader is known to be immutable.
206      *
207      * @param loader Classloader for which {@link YangModuleInfo} should be retrieved.
208      * @return Set of {@link YangModuleInfo} available for supplied classloader.
209      */
210     public static @NonNull ImmutableSet<YangModuleInfo> loadModuleInfos(final ClassLoader loader) {
211         Builder<YangModuleInfo> moduleInfoSet = ImmutableSet.builder();
212         ServiceLoader<YangModelBindingProvider> serviceLoader = ServiceLoader.load(YangModelBindingProvider.class,
213                 loader);
214         for (YangModelBindingProvider bindingProvider : serviceLoader) {
215             YangModuleInfo moduleInfo = bindingProvider.getModuleInfo();
216             checkState(moduleInfo != null, "Module Info for %s is not available.", bindingProvider.getClass());
217             collectYangModuleInfo(bindingProvider.getModuleInfo(), moduleInfoSet);
218         }
219         return moduleInfoSet.build();
220     }
221
222     /**
223      * Loads {@link YangModuleInfo} instances available on supplied {@link ClassLoader}, assuming the set of available
224      * information does not change. Subsequent accesses may return cached values.
225      *
226      * <p>
227      * {@link YangModuleInfo} are discovered using {@link ServiceLoader} for {@link YangModelBindingProvider}.
228      * {@link YangModelBindingProvider} are simple classes which holds only pointers to actual instance
229      * {@link YangModuleInfo}.
230      *
231      * <p>
232      * When {@link YangModuleInfo} is available, all dependencies are recursively collected into returning set by
233      * collecting results of {@link YangModuleInfo#getImportedModules()}.
234      *
235      * @param loader Class loader for which {@link YangModuleInfo} should be retrieved.
236      * @return Set of {@link YangModuleInfo} available for supplied classloader.
237      */
238     @Beta
239     public static @NonNull ImmutableSet<YangModuleInfo> cacheModuleInfos(final ClassLoader loader) {
240         return MODULE_INFO_CACHE.getUnchecked(loader);
241     }
242
243     private static void collectYangModuleInfo(final YangModuleInfo moduleInfo,
244             final Builder<YangModuleInfo> moduleInfoSet) {
245         moduleInfoSet.add(moduleInfo);
246         for (YangModuleInfo dependency : moduleInfo.getImportedModules()) {
247             collectYangModuleInfo(dependency, moduleInfoSet);
248         }
249     }
250
251     /**
252      * Checks if supplied class represents RPC Input / RPC Output.
253      *
254      * @param targetType
255      *            Class to be checked
256      * @return true if class represents RPC Input or RPC Output class.
257      */
258     public static boolean isRpcType(final Class<? extends DataObject> targetType) {
259         return DataContainer.class.isAssignableFrom(targetType)
260                 && !ChildOf.class.isAssignableFrom(targetType)
261                 && !Notification.class.isAssignableFrom(targetType)
262                 && (targetType.getName().endsWith("Input") || targetType.getName().endsWith("Output"));
263     }
264
265     private static class ClassToQNameLoader extends CacheLoader<Class<?>, Optional<QName>> {
266
267         @Override
268         public Optional<QName> load(@SuppressWarnings("NullableProblems") final Class<?> key) throws Exception {
269             return resolveQNameNoCache(key);
270         }
271
272         /**
273          * Tries to resolve QName for supplied class. Looks up for static field with name from constant
274          * {@link Naming#QNAME_STATIC_FIELD_NAME} and returns value if present. If field is not present uses
275          * {@link #computeQName(Class)} to compute QName for missing types.
276          */
277         private static Optional<QName> resolveQNameNoCache(final Class<?> key) {
278             try {
279                 final Field field;
280                 try {
281                     field = key.getField(Naming.QNAME_STATIC_FIELD_NAME);
282                 } catch (NoSuchFieldException e) {
283                     LOG.debug("{} does not have a {} field, falling back to computation", key,
284                         Naming.QNAME_STATIC_FIELD_NAME, e);
285                     return Optional.of(computeQName(key));
286                 }
287
288                 final Object obj = field.get(null);
289                 if (obj instanceof QName qname) {
290                     return Optional.of(qname);
291                 }
292             } catch (SecurityException | IllegalArgumentException | IllegalAccessException e) {
293                 /*
294                  * It is safe to log this this exception on debug, since this method should not fail. Only failures are
295                  * possible if the runtime / backing is inconsistent.
296                  */
297                 LOG.debug("Unexpected exception during extracting QName for {}", key, e);
298             }
299             return Optional.empty();
300         }
301
302         /**
303          * Computes QName for supplied class. Namespace and revision are same as {@link YangModuleInfo} associated with
304          * supplied class.
305          *
306          * <p>
307          * If class is
308          * <ul>
309          * <li>rpc input: local name is "input".
310          * <li>rpc output: local name is "output".
311          * <li>augmentation: local name is "module name".
312          * </ul>
313          *
314          * <p>
315          * There is also fallback, if it is not possible to compute QName using following algorithm returns module
316          * QName.
317          *
318          * @throws IllegalStateException If YangModuleInfo could not be resolved
319          * @throws IllegalArgumentException If supplied class was not derived from YANG model.
320          */
321         // FIXME: Extend this algorithm to also provide QName for YANG modeled simple types.
322         @SuppressWarnings({ "rawtypes", "unchecked" })
323         private static QName computeQName(final Class key) {
324             checkArgument(isBindingClass(key), "Supplied class %s is not derived from YANG.", key);
325
326             final QName module = getModuleInfo(key).getName();
327             if (Augmentation.class.isAssignableFrom(key)) {
328                 return module;
329             } else if (isRpcType(key)) {
330                 final String className = key.getSimpleName();
331                 if (className.endsWith(Naming.RPC_OUTPUT_SUFFIX)) {
332                     return YangConstants.operationOutputQName(module.getModule()).intern();
333                 }
334
335                 return YangConstants.operationInputQName(module.getModule()).intern();
336             }
337
338             /*
339              * Fallback for Binding types which do not have QNAME field
340              */
341             return module;
342         }
343     }
344 }