001/*
002 * Copyright 2002-2019 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.core.annotation;
018
019import java.lang.annotation.Annotation;
020import java.lang.reflect.Array;
021import java.util.Iterator;
022import java.util.LinkedHashMap;
023import java.util.Map;
024
025import org.springframework.lang.Nullable;
026import org.springframework.util.Assert;
027import org.springframework.util.StringUtils;
028
029/**
030 * {@link LinkedHashMap} subclass representing annotation attribute
031 * <em>key-value</em> pairs as read by {@link AnnotationUtils},
032 * {@link AnnotatedElementUtils}, and Spring's reflection- and ASM-based
033 * {@link org.springframework.core.type.AnnotationMetadata} implementations.
034 *
035 * <p>Provides 'pseudo-reification' to avoid noisy Map generics in the calling
036 * code as well as convenience methods for looking up annotation attributes
037 * in a type-safe fashion.
038 *
039 * @author Chris Beams
040 * @author Sam Brannen
041 * @author Juergen Hoeller
042 * @since 3.1.1
043 * @see AnnotationUtils#getAnnotationAttributes
044 * @see AnnotatedElementUtils
045 */
046@SuppressWarnings("serial")
047public class AnnotationAttributes extends LinkedHashMap<String, Object> {
048
049        private static final String UNKNOWN = "unknown";
050
051        @Nullable
052        private final Class<? extends Annotation> annotationType;
053
054        final String displayName;
055
056        boolean validated = false;
057
058
059        /**
060         * Create a new, empty {@link AnnotationAttributes} instance.
061         */
062        public AnnotationAttributes() {
063                this.annotationType = null;
064                this.displayName = UNKNOWN;
065        }
066
067        /**
068         * Create a new, empty {@link AnnotationAttributes} instance with the
069         * given initial capacity to optimize performance.
070         * @param initialCapacity initial size of the underlying map
071         */
072        public AnnotationAttributes(int initialCapacity) {
073                super(initialCapacity);
074                this.annotationType = null;
075                this.displayName = UNKNOWN;
076        }
077
078        /**
079         * Create a new {@link AnnotationAttributes} instance, wrapping the provided
080         * map and all its <em>key-value</em> pairs.
081         * @param map original source of annotation attribute <em>key-value</em> pairs
082         * @see #fromMap(Map)
083         */
084        public AnnotationAttributes(Map<String, Object> map) {
085                super(map);
086                this.annotationType = null;
087                this.displayName = UNKNOWN;
088        }
089
090        /**
091         * Create a new {@link AnnotationAttributes} instance, wrapping the provided
092         * map and all its <em>key-value</em> pairs.
093         * @param other original source of annotation attribute <em>key-value</em> pairs
094         * @see #fromMap(Map)
095         */
096        public AnnotationAttributes(AnnotationAttributes other) {
097                super(other);
098                this.annotationType = other.annotationType;
099                this.displayName = other.displayName;
100                this.validated = other.validated;
101        }
102
103        /**
104         * Create a new, empty {@link AnnotationAttributes} instance for the
105         * specified {@code annotationType}.
106         * @param annotationType the type of annotation represented by this
107         * {@code AnnotationAttributes} instance; never {@code null}
108         * @since 4.2
109         */
110        public AnnotationAttributes(Class<? extends Annotation> annotationType) {
111                Assert.notNull(annotationType, "'annotationType' must not be null");
112                this.annotationType = annotationType;
113                this.displayName = annotationType.getName();
114        }
115
116        /**
117         * Create a possibly already validated new, empty
118         * {@link AnnotationAttributes} instance for the specified
119         * {@code annotationType}.
120         * @param annotationType the type of annotation represented by this
121         * {@code AnnotationAttributes} instance; never {@code null}
122         * @param validated if the attributes are considered already validated
123         * @since 5.2
124         */
125        AnnotationAttributes(Class<? extends Annotation> annotationType, boolean validated) {
126                Assert.notNull(annotationType, "'annotationType' must not be null");
127                this.annotationType = annotationType;
128                this.displayName = annotationType.getName();
129                this.validated = validated;
130        }
131
132        /**
133         * Create a new, empty {@link AnnotationAttributes} instance for the
134         * specified {@code annotationType}.
135         * @param annotationType the annotation type name represented by this
136         * {@code AnnotationAttributes} instance; never {@code null}
137         * @param classLoader the ClassLoader to try to load the annotation type on,
138         * or {@code null} to just store the annotation type name
139         * @since 4.3.2
140         */
141        public AnnotationAttributes(String annotationType, @Nullable ClassLoader classLoader) {
142                Assert.notNull(annotationType, "'annotationType' must not be null");
143                this.annotationType = getAnnotationType(annotationType, classLoader);
144                this.displayName = annotationType;
145        }
146
147        @SuppressWarnings("unchecked")
148        @Nullable
149        private static Class<? extends Annotation> getAnnotationType(String annotationType, @Nullable ClassLoader classLoader) {
150                if (classLoader != null) {
151                        try {
152                                return (Class<? extends Annotation>) classLoader.loadClass(annotationType);
153                        }
154                        catch (ClassNotFoundException ex) {
155                                // Annotation Class not resolvable
156                        }
157                }
158                return null;
159        }
160
161
162        /**
163         * Get the type of annotation represented by this {@code AnnotationAttributes}.
164         * @return the annotation type, or {@code null} if unknown
165         * @since 4.2
166         */
167        @Nullable
168        public Class<? extends Annotation> annotationType() {
169                return this.annotationType;
170        }
171
172        /**
173         * Get the value stored under the specified {@code attributeName} as a string.
174         * @param attributeName the name of the attribute to get;
175         * never {@code null} or empty
176         * @return the value
177         * @throws IllegalArgumentException if the attribute does not exist or
178         * if it is not of the expected type
179         */
180        public String getString(String attributeName) {
181                return getRequiredAttribute(attributeName, String.class);
182        }
183
184        /**
185         * Get the value stored under the specified {@code attributeName} as an
186         * array of strings.
187         * <p>If the value stored under the specified {@code attributeName} is
188         * a string, it will be wrapped in a single-element array before
189         * returning it.
190         * @param attributeName the name of the attribute to get;
191         * never {@code null} or empty
192         * @return the value
193         * @throws IllegalArgumentException if the attribute does not exist or
194         * if it is not of the expected type
195         */
196        public String[] getStringArray(String attributeName) {
197                return getRequiredAttribute(attributeName, String[].class);
198        }
199
200        /**
201         * Get the value stored under the specified {@code attributeName} as a boolean.
202         * @param attributeName the name of the attribute to get;
203         * never {@code null} or empty
204         * @return the value
205         * @throws IllegalArgumentException if the attribute does not exist or
206         * if it is not of the expected type
207         */
208        public boolean getBoolean(String attributeName) {
209                return getRequiredAttribute(attributeName, Boolean.class);
210        }
211
212        /**
213         * Get the value stored under the specified {@code attributeName} as a number.
214         * @param attributeName the name of the attribute to get;
215         * never {@code null} or empty
216         * @return the value
217         * @throws IllegalArgumentException if the attribute does not exist or
218         * if it is not of the expected type
219         */
220        @SuppressWarnings("unchecked")
221        public <N extends Number> N getNumber(String attributeName) {
222                return (N) getRequiredAttribute(attributeName, Number.class);
223        }
224
225        /**
226         * Get the value stored under the specified {@code attributeName} as an enum.
227         * @param attributeName the name of the attribute to get;
228         * never {@code null} or empty
229         * @return the value
230         * @throws IllegalArgumentException if the attribute does not exist or
231         * if it is not of the expected type
232         */
233        @SuppressWarnings("unchecked")
234        public <E extends Enum<?>> E getEnum(String attributeName) {
235                return (E) getRequiredAttribute(attributeName, Enum.class);
236        }
237
238        /**
239         * Get the value stored under the specified {@code attributeName} as a class.
240         * @param attributeName the name of the attribute to get;
241         * never {@code null} or empty
242         * @return the value
243         * @throws IllegalArgumentException if the attribute does not exist or
244         * if it is not of the expected type
245         */
246        @SuppressWarnings("unchecked")
247        public <T> Class<? extends T> getClass(String attributeName) {
248                return getRequiredAttribute(attributeName, Class.class);
249        }
250
251        /**
252         * Get the value stored under the specified {@code attributeName} as an
253         * array of classes.
254         * <p>If the value stored under the specified {@code attributeName} is a class,
255         * it will be wrapped in a single-element array before returning it.
256         * @param attributeName the name of the attribute to get;
257         * never {@code null} or empty
258         * @return the value
259         * @throws IllegalArgumentException if the attribute does not exist or
260         * if it is not of the expected type
261         */
262        public Class<?>[] getClassArray(String attributeName) {
263                return getRequiredAttribute(attributeName, Class[].class);
264        }
265
266        /**
267         * Get the {@link AnnotationAttributes} stored under the specified
268         * {@code attributeName}.
269         * <p>Note: if you expect an actual annotation, invoke
270         * {@link #getAnnotation(String, Class)} instead.
271         * @param attributeName the name of the attribute to get;
272         * never {@code null} or empty
273         * @return the {@code AnnotationAttributes}
274         * @throws IllegalArgumentException if the attribute does not exist or
275         * if it is not of the expected type
276         */
277        public AnnotationAttributes getAnnotation(String attributeName) {
278                return getRequiredAttribute(attributeName, AnnotationAttributes.class);
279        }
280
281        /**
282         * Get the annotation of type {@code annotationType} stored under the
283         * specified {@code attributeName}.
284         * @param attributeName the name of the attribute to get;
285         * never {@code null} or empty
286         * @param annotationType the expected annotation type; never {@code null}
287         * @return the annotation
288         * @throws IllegalArgumentException if the attribute does not exist or
289         * if it is not of the expected type
290         * @since 4.2
291         */
292        public <A extends Annotation> A getAnnotation(String attributeName, Class<A> annotationType) {
293                return getRequiredAttribute(attributeName, annotationType);
294        }
295
296        /**
297         * Get the array of {@link AnnotationAttributes} stored under the specified
298         * {@code attributeName}.
299         * <p>If the value stored under the specified {@code attributeName} is
300         * an instance of {@code AnnotationAttributes}, it will be wrapped in
301         * a single-element array before returning it.
302         * <p>Note: if you expect an actual array of annotations, invoke
303         * {@link #getAnnotationArray(String, Class)} instead.
304         * @param attributeName the name of the attribute to get;
305         * never {@code null} or empty
306         * @return the array of {@code AnnotationAttributes}
307         * @throws IllegalArgumentException if the attribute does not exist or
308         * if it is not of the expected type
309         */
310        public AnnotationAttributes[] getAnnotationArray(String attributeName) {
311                return getRequiredAttribute(attributeName, AnnotationAttributes[].class);
312        }
313
314        /**
315         * Get the array of type {@code annotationType} stored under the specified
316         * {@code attributeName}.
317         * <p>If the value stored under the specified {@code attributeName} is
318         * an {@code Annotation}, it will be wrapped in a single-element array
319         * before returning it.
320         * @param attributeName the name of the attribute to get;
321         * never {@code null} or empty
322         * @param annotationType the expected annotation type; never {@code null}
323         * @return the annotation array
324         * @throws IllegalArgumentException if the attribute does not exist or
325         * if it is not of the expected type
326         * @since 4.2
327         */
328        @SuppressWarnings("unchecked")
329        public <A extends Annotation> A[] getAnnotationArray(String attributeName, Class<A> annotationType) {
330                Object array = Array.newInstance(annotationType, 0);
331                return (A[]) getRequiredAttribute(attributeName, array.getClass());
332        }
333
334        /**
335         * Get the value stored under the specified {@code attributeName},
336         * ensuring that the value is of the {@code expectedType}.
337         * <p>If the {@code expectedType} is an array and the value stored
338         * under the specified {@code attributeName} is a single element of the
339         * component type of the expected array type, the single element will be
340         * wrapped in a single-element array of the appropriate type before
341         * returning it.
342         * @param attributeName the name of the attribute to get;
343         * never {@code null} or empty
344         * @param expectedType the expected type; never {@code null}
345         * @return the value
346         * @throws IllegalArgumentException if the attribute does not exist or
347         * if it is not of the expected type
348         */
349        @SuppressWarnings("unchecked")
350        private <T> T getRequiredAttribute(String attributeName, Class<T> expectedType) {
351                Assert.hasText(attributeName, "'attributeName' must not be null or empty");
352                Object value = get(attributeName);
353                assertAttributePresence(attributeName, value);
354                assertNotException(attributeName, value);
355                if (!expectedType.isInstance(value) && expectedType.isArray() &&
356                                expectedType.getComponentType().isInstance(value)) {
357                        Object array = Array.newInstance(expectedType.getComponentType(), 1);
358                        Array.set(array, 0, value);
359                        value = array;
360                }
361                assertAttributeType(attributeName, value, expectedType);
362                return (T) value;
363        }
364
365        private void assertAttributePresence(String attributeName, Object attributeValue) {
366                Assert.notNull(attributeValue, () -> String.format(
367                                "Attribute '%s' not found in attributes for annotation [%s]",
368                                attributeName, this.displayName));
369        }
370
371        private void assertNotException(String attributeName, Object attributeValue) {
372                if (attributeValue instanceof Throwable) {
373                        throw new IllegalArgumentException(String.format(
374                                        "Attribute '%s' for annotation [%s] was not resolvable due to exception [%s]",
375                                        attributeName, this.displayName, attributeValue), (Throwable) attributeValue);
376                }
377        }
378
379        private void assertAttributeType(String attributeName, Object attributeValue, Class<?> expectedType) {
380                if (!expectedType.isInstance(attributeValue)) {
381                        throw new IllegalArgumentException(String.format(
382                                        "Attribute '%s' is of type %s, but %s was expected in attributes for annotation [%s]",
383                                        attributeName, attributeValue.getClass().getSimpleName(), expectedType.getSimpleName(),
384                                        this.displayName));
385                }
386        }
387
388        @Override
389        public String toString() {
390                Iterator<Map.Entry<String, Object>> entries = entrySet().iterator();
391                StringBuilder sb = new StringBuilder("{");
392                while (entries.hasNext()) {
393                        Map.Entry<String, Object> entry = entries.next();
394                        sb.append(entry.getKey());
395                        sb.append('=');
396                        sb.append(valueToString(entry.getValue()));
397                        sb.append(entries.hasNext() ? ", " : "");
398                }
399                sb.append("}");
400                return sb.toString();
401        }
402
403        private String valueToString(Object value) {
404                if (value == this) {
405                        return "(this Map)";
406                }
407                if (value instanceof Object[]) {
408                        return "[" + StringUtils.arrayToDelimitedString((Object[]) value, ", ") + "]";
409                }
410                return String.valueOf(value);
411        }
412
413
414        /**
415         * Return an {@link AnnotationAttributes} instance based on the given map.
416         * <p>If the map is already an {@code AnnotationAttributes} instance, it
417         * will be cast and returned immediately without creating a new instance.
418         * Otherwise a new instance will be created by passing the supplied map
419         * to the {@link #AnnotationAttributes(Map)} constructor.
420         * @param map original source of annotation attribute <em>key-value</em> pairs
421         */
422        @Nullable
423        public static AnnotationAttributes fromMap(@Nullable Map<String, Object> map) {
424                if (map == null) {
425                        return null;
426                }
427                if (map instanceof AnnotationAttributes) {
428                        return (AnnotationAttributes) map;
429                }
430                return new AnnotationAttributes(map);
431        }
432
433}