io.github.classgraph.AnnotationInfo Maven / Gradle / Ivy
Show all versions of classgraph Show documentation
/*
* This file is part of ClassGraph.
*
* Author: Luke Hutchison
*
* Hosted at: https://github.com/classgraph/classgraph
*
* --
*
* The MIT License (MIT)
*
* Copyright (c) 2019 Luke Hutchison
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without
* limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial
* portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
* EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
* OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.classgraph;
import java.lang.annotation.Annotation;
import java.lang.annotation.IncompleteAnnotationException;
import java.lang.annotation.Inherited;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import nonapi.io.github.classgraph.reflection.ReflectionUtils;
import nonapi.io.github.classgraph.utils.LogNode;
/** Holds metadata about a specific annotation instance on a class, method, method parameter or field. */
public class AnnotationInfo extends ScanResultObject implements Comparable, HasName {
/** The name. */
private String name;
/** The annotation param values. */
private AnnotationParameterValueList annotationParamValues;
/**
* Set to true once any Object[] arrays of boxed types in annotationParamValues have been lazily converted to
* primitive arrays.
*/
private transient boolean annotationParamValuesHasBeenConvertedToPrimitive;
/** The annotation param values with defaults. */
private transient AnnotationParameterValueList annotationParamValuesWithDefaults;
/** Default constructor for deserialization. */
AnnotationInfo() {
super();
}
// -------------------------------------------------------------------------------------------------------------
/**
* Constructor.
*
* @param name
* The name of the annotation.
* @param annotationParamValues
* The annotation parameter values, or null if none.
*/
AnnotationInfo(final String name, final AnnotationParameterValueList annotationParamValues) {
super();
this.name = name;
this.annotationParamValues = annotationParamValues;
}
// -------------------------------------------------------------------------------------------------------------
/**
* Get the name.
*
* @return The name of the annotation class.
*/
@Override
public String getName() {
return name;
}
/**
* Checks if the annotation is inherited.
*
* @return true if this annotation is meta-annotated with {@link Inherited}.
*/
public boolean isInherited() {
return getClassInfo().isInherited;
}
/**
* Get the default parameter values.
*
* @return the list of default parameter values for this annotation, or the empty list if none.
*/
public AnnotationParameterValueList getDefaultParameterValues() {
return getClassInfo().getAnnotationDefaultParameterValues();
}
/**
* Get the parameter values.
*
* @param includeDefaultValues
* if true, include default values for any annotation parameter value that is missing.
* @return The parameter values of this annotation, including any default parameter values inherited from the
* annotation class definition (if requested), or the empty list if none.
*/
public AnnotationParameterValueList getParameterValues(final boolean includeDefaultValues) {
final ClassInfo classInfo = getClassInfo();
if (classInfo == null) {
// ClassInfo has not yet been set, just return values without defaults
// (happens when trying to log AnnotationInfo during scanning, before ScanResult is available)
return annotationParamValues == null ? AnnotationParameterValueList.EMPTY_LIST : annotationParamValues;
}
// Lazily convert any Object[] arrays of boxed types to primitive arrays
if (annotationParamValues != null && !annotationParamValuesHasBeenConvertedToPrimitive) {
annotationParamValues.convertWrapperArraysToPrimitiveArrays(classInfo);
annotationParamValuesHasBeenConvertedToPrimitive = true;
}
if (!includeDefaultValues) {
// Don't include defaults
return annotationParamValues == null ? AnnotationParameterValueList.EMPTY_LIST : annotationParamValues;
}
if (annotationParamValuesWithDefaults == null) {
if (classInfo.annotationDefaultParamValues != null
&& !classInfo.annotationDefaultParamValuesHasBeenConvertedToPrimitive) {
classInfo.annotationDefaultParamValues.convertWrapperArraysToPrimitiveArrays(classInfo);
classInfo.annotationDefaultParamValuesHasBeenConvertedToPrimitive = true;
}
// Check if one or both of the defaults and the values in this annotation instance are null (empty)
final AnnotationParameterValueList defaultParamValues = classInfo.annotationDefaultParamValues;
if (defaultParamValues == null && annotationParamValues == null) {
return AnnotationParameterValueList.EMPTY_LIST;
} else if (defaultParamValues == null) {
return annotationParamValues;
} else if (annotationParamValues == null) {
return defaultParamValues;
}
// Overwrite defaults with non-defaults
final Map allParamValues = new HashMap<>();
for (final AnnotationParameterValue defaultParamValue : defaultParamValues) {
allParamValues.put(defaultParamValue.getName(), defaultParamValue.getValue());
}
for (final AnnotationParameterValue annotationParamValue : this.annotationParamValues) {
allParamValues.put(annotationParamValue.getName(), annotationParamValue.getValue());
}
// Put annotation values in the same order as the annotation methods (there is one method for each
// annotation constant)
if (classInfo.methodInfo == null) {
// Should not happen (when classfile is read, methods are always read, whether or not
// scanSpec.enableMethodInfo is true)
throw new IllegalArgumentException("Could not find methods for annotation " + classInfo.getName());
}
annotationParamValuesWithDefaults = new AnnotationParameterValueList();
for (final MethodInfo mi : classInfo.methodInfo) {
final String paramName = mi.getName();
switch (paramName) {
// None of these method names should be present in the @interface class itself, it should only
// contain methods for the annotation constants (but skip them anyway to be safe). These methods
// should only exist in concrete instances of the annotation.
case "":
case "":
case "hashCode":
case "equals":
case "toString":
case "annotationType":
// Skip
break;
default:
// Annotation constant
final Object paramValue = allParamValues.get(paramName);
// Annotation values cannot be null (or absent, from either defaults or annotation instance)
if (paramValue != null) {
annotationParamValuesWithDefaults.add(new AnnotationParameterValue(paramName, paramValue));
}
break;
}
}
}
return annotationParamValuesWithDefaults;
}
/**
* Get the parameter values.
*
* @return The parameter values of this annotation, including any default parameter values inherited from the
* annotation class definition, or the empty list if none.
*/
public AnnotationParameterValueList getParameterValues() {
return getParameterValues(true);
}
// -------------------------------------------------------------------------------------------------------------
/**
* Get the name of the annotation class, for {@link #getClassInfo()}.
*
* @return the class name
*/
@Override
protected String getClassName() {
return name;
}
/* (non-Javadoc)
* @see io.github.classgraph.ScanResultObject#setScanResult(io.github.classgraph.ScanResult)
*/
@Override
void setScanResult(final ScanResult scanResult) {
super.setScanResult(scanResult);
if (annotationParamValues != null) {
for (final AnnotationParameterValue a : annotationParamValues) {
a.setScanResult(scanResult);
}
}
}
/**
* Get {@link ClassInfo} objects for any classes referenced in the type descriptor or type signature.
*
* @param classNameToClassInfo
* the map from class name to {@link ClassInfo}.
* @param refdClassInfo
* the referenced class info
*/
@Override
protected void findReferencedClassInfo(final Map classNameToClassInfo,
final Set refdClassInfo, final LogNode log) {
super.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log);
if (annotationParamValues != null) {
for (final AnnotationParameterValue annotationParamValue : annotationParamValues) {
annotationParamValue.findReferencedClassInfo(classNameToClassInfo, refdClassInfo, log);
}
}
}
// -------------------------------------------------------------------------------------------------------------
/** Return the {@link ClassInfo} object for the annotation class. */
@Override
public ClassInfo getClassInfo() {
return super.getClassInfo();
}
/**
* Load the {@link Annotation} class corresponding to this {@link AnnotationInfo} object, by calling
* {@code getClassInfo().loadClass()}, then create a new instance of the annotation, with the annotation
* parameter values obtained from this {@link AnnotationInfo} object, possibly overriding default annotation
* parameter values obtained from calling {@link AnnotationInfo#getClassInfo()} then
* {@link ClassInfo#getAnnotationDefaultParameterValues()}.
*
*
* Note that the returned {@link Annotation} will have some sort of {@link InvocationHandler} proxy type, such
* as {@code io.github.classgraph.features.$Proxy4} or {@code com.sun.proxy.$Proxy6}. This is an unavoidable
* side effect of the fact that concrete {@link Annotation} instances cannot be instantiated directly.
* (ClassGraph uses the
* same approach that the JDK uses to instantiate annotations from a map.) However, proxy instances are
* handled
* specially when it comes to casting and {@code instanceof}: you are able to cast the returned proxy
* instance to the annotation type, and {@code instanceof} checks against the annotation class will succeed.
*
*
* Of course another option you have for getting the concrete annotations, rather than instantiating the
* annotations on a {@link ClassInfo} object via this method, is to call {@link ClassInfo#loadClass()}, and read
* the annotations directly from the returned {@link Class} object.
*
* @return The new {@link Annotation} instance, as a dynamic proxy object that can be cast to the expected
* annotation type.
*/
public Annotation loadClassAndInstantiate() {
final Class extends Annotation> annotationClass = getClassInfo().loadClass(Annotation.class);
return (Annotation) Proxy.newProxyInstance(annotationClass.getClassLoader(),
new Class>[] { annotationClass }, new AnnotationInvocationHandler(annotationClass, this));
}
/** {@link InvocationHandler} for dynamically instantiating an {@link Annotation} object. */
private static class AnnotationInvocationHandler implements InvocationHandler {
/** The annotation class. */
private final Class extends Annotation> annotationClass;
/** The {@link AnnotationInfo} object for this annotation. */
private final AnnotationInfo annotationInfo;
/** The annotation parameter values instantiated. */
private final Map annotationParameterValuesInstantiated = new HashMap<>();
/**
* Constructor.
*
* @param annotationClass
* the annotation class
* @param annotationInfo
* the annotation info
*/
AnnotationInvocationHandler(final Class extends Annotation> annotationClass,
final AnnotationInfo annotationInfo) {
this.annotationClass = annotationClass;
this.annotationInfo = annotationInfo;
// Instantiate the annotation parameter values (this loads and gets references for class literals,
// enum constants, etc.)
for (final AnnotationParameterValue apv : annotationInfo.getParameterValues()) {
final Object instantiatedValue = apv.instantiate(annotationInfo.getClassInfo());
if (instantiatedValue == null) {
// Annotations cannot contain null values
throw new IllegalArgumentException("Got null value for annotation parameter " + apv.getName()
+ " of annotation " + annotationInfo.name);
}
this.annotationParameterValuesInstantiated.put(apv.getName(), instantiatedValue);
}
}
/* (non-Javadoc)
* @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method,
* java.lang.Object[])
*/
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) {
final String methodName = method.getName();
final Class>[] paramTypes = method.getParameterTypes();
if ((args == null ? 0 : args.length) != paramTypes.length) {
throw new IllegalArgumentException(
"Wrong number of arguments for " + annotationClass.getName() + "." + methodName + ": got "
+ (args == null ? 0 : args.length) + ", expected " + paramTypes.length);
}
if (args != null && paramTypes.length == 1) {
if ("equals".equals(methodName) && paramTypes[0] == Object.class) {
// equals() needs to function the same as the JDK implementation
// (see src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java in the JDK)
if (this == args[0]) {
return true;
} else if (!annotationClass.isInstance(args[0])) {
return false;
}
final ReflectionUtils reflectionUtils = annotationInfo.scanResult == null
? new ReflectionUtils()
: annotationInfo.scanResult.reflectionUtils;
for (final Entry ent : annotationParameterValuesInstantiated.entrySet()) {
final String paramName = ent.getKey();
final Object paramVal = ent.getValue();
final Object otherParamVal = reflectionUtils.invokeMethod(/* throwException = */ false,
args[0], paramName);
if ((paramVal == null) != (otherParamVal == null)) {
// Annotation values should never be null, but just to be safe
return false;
} else if (paramVal == null && otherParamVal == null) {
return true;
} else if (paramVal == null || !paramVal.equals(otherParamVal)) {
return false;
}
}
return true;
} else {
// .equals(Object) is the only method of an enum that can take one parameter
throw new IllegalArgumentException();
}
} else if (paramTypes.length == 0) {
// Handle .toString(), .hashCode(), .annotationType()
switch (methodName) {
case "toString":
return annotationInfo.toString();
case "hashCode": {
// hashCode() needs to function the same as the JDK implementation
// (see src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java in the JDK)
int result = 0;
for (final Entry ent : annotationParameterValuesInstantiated.entrySet()) {
final String paramName = ent.getKey();
final Object paramVal = ent.getValue();
int paramValHashCode;
if (paramVal == null) {
// Annotation values should never be null, but just to be safe
paramValHashCode = 0;
} else {
final Class> type = paramVal.getClass();
if (!type.isArray()) {
paramValHashCode = paramVal.hashCode();
} else if (type == byte[].class) {
paramValHashCode = Arrays.hashCode((byte[]) paramVal);
} else if (type == char[].class) {
paramValHashCode = Arrays.hashCode((char[]) paramVal);
} else if (type == double[].class) {
paramValHashCode = Arrays.hashCode((double[]) paramVal);
} else if (type == float[].class) {
paramValHashCode = Arrays.hashCode((float[]) paramVal);
} else if (type == int[].class) {
paramValHashCode = Arrays.hashCode((int[]) paramVal);
} else if (type == long[].class) {
paramValHashCode = Arrays.hashCode((long[]) paramVal);
} else if (type == short[].class) {
paramValHashCode = Arrays.hashCode((short[]) paramVal);
} else if (type == boolean[].class) {
paramValHashCode = Arrays.hashCode((boolean[]) paramVal);
} else {
paramValHashCode = Arrays.hashCode((Object[]) paramVal);
}
}
result += (127 * paramName.hashCode()) ^ paramValHashCode;
}
return result;
}
case "annotationType":
return annotationClass;
default:
// Fall through (other method names are used for returning annotation parameter values)
break;
}
} else {
// Throw exception for 2 or more params
throw new IllegalArgumentException();
}
// Instantiate the annotation parameter value (this loads and gets references for class literals,
// enum constants, etc.)
final Object annotationParameterValue = annotationParameterValuesInstantiated.get(methodName);
if (annotationParameterValue == null) {
// Undefined enum constant (enum values cannot be null)
throw new IncompleteAnnotationException(annotationClass, methodName);
}
// Clone any array-typed annotation parameter values, in keeping with the Java Annotation API
final Class> annotationParameterValueClass = annotationParameterValue.getClass();
if (annotationParameterValueClass.isArray()) {
// Handle array types
if (annotationParameterValueClass == String[].class) {
return ((String[]) annotationParameterValue).clone();
} else if (annotationParameterValueClass == byte[].class) {
return ((byte[]) annotationParameterValue).clone();
} else if (annotationParameterValueClass == char[].class) {
return ((char[]) annotationParameterValue).clone();
} else if (annotationParameterValueClass == double[].class) {
return ((double[]) annotationParameterValue).clone();
} else if (annotationParameterValueClass == float[].class) {
return ((float[]) annotationParameterValue).clone();
} else if (annotationParameterValueClass == int[].class) {
return ((int[]) annotationParameterValue).clone();
} else if (annotationParameterValueClass == long[].class) {
return ((long[]) annotationParameterValue).clone();
} else if (annotationParameterValueClass == short[].class) {
return ((short[]) annotationParameterValue).clone();
} else if (annotationParameterValueClass == boolean[].class) {
return ((boolean[]) annotationParameterValue).clone();
} else {
// Handle arrays of nested annotation types
final Object[] arr = (Object[]) annotationParameterValue;
return arr.clone();
}
}
return annotationParameterValue;
}
}
/**
* Convert wrapper arrays to primitive arrays.
*/
void convertWrapperArraysToPrimitiveArrays() {
if (annotationParamValues != null) {
annotationParamValues.convertWrapperArraysToPrimitiveArrays(getClassInfo());
}
}
// -------------------------------------------------------------------------------------------------------------
/* (non-Javadoc)
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
@Override
public int compareTo(final AnnotationInfo o) {
final int diff = this.name.compareTo(o.name);
if (diff != 0) {
return diff;
}
if (annotationParamValues == null && o.annotationParamValues == null) {
return 0;
} else if (annotationParamValues == null) {
return -1;
} else if (o.annotationParamValues == null) {
return 1;
} else {
for (int i = 0,
max = Math.max(annotationParamValues.size(), o.annotationParamValues.size()); i < max; i++) {
if (i >= annotationParamValues.size()) {
return -1;
} else if (i >= o.annotationParamValues.size()) {
return 1;
} else {
final int diff2 = annotationParamValues.get(i).compareTo(o.annotationParamValues.get(i));
if (diff2 != 0) {
return diff2;
}
}
}
}
return 0;
}
/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(final Object obj) {
if (obj == this) {
return true;
} else if (!(obj instanceof AnnotationInfo)) {
return false;
}
final AnnotationInfo other = (AnnotationInfo) obj;
return this.compareTo(other) == 0;
}
/* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
int h = name.hashCode();
if (annotationParamValues != null) {
for (final AnnotationParameterValue e : annotationParamValues) {
h = h * 7 + e.getName().hashCode() * 3 + e.getValue().hashCode();
}
}
return h;
}
@Override
protected void toString(final boolean useSimpleNames, final StringBuilder buf) {
buf.append('@').append(useSimpleNames ? ClassInfo.getSimpleName(name) : name);
final AnnotationParameterValueList paramVals = getParameterValues();
if (!paramVals.isEmpty()) {
buf.append('(');
for (int i = 0; i < paramVals.size(); i++) {
if (i > 0) {
buf.append(", ");
}
final AnnotationParameterValue paramVal = paramVals.get(i);
if (paramVals.size() > 1 || !"value".equals(paramVal.getName())) {
paramVal.toString(useSimpleNames, buf);
} else {
paramVal.toStringParamValueOnly(useSimpleNames, buf);
}
}
buf.append(')');
}
}
}