/**
* @file AnnotationUtil.java
* AnnotationUtil is a utility to get all annotations of a class, its
* methods,
* and the method parameters. Returned annotations include all annotations
* of
* the classes interfaces and super classes.
* Requested classes are cached, so requesting a classes annotations
* repeatedly
* is fast.
* Example usage:
* AnnotatedClass annotatedClass = AnnotationUtil.get(MyClass.class);
* List<AnnotatedMethod> methods = annotatedClass.getMethods();
* for (AnnotatedMethod method : methods) {
* System.out.println("Method: " + method.getName());
* List<Annotation> annotations = method.getAnnotations();
* for (Annotation annotation : annotations) {
* System.out.println(" Annotation: " + annotation.toString());
* }
* }
* @brief
* AnnotationUtil is a utility to retrieve merged annotations from a
* class
* including all its superclasses and interfaces.
* @license
* Licensed under the Apache License, Version 2.0 (the "License"); you
* may not
* use this file except in compliance with the License. You may obtain
* a copy
* of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
* the
* License for the specific language governing permissions and
* limitations under
* the License.
* Copyright (c) 2013 Almende B.V.
* @author Jos de Jong, <jos@almende.org>
* @date 2013-01-21
*/
package com.almende.util;
import java.lang.annotation.Annotation;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.invoke.WrongMethodTypeException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* The Class AnnotationUtil.
*/
public final class AnnotationUtil {
private static final Logger LOG = Logger.getLogger(AnnotationUtil.class
.getName());
private static Map<String, AnnotatedClass> cache = new ConcurrentHashMap<String, AnnotatedClass>();
private AnnotationUtil() {}
/**
* Get all annotations of a class, methods, and parameters.
* Returned annotations include all annotations of the classes interfaces
* and super classes (excluding java.lang.Object).
*
* @param clazz
* the clazz
* @return annotatedClazz
*/
public static AnnotatedClass get(final Class<?> clazz) {
AnnotatedClass annotatedClazz = cache.get(clazz.getName());
if (annotatedClazz == null) {
annotatedClazz = new AnnotatedClass(clazz);
cache.put(clazz.getName(), annotatedClazz);
}
return annotatedClazz;
}
/**
* The Class CachedAnnotation.
*/
public static class CachedAnnotation {
private Annotation annotation = null;
private Object value = null;
/**
* Instantiates a new cached annotation.
*
* @param annotation
* the annotation
*/
public CachedAnnotation(Annotation annotation) {
this.annotation = annotation;
try {
Method method = annotation.getClass().getMethod("value",
new Class<?>[0]);
if (method != null) {
value = method.invoke(annotation, new Object[0]);
}
} catch (NoSuchMethodException | SecurityException
| IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {}
}
/**
* Value.
*
* @return the object
*/
public Object value() {
return value;
}
/**
* Gets the annotation.
*
* @return the annotation
*/
public Annotation getAnnotation() {
return annotation;
}
}
/**
* AnnotatedClass describes a class, its annotations, and its methods.
*/
public static class AnnotatedClass extends AnnotatedThing {
/** The clazz. */
private Class<?> clazz = null;
private final List<AnnotatedMethod> methodList = new ArrayList<AnnotatedMethod>(
5);
private final Map<Class<?>, List<AnnotatedMethod>> methods = new HashMap<Class<?>, List<AnnotatedMethod>>(
5);
private final Map<String, List<AnnotatedMethod>> methodNames = new HashMap<String, List<AnnotatedMethod>>(
5);
private final List<AnnotatedField> fieldList = new ArrayList<AnnotatedField>(
1);
private final Map<Class<?>, List<AnnotatedField>> fields = new HashMap<Class<?>, List<AnnotatedField>>(
1);
/**
* Create a new AnnotatedClass.
*
* @param clazz
* the clazz
*/
public AnnotatedClass(final Class<?> clazz) {
this.clazz = clazz;
merge(clazz);
AnnotationUtil.convertMethods(methods, methodList);
AnnotationUtil.convertMethodNames(methodNames, methodList);
AnnotationUtil.convertFields(fields, fieldList);
}
/**
* Recursively merge a class into this AnnotatedClass.
* The method loops over all the classess interfaces and superclasses
* Methods with will be merged.
*
* @param clazz
* the clazz
* @param includeObject
* if true, superclass java.lang.Object will
* be included too.
*/
private void merge(final Class<?> clazz) {
Class<?> c = clazz;
while (c != null && c != Object.class) {
// merge the annotations
super.merge(c.getDeclaredAnnotations());
// merge the methods
AnnotationUtil.merge(methodList, c.getDeclaredMethods());
AnnotationUtil.merge(fieldList, c.getDeclaredFields());
// merge all interfaces and the superclasses of the interfaces
for (final Class<?> i : c.getInterfaces()) {
merge(i);
}
// ok now again for the superclass
c = c.getSuperclass();
}
}
/**
* Get the actual Java class described by this AnnotatedClass.
*
* @return clazz
*/
public Class<?> getActualClass() {
return clazz;
}
/**
* Get all methods including methods declared in superclasses.
*
* @return methods
*/
public List<AnnotatedMethod> getMethods() {
return methodList;
}
/**
* Get all methods, grouped by name allowing detection of overloading.
*
* @return the method names
*/
public Map<String, List<AnnotatedMethod>> getMethodNames() {
return methodNames;
}
/**
* Get all methods including methods declared in superclasses, filtered
* by name.
*
* @param name
* the name
* @return filteredMethods
*/
public List<AnnotatedMethod> getMethods(final String name) {
if (methodNames.containsKey(name)) {
return methodNames.get(name);
} else {
return Collections.emptyList();
}
}
/**
* Get all methods including methods declared in superclasses, filtered
* by annotation.
*
* @param <T>
* the generic type
* @param annotation
* the annotation
* @return filteredMethods
*/
public <T> List<AnnotatedMethod> getAnnotatedMethods(
final Class<T> annotation) {
if (methods.containsKey(annotation)) {
return methods.get(annotation);
} else {
return Collections.emptyList();
}
}
/**
* Get all fields including fields declared in superclasses, filtered
* by annotation.
*
* @param <T>
* the generic type
* @param annotation
* the annotation
* @return filteredMethods
*/
public <T> List<AnnotatedField> getAnnotatedFields(
final Class<T> annotation) {
if (fields.containsKey(annotation)) {
return fields.get(annotation);
} else {
return Collections.emptyList();
}
}
}
/**
* The Class AnnotatedField.
*/
public static class AnnotatedField extends AnnotatedThing {
/** The field. */
private Field field = null;
/** The name. */
private String name = null;
/** The type. */
private Type type = null;
/**
* Instantiates a new annotated field.
*
* @param field
* the field
*/
public AnnotatedField(final Field field) {
super();
this.field = field;
name = field.getName();
type = field.getType();
merge(field);
}
/**
* Merge a java method into this Annotated method.
* Annotations and parameter annotations will be merged.
*
* @param field
* the field
*/
private void merge(final Field field) {
super.merge(field.getDeclaredAnnotations());
}
/**
* Gets the field.
*
* @return the field
*/
public Field getField() {
return field;
}
/**
* Gets the name.
*
* @return the name
*/
public String getName() {
return name;
}
/**
* Gets the type.
*
* @return the type
*/
public Type getType() {
return type;
}
}
/**
* AnnotatedMethod describes a method and its parameters.
*/
public static class AnnotatedMethod extends AnnotatedThing {
/** The method. */
private final Method method;
/** The name. */
private String name = null;
/** The return type. */
private Class<?> returnType = null;
/** The generic return type. */
private Type genericReturnType = null;
/** The parameters. */
private final List<AnnotatedParam> parameters = new ArrayList<AnnotatedParam>(
5);
private boolean isVoid = false;
private MethodHandle methodHandle;
/**
* Instantiates a new annotated method.
*
* @param method
* the method
* @throws IllegalAccessException
* the illegal access exception
*/
public AnnotatedMethod(final Method method)
throws IllegalAccessException {
super();
this.method = method;
method.setAccessible(true);
name = method.getName();
returnType = method.getReturnType();
genericReturnType = method.getGenericReturnType();
isVoid = (returnType == void.class || returnType == Void.class);
merge(method);
if (Defines.HASMETHODHANDLES) {
MethodType newType;
int i = 0;
if (!Modifier.isStatic(method.getModifiers())) {
i = 1;
}
Class<?>[] parameterArray = new Class<?>[parameters.size() + i];
if (i == 1) {
parameterArray[0] = method.getDeclaringClass();
}
for (AnnotatedParam parm : parameters) {
parameterArray[i++] = parm.getType();
}
if (isVoid) {
newType = MethodType.methodType(void.class, parameterArray);
} else {
newType = MethodType.methodType(Object.class,
parameterArray);
}
try {
methodHandle = new ConstantCallSite(MethodHandles
.lookup()
.unreflect(method)
.asType(newType)
.asSpreader(Object[].class,
newType.parameterCount())).getTarget();
} catch (WrongMethodTypeException e) {
final IllegalAccessException res = new IllegalAccessException();
res.initCause(e);
throw res;
}
}
}
/**
* Merge a java method into this Annotated method.
* Annotations and parameter annotations will be merged.
*
* @param method
* the method
*/
private void merge(final Method method) {
// merge the annotations
super.merge(method.getDeclaredAnnotations());
// merge the params
final Annotation[][] params = method.getParameterAnnotations();
final Class<?>[] types = method.getParameterTypes();
final Type[] genericTypes = method.getGenericParameterTypes();
for (int i = 0; i < params.length; i++) {
if (i > parameters.size() - 1) {
parameters.add(new AnnotatedParam(params[i], types[i],
genericTypes[i]));
} else {
parameters.get(i).merge(params[i]);
}
}
}
/**
* Checks if is void.
*
* @return true, if is void
*/
public boolean isVoid() {
return isVoid;
}
/**
* Get the actual method as MethodHandler.
*
* @return methodHandle
*/
public MethodHandle getMethodHandle() {
return methodHandle;
}
/**
* Get the actual Java method described by this AnnotatedMethod.
*
* @return method
*/
public Method getActualMethod() {
return method;
}
/**
* Get the method name.
*
* @return name
*/
public String getName() {
return name;
}
/**
* Get the return type of the method.
*
* @return returnType
*/
public Class<?> getReturnType() {
return returnType;
}
/**
* Get the generic return type of the method.
*
* @return genericType
*/
public Type getGenericReturnType() {
return genericReturnType;
}
/**
* Get all parameter annotations of this method, defined in all
* implementations and interfaces of the methods declaring class.
*
* @return params
*/
public List<AnnotatedParam> getParams() {
return parameters;
}
}
/**
* AnnotatedParam describes all annotations of a parameter.
*/
public static class AnnotatedParam extends AnnotatedThing {
/** The annotations. */
/** The type. */
private Class<?> type = null;
/** The generic type. */
private Type genericType = null;
/**
* Instantiates a new annotated param.
*/
public AnnotatedParam() {
super();
}
/**
* Instantiates a new annotated param.
*
* @param annotations
* the annotations
* @param type
* the type
* @param genericType
* the generic type
*/
public AnnotatedParam(final Annotation[] annotations,
final Class<?> type, final Type genericType) {
super();
this.type = type;
this.genericType = genericType;
merge(annotations);
}
/**
* Get the type of the parameter.
*
* @return type
*/
public Class<?> getType() {
return type;
}
/**
* Get the generic type of the parameter.
*
* @return genericType
*/
public Type getGenericType() {
return genericType;
}
}
/**
* The Class AnnotatedThing.
*/
public static class AnnotatedThing {
private final List<CachedAnnotation> annotationList = new ArrayList<CachedAnnotation>();
private final Map<Class<?>, List<CachedAnnotation>> annotations = new HashMap<Class<?>, List<CachedAnnotation>>(
1);
/**
* Instantiates a new annotated thing.
*/
public AnnotatedThing() {}
/**
* Merge.
*
* @param annotations
* the annotations
*/
public void merge(final Annotation[] annotations) {
// merge the annotations
AnnotationUtil.merge(this.annotationList, annotations);
AnnotationUtil.convertAnnotations(this.annotations,
this.annotationList);
}
/**
* Get all annotations of this parameter, defined in all implementations
* and interfaces of the class.
*
* @return annotations
*/
public List<CachedAnnotation> getAnnotations() {
if (annotations.size() == 1) {
return annotations.values().iterator().next();
}
List<CachedAnnotation> res = new ArrayList<CachedAnnotation>();
for (List<CachedAnnotation> sublist : annotations.values()) {
res.addAll(sublist);
}
return res;
}
/**
* Get an annotation of this parameter by type.
* Returns null if not available.
*
* @param <T>
* the generic type
* @param type
* the type
* @return annotation
*/
public <T extends Annotation> CachedAnnotation getAnnotation(
final Class<T> type) {
List<CachedAnnotation> res = annotations.get(type);
if (res != null && !res.isEmpty()) {
return res.get(0);
}
return null;
}
}
/**
* Merge an array with annotations (listB) into a list with
* annotations (listA).
*
* @param listA
* the list a
* @param listB
* the list b
*/
private static void merge(final List<CachedAnnotation> listA,
final Annotation[] listB) {
for (final Annotation b : listB) {
boolean found = false;
for (final CachedAnnotation a : listA) {
if (a.getAnnotation().getClass() == b.getClass()) {
found = true;
break;
}
}
if (!found) {
listA.add(new CachedAnnotation(b));
}
}
}
private static void convertFields(
final Map<Class<?>, List<AnnotatedField>> fields,
final List<AnnotatedField> fieldList) {
for (AnnotatedField field : fieldList) {
for (CachedAnnotation annotation : field.getAnnotations()) {
List<AnnotatedField> list = fields.get(annotation
.getAnnotation().annotationType());
if (list == null) {
list = new ArrayList<AnnotatedField>(1);
fields.put(annotation.getAnnotation().annotationType(),
list);
}
list.add(field);
}
}
}
private static void convertMethodNames(
final Map<String, List<AnnotatedMethod>> methodNames,
final List<AnnotatedMethod> methodList) {
for (AnnotatedMethod method : methodList) {
List<AnnotatedMethod> list = methodNames.get(method.name);
if (list == null) {
list = new ArrayList<AnnotatedMethod>(1);
methodNames.put(method.name, list);
}
list.add(method);
}
}
private static void convertMethods(
final Map<Class<?>, List<AnnotatedMethod>> methods,
final List<AnnotatedMethod> methodList) {
for (AnnotatedMethod method : methodList) {
for (CachedAnnotation annotation : method.getAnnotations()) {
List<AnnotatedMethod> list = methods.get(annotation
.getAnnotation().annotationType());
if (list == null) {
list = new ArrayList<AnnotatedMethod>(1);
methods.put(annotation.getAnnotation().annotationType(),
list);
}
list.add(method);
}
}
}
private static void convertAnnotations(
final Map<Class<?>, List<CachedAnnotation>> annotations,
final List<CachedAnnotation> annotationList) {
for (CachedAnnotation annotation : annotationList) {
List<CachedAnnotation> list = annotations.get(annotation
.getAnnotation().annotationType());
if (list == null) {
list = new ArrayList<CachedAnnotation>(1);
annotations.put(annotation.getAnnotation().annotationType(),
list);
}
list.add(annotation);
}
}
/**
* Merge an array of methods (listB) into a list with method
* annotations (listA).
*
* @param listA
* the list a
* @param listB
* the list b
* @throws IllegalAccessException
*/
private static void merge(final List<AnnotatedMethod> listA,
final Method[] listB) {
for (final Method b : listB) {
AnnotatedMethod methodAnnotations = null;
for (final AnnotatedMethod a : listA) {
if (areEqual(a.method, b)) {
methodAnnotations = a;
break;
}
}
if (methodAnnotations != null) {
methodAnnotations.merge(b);
} else {
try {
listA.add(new AnnotatedMethod(b));
} catch (IllegalAccessException e) {
LOG.log(Level.SEVERE, "Failed to obtain AnnotatedMethod:"
+ b.getName(), e);
}
}
}
}
/**
* Merge an array with annotations (listB) into a list with
* annotations (listA).
*
* @param listA
* the list a
* @param listB
* the list b
*/
private static void merge(final List<AnnotatedField> listA,
final Field[] listB) {
for (final Field b : listB) {
AnnotatedField fieldAnnotations = null;
for (final AnnotatedField a : listA) {
if (areEqual(a.field, b)) {
fieldAnnotations = a;
break;
}
}
if (fieldAnnotations != null) {
fieldAnnotations.merge(b);
} else {
listA.add(new AnnotatedField(b));
}
}
}
/**
* Test if two methods have equal names, return type, param count,
* and param types.
*
* @param a
* the a
* @param b
* the b
* @return true, if successful
*/
private static boolean areEqual(final Method a, final Method b) {
// http://stackoverflow.com/q/10062957/1262753
if (!a.getName().equals(b.getName())) {
return false;
}
if (a.getReturnType() != b.getReturnType()) {
return false;
}
final Class<?>[] paramsa = a.getParameterTypes();
final Class<?>[] paramsb = b.getParameterTypes();
if (paramsa.length != paramsb.length) {
return false;
}
for (int i = 0; i < paramsa.length; i++) {
if (paramsa[i] != paramsb[i]) {
return false;
}
}
return true;
}
/**
* Test if two fields have equal names and types.
*
* @param a
* the a
* @param b
* the b
* @return true, if successful
*/
private static boolean areEqual(final Field a, final Field b) {
// http://stackoverflow.com/q/10062957/1262753
if (!a.getName().equals(b.getName())) {
return false;
}
if (a.getType() != b.getType()) {
return false;
}
return true;
}
}