org.springframework.core.annotation.AttributeMethods Maven / Gradle / Ivy
/*
* Copyright 2002-2022 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.core.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ReflectionUtils;
/**
* Provides a quick way to access the attribute methods of an {@link Annotation}
* with consistent ordering as well as a few useful utility methods.
*
* @author Phillip Webb
* @author Sam Brannen
* @since 5.2
*/
final class AttributeMethods {
static final AttributeMethods NONE = new AttributeMethods(null, new Method[0]);
private static final Map, AttributeMethods> cache =
new ConcurrentReferenceHashMap<>();
private static final Comparator methodComparator = (m1, m2) -> {
if (m1 != null && m2 != null) {
return m1.getName().compareTo(m2.getName());
}
return m1 != null ? -1 : 1;
};
@Nullable
private final Class annotationType;
private final Method[] attributeMethods;
private final boolean[] canThrowTypeNotPresentException;
private final boolean hasDefaultValueMethod;
private final boolean hasNestedAnnotation;
private AttributeMethods(@Nullable Class annotationType, Method[] attributeMethods) {
this.annotationType = annotationType;
this.attributeMethods = attributeMethods;
this.canThrowTypeNotPresentException = new boolean[attributeMethods.length];
boolean foundDefaultValueMethod = false;
boolean foundNestedAnnotation = false;
for (int i = 0; i < attributeMethods.length; i++) {
Method method = this.attributeMethods[i];
Class type = method.getReturnType();
if (!foundDefaultValueMethod && (method.getDefaultValue() != null)) {
foundDefaultValueMethod = true;
}
if (!foundNestedAnnotation && (type.isAnnotation() || (type.isArray() && type.getComponentType().isAnnotation()))) {
foundNestedAnnotation = true;
}
ReflectionUtils.makeAccessible(method);
this.canThrowTypeNotPresentException[i] = (type == Class.class || type == Class[].class || type.isEnum());
}
this.hasDefaultValueMethod = foundDefaultValueMethod;
this.hasNestedAnnotation = foundNestedAnnotation;
}
/**
* Determine if this instance only contains a single attribute named
* {@code value}.
* @return {@code true} if there is only a value attribute
*/
boolean hasOnlyValueAttribute() {
return (this.attributeMethods.length == 1 &&
MergedAnnotation.VALUE.equals(this.attributeMethods[0].getName()));
}
/**
* Determine if values from the given annotation can be safely accessed without
* causing any {@link TypeNotPresentException TypeNotPresentExceptions}.
* @param annotation the annotation to check
* @return {@code true} if all values are present
* @see #validate(Annotation)
*/
boolean isValid(Annotation annotation) {
assertAnnotation(annotation);
for (int i = 0; i < size(); i++) {
if (canThrowTypeNotPresentException(i)) {
try {
get(i).invoke(annotation);
}
catch (Throwable ex) {
return false;
}
}
}
return true;
}
/**
* Check if values from the given annotation can be safely accessed without causing
* any {@link TypeNotPresentException TypeNotPresentExceptions}. In particular,
* this method is designed to cover Google App Engine's late arrival of such
* exceptions for {@code Class} values (instead of the more typical early
* {@code Class.getAnnotations() failure}).
* @param annotation the annotation to validate
* @throws IllegalStateException if a declared {@code Class} attribute could not be read
* @see #isValid(Annotation)
*/
void validate(Annotation annotation) {
assertAnnotation(annotation);
for (int i = 0; i < size(); i++) {
if (canThrowTypeNotPresentException(i)) {
try {
get(i).invoke(annotation);
}
catch (Throwable ex) {
throw new IllegalStateException("Could not obtain annotation attribute value for " +
get(i).getName() + " declared on " + annotation.annotationType(), ex);
}
}
}
}
private void assertAnnotation(Annotation annotation) {
Assert.notNull(annotation, "Annotation must not be null");
if (this.annotationType != null) {
Assert.isInstanceOf(this.annotationType, annotation);
}
}
/**
* Get the attribute with the specified name or {@code null} if no
* matching attribute exists.
* @param name the attribute name to find
* @return the attribute method or {@code null}
*/
@Nullable
Method get(String name) {
int index = indexOf(name);
return index != -1 ? this.attributeMethods[index] : null;
}
/**
* Get the attribute at the specified index.
* @param index the index of the attribute to return
* @return the attribute method
* @throws IndexOutOfBoundsException if the index is out of range
* ({@code index < 0 || index >= size()})
*/
Method get(int index) {
return this.attributeMethods[index];
}
/**
* Determine if the attribute at the specified index could throw a
* {@link TypeNotPresentException} when accessed.
* @param index the index of the attribute to check
* @return {@code true} if the attribute can throw a
* {@link TypeNotPresentException}
*/
boolean canThrowTypeNotPresentException(int index) {
return this.canThrowTypeNotPresentException[index];
}
/**
* Get the index of the attribute with the specified name, or {@code -1}
* if there is no attribute with the name.
* @param name the name to find
* @return the index of the attribute, or {@code -1}
*/
int indexOf(String name) {
for (int i = 0; i < this.attributeMethods.length; i++) {
if (this.attributeMethods[i].getName().equals(name)) {
return i;
}
}
return -1;
}
/**
* Get the index of the specified attribute, or {@code -1} if the
* attribute is not in this collection.
* @param attribute the attribute to find
* @return the index of the attribute, or {@code -1}
*/
int indexOf(Method attribute) {
for (int i = 0; i < this.attributeMethods.length; i++) {
if (this.attributeMethods[i].equals(attribute)) {
return i;
}
}
return -1;
}
/**
* Get the number of attributes in this collection.
* @return the number of attributes
*/
int size() {
return this.attributeMethods.length;
}
/**
* Determine if at least one of the attribute methods has a default value.
* @return {@code true} if there is at least one attribute method with a default value
*/
boolean hasDefaultValueMethod() {
return this.hasDefaultValueMethod;
}
/**
* Determine if at least one of the attribute methods is a nested annotation.
* @return {@code true} if there is at least one attribute method with a nested
* annotation type
*/
boolean hasNestedAnnotation() {
return this.hasNestedAnnotation;
}
/**
* Get the attribute methods for the given annotation type.
* @param annotationType the annotation type
* @return the attribute methods for the annotation type
*/
static AttributeMethods forAnnotationType(@Nullable Class annotationType) {
if (annotationType == null) {
return NONE;
}
return cache.computeIfAbsent(annotationType, AttributeMethods::compute);
}
private static AttributeMethods compute(Class annotationType) {
Method[] methods = annotationType.getDeclaredMethods();
int size = methods.length;
for (int i = 0; i < methods.length; i++) {
if (!isAttributeMethod(methods[i])) {
methods[i] = null;
size--;
}
}
if (size == 0) {
return NONE;
}
Arrays.sort(methods, methodComparator);
Method[] attributeMethods = Arrays.copyOf(methods, size);
return new AttributeMethods(annotationType, attributeMethods);
}
private static boolean isAttributeMethod(Method method) {
return (method.getParameterCount() == 0 && method.getReturnType() != void.class);
}
/**
* Create a description for the given attribute method suitable to use in
* exception messages and logs.
* @param attribute the attribute to describe
* @return a description of the attribute
*/
static String describe(@Nullable Method attribute) {
if (attribute == null) {
return "(none)";
}
return describe(attribute.getDeclaringClass(), attribute.getName());
}
/**
* Create a description for the given attribute method suitable to use in
* exception messages and logs.
* @param annotationType the annotation type
* @param attributeName the attribute name
* @return a description of the attribute
*/
static String describe(@Nullable Class annotationType, @Nullable String attributeName) {
if (attributeName == null) {
return "(none)";
}
String in = (annotationType != null ? " in annotation [" + annotationType.getName() + "]" : "");
return "attribute '" + attributeName + "'" + in;
}
}