/
AttributeMethods.java
305 lines (268 loc) · 9.4 KB
/
AttributeMethods.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
/*
* 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<Class<? extends Annotation>, AttributeMethods> cache =
new ConcurrentReferenceHashMap<>();
private static final Comparator<Method> methodComparator = (m1, m2) -> {
if (m1 != null && m2 != null) {
return m1.getName().compareTo(m2.getName());
}
return m1 != null ? -1 : 1;
};
@Nullable
private final Class<? extends Annotation> annotationType;
private final Method[] attributeMethods;
private final boolean[] canThrowTypeNotPresentException;
private final boolean hasDefaultValueMethod;
private final boolean hasNestedAnnotation;
private AttributeMethods(@Nullable Class<? extends Annotation> 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 {
AnnotationUtils.invokeAnnotationMethod(get(i), 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 {
AnnotationUtils.invokeAnnotationMethod(get(i), 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<? extends Annotation> annotationType) {
if (annotationType == null) {
return NONE;
}
return cache.computeIfAbsent(annotationType, AttributeMethods::compute);
}
private static AttributeMethods compute(Class<? extends Annotation> 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;
}
}