diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AbstractMergedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/AbstractMergedAnnotation.java new file mode 100644 index 000000000000..0edb680031b8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AbstractMergedAnnotation.java @@ -0,0 +1,226 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.function.Predicate; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Abstract base class for {@link MergedAnnotation} implementations. + * + * @author Phillip Webb + * @since 5.2 + * @param the annotation type + */ +abstract class AbstractMergedAnnotation + implements MergedAnnotation { + + @Nullable + private volatile A synthesizedAnnotation; + + + @Override + public boolean isDirectlyPresent() { + return isPresent() && getDepth() == 0; + } + + @Override + public boolean isMetaPresent() { + return isPresent() && getDepth() > 0; + } + + @Override + public boolean hasNonDefaultValue(String attributeName) { + return !hasDefaultValue(attributeName); + } + + public byte getByte(String attributeName) { + return getRequiredAttributeValue(attributeName, Byte.class); + } + + public byte[] getByteArray(String attributeName) { + return getRequiredAttributeValue(attributeName, byte[].class); + } + + public boolean getBoolean(String attributeName) { + return getRequiredAttributeValue(attributeName, Boolean.class); + } + + public boolean[] getBooleanArray(String attributeName) { + return getRequiredAttributeValue(attributeName, boolean[].class); + } + + public char getChar(String attributeName) { + return getRequiredAttributeValue(attributeName, Character.class); + } + + public char[] getCharArray(String attributeName) { + return getRequiredAttributeValue(attributeName, char[].class); + } + + public short getShort(String attributeName) { + return getRequiredAttributeValue(attributeName, Short.class); + } + + public short[] getShortArray(String attributeName) { + return getRequiredAttributeValue(attributeName, short[].class); + } + + public int getInt(String attributeName) { + return getRequiredAttributeValue(attributeName, Integer.class); + } + + public int[] getIntArray(String attributeName) { + return getRequiredAttributeValue(attributeName, int[].class); + } + + public long getLong(String attributeName) { + return getRequiredAttributeValue(attributeName, Long.class); + } + + public long[] getLongArray(String attributeName) { + return getRequiredAttributeValue(attributeName, long[].class); + } + + public double getDouble(String attributeName) { + return getRequiredAttributeValue(attributeName, Double.class); + } + + public double[] getDoubleArray(String attributeName) { + return getRequiredAttributeValue(attributeName, double[].class); + } + + public float getFloat(String attributeName) { + return getRequiredAttributeValue(attributeName, Float.class); + } + + public float[] getFloatArray(String attributeName) { + return getRequiredAttributeValue(attributeName, float[].class); + } + + public String getString(String attributeName) { + return getRequiredAttributeValue(attributeName, String.class); + } + + public String[] getStringArray(String attributeName) { + return getRequiredAttributeValue(attributeName, String[].class); + } + + public Class getClass(String attributeName) { + return getRequiredAttributeValue(attributeName, Class.class); + } + + public Class[] getClassArray(String attributeName) { + return getRequiredAttributeValue(attributeName, Class[].class); + } + + public > E getEnum(String attributeName, Class type) { + Assert.notNull(type, "Type must not be null"); + return getRequiredAttributeValue(attributeName, type); + } + + @SuppressWarnings("unchecked") + public > E[] getEnumArray(String attributeName, Class type) { + Assert.notNull(type, "Type must not be null"); + Class arrayType = Array.newInstance(type, 0).getClass(); + return (E[]) getRequiredAttributeValue(attributeName, arrayType); + } + + @Override + public Optional getValue(String attributeName) { + return getValue(attributeName, Object.class); + } + + @Override + public Optional getValue(String attributeName, Class type) { + return Optional.ofNullable(getAttributeValue(attributeName, type)); + } + + @Override + public Optional getDefaultValue(String attributeName) { + return getDefaultValue(attributeName, Object.class); + } + + @Override + public MergedAnnotation filterDefaultValues() { + return filterAttributes(this::hasNonDefaultValue); + } + + @Override + public Map asMap(MapValues... options) { + return asMap(null, options); + } + + @Override + public Optional synthesize( + @Nullable Predicate> condition) + throws NoSuchElementException { + + if (condition == null || condition.test(this)) { + return Optional.of(synthesize()); + } + return Optional.empty(); + } + + @Override + public A synthesize() { + if (!isPresent()) { + throw new NoSuchElementException("Unable to synthesize missing annotation"); + } + A synthesized = this.synthesizedAnnotation; + if (synthesized == null) { + synthesized = createSynthesized(); + this.synthesizedAnnotation = synthesized; + } + return synthesized; + } + + private T getRequiredAttributeValue(String attributeName, Class type) { + T value = getAttributeValue(attributeName, type); + if (value == null) { + throw new NoSuchElementException("No attribute named '" + attributeName + + "' present in merged annotation " + getType()); + } + return value; + } + + /** + * Get the underlying attribute value. + * @param attributeName the attribute name + * @param type the type to return (see {@link MergedAnnotation} class + * documentation for details). + * @return the attribute value or {@code null} if the value is not found and + * is not required + * @throws IllegalArgumentException if the source type is not compatible + * @throws NoSuchElementException if the value is required but not found + */ + @Nullable + protected abstract T getAttributeValue(String attributeName, Class type); + + /** + * Factory method used to create the synthesized annotation. + */ + protected abstract A createSynthesized(); + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationFilter.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationFilter.java new file mode 100644 index 000000000000..be2fb4b2b158 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationFilter.java @@ -0,0 +1,179 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collection; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Callback interface that can be used to filter specific annotation types. + * + * @author Phillip Webb + * @since 5.2 + */ +@FunctionalInterface +public interface AnnotationFilter { + + /** + * {@link AnnotationFilter} that matches annotations is in the + * {@code java.lang.*} or in the + * {@code org.springframework.lang.*} package. + */ + static final AnnotationFilter PLAIN = packages("java.lang", + "org.springframework.lang"); + + /** + * {@link AnnotationFilter} that matches annotations in the + * {@code java.lang.*} package. + */ + static final AnnotationFilter JAVA = packages("java.lang"); + + /** + * {@link AnnotationFilter} that never matches and can be used when no + * filtering is needed. + */ + static final AnnotationFilter NONE = new AnnotationFilter() { + + @Override + public boolean matches(@Nullable String typeName) { + return false; + } + + @Override + public String toString() { + return "No annotation filtering"; + } + + }; + + + /** + * Test if the given annotation matches the filter. + * @param annotation the annotation to test + * @return {@code true} if the annotation matches + */ + default boolean matches(@Nullable Annotation annotation) { + return matches(annotation != null ? annotation.annotationType() : null); + } + + /** + * Test if the given type matches the filter. + * @param type the annotation type to test + * @return {@code true} if the annotation matches + */ + default boolean matches(@Nullable Class type) { + return matches(type != null ? type.getName() : null); + } + + /** + * Test if the given type name matches the filter. + * @param typeName the annotation type to test + * @return {@code true} if the annotation matches + */ + boolean matches(@Nullable String typeName); + + /** + * Return a new {@link AnnotationFilter} that matches annotations in the + * specified packages. + * @param packages the annotation packages that should match + * @return a new {@link AnnotationFilter} instance + */ + static AnnotationFilter packages(String... packages) { + return new PackagesAnnotationFilter(packages); + } + + /** + * Return an {@link AnnotationFilter} that is the most appropriate for, and + * will always match the given annotation type. Whenever possible, + * {@link AnnotationFilter#PLAIN} will be returned. + * @param annotationType the annotation type to check + * @return the most appropriate annotation filter + */ + static AnnotationFilter mostAppropriateFor(@Nullable Class annotationType) { + return PLAIN.matches(annotationType) ? NONE : PLAIN; + } + + /** + * Return an {@link AnnotationFilter} that is the most appropriate for, and + * will always match all the given annotation types. Whenever possible, + * {@link AnnotationFilter#PLAIN} will be returned. + * @param annotationTypes the annotation types to check + * @return the most appropriate annotation filter + */ + static AnnotationFilter mostAppropriateFor(Class... annotationTypes) { + Assert.notNull(annotationTypes, "AnnotationTypes must not be null"); + return mostAppropriateFor(Arrays.asList(annotationTypes)); + } + + /** + * Return an {@link AnnotationFilter} that is the most appropriate for, and + * will always match all the given annotation type. Whenever possible, + * {@link AnnotationFilter#PLAIN} will be returned. + * @param annotationType the annotation type to check + * @return the most appropriate annotation filter + */ + static AnnotationFilter mostAppropriateFor(@Nullable String annotationType) { + return PLAIN.matches(annotationType) ? NONE : PLAIN; + } + + /** + * Return an {@link AnnotationFilter} that is the most appropriate for, and + * will always match all the given annotation types. Whenever possible, + * {@link AnnotationFilter#PLAIN} will be returned. + * @param annotationTypes the annotation types to check + * @return the most appropriate annotation filter + */ + static AnnotationFilter mostAppropriateFor(String... annotationTypes) { + Assert.notNull(annotationTypes, "AnnotationTypes must not be null"); + return mostAppropriateFor(Arrays.asList(annotationTypes)); + } + + /** + * Return an {@link AnnotationFilter} that is the most appropriate for, and + * will always match all the given annotation types. Whenever possible, + * {@link AnnotationFilter#PLAIN} will be returned. + * @param annotationTypes the annotation types to check (may be class names + * or class types) + * @return the most appropriate annotation filter + */ + @SuppressWarnings("unchecked") + static AnnotationFilter mostAppropriateFor(Collection annotationTypes) { + Assert.notNull(annotationTypes, "AnnotationTypes must not be null"); + for (Object annotationType : annotationTypes) { + if (annotationType == null) { + continue; + } + Assert.isTrue( + annotationType instanceof Class || annotationType instanceof String, + "AnnotationType must be a Class or String"); + if (annotationType instanceof Class + && PLAIN.matches((Class) annotationType)) { + return NONE; + } + if (annotationType instanceof String + && PLAIN.matches((String) annotationType)) { + return NONE; + } + } + return PLAIN; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java new file mode 100644 index 000000000000..f45ecac45f1c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java @@ -0,0 +1,619 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiFunction; + +import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Provides mapping information for a single annotation (or meta-annotation) in + * the context of a root annotation type. + * + * @author Phillip Webb + * @since 5.2 + * @see AnnotationTypeMappings + */ +class AnnotationTypeMapping { + + @Nullable + private final AnnotationTypeMapping parent; + + private final AnnotationTypeMapping root; + + private final int depth; + + private final Class annotationType; + + @Nullable + private final Annotation annotation; + + private final AttributeMethods attributes; + + private final MirrorSets mirrorSets; + + private final int[] aliasMappings; + + private final int[] conventionMappings; + + private final Map> aliasedBy; + + private final Set claimedAliases = new HashSet<>(); + + + AnnotationTypeMapping(Class annotationType) { + this(null, annotationType, null); + } + + AnnotationTypeMapping(AnnotationTypeMapping parent, Annotation annotation) { + this(parent, annotation.annotationType(), annotation); + } + + AnnotationTypeMapping(@Nullable AnnotationTypeMapping parent, + Class annotationType, @Nullable Annotation annotation) { + + this.parent = parent; + this.root = parent != null ? parent.getRoot() : this; + this.depth = parent == null ? 0 : parent.getDepth() + 1; + this.annotationType = annotationType; + this.annotation = annotation; + this.attributes = AttributeMethods.forAnnotationType(annotationType); + this.mirrorSets = new MirrorSets(); + this.aliasMappings = filledIntArray(this.attributes.size(), -1); + this.conventionMappings = filledIntArray(this.attributes.size(), -1); + this.aliasedBy = resolveAliasedForTargets(); + processAliases(); + addConventionMappings(); + } + + + private Map> resolveAliasedForTargets() { + Map> aliasedBy = new HashMap<>(); + for (int i = 0; i < this.attributes.size(); i++) { + Method attribute = this.attributes.get(i); + AliasFor aliasFor = AnnotationsScanner.getDeclaredAnnotation(attribute, + AliasFor.class); + if (aliasFor != null) { + Method target = resolveAliasTarget(attribute, aliasFor); + aliasedBy.computeIfAbsent(target, key -> new ArrayList<>()).add( + attribute); + } + } + return Collections.unmodifiableMap(aliasedBy); + } + + private Method resolveAliasTarget(Method attribute, AliasFor aliasFor) { + return resolveAliasTarget(attribute, aliasFor, true); + } + + private Method resolveAliasTarget(Method attribute, AliasFor aliasFor, boolean checkAliasPair) { + if (StringUtils.hasText(aliasFor.value()) && + StringUtils.hasText(aliasFor.attribute())) { + throw new AnnotationConfigurationException(String.format( + "In @AliasFor declared on %s, attribute 'attribute' and its alias " + + "'value' are present with values of '%s' and '%s', but " + + "only one is permitted.", + AttributeMethods.describe(attribute), aliasFor.attribute(), + aliasFor.value())); + } + Class targetAnnotation = aliasFor.annotation(); + if (targetAnnotation == Annotation.class) { + targetAnnotation = this.annotationType; + } + String targetAttributeName = aliasFor.attribute(); + if (!StringUtils.hasLength(targetAttributeName)) { + targetAttributeName = aliasFor.value(); + } + if (!StringUtils.hasLength(targetAttributeName)) { + targetAttributeName = attribute.getName(); + } + Method target = AttributeMethods.forAnnotationType(targetAnnotation) + .get(targetAttributeName); + if (target == null) { + if (targetAnnotation == this.annotationType) { + throw new AnnotationConfigurationException(String.format( + "@AliasFor declaration on %s declares an " + + "alias for '%s' which is not present.", + AttributeMethods.describe(attribute), targetAttributeName)); + } + throw new AnnotationConfigurationException(String.format( + "%s is declared as an @AliasFor nonexistent %s.", + StringUtils.capitalize(AttributeMethods.describe(attribute)), + AttributeMethods.describe(targetAnnotation, targetAttributeName))); + } + if (target == attribute) { + throw new AnnotationConfigurationException(String.format( + "@AliasFor declaration on %s points to itself. " + + "Specify 'annotation' to point to a same-named " + + "attribute on a meta-annotation.", + AttributeMethods.describe(attribute))); + } + if (!isCompatibleReturnType(attribute.getReturnType(), target.getReturnType())) { + throw new AnnotationConfigurationException(String.format( + "Misconfigured aliases: %s and %s must declare the same return type.", + AttributeMethods.describe(attribute), + AttributeMethods.describe(target))); + } + if (isAliasPair(target) && checkAliasPair) { + AliasFor targetAliasFor = target.getAnnotation(AliasFor.class); + if (targetAliasFor == null) { + throw new AnnotationConfigurationException( + String.format("%s must be declared as an @AliasFor '%s'.", + StringUtils.capitalize(AttributeMethods.describe(target)), + attribute.getName())); + } + Method mirror = resolveAliasTarget(target, targetAliasFor, false); + if (mirror != attribute) { + throw new AnnotationConfigurationException(String.format( + "%s must be declared as an @AliasFor '%s', not '%s'.", + StringUtils.capitalize(AttributeMethods.describe(target)), + attribute.getName(), mirror.getName())); + } + } + return target; + } + + private boolean isAliasPair(Method target) { + return target.getDeclaringClass().equals(this.annotationType); + } + + private boolean isCompatibleReturnType(Class attributeType, Class targetType) { + return Objects.equals(attributeType, targetType) || + Objects.equals(attributeType, targetType.getComponentType()); + } + + private void processAliases() { + List aliases = new ArrayList<>(); + for (int i = 0; i < this.attributes.size(); i++) { + aliases.clear(); + aliases.add(this.attributes.get(i)); + collectAliases(aliases); + if (aliases.size() > 1) { + processAliases(aliases); + } + } + } + + private void collectAliases(List aliases) { + AnnotationTypeMapping mapping = this; + while (mapping != null) { + int size = aliases.size(); + for (int j = 0; j < size; j++) { + List additional = mapping.aliasedBy.get(aliases.get(j)); + if (additional != null) { + aliases.addAll(additional); + } + } + mapping = mapping.parent; + } + } + + private void processAliases(List aliases) { + int rootAttributeIndex = getFirstRootAttributeIndex(aliases); + AnnotationTypeMapping mapping = this; + while (mapping != null) { + if (rootAttributeIndex != -1 && mapping != this.root) { + for (int i = 0; i < mapping.attributes.size(); i++) { + if (aliases.contains(mapping.attributes.get(i))) { + mapping.aliasMappings[i] = rootAttributeIndex; + } + } + } + mapping.mirrorSets.updateFrom(aliases); + mapping.claimedAliases.addAll(aliases); + mapping = mapping.parent; + } + } + + private int getFirstRootAttributeIndex(Collection aliases) { + AttributeMethods rootAttributes = this.root.getAttributes(); + for (int i = 0; i < rootAttributes.size(); i++) { + if (aliases.contains(rootAttributes.get(i))) { + return i; + } + } + return -1; + } + + private void addConventionMappings() { + if (this.depth == 0) { + return; + } + AttributeMethods rootAttributes = this.root.getAttributes(); + int[] mappings = this.conventionMappings; + for (int i = 0; i < mappings.length; i++) { + String name = this.attributes.get(i).getName(); + MirrorSet mirrors = getMirrorSets().getAssigned(i); + int mapped = rootAttributes.indexOf(name); + if (!MergedAnnotation.VALUE.equals(name) && mapped != -1) { + mappings[i] = mapped; + if (mirrors != null) { + for (int j = 0; j < mirrors.size(); j++) { + mappings[mirrors.getAttributeIndex(j)] = mapped; + } + } + } + } + } + + /** + * Method called after all mappings have been set. At this point no further + * lookups from child mappings will occur. + */ + void afterAllMappingsSet() { + validateAllAliasesClaimed(); + for (int i = 0; i < this.mirrorSets.size(); i++) { + validateMirrorSet(this.mirrorSets.get(i)); + } + this.claimedAliases.clear(); + } + + private void validateAllAliasesClaimed() { + for (int i = 0; i < this.attributes.size(); i++) { + Method attribute = this.attributes.get(i); + AliasFor aliasFor = AnnotationsScanner.getDeclaredAnnotation(attribute, AliasFor.class); + if (aliasFor != null && !this.claimedAliases.contains(attribute)) { + Method target = resolveAliasTarget(attribute, aliasFor); + throw new AnnotationConfigurationException(String.format( + "@AliasFor declaration on %s declares an alias for %s which is not meta-present.", + AttributeMethods.describe(attribute), + AttributeMethods.describe(target))); + } + } + } + + private void validateMirrorSet(MirrorSet mirrorSet) { + Method firstAttribute = mirrorSet.get(0); + Object firstDefaultValue = firstAttribute.getDefaultValue(); + for (int i = 1; i <= mirrorSet.size() - 1; i++) { + Method mirrorAttribute = mirrorSet.get(i); + Object mirrorDefaultValue = mirrorAttribute.getDefaultValue(); + if (firstDefaultValue == null || mirrorDefaultValue == null) { + throw new AnnotationConfigurationException(String.format( + "Misconfigured aliases: %s and %s must declare default values.", + AttributeMethods.describe(firstAttribute), + AttributeMethods.describe(mirrorAttribute))); + } + if (!ObjectUtils.nullSafeEquals(firstDefaultValue, mirrorDefaultValue)) { + throw new AnnotationConfigurationException(String.format( + "Misconfigured aliases: %s and %s must declare the same default value.", + AttributeMethods.describe(firstAttribute), + AttributeMethods.describe(mirrorAttribute))); + } + } + } + + /** + * Return the root mapping. + * @return the root mapping + */ + AnnotationTypeMapping getRoot() { + return this.root; + } + + /** + * Return the parent mapping or {@code null}. + * @return the parent mapping + */ + @Nullable + AnnotationTypeMapping getParent() { + return this.parent; + } + + /** + * Return the depth of this mapping. + * @return the depth of the mapping + */ + int getDepth() { + return this.depth; + } + + /** + * Return the type of the mapped annotation. + * @return the annotation type + */ + Class getAnnotationType() { + return this.annotationType; + } + + /** + * Return the source annotation for this mapping. This will be the + * meta-annotation, or {@code null} if this is the root mapping. + * @return the source annotation of the mapping + */ + @Nullable + Annotation getAnnotation() { + return this.annotation; + } + + /** + * Return the annotation attributes for the mapping annotation type. + * @return the attribute methods + */ + AttributeMethods getAttributes() { + return this.attributes; + } + + /** + * Return the related index of an alias mapped attribute, or {@code -1} if + * there is no mapping. The resulting value is the index of the attribute on + * the root annotation that can be invoked in order to obtain the actual + * value. + * @param attributeIndex the attribute index of the source attribute + * @return the mapped attribute index or {@code -1} + */ + int getAliasMapping(int attributeIndex) { + return this.aliasMappings[attributeIndex]; + } + + /** + * Return the related index of a convention mapped attribute, or {@code -1} + * if there is no mapping. The resulting value is the index of the attribute + * on the root annotation that can be invoked in order to obtain the actual + * value. + * @param attributeIndex the attribute index of the source attribute + * @return the mapped attribute index or {@code -1} + */ + int getConventionMapping(int attributeIndex) { + return this.conventionMappings[attributeIndex]; + } + + /** + * Return if the specified value is equivalent to the default value of the + * attribute at the given index. + * @param attributeIndex the attribute index of the source attribute + * @param value the value to check + * @param valueExtractor the value extractor used to extract value from any + * nested annotations + * @return {@code true} if the value is equivalent to the default value + */ + boolean isEquivalentToDefaultValue(int attributeIndex, Object value, + BiFunction valueExtractor) { + Method attribute = this.attributes.get(attributeIndex); + return isEquivalentToDefaultValue(attribute, value, valueExtractor); + } + + /** + * Return the mirror sets for this type mapping. + * @return the mirrorSets the attribute mirror sets. + */ + MirrorSets getMirrorSets() { + return this.mirrorSets; + } + + private static int[] filledIntArray(int size, int value) { + int[] array = new int[size]; + Arrays.fill(array, value); + return array; + } + + private static boolean isEquivalentToDefaultValue(Method attribute, Object value, + BiFunction valueExtractor) { + return areEquivalent(attribute.getDefaultValue(), value, valueExtractor); + } + + private static boolean areEquivalent(@Nullable Object value, + @Nullable Object extractedValue, + BiFunction valueExtractor) { + if (ObjectUtils.nullSafeEquals(value, extractedValue)) { + return true; + } + if (value instanceof Class && extractedValue instanceof String) { + return areEquivalent((Class) value, (String) extractedValue); + } + if (value instanceof Class[] && extractedValue instanceof String[]) { + return areEquivalent((Class[]) value, (String[]) extractedValue); + } + if (value instanceof Annotation) { + return areEquivalent((Annotation) value, extractedValue, valueExtractor); + } + return false; + } + + private static boolean areEquivalent(Class[] value, String[] extractedValue) { + if (value.length != extractedValue.length) { + return false; + } + for (int i = 0; i < value.length; i++) { + if (!areEquivalent(value[i], extractedValue[i])) { + return false; + } + } + return true; + } + + private static boolean areEquivalent(Class value, String extractedValue) { + return value.getName().equals(extractedValue); + } + + private static boolean areEquivalent(Annotation value, @Nullable Object extractedValue, + BiFunction valueExtractor) { + AttributeMethods attributes = AttributeMethods.forAnnotationType( + value.annotationType()); + for (int i = 0; i < attributes.size(); i++) { + Method attribute = attributes.get(i); + if (!areEquivalent(ReflectionUtils.invokeMethod(attribute, value), + valueExtractor.apply(attribute, extractedValue), valueExtractor)) { + return false; + } + } + return true; + } + + + /** + * A collection of {@link MirrorSet} instances that provides details of all + * defined mirrors. + */ + class MirrorSets { + + private MirrorSet[] mirrorSets; + + private final MirrorSet[] assigned; + + + MirrorSets() { + this.assigned = new MirrorSet[attributes.size()]; + this.mirrorSets = new MirrorSet[0]; + } + + + void updateFrom(Collection aliases) { + MirrorSet mirrorSet = null; + int size = 0; + int last = -1; + for (int i = 0; i < attributes.size(); i++) { + Method attribute = attributes.get(i); + if (aliases.contains(attribute)) { + size++; + if (size > 1) { + if (mirrorSet == null) { + mirrorSet = new MirrorSet(); + this.assigned[last] = mirrorSet; + } + this.assigned[i] = mirrorSet; + } + last = i; + } + } + if (mirrorSet != null) { + mirrorSet.update(); + LinkedHashSet unique = new LinkedHashSet<>( + Arrays.asList(this.assigned)); + unique.remove(null); + this.mirrorSets = unique.toArray(new MirrorSet[0]); + } + } + + int size() { + return this.mirrorSets.length; + } + + MirrorSet get(int index) { + return this.mirrorSets[index]; + } + + @Nullable + MirrorSet getAssigned(int attributeIndex) { + return this.assigned[attributeIndex]; + } + + int[] resolve(@Nullable Object source, @Nullable Object annotation, + BiFunction valueExtractor) { + + int[] result = new int[attributes.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = i; + } + for (int i = 0; i < size(); i++) { + MirrorSet mirrorSet = get(i); + int resolved = mirrorSet.resolve(source, annotation, valueExtractor); + for (int j = 0; j < mirrorSet.size; j++) { + result[mirrorSet.indexes[j]] = resolved; + } + } + return result; + } + + + /** + * A single set of mirror attributes. + */ + class MirrorSet { + + private int size; + + private final int[] indexes = new int[attributes.size()]; + + + void update() { + this.size = 0; + Arrays.fill(this.indexes, -1); + for (int i = 0; i < MirrorSets.this.assigned.length; i++) { + if (MirrorSets.this.assigned[i] == this) { + this.indexes[this.size] = i; + this.size++; + } + } + } + + int resolve(@Nullable Object source, @Nullable A annotation, + BiFunction valueExtractor) { + + int result = -1; + Object lastValue = null; + for (int i = 0; i < this.size; i++) { + Method attribute = attributes.get(this.indexes[i]); + Object value = valueExtractor.apply(attribute, annotation); + boolean isDefaultValue = value == null || isEquivalentToDefaultValue( + attribute, value, valueExtractor); + if (isDefaultValue || ObjectUtils.nullSafeEquals(lastValue, value)) { + continue; + } + if (lastValue != null && + !ObjectUtils.nullSafeEquals(lastValue, value)) { + String on = (source != null) ? " declared on " + source : ""; + throw new AnnotationConfigurationException(String.format( + "Different @AliasFor mirror values for annotation [%s]%s, " + + "attribute '%s' and its alias '%s' are declared with values of [%s] and [%s].", + getAnnotationType().getName(), on, + attributes.get(result).getName(), + attribute.getName(), + ObjectUtils.nullSafeToString(lastValue), + ObjectUtils.nullSafeToString(value))); + } + result = this.indexes[i]; + lastValue = value; + } + return result; + } + + int size() { + return this.size; + } + + Method get(int index) { + int attributeIndex = this.indexes[index]; + return attributes.get(attributeIndex); + } + + int getAttributeIndex(int index) { + return this.indexes[index]; + } + + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java new file mode 100644 index 000000000000..eae514147132 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java @@ -0,0 +1,239 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; + +/** + * Provides {@link AnnotationTypeMapping} information for a single source + * annotation type. Performs a recursive breadth first crawl of all + * meta-annotations to ultimately provide a quick way to map the attributes of + * root {@link Annotation}. + * + *

Supports convention based merging of meta-annotations as well as implicit + * and explicit {@link AliasFor @AliasFor} aliases. Also provide information + * about mirrored attributes. + * + *

This class is designed to be cached so that meta-annotations only need to + * be searched once, regardless of how many times they are actually used. + * + * @author Phillip Webb + * @since 5.2 + * @see AnnotationTypeMapping + */ +final class AnnotationTypeMappings { + + private static final IntrospectionFailureLogger failureLogger = IntrospectionFailureLogger.DEBUG; + + private static final Map cache = new ConcurrentReferenceHashMap<>(); + + + private final AnnotationFilter filter; + + private final List mappings; + + + private AnnotationTypeMappings(AnnotationFilter filter, + Class annotationType) { + this.filter = filter; + this.mappings = new ArrayList<>(); + addAllMappings(annotationType); + this.mappings.forEach(AnnotationTypeMapping::afterAllMappingsSet); + } + + + private void addAllMappings(Class annotationType) { + Deque queue = new ArrayDeque<>(); + addIfPossible(queue, null, annotationType, null); + while (!queue.isEmpty()) { + AnnotationTypeMapping mapping = queue.removeFirst(); + this.mappings.add(mapping); + addMetaAnnotationsToQueue(queue, mapping); + } + } + + private void addMetaAnnotationsToQueue(Deque queue, + AnnotationTypeMapping parent) { + + Annotation[] metaAnnotations = AnnotationsScanner.getDeclaredAnnotations( + parent.getAnnotationType(), false); + for (Annotation metaAnnotation : metaAnnotations) { + if (!isMappable(parent, metaAnnotation)) { + continue; + } + Annotation[] repeatedAnnotations = RepeatableContainers.standardRepeatables() + .findRepeatedAnnotations(metaAnnotation); + if (repeatedAnnotations != null) { + for (Annotation repeatedAnnotation : repeatedAnnotations) { + if (!isMappable(parent, metaAnnotation)) { + continue; + } + addIfPossible(queue, parent, repeatedAnnotation); + } + } + else { + addIfPossible(queue, parent, metaAnnotation); + } + } + } + + private void addIfPossible(Deque queue, + AnnotationTypeMapping parent, Annotation annotation) { + addIfPossible(queue, parent, annotation.annotationType(), annotation); + } + + private void addIfPossible(Deque queue, + @Nullable AnnotationTypeMapping parent, + Class annotationType, @Nullable Annotation annotation) { + + try { + queue.addLast(new AnnotationTypeMapping(parent, annotationType, annotation)); + } + catch (Exception ex) { + if (ex instanceof AnnotationConfigurationException) { + throw (AnnotationConfigurationException) ex; + } + if (failureLogger.isEnabled()) { + failureLogger.log( + "Failed to introspect meta-annotation " + + annotationType.getName(), + (parent != null) ? parent.getAnnotationType() : null, ex); + } + } + } + + private boolean isMappable(AnnotationTypeMapping parent, Annotation metaAnnotation) { + return !this.filter.matches(metaAnnotation) && + !AnnotationFilter.PLAIN.matches(parent.getAnnotationType()) && + !isAlreadyMapped(parent, metaAnnotation); + } + + private boolean isAlreadyMapped(AnnotationTypeMapping parent, + Annotation metaAnnotation) { + + Class annotationType = metaAnnotation.annotationType(); + AnnotationTypeMapping mapping = parent; + while (mapping != null) { + if (mapping.getAnnotationType().equals(annotationType)) { + return true; + } + mapping = mapping.getParent(); + } + return false; + } + + /** + * Return the total number of contained mappings. + * @return the total number of mappings + */ + int size() { + return this.mappings.size(); + } + + /** + * Return an individual mapping from this instance. Index {@code 0} will + * always be return the root mapping, higer indexes will return + * meta-annotation mappings. + * @param index the index to return + * @return the {@link AnnotationTypeMapping} + * @throws IndexOutOfBoundsException if the index is out of range + * (index < 0 || index >= size()) + */ + AnnotationTypeMapping get(int index) { + return this.mappings.get(index); + } + + /** + * Return {@link AnnotationTypeMappings} for the specified annotation type. + * @param annotationType the source annotation type + * @return type mappings for the annotation type + */ + static AnnotationTypeMappings forAnnotationType( + Class annotationType) { + + return forAnnotationType(annotationType, + AnnotationFilter.mostAppropriateFor(annotationType)); + } + + /** + * Return {@link AnnotationTypeMappings} for the specified annotation type. + * @param annotationType the source annotation type + * @param annotationFilter the annotation filter used to limit which + * annotations are considered + * @return type mappings for the annotation type + */ + static AnnotationTypeMappings forAnnotationType( + Class annotationType, + AnnotationFilter annotationFilter) { + + Assert.notNull(annotationType, "AnnotationType must not be null"); + Assert.notNull(annotationFilter, "AnnotationFilter must not be null"); + return cache.computeIfAbsent(annotationFilter, Cache::new).get(annotationType); + } + + static void clearCache() { + cache.clear(); + } + + + /** + * Cache created per {@link AnnotationFilter}. + */ + private static class Cache { + + private final AnnotationFilter filter; + + private final Map, AnnotationTypeMappings> mappings; + + + /** + * Create a cache instance with the specified filter. + * @param filter the annotation filter + */ + Cache(AnnotationFilter filter) { + this.filter = filter; + this.mappings = new ConcurrentReferenceHashMap<>(); + } + + + /** + * Return or create {@link AnnotationTypeMappings} for the specified + * annotation type. + * @param annotationType the annotation type + * @return a new or existing {@link AnnotationTypeMapping} instance + */ + AnnotationTypeMappings get(Class annotationType) { + return this.mappings.computeIfAbsent(annotationType, this::createMappings); + } + + AnnotationTypeMappings createMappings( + Class annotationType) { + return new AnnotationTypeMappings(this.filter, annotationType); + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsProcessor.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsProcessor.java new file mode 100644 index 000000000000..19b2e01d57c8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsProcessor.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; + +import org.springframework.lang.Nullable; + +/** + * Callback interface used to process annotations. + * + * @param the context type + * @param the result type + * @author Phillip Webb + * @see AnnotationsScanner + * @see TypeMappedAnnotations + */ +@FunctionalInterface +interface AnnotationsProcessor { + + /** + * Called when an aggregate is about to be processed. This method may return + * a {@code non-null} result to short-circuit any further processing. + * @param context context information relevant to the processor + * @param aggregateIndex the aggregate index about to be processed + * @return a {@code non-null} result if no further processing is required + */ + @Nullable + default R doWithAggregate(C context, int aggregateIndex) { + return null; + } + + /** + * Called when an array of annotations can be processed. This method may + * return a {@code non-null} result to short-circuit any further processing. + * @param context context information relevant to the processor + * @param aggregateIndex the aggregate index of the provided annotations + * @param source the original source of the annotations, if known + * @param annotations the annotations to process (this array may contain + * {@code null} elements) + * @return a {@code non-null} result if no further processing is required + */ + @Nullable + R doWithAnnotations(C context, int aggregateIndex, @Nullable Object source, + Annotation[] annotations); + + /** + * Return the final result to be returned. By default this method returns + * the last process result. + * @param result the last early exit result, or {@code null}. + * @return the final result to be returned to the caller + */ + @Nullable + default R finish(@Nullable R result) { + return result; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java new file mode 100644 index 000000000000..edc240a80307 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java @@ -0,0 +1,552 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Map; +import java.util.function.BiPredicate; + +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.lang.Nullable; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Scanner to search for relevant annotations on the hierarchy of an + * {@link AnnotatedElement}. + * + * @author Phillip Webb + * @since 5.2 + * @see AnnotationsProcessor + */ +abstract class AnnotationsScanner { + + private static final Annotation[] NO_ANNOTATIONS = {}; + + private static final Method[] NO_METHODS = {}; + + + private static final Map declaredAnnotationCache = + new ConcurrentReferenceHashMap<>(256); + + private static final Map, Method[]> baseTypeMethodsCache = + new ConcurrentReferenceHashMap<>(256); + + + private AnnotationsScanner() { + } + + + /** + * Scan the hierarchy of the specified element for relevant annotations and + * call the processor as required. + * @param context an optional context object that will be passed back to the + * processor + * @param source the source element to scan + * @param searchStrategy the search strategy to use + * @param processor the processor that receives the annotations + * @return the result of {@link AnnotationsProcessor#finish(Object)} + */ + @Nullable + static R scan(C context, AnnotatedElement source, + SearchStrategy searchStrategy, AnnotationsProcessor processor) { + return scan(context, source, searchStrategy, processor, null); + } + + /** + * Scan the hierarchy of the specified element for relevant annotations and + * call the processor as required. + * @param context an optional context object that will be passed back to the + * processor + * @param source the source element to scan + * @param searchStrategy the search strategy to use + * @param processor the processor that receives the annotations + * @param classFilter an optional filter that can be used to entirely filter + * out a specific class from the hierarchy + * @return the result of {@link AnnotationsProcessor#finish(Object)} + */ + @Nullable + static R scan(C context, AnnotatedElement source, + SearchStrategy searchStrategy, AnnotationsProcessor processor, + @Nullable BiPredicate> classFilter) { + + R result = process(context, source, searchStrategy, processor, classFilter); + return processor.finish(result); + } + + @Nullable + private static R process(C context, AnnotatedElement source, + SearchStrategy searchStrategy, AnnotationsProcessor processor, + @Nullable BiPredicate> classFilter) { + + if (source instanceof Class) { + return processClass(context, (Class) source, searchStrategy, processor, classFilter); + } + if (source instanceof Method) { + return processMethod(context, (Method) source, searchStrategy, processor, classFilter); + } + return processElement(context, source, processor, classFilter); + } + + @Nullable + private static R processClass(C context, Class source, + SearchStrategy searchStrategy, AnnotationsProcessor processor, + @Nullable BiPredicate> classFilter) { + + switch (searchStrategy) { + case DIRECT: + return processElement(context, source, + processor, classFilter); + case INHERITED_ANNOTATIONS: + return processClassInheritedAnnotations(context, source, + processor, classFilter); + case SUPER_CLASS: + return processClassHierarchy(context, new int[] { 0 }, source, + processor, classFilter, false); + case EXHAUSTIVE: + return processClassHierarchy(context, new int[] { 0 }, source, + processor, classFilter, true); + } + throw new IllegalStateException("Unsupported search strategy " + searchStrategy); + } + + @Nullable + private static R processClassInheritedAnnotations(C context, Class source, + AnnotationsProcessor processor, + @Nullable BiPredicate> classFilter) { + + if (isWithoutHierarchy(source)) { + return processElement(context, source, processor, classFilter); + } + Annotation[] relevant = null; + int remaining = Integer.MAX_VALUE; + int aggregateIndex = 0; + Class root = source; + while (source != null && source != Object.class + && !hasPlainJavaAnnotationsOnly(source) && remaining > 0) { + R result = processor.doWithAggregate(context, aggregateIndex); + if (result != null) { + return result; + } + if (isFiltered(source, context, classFilter)) { + continue; + } + Annotation[] declaredAnnotations = + getDeclaredAnnotations(context, source, classFilter, true); + if (relevant == null && declaredAnnotations.length > 0) { + relevant = root.getAnnotations(); + remaining = relevant.length; + } + for (int i = 0; i < declaredAnnotations.length; i++) { + if (declaredAnnotations[i] != null) { + boolean isRelevant = false; + for (int relevantIndex = 0; relevantIndex < relevant.length; relevantIndex++) { + if (relevant[relevantIndex] != null && + declaredAnnotations[i].annotationType() == relevant[relevantIndex].annotationType()) { + isRelevant = true; + relevant[relevantIndex] = null; + remaining--; + break; + } + } + if (!isRelevant) { + declaredAnnotations[i] = null; + } + } + } + result = processor.doWithAnnotations(context, aggregateIndex, source, declaredAnnotations); + if (result != null) { + return result; + } + source = source.getSuperclass(); + aggregateIndex++; + } + return null; + } + + @Nullable + private static R processClassHierarchy(C context, int[] aggregateIndex, + Class source, AnnotationsProcessor processor, + @Nullable BiPredicate> classFilter, boolean includeInterfaces) { + + R result = processor.doWithAggregate(context, aggregateIndex[0]); + if (result != null) { + return result; + } + if (hasPlainJavaAnnotationsOnly(source)) { + return null; + } + Annotation[] annotations = getDeclaredAnnotations(context, source, classFilter, false); + result = processor.doWithAnnotations(context, aggregateIndex[0], source, annotations); + if (result != null) { + return result; + } + aggregateIndex[0]++; + if (includeInterfaces) { + for (Class interfaceType : source.getInterfaces()) { + R interfacesResult = processClassHierarchy(context, aggregateIndex, + interfaceType, processor, classFilter, includeInterfaces); + if (interfacesResult != null) { + return interfacesResult; + } + } + } + Class superclass = source.getSuperclass(); + if (superclass != Object.class && superclass != null) { + R superclassResult = processClassHierarchy(context, aggregateIndex, + superclass, processor, classFilter, includeInterfaces); + if (superclassResult != null) { + return superclassResult; + } + } + return null; + } + + @Nullable + private static R processMethod(C context, Method source, + SearchStrategy searchStrategy, AnnotationsProcessor processor, + @Nullable BiPredicate> classFilter) { + + switch (searchStrategy) { + case DIRECT: + case INHERITED_ANNOTATIONS: + return processMethodInheritedAnnotations(context, source, + processor, classFilter); + case SUPER_CLASS: + return processMethodHierarchy(context, new int[] { 0 }, source.getDeclaringClass(), + processor, classFilter, source, false); + case EXHAUSTIVE: + return processMethodHierarchy(context, new int[] { 0 }, source.getDeclaringClass(), + processor, classFilter, source, true); + } + throw new IllegalStateException("Unsupported search strategy " + searchStrategy); + } + + @Nullable + private static R processMethodInheritedAnnotations(C context, Method source, + AnnotationsProcessor processor, + @Nullable BiPredicate> classFilter) { + + R result = processor.doWithAggregate(context, 0); + return result != null ? result : + processMethodAnnotations(context, 0, source, processor, classFilter); + } + + @Nullable + private static R processMethodHierarchy(C context, int[] aggregateIndex, + Class sourceClass, AnnotationsProcessor processor, + @Nullable BiPredicate> classFilter, Method rootMethod, + boolean includeInterfaces) { + + R result = processor.doWithAggregate(context, aggregateIndex[0]); + if (result != null) { + return result; + } + if (hasPlainJavaAnnotationsOnly(sourceClass)) { + return null; + } + boolean calledProcessor = false; + if (sourceClass == rootMethod.getDeclaringClass()) { + result = processMethodAnnotations(context, aggregateIndex[0], + rootMethod, processor, classFilter); + calledProcessor = true; + if (result != null) { + return result; + } + } + else { + for (Method candidateMethod : getBaseTypeMethods(context, sourceClass, classFilter)) { + if (candidateMethod != null && isOverride(rootMethod, candidateMethod)) { + result = processMethodAnnotations(context, aggregateIndex[0], + candidateMethod, processor, classFilter); + calledProcessor = true; + if (result != null) { + return result; + } + } + } + } + if (Modifier.isPrivate(rootMethod.getModifiers())) { + return null; + } + if (calledProcessor) { + aggregateIndex[0]++; + } + if (includeInterfaces) { + for (Class interfaceType : sourceClass.getInterfaces()) { + R interfacesResult = processMethodHierarchy(context, aggregateIndex, + interfaceType, processor, classFilter, rootMethod, includeInterfaces); + if (interfacesResult != null) { + return interfacesResult; + } + } + } + Class superclass = sourceClass.getSuperclass(); + if (superclass != Object.class && superclass != null) { + R superclassResult = processMethodHierarchy(context, aggregateIndex, + superclass, processor, classFilter, rootMethod, includeInterfaces); + if (superclassResult != null) { + return superclassResult; + } + } + return null; + } + + private static Method[] getBaseTypeMethods(C context, Class baseType, + @Nullable BiPredicate> classFilter) { + + if (baseType == Object.class || hasPlainJavaAnnotationsOnly(baseType) || + isFiltered(baseType, context, classFilter)) { + return NO_METHODS; + } + + Method[] methods = baseTypeMethodsCache.get(baseType); + if (methods == null) { + boolean isInterface = baseType.isInterface(); + methods = isInterface ? baseType.getMethods() + : ReflectionUtils.getDeclaredMethods(baseType); + int cleared = 0; + for (int i = 0; i < methods.length; i++) { + if ((!isInterface && Modifier.isPrivate(methods[i].getModifiers())) || + hasPlainJavaAnnotationsOnly(methods[i]) || + getDeclaredAnnotations(methods[i], false).length == 0) { + methods[i] = null; + cleared++; + } + } + if (cleared == methods.length) { + methods = NO_METHODS; + } + baseTypeMethodsCache.put(baseType, methods); + } + return methods; + } + + private static boolean isOverride(Method rootMethod, Method candidateMethod) { + return !Modifier.isPrivate(candidateMethod.getModifiers()) && + candidateMethod.getName().equals(rootMethod.getName()) && + hasSameParameterTypes(rootMethod, candidateMethod); + } + + private static boolean hasSameParameterTypes(Method rootMethod, + Method candidateMethod) { + + if (candidateMethod.getParameterCount() != rootMethod.getParameterCount()) { + return false; + } + Class[] rootParameterTypes = rootMethod.getParameterTypes(); + Class[] candidateParameterTypes = candidateMethod.getParameterTypes(); + if (Arrays.equals(candidateParameterTypes, rootParameterTypes)) { + return true; + } + return hasSameGenericTypeParameters(rootMethod, candidateMethod, + rootParameterTypes); + } + + private static boolean hasSameGenericTypeParameters(Method rootMethod, + Method candidateMethod, Class[] rootParameterTypes) { + + Class sourceDeclaringClass = rootMethod.getDeclaringClass(); + Class candidateDeclaringClass = candidateMethod.getDeclaringClass(); + if (!candidateDeclaringClass.isAssignableFrom(sourceDeclaringClass)) { + return false; + } + for (int i = 0; i < rootParameterTypes.length; i++) { + Class resolvedParameterType = ResolvableType.forMethodParameter( + candidateMethod, i, sourceDeclaringClass).resolve(); + if (rootParameterTypes[i] != resolvedParameterType) { + return false; + } + } + return true; + } + + @Nullable + private static R processMethodAnnotations(C context, int aggregateIndex, + Method source, AnnotationsProcessor processor, + @Nullable BiPredicate> classFilter) { + + Annotation[] annotations = getDeclaredAnnotations(context, source, classFilter, + false); + R result = processor.doWithAnnotations(context, aggregateIndex, source, + annotations); + if (result != null) { + return result; + } + Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(source); + if (bridgedMethod != source) { + Annotation[] bridgedAnnotations = getDeclaredAnnotations(context, + bridgedMethod, classFilter, true); + for (int i = 0; i < bridgedAnnotations.length; i++) { + if (ObjectUtils.containsElement(annotations, bridgedAnnotations[i])) { + bridgedAnnotations[i] = null; + } + } + return processor.doWithAnnotations(context, aggregateIndex, source, + bridgedAnnotations); + } + return null; + } + + @Nullable + private static R processElement(C context, AnnotatedElement source, + AnnotationsProcessor processor, + @Nullable BiPredicate> classFilter) { + + R result = processor.doWithAggregate(context, 0); + return result != null ? result + : processor.doWithAnnotations(context, 0, source, + getDeclaredAnnotations(context, source, classFilter, false)); + } + + private static Annotation[] getDeclaredAnnotations(C context, + AnnotatedElement source, @Nullable BiPredicate> classFilter, + boolean copy) { + + if (source instanceof Class && + isFiltered((Class) source, context, classFilter)) { + return NO_ANNOTATIONS; + } + if (source instanceof Method && + isFiltered(((Method) source).getDeclaringClass(), context, classFilter)) { + return NO_ANNOTATIONS; + } + return getDeclaredAnnotations(source, copy); + } + + @SuppressWarnings("unchecked") + @Nullable + static A getDeclaredAnnotation(AnnotatedElement source, + Class annotationType) { + + Annotation[] annotations = getDeclaredAnnotations(source, false); + for (int i = 0; i < annotations.length; i++) { + if (annotations[i] != null && + annotationType.equals(annotations[i].annotationType())) { + return (A) annotations[i]; + } + } + return null; + } + + static Annotation[] getDeclaredAnnotations(AnnotatedElement source, + boolean defensive) { + + boolean cached = true; + Annotation[] annotations = declaredAnnotationCache.get(source); + if (annotations == null) { + annotations = source.getDeclaredAnnotations(); + if (annotations.length != 0) { + boolean allIgnored = true; + for (int i = 0; i < annotations.length; i++) { + Annotation annotation = annotations[i]; + if (isIgnorable(annotation.annotationType()) || + !AttributeMethods.forAnnotationType( + annotation.annotationType()).isValid(annotation)) { + annotations[i] = null; + } + else { + allIgnored = false; + } + } + annotations = allIgnored ? NO_ANNOTATIONS : annotations; + if (source instanceof Class || source instanceof Member) { + declaredAnnotationCache.put(source, annotations); + cached = true; + } + } + } + if (!defensive || annotations.length == 0 || !cached) { + return annotations; + } + return annotations.clone(); + } + + private static boolean isIgnorable(Class annotationType) { + return (annotationType == Nullable.class || + annotationType == FunctionalInterface.class); + } + + private static boolean isFiltered(Class sourceClass, C context, + @Nullable BiPredicate> classFilter) { + return classFilter != null && classFilter.test(context, sourceClass); + } + + static boolean isKnownEmpty(AnnotatedElement source, + SearchStrategy searchStrategy, AnnotationFilter annotationFilter) { + if (annotationFilter == AnnotationFilter.PLAIN && + hasPlainJavaAnnotationsOnly(source)) { + return true; + } + if (searchStrategy == SearchStrategy.DIRECT || isWithoutHierarchy(source)) { + if (source instanceof Method && ((Method) source).isBridge()) { + return false; + } + return getDeclaredAnnotations(source, false).length == 0; + } + return false; + } + + static boolean hasPlainJavaAnnotationsOnly(@Nullable Object annotatedElement) { + Class type = null; + if (annotatedElement instanceof Class) { + type = (Class) annotatedElement; + } + else if (annotatedElement instanceof Member) { + type = ((Member) annotatedElement).getDeclaringClass(); + } + else { + return false; + } + String name = type.getName(); + return type.equals(Ordered.class) || + name.startsWith("java") || + name.startsWith("org.springframework.lang."); + } + + private static boolean isWithoutHierarchy(AnnotatedElement source) { + if (source == Object.class) { + return true; + } + if (source instanceof Class) { + Class sourceClass = (Class) source; + return sourceClass.getSuperclass() == Object.class && + sourceClass.getInterfaces().length == 0; + } + if (source instanceof Method) { + Method sourceMethod = (Method) source; + return Modifier.isPrivate(sourceMethod.getModifiers()) || + isWithoutHierarchy(sourceMethod.getDeclaringClass()); + } + return true; + } + + static void clearCache() { + declaredAnnotationCache.clear(); + baseTypeMethodsCache.clear(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java new file mode 100644 index 000000000000..6f5f9ef53c99 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java @@ -0,0 +1,316 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +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; + +/** + * 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 + * @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 (method.getDefaultValue() != null) { + foundDefaultValueMethod = true; + } + if (type.isAnnotation() || + (type.isArray() && type.getComponentType().isAnnotation())) { + foundNestedAnnotation = true; + } + method.setAccessible(true); + this.canThrowTypeNotPresentException[i] = + type == Class.class || + type == Class[].class || + type.isEnum(); + } + this.hasDefaultValueMethod = foundDefaultValueMethod; + this.hasNestedAnnotation = foundNestedAnnotation; + } + + + /** + * Return if this instance only contains only a single attribute named + * {@code value}. + * @return {@code true} if this is only a value attribute + */ + boolean isOnlyValueAttribute() { + return this.attributeMethods.length == 1 && + MergedAnnotation.VALUE.equals(this.attributeMethods[0].getName()); + } + + + /** + * Returns {@code true} if values from the given annotation can be safely + * accessed without causing any {@link TypeNotPresentException + * TypeNotPresentExceptions}. + * @param annotation the annotation to check + * @return {@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; + } + + /** + * Checks 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); + } + } + + /** + * Return 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; + } + + /** + * Return 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 + * (index < 0 || index >= size()) + */ + Method get(int index) { + return this.attributeMethods[index]; + } + + /** + * Return {@code true} 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]; + } + + /** + * Return 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; + } + + /** + * Return the index of the specified attribute , or {@code -1} if the + * attribute is not 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; + } + + /** + * Return the number of attributes in this collection. + * @return the number of attributes + */ + int size() { + return this.attributeMethods.length; + } + + /** + * Return if at least one of the attribute methods has a default value. + * @return if there is at least one attribute method with a default value + */ + boolean hasDefaultValueMethod() { + return this.hasDefaultValueMethod; + } + + /** + * Return if at least on of the attribute methods is a nested annotation. + * @return if there is at least one attribute method with a annotation type + */ + boolean hasNestedAnnotation() { + return this.hasNestedAnnotation; + } + + /** + * Return the attribute methods for the given annotation type. + * @param annotationType the annotation type + * @return the attribute methods for the annotation + */ + 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 = new Method[size]; + System.arraycopy(methods, 0, attributeMethods, 0, size); + return new AttributeMethods(annotationType, attributeMethods); + } + + private static boolean isAttributeMethod(Method method) { + return method.getParameterCount() == 0 && method.getReturnType() != void.class; + } + + /** + * Return 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()); + } + + /** + * Return 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; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/IntrospectionFailureLogger.java b/spring-core/src/main/java/org/springframework/core/annotation/IntrospectionFailureLogger.java new file mode 100644 index 000000000000..08f3f79e4d10 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/IntrospectionFailureLogger.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; + +/** + * Log facade used to handle annotation introspection failures (in particular + * {@code TypeNotPresentExceptions}). Allows annotation processing to continue, + * assuming that when Class attribute values are not resolvable the annotation + * should effectively disappear. + * + * @author Phillip Webb + * @since 5.2 + */ +enum IntrospectionFailureLogger { + + DEBUG { + + @Override + public boolean isEnabled() { + return getLogger().isDebugEnabled(); + } + + @Override + public void log(String message) { + getLogger().debug(message); + } + }, + + INFO { + + @Override + public boolean isEnabled() { + return getLogger().isInfoEnabled(); + } + + @Override + public void log(String message) { + getLogger().info(message); + } + }; + + + @Nullable + private static Log logger; + + + abstract boolean isEnabled(); + + void log(String message, @Nullable Object source, Exception ex) { + String on = source != null ? " on " + source : ""; + log(message + on + ": " + ex); + } + + abstract void log(String message); + + private static Log getLogger() { + Log logger = IntrospectionFailureLogger.logger; + if (logger == null) { + logger = LogFactory.getLog(MergedAnnotation.class); + IntrospectionFailureLogger.logger = logger; + } + return logger; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java new file mode 100644 index 000000000000..6984b03b846c --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java @@ -0,0 +1,597 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Proxy; +import java.util.EnumSet; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.lang.Nullable; + +/** + * A single merged annotation returned from a {@link MergedAnnotations} + * collection. Presents a view onto an annotation where attribute values may + * have been "merged" from different source values. + * + *

Attribute values may be accessed using the various {@code get} methods. + * For example, to access an {@code int} attribute the {@link #getInt(String)} + * method would be used. + * + *

Note that attribute values are not converted when accessed. For + * example, it is not possible to call {@link #getString(String)} if the + * underlying attribute is an {@code int}. The only exception to this rule is + * {@code Class} and {@code Class[]} values which may be accessed as + * {@code String} and {@code String[]} respectively to prevent potential early + * class initialization. + * + *

If necessary, a {@link MergedAnnotation} can be {@link #synthesize() + * synthesized} back into an actual {@link java.lang.annotation.Annotation}. + * + * @author Phillip Webb + * @since 5.2 + * @param the annotation type + * @see MergedAnnotations + * @see MergedAnnotationPredicates + */ +public interface MergedAnnotation { + + /** + * The attribute name for annotations with a single element. + */ + String VALUE = "value"; + + /** + * Return the class name of the actual annotation type. + * @return the annotation type + */ + String getType(); + + /** + * Return if the annotation is present on the source. Considers + * {@link #isDirectlyPresent() direct annotations}, and + * {@link #isMetaPresent() meta-annotation} annotations within the context + * of the {@link SearchStrategy} used. + * @return {@code true} if the annotation is present + */ + boolean isPresent(); + + /** + * Return if the annotation is directly present on the source. A directly + * present annotation is one that the user has explicitly defined and not + * one that is {@link #isMetaPresent() meta-present} or + * {@link Inherited @Inherited}. + * @return {@code true} if the annotation is directly present + */ + boolean isDirectlyPresent(); + + /** + * Return if the annotation is meta-present on the source. A meta-present + * annotation is an annotation that the user hasn't explicitly defined, but + * has been used as a meta-annotation somewhere in the annotation hierarchy. + * @return {@code true} if the annotation is meta-present + */ + boolean isMetaPresent(); + + /** + * Return the depth of this annotation related to its use as a + * meta-annotation. A directly declared annotation has a depth of {@code 0}, + * a meta-annotation has a depth of {@code 1}, a meta-annotation on a + * meta-annotation has a depth of {@code 2}, etc. A {@link #missing() + * missing} annotation will always return a depth of {@code -1}. + * @return the annotation depth or {@code -1} if the annotation is missing + */ + int getDepth(); + + /** + * Return the index of the aggregate collection containing this annotation. + * Can be used to reorder a stream of annotations, for example, to give a + * higher priority to annotations declared on a superclass or interface. A + * {@link #missing() missing} annotation will always return an aggregate + * index of {@code -1}. + * @return the aggregate index (starting at {@code 0}) or {@code -1} if the + * annotation is missing + */ + int getAggregateIndex(); + + /** + * Return the source that ultimately declared the annotation, or + * {@code null} if the source is not known. If this merged annotation was + * created {@link MergedAnnotations#from(java.lang.reflect.AnnotatedElement) + * from} an {@link AnnotatedElement} then this source will be an element of + * the same type. If the annotation was loaded without using reflection, the + * source can be of any type, but should have a sensible {@code toString()}. + * Meta-annotations will return the same source as the {@link #getParent()}. + * @return the source, or {@code null} + */ + @Nullable + Object getSource(); + + /** + * Return the parent of the meta-annotation, or {@code null} if the + * annotation is not {@link #isMetaPresent() meta-present}. + * @return the parent annotation or {@code null} + */ + @Nullable + MergedAnnotation getParent(); + + /** + * Return if the specified attribute name as a non-default value when + * compared to the annotation declaration. + * @param attributeName the attribute name + * @return {@code true} if the attribute value is different from the default + * value + */ + boolean hasNonDefaultValue(String attributeName); + + /** + * Return if the specified attribute name as a default value when compared + * to the annotation declaration. + * @param attributeName the attribute name + * @return {@code true} if the attribute value is the same as the default + * value + */ + boolean hasDefaultValue(String attributeName) throws NoSuchElementException; + + /** + * Return a required byte attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a byte + * @throws NoSuchElementException if there is no matching attribute + */ + byte getByte(String attributeName) throws NoSuchElementException; + + /** + * Return a required byte array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a byte array + * @throws NoSuchElementException if there is no matching attribute + */ + byte[] getByteArray(String attributeName) throws NoSuchElementException; + + /** + * Return a required boolean attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a boolean + * @throws NoSuchElementException if there is no matching attribute + */ + boolean getBoolean(String attributeName) throws NoSuchElementException; + + /** + * Return a required boolean array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a boolean array + * @throws NoSuchElementException if there is no matching attribute + */ + boolean[] getBooleanArray(String attributeName) throws NoSuchElementException; + + /** + * Return a required char attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a char + * @throws NoSuchElementException if there is no matching attribute + */ + char getChar(String attributeName) throws NoSuchElementException; + + /** + * Return a required char array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a char array + * @throws NoSuchElementException if there is no matching attribute + */ + char[] getCharArray(String attributeName) throws NoSuchElementException; + + /** + * Return a required short attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a short + * @throws NoSuchElementException if there is no matching attribute + */ + short getShort(String attributeName) throws NoSuchElementException; + + /** + * Return a required short array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a short array + * @throws NoSuchElementException if there is no matching attribute + */ + short[] getShortArray(String attributeName) throws NoSuchElementException; + + /** + * Return a required int attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as an int + * @throws NoSuchElementException if there is no matching attribute + */ + int getInt(String attributeName) throws NoSuchElementException; + + /** + * Return a required int array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as an int array + * @throws NoSuchElementException if there is no matching attribute + */ + int[] getIntArray(String attributeName) throws NoSuchElementException; + + /** + * Return a required long attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a long + * @throws NoSuchElementException if there is no matching attribute + */ + long getLong(String attributeName) throws NoSuchElementException; + + /** + * Return a required long array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a long array + * @throws NoSuchElementException if there is no matching attribute + */ + long[] getLongArray(String attributeName) throws NoSuchElementException; + + /** + * Return a required double attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a double + * @throws NoSuchElementException if there is no matching attribute + */ + double getDouble(String attributeName) throws NoSuchElementException; + + /** + * Return a required double array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a double array + * @throws NoSuchElementException if there is no matching attribute + */ + double[] getDoubleArray(String attributeName) throws NoSuchElementException; + + /** + * Return a required float attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a float + * @throws NoSuchElementException if there is no matching attribute + */ + float getFloat(String attributeName) throws NoSuchElementException; + + /** + * Return a required float array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a float array + * @throws NoSuchElementException if there is no matching attribute + */ + float[] getFloatArray(String attributeName) throws NoSuchElementException; + + /** + * Return a required string attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a string + * @throws NoSuchElementException if there is no matching attribute + */ + String getString(String attributeName) throws NoSuchElementException; + + /** + * Return a required string array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a string array + * @throws NoSuchElementException if there is no matching attribute + */ + String[] getStringArray(String attributeName) throws NoSuchElementException; + + /** + * Return a required class attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a class + * @throws NoSuchElementException if there is no matching attribute + */ + Class getClass(String attributeName) throws NoSuchElementException; + + /** + * Return a required class array attribute value from the annotation. + * @param attributeName the attribute name + * @return the value as a class array + * @throws NoSuchElementException if there is no matching attribute + */ + Class[] getClassArray(String attributeName) throws NoSuchElementException; + + /** + * Return a required enum attribute value from the annotation. + * @param attributeName the attribute name + * @param type the enum type + * @return the value as a enum + * @throws NoSuchElementException if there is no matching attribute + */ + > E getEnum(String attributeName, Class type) + throws NoSuchElementException; + + /** + * Return a required enum array attribute value from the annotation. + * @param attributeName the attribute name + * @param type the enum type + * @return the value as a enum array + * @throws NoSuchElementException if there is no matching attribute + */ + > E[] getEnumArray(String attributeName, Class type) + throws NoSuchElementException; + + /** + * Return a required annotation attribute value from the annotation. + * @param attributeName the attribute name + * @param type the annotation type + * @return the value as a {@link MergedAnnotation} + * @throws NoSuchElementException if there is no matching attribute + */ + MergedAnnotation getAnnotation(String attributeName, + Class type) throws NoSuchElementException; + + /** + * Return a required annotation array attribute value from the annotation. + * @param attributeName the attribute name + * @param type the annotation type + * @return the value as a {@link MergedAnnotation} array + * @throws NoSuchElementException if there is no matching attribute + */ + MergedAnnotation[] getAnnotationArray(String attributeName, + Class type) throws NoSuchElementException; + + /** + * Return an optional attribute value from the annotation. + * @param attributeName the attribute name + * @return an optional value or {@link Optional#empty()} if there is no + * matching attribute + */ + Optional getValue(String attributeName); + + /** + * Return an optional attribute value from the annotation. + * @param attributeName the attribute name + * @param type the attribute type. Must be compatible with the underlying + * attribute type or {@code Object.class}. + * @return an optional value or {@link Optional#empty()} if there is no + * matching attribute + */ + Optional getValue(String attributeName, Class type); + + /** + * Return the default attribute value from the annotation as specified in + * the annotation declaration. + * @param attributeName the attribute name + * @return an optional of the default value or {@link Optional#empty()} if + * there is no matching attribute or no defined default + */ + Optional getDefaultValue(String attributeName); + + /** + * Return the default attribute value from the annotation as specified in + * the annotation declaration. + * @param attributeName the attribute name + * @param type the attribute type. Must be compatible with the underlying + * attribute type or {@code Object.class}. + * @return an optional of the default value or {@link Optional#empty()} if + * there is no matching attribute or no defined default + */ + Optional getDefaultValue(String attributeName, Class type); + + /** + * Return a new view of the annotation with all attributes that have default + * values removed. + * @return a filtered view of the annotation without any attributes that + * have a default value + * @see #filterAttributes(Predicate) + */ + MergedAnnotation filterDefaultValues(); + + /** + * Return a new view of the annotation with only attributes that match the + * given predicate. + * @param predicate a predicate used to filter attribute names + * @return a filtered view of the annotation + * @see #filterDefaultValues() + * @see MergedAnnotationPredicates + */ + MergedAnnotation filterAttributes(Predicate predicate); + + /** + * Return a new view of the annotation that exposes non-merged attribute + * values. Methods from this view will return attribute values with only + * alias mirroring rules applied. Aliases to parent attributes will not be + * applied. + * @return a non-merged view of the annotation + */ + MergedAnnotation withNonMergedAttributes(); + + /** + * Return an immutable {@link Map} that contains all the annotation + * attributes. The {@link MapValues} options may be used to change the way + * that values are added. + * @param options map value options + * @return a map containing the attributes and values + */ + Map asMap(MapValues... options); + + /** + * Return a {@link Map} of the supplied type that contains all the + * annotation attributes. The {@link MapValues} options may be used to + * change the way that values are added. + * @param factory a map factory or {@code null} to return an immutable map. + * @param options map value options + * @return a map containing the attributes and values + */ + > T asMap( + @Nullable Function, T> factory, MapValues... options); + + /** + * Return a type-safe synthesized version of this annotation that can be + * used directly in code. The result is synthesized using a JDK + * {@link Proxy} and as a result may incur a computational cost when first + * invoked. + * @return a sythesized version of the annotation. + * @throws NoSuchElementException on a missing annotation + */ + A synthesize() throws NoSuchElementException; + + /** + * Optionally return type-safe synthesized version of this annotation based + * on a condition predicate. The result is synthesized using a JDK + * {@link Proxy} and as a result may incur a computational cost when first + * invoked. + * @param condition the test to determine if the annotation can be + * sythesized + * @return a optional containing the sythesized version of the annotation or + * an empty optional if the condition doesn't match + * @throws NoSuchElementException on a missing annotation + * @see MergedAnnotationPredicates + */ + Optional synthesize(@Nullable Predicate> condition) + throws NoSuchElementException; + + /** + * Return an {@link MergedAnnotation} that represents a missing annotation + * (i.e. one that is not present). + * @return an instance representing a missing annotation + */ + static MergedAnnotation missing() { + return MissingMergedAnnotation.getInstance(); + } + + /** + * Create a new {@link MergedAnnotation} instance from the specified + * annotation. + * @param annotation the annotation to include + * @return a {@link MergedAnnotation} instance containing the annotation + */ + static MergedAnnotation from(A annotation) { + return from(null, annotation); + } + + /** + * Create a new {@link MergedAnnotations} instance from the specified + * annotation. + * @param source the source for the annotation. This source is used only for + * information and logging. It does not need to actually contain + * the specified annotations and it will not be searched. + * @param annotation the annotation to include + * @return a {@link MergedAnnotation} instance for the annotation + */ + static MergedAnnotation from(@Nullable Object source, + A annotation) { + return TypeMappedAnnotation.from(source, annotation); + } + + /** + * Create a new {@link MergedAnnotations} instance from the specified + * annotation type. The resulting annotation will not have any attribute + * values, but may still be used to query default values. + * @param annotationType the annotation type + * @return a {@link MergedAnnotation} instance for the annotation + */ + static MergedAnnotation from(Class annotationType) { + return from(null, annotationType, null); + } + + /** + * Create a new {@link MergedAnnotations} instance from the specified + * annotation type and attributes maps. + * @param annotationType the annotation type + * @param attributes the annotation attributes or {@code null} if just + * default values should be used + * @return a {@link MergedAnnotation} instance for the annotation and + * attributes + * @see #from(AnnotatedElement, Class, Map) + */ + static MergedAnnotation from(Class annotationType, + @Nullable Map attributes) { + return from(null, annotationType, attributes); + } + + /** + * Create a new {@link MergedAnnotations} instance from the specified + * annotation type and attributes maps. + * @param source the source for the annotation. This source is used only for + * information and logging. It does not need to actually contain + * the specified annotations and it will not be searched. + * @param annotationType the annotation type + * @param attributes the annotation attributes or {@code null} if just + * default values should be used + * @return a {@link MergedAnnotation} instance for the annotation and + * attributes + */ + static MergedAnnotation from( + @Nullable AnnotatedElement source, Class annotationType, + @Nullable Map attributes) { + return TypeMappedAnnotation.from(source, annotationType, attributes); + } + + + /** + * Options that effect the way map values are + * {@link MergedAnnotation#asMap(MapValues...) converted}. + */ + enum MapValues { + + /** + * Add class or class array attributes as strings. + */ + CLASS_TO_STRING, + + /** + * Convert any nested annotation or annotation arrays to maps rather + * than synthesizing the values. + */ + ANNOTATION_TO_MAP; + + + protected final boolean isIn(MapValues... options) { + for (MapValues candidate : options) { + if (candidate == this) { + return true; + } + } + return false; + } + + /** + * Factory method to create a {@link MapValues} array from a set of + * boolean flags. + * @param classToString if {@link MapValues#CLASS_TO_STRING} is included + * @param annotationsToMap if {@link MapValues#ANNOTATION_TO_MAP} is + * included + * @return a new {@link MapValues} array + */ + public static MapValues[] of(boolean classToString, boolean annotationsToMap) { + EnumSet result = EnumSet.noneOf(MapValues.class); + addIfTrue(result, MapValues.CLASS_TO_STRING, classToString); + addIfTrue(result, MapValues.ANNOTATION_TO_MAP, annotationsToMap); + return result.toArray(new MapValues[0]); + } + + private static void addIfTrue(Set result, T value, boolean test) { + if (test) { + result.add(value); + } + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationCollectors.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationCollectors.java new file mode 100644 index 000000000000..cf45a40442e5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationCollectors.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.stream.Collector; +import java.util.stream.Collector.Characteristics; + +import org.springframework.core.annotation.MergedAnnotation.MapValues; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Collector implementations that provide various reduction operations for + * {@link MergedAnnotation MergedAnnotations}. + * + * @author Phillip Webb + * @since 5.2 + */ +public abstract class MergedAnnotationCollectors { + + private static final Characteristics[] NO_CHARACTERISTICS = {}; + + private static final Characteristics[] IDENTITY_FINISH_CHARACTERISTICS = { + Characteristics.IDENTITY_FINISH }; + + + private MergedAnnotationCollectors() { + } + + + /** + * Returns a new {@link Collector} that accumulates merged annotations to a + * {@link LinkedHashSet} containing {@link MergedAnnotation#synthesize() + * synthesized} versions. + * @param the annotation type + * @return a {@link Collector} which collects and synthesizes the + * annotations into a {@link Set} + */ + public static Collector, ?, Set> toAnnotationSet() { + return Collector.of(ArrayList::new, (list, annotation) -> list.add(annotation.synthesize()), + MergedAnnotationCollectors::addAll, LinkedHashSet::new); + } + + /** + * Returns a new {@link Collector} that accumulates merged annotations to an + * {@link Annotation} array containing {@link MergedAnnotation#synthesize() + * synthesized} versions. + * @param the annotation type + * @return a {@link Collector} which collects and synthesizes the + * annotations into an {@code Annotation[]} + * @see #toAnnotationArray(IntFunction) + */ + public static Collector, ?, Annotation[]> toAnnotationArray() { + return toAnnotationArray(Annotation[]::new); + } + + /** + * Returns a new {@link Collector} that accumulates merged annotations to an + * {@link Annotation} array containing {@link MergedAnnotation#synthesize() + * synthesized} versions. + * @param the annotation type + * @param the resulting array type + * @param generator a function which produces a new array of the desired + * type and the provided length + * @return a {@link Collector} which collects and synthesizes the + * annotations into an annotation array + * @see #toAnnotationArray + */ + public static Collector, ?, R[]> toAnnotationArray( + IntFunction generator) { + + return Collector.of(ArrayList::new, (list, annotation) -> list.add(annotation.synthesize()), + MergedAnnotationCollectors::addAll, list -> list.toArray(generator.apply(list.size()))); + } + + /** + * Returns a new {@link Collector} that accumulates merged annotations to an + * {@link MultiValueMap} with items {@link MultiValueMap#add(Object, Object) + * added} from each merged annotation + * {@link MergedAnnotation#asMap(MapValues...) as a map}. + * @param the annotation type + * @param options the map conversion options + * @return a {@link Collector} which collects and synthesizes the + * annotations into a {@link LinkedMultiValueMap} + * @see #toMultiValueMap(Function, MapValues...) + */ + public static Collector, ?, MultiValueMap> toMultiValueMap( + MapValues... options) { + + return toMultiValueMap(Function.identity(), options); + } + + /** + * Returns a new {@link Collector} that accumulates merged annotations to an + * {@link MultiValueMap} with items {@link MultiValueMap#add(Object, Object) + * added} from each merged annotation + * {@link MergedAnnotation#asMap(MapValues...) as a map}. + * @param the annotation type + * @param options the map conversion options + * @param finisher the finisher function for the new {@link MultiValueMap} + * @return a {@link Collector} which collects and synthesizes the + * annotations into a {@link LinkedMultiValueMap} + * @see #toMultiValueMap(MapValues...) + */ + public static Collector, ?, MultiValueMap> toMultiValueMap( + Function, MultiValueMap> finisher, + MapValues... options) { + + Assert.notNull(finisher, "Finisher must not be null"); + Characteristics[] characteristics = isSameInstance(finisher, Function.identity()) ? + IDENTITY_FINISH_CHARACTERISTICS : + NO_CHARACTERISTICS; + return Collector.of(LinkedMultiValueMap::new, + (map, annotation) -> annotation.asMap(options).forEach(map::add), + MergedAnnotationCollectors::merge, finisher, characteristics); + } + + private static boolean isSameInstance(Object instance, Object candidate) { + return instance == candidate; + } + + private static > L addAll(L list, L additions) { + list.addAll(additions); + return list; + } + + private static MultiValueMap merge(MultiValueMap map, + MultiValueMap additions) { + map.addAll(additions); + return map; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationPredicates.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationPredicates.java new file mode 100644 index 000000000000..6255729376d0 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationPredicates.java @@ -0,0 +1,188 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Predicate implementations that provide various test operations for + * {@link MergedAnnotation MergedAnnotations}. + * + * @author Phillip Webb + * @since 5.2 + */ +public abstract class MergedAnnotationPredicates { + + private MergedAnnotationPredicates() { + } + + + /** + * Returns a new {@link Predicate} that evaluates {@code true} if the + * {@link MergedAnnotation#getType() merged annotation type} is contained in + * the specified array. + * @param the annotation type + * @param typeNames the names that should be matched + * @return a {@link Predicate} to test the annotation type + */ + public static Predicate> typeIn( + String... typeNames) { + + Assert.notNull(typeNames, "TypeNames must not be null"); + return annotation -> ObjectUtils.containsElement(typeNames, annotation.getType()); + } + + /** + * Returns a new {@link Predicate} that evaluates {@code true} if the + * {@link MergedAnnotation#getType() merged annotation type} is contained in + * the specified array. + * @param the annotation type + * @param types the types that should be matched + * @return a {@link Predicate} to test the annotation type + */ + public static Predicate> typeIn( + Class... types) { + + Assert.notNull(types, "Types must not be null"); + return annotation -> Arrays.stream(types) + .anyMatch(type -> type.getName().equals(annotation.getType())); + } + + /** + * Returns a new {@link Predicate} that evaluates {@code true} if the + * {@link MergedAnnotation#getType() merged annotation type} is contained in + * the collection. + * @param the annotation type + * @param types the type names or classes that should be matched + * @return a {@link Predicate} to test the annotation type + */ + public static Predicate> typeIn( + Collection types) { + + Assert.notNull(types, "Types must not be null"); + return annotation -> types.stream() + .map(type -> type instanceof Class ? ((Class) type).getName() : type.toString()) + .anyMatch(typeName -> typeName.equals(annotation.getType())); + } + + /** + * Returns a new stateful, single use {@link Predicate} that matches only + * the first run of an extracted value. For example, + * {@code MergedAnnotationPredicates.firstRunOf(MergedAnnotation::depth)} + * will return the first annotation and a subsequent run of the same depth. + * NOTE: this predicate only matches the first first run, once the extracted + * value changes the predicate always returns {@code false}. + * @param valueExtractor function used to extract the value to check + * @return a {@link Predicate} that matches the first run of the extracted + * values + */ + public static Predicate> firstRunOf( + Function, ?> valueExtractor) { + + Assert.notNull(valueExtractor, "ValueExtractor must not be null"); + return new FirstRunOfPredicate<>(valueExtractor); + } + + /** + * Returns a new stateful, single use {@link Predicate} that matches + * annotations that are unique based on extracted key. For example + * {@code MergedAnnotationPredicates.unique(MergedAnnotation::type)} will + * match the first time a unique type is seen. + * @param keyExtractor function used to extract the key used to test for + * uniqueness + * @return a {@link Predicate} that matches unique annotation based on the + * extracted key + */ + public static Predicate> unique( + Function, K> keyExtractor) { + + Assert.notNull(keyExtractor, "KeyExtractor must not be null"); + return new UniquePredicate<>(keyExtractor); + } + + + /** + * {@link Predicate} implementation used for + * {@link MergedAnnotationPredicates#firstRunOf(Function)}. + */ + private static class FirstRunOfPredicate + implements Predicate> { + + private final Function, ?> valueExtractor; + + private boolean hasLastValue; + + @Nullable + private Object lastValue; + + + FirstRunOfPredicate( + Function, ?> valueExtractor) { + this.valueExtractor = valueExtractor; + } + + + @Override + public boolean test(@Nullable MergedAnnotation annotation) { + if (!this.hasLastValue) { + this.hasLastValue = true; + this.lastValue = this.valueExtractor.apply(annotation); + } + Object value = this.valueExtractor.apply(annotation); + return ObjectUtils.nullSafeEquals(value, this.lastValue); + + } + + } + + + /** + * {@link Predicate} implementation used for + * {@link MergedAnnotationPredicates#unique(Function)}. + */ + private static class UniquePredicate + implements Predicate> { + + private final Function, K> keyExtractor; + + private final Set seen = new HashSet<>(); + + + UniquePredicate(Function, K> keyExtractor) { + this.keyExtractor = keyExtractor; + } + + + @Override + public boolean test(@Nullable MergedAnnotation annotation) { + K key = this.keyExtractor.apply(annotation); + return this.seen.add(key); + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationSelector.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationSelector.java new file mode 100644 index 000000000000..0aa5c22a4c4f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationSelector.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; + +/** + * Strategy interface used to select between two {@link MergedAnnotation} + * instances. + * + * @author Phillip Webb + * @since 5.2 + * @param the annotation type + * @see MergedAnnotationSelectors + */ +@FunctionalInterface +public interface MergedAnnotationSelector { + + /** + * Return {@code true} if the existing annotation is known to be the best + * candidate and any subsequent selections may be skipped. + * @param annotation the annotation to check + * @return {@code true} if the annotation is known to be the best candidate + */ + default boolean isBestCandidate(MergedAnnotation annotation) { + return false; + } + + /** + * Select the annotation that should be used. + * @param existing an existing annotation returned from an earlier result + * @param candidate a candidate annotation that may be better suited + * @return the most appropriate annotation from the {@code existing} or + * {@code candidate} + */ + MergedAnnotation select(MergedAnnotation existing, + MergedAnnotation candidate); + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationSelectors.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationSelectors.java new file mode 100644 index 000000000000..8db4efaefdfd --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationSelectors.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.util.function.Predicate; + +/** + * {@link MergedAnnotationSelector} implementations that provide various options + * for {@link MergedAnnotation MergedAnnotations}. + * + * @author Phillip Webb + * @since 5.2 + * @see MergedAnnotations#get(Class, Predicate, MergedAnnotationSelector) + * @see MergedAnnotations#get(String, Predicate, MergedAnnotationSelector) + */ +public abstract class MergedAnnotationSelectors { + + private static final MergedAnnotationSelector NEAREST = new Nearest(); + + private static final MergedAnnotationSelector FIRST_DIRECTLY_DECLARED = new FirstDirectlyDeclared(); + + + private MergedAnnotationSelectors() { + } + + + /** + * Select the nearest annotation, i.e. the one with the lowest depth. + * @return a selector that picks the annotation with the lowest depth + */ + @SuppressWarnings("unchecked") + public static MergedAnnotationSelector nearest() { + return (MergedAnnotationSelector) NEAREST; + } + + /** + * Select the first directly declared annotation when possible. If not direct + * annotations are declared then the earliest annotation is selected. + * @return a selector that picks the first directly declared annotation whenever possible + */ + @SuppressWarnings("unchecked") + public static MergedAnnotationSelector firstDirectlyDeclared() { + return (MergedAnnotationSelector) FIRST_DIRECTLY_DECLARED; + } + + + /** + * {@link MergedAnnotationSelector} to select the nearest annotation. + */ + private static class Nearest implements MergedAnnotationSelector { + + @Override + public boolean isBestCandidate(MergedAnnotation annotation) { + return annotation.getDepth() == 0; + } + + @Override + public MergedAnnotation select(MergedAnnotation existing, + MergedAnnotation candidate) { + if (candidate.getDepth() < existing.getDepth()) { + return candidate; + } + return existing; + } + + } + + + /** + * {@link MergedAnnotationSelector} to select the first directly declared + * annotation. + */ + private static class FirstDirectlyDeclared implements MergedAnnotationSelector { + + @Override + public boolean isBestCandidate(MergedAnnotation annotation) { + return annotation.getDepth() == 0; + } + + @Override + public MergedAnnotation select(MergedAnnotation existing, + MergedAnnotation candidate) { + if (existing.getDepth() > 0 && candidate.getDepth() == 0) { + return candidate; + } + return existing; + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java new file mode 100644 index 000000000000..8df5091287e6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java @@ -0,0 +1,412 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.springframework.lang.Nullable; + +/** + * Provides access to a collection of merged annotations, usually obtained from + * a from a source such as a {@link Class} or {@link Method}. Each merged + * annotation represent a view where the attribute values may be "merged" from + * different source values, typically: + * + *
    + *
  • Explicit and Implicit {@link AliasFor @AliasFor} declarations on one or + * attributes within the annotation.
  • + *
  • Explicit {@link AliasFor @AliasFor} declarations for a + * meta-annotation.
  • + *
  • Convention based attribute aliases for a meta-annotation
  • + *
  • From a meta-annotation declaration.
  • + *
+ * + *

For example, a {@code @PostMapping} annotation might be defined as follows: + * + *

+ * @Retention(RetentionPolicy.RUNTIME)
+ * @RequestMapping(method = RequestMethod.POST)
+ * public @interface PostMapping {
+ *
+ * 	@AliasFor(attribute = "path")
+ * 	String[] value() default {};
+ *
+ * 	@AliasFor(attribute = "value")
+ * 	String[] path() default {};
+ *
+ * }
+ * 
+ * + * If a method is annotated with {@code @PostMapping("/home")} it will contain + * merged annotations for both {@code @PostMapping} and the meta-annotation + * {@code @RequestMapping}. The merged view of the {@code @RequestMapping} + * annotation will contain the following attributes: + * + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
NameValueSource
value"/home"Declared {@code @PostMapping}
path"/home"Explicit {@code @AliasFor}
methodRequestMethod.POSTDeclared meta-annotation
+ * + *

{@link MergedAnnotations} can be obtained {@link #from(AnnotatedElement) + * from} any Java {@link AnnotatedElement}. They may also used for sources that + * don't use reflection (such as those that directly parse bytecode). + * + *

Different {@link SearchStrategy search strategies} can be used to locate + * related source elements that contain the annotations to be aggregated + * together. For example, {@link SearchStrategy#EXHAUSTIVE} will search both + * superclasses and implemented interfaces. + * + *

From a {@link MergedAnnotations} instance you can either {@link #get(String)} + * a single annotation, or {@link #stream() stream all annotations} or just + * those that match {@link #stream(String) a specific type}. You can also + * quickly tell if an annotation {@link #isPresent(String) is present}. + * + *

Here are some typical examples: + * + *

+ * // is an annotation present or meta-present
+ * mergedAnnotations.isPresent(ExampleAnnotation.class);
+ *
+ * // get the merged "value" attribute of ExampleAnnotation (either direct or
+ * // meta-present)
+ * mergedAnnotations.get(ExampleAnnotation.class).getString("value");
+ *
+ * // get all meta-annotations but no direct annotations
+ * mergedAnnotations.stream().anyMatch(MergedAnnotation::isMetaPresent);
+ *
+ * // get all ExampleAnnotation declarations (include any meta-annotations) and
+ * // print the merged "value" attributes
+ * mergedAnnotations.stream(ExampleAnnotation.class).map(
+ * 		a -> a.getString("value")).forEach(System.out::println);
+ * 
+ * + * @author Phillip Webb + * @since 5.2 + * @see MergedAnnotation + * @see MergedAnnotationCollectors + * @see MergedAnnotationPredicates + * @see MergedAnnotationSelectors + */ +public interface MergedAnnotations extends Iterable> { + + /** + * Return if the specified annotation is either directly present, or + * meta-present. Equivalent to calling + * {@code get(annotationType).isPresent()}. + * @param annotationType the annotation type to check + * @return {@code true} if the annotation is present + */ +
boolean isPresent(@Nullable Class annotationType); + + /** + * Return if the specified annotation is directly present. Equivalent to + * calling {@code get(annotationType).isDirectlyPresent()}. + * @param annotationType the annotation type to check + * @return {@code true} if the annotation is present + */ + boolean isPresent(@Nullable String annotationType); + + /** + * Return if the specified annotation is directly present. Equivalent to + * calling {@code get(annotationType).isDirectlyPresent()}. + * @param annotationType the annotation type to check + * @return {@code true} if the annotation is present + */ + boolean isDirectlyPresent(@Nullable Class annotationType); + + /** + * Return if the specified annotation is either directly present, or + * meta-present. Equivalent to calling + * {@code get(annotationType).isPresent()}. + * @param annotationType the annotation type to check + * @return {@code true} if the annotation is present + */ + boolean isDirectlyPresent(@Nullable String annotationType); + + /** + * Return the {@link MergedAnnotationSelectors#nearest() nearest} matching + * annotation or meta-annotation of the specified type, or + * {@link MergedAnnotation#missing()} if none is present. + * @param annotationType the annotation type to get + * @return a {@link MergedAnnotation} instance + */ + MergedAnnotation get(@Nullable Class annotationType); + + /** + * Return the {@link MergedAnnotationSelectors#nearest() nearest} matching + * annotation or meta-annotation of the specified type, or + * {@link MergedAnnotation#missing()} if none is present. + * @param annotationType the annotation type to get + * @param predicate a predicate that must match, or {@code null} if only + * type matching is required + * @return a {@link MergedAnnotation} instance + * @see MergedAnnotationPredicates + */ + MergedAnnotation get(@Nullable Class annotationType, + @Nullable Predicate> predicate); + + /** + * Return a matching annotation or meta-annotation of the specified type, or + * {@link MergedAnnotation#missing()} if none is present. + * @param annotationType the annotation type to get + * @param predicate a predicate that must match, or {@code null} if only + * type matching is required + * @param selector a selector used to choose the most appropriate annotation + * within an aggregate, or {@code null} to select the + * {@link MergedAnnotationSelectors#nearest() nearest}. + * @return a {@link MergedAnnotation} instance + * @see MergedAnnotationPredicates + * @see MergedAnnotationSelectors + */ + MergedAnnotation get(@Nullable Class annotationType, + @Nullable Predicate> predicate, + @Nullable MergedAnnotationSelector selector); + + /** + * Return the {@link MergedAnnotationSelectors#nearest() nearest} matching + * annotation or meta-annotation of the specified type, or + * {@link MergedAnnotation#missing()} if none is present. + * @param annotationType the annotation type to get + * @return a {@link MergedAnnotation} instance + */ + MergedAnnotation get(@Nullable String annotationType); + + /** + * Return the {@link MergedAnnotationSelectors#nearest() nearest} matching + * annotation or meta-annotation of the specified type, or + * {@link MergedAnnotation#missing()} if none is present. + * @param annotationType the annotation type to get + * @param predicate a predicate that must match, or {@code null} if only + * type matching is required + * @return a {@link MergedAnnotation} instance + * @see MergedAnnotationPredicates + */ + MergedAnnotation get(@Nullable String annotationType, + @Nullable Predicate> predicate); + + /** + * Return a matching annotation or meta-annotation of the specified type, or + * {@link MergedAnnotation#missing()} if none is present. + * @param annotationType the annotation type to get + * @param predicate a predicate that must match, or {@code null} if only + * type matching is required + * @param selector a selector used to choose the most appropriate annotation + * within an aggregate, or {@code null} to select the + * {@link MergedAnnotationSelectors#nearest() nearest}. + * @return a {@link MergedAnnotation} instance + * @see MergedAnnotationPredicates + * @see MergedAnnotationSelectors + */ + MergedAnnotation get(@Nullable String annotationType, + @Nullable Predicate> predicate, + @Nullable MergedAnnotationSelector selector); + + /** + * Stream all annotations and meta-annotations that match the specified + * type. The resulting stream follows the same ordering rules are + * {@link #stream()}. + * @param annotationType the annotation type to match + * @return a stream of matching annotations + */ + Stream> stream( + @Nullable Class annotationType); + + /** + * Stream all annotations and meta-annotations that match the specified + * type.The resulting stream follows the same ordering rules are + * {@link #stream()}. + * @param annotationType the annotation type to match + * @return a stream of matching annotations + */ + Stream> stream( + @Nullable String annotationType); + + /** + * Stream all contained annotations and meta-annotations contained in this + * collection. The resulting stream is ordered first by the + * {@link MergedAnnotation#getAggregateIndex() aggregate index}, and then by + * the annotation depth (with the closest annotations first). This ordering + * means that, for most use-cases, the most suitable annotations appear + * earliest in the stream. + * @return a stream of annotations + */ + Stream> stream(); + + /** + * Create a new {@link MergedAnnotations} instance containing all + * annotations and meta-annotations from the specified element. The + * resulting instance will not include any inherited annotations, if you + * want to include those as well you should use + * {@link #from(AnnotatedElement, SearchStrategy)} with an appropriate + * {@link SearchStrategy}. + * @param element the source element + * @return a {@link MergedAnnotations} instance containing the element + * annotations + */ + static MergedAnnotations from(@Nullable AnnotatedElement element) { + return from(element, SearchStrategy.DIRECT); + } + + /** + * Create a new {@link MergedAnnotations} instance containing all + * annotations and meta-annotations from the specified element and, + * depending on the {@link SearchStrategy}, related inherited elements. + * @param element the source element + * @param searchStrategy the search strategy to use + * @return a {@link MergedAnnotations} instance containing the merged + * element annotations + */ + static MergedAnnotations from(@Nullable AnnotatedElement element, + SearchStrategy searchStrategy) { + return from(element, searchStrategy, RepeatableContainers.standardRepeatables(), + AnnotationFilter.PLAIN); + } + + /** + * Create a new {@link MergedAnnotations} instance containing all + * annotations and meta-annotations from the specified element and, + * depending on the {@link SearchStrategy}, related inherited elements. + * @param element the source element + * @param searchStrategy the search strategy to use + * @param repeatableContainers the repeatable containers that may be used by + * the element annotations or the meta-annotations + * @param annotationFilter an annotation filter used to restrict the + * annotations considered + * @return a {@link MergedAnnotations} instance containing the merged + * element annotations + */ + static MergedAnnotations from(@Nullable AnnotatedElement element, + SearchStrategy searchStrategy, RepeatableContainers repeatableContainers, + AnnotationFilter annotationFilter) { + return TypeMappedAnnotations.from(element, searchStrategy, repeatableContainers, + annotationFilter); + } + + /** + * Create a new {@link MergedAnnotations} instance from the specified + * annotations. + * @param annotations the annotations to include + * @return a {@link MergedAnnotations} instance containing the annotations + * @see #from(Object, Annotation...) + */ + static MergedAnnotations from(Annotation... annotations) { + return from(null, annotations); + } + + /** + * Create a new {@link MergedAnnotations} instance from the specified + * annotations. + * @param source the source for the annotations. This source is used only + * for information and logging. It does not need to actually + * contain the specified annotations and it will not be searched. + * @param annotations the annotations to include + * @return a {@link MergedAnnotations} instance containing the annotations + * @see #from(Annotation...) + * @see #from(AnnotatedElement) + */ + static MergedAnnotations from(@Nullable Object source, Annotation... annotations) { + return from(source, annotations, RepeatableContainers.standardRepeatables(), + AnnotationFilter.PLAIN); + } + + /** + * Create a new {@link MergedAnnotations} instance from the specified + * annotations. + * @param source the source for the annotations. This source is used only + * for information and logging. It does not need to actually + * contain the specified annotations and it will not be searched. + * @param annotations the annotations to include + * @param repeatableContainers the repeatable containers that may be used by + * meta-annotations + * @param annotationFilter an annotation filter used to restrict the + * annotations considered + * @return a {@link MergedAnnotations} instance containing the annotations + */ + static MergedAnnotations from(@Nullable Object source, Annotation[] annotations, + RepeatableContainers repeatableContainers, + AnnotationFilter annotationFilter) { + return TypeMappedAnnotations.from(source, annotations, repeatableContainers, + annotationFilter); + } + + + /** + * Search strategies supported by + * {@link MergedAnnotations#from(AnnotatedElement, SearchStrategy)}. Each + * strategy creates a different set of aggregates that will be combined to + * create the final {@link MergedAnnotations}. + */ + enum SearchStrategy { + + /** + * Find only directly declared annotations, without considering + * {@link Inherited @Inherited} annotations and without searching + * super-classes or implemented interfaces. + */ + DIRECT, + + /** + * Find all directly declared annotations as well any + * {@link Inherited @Inherited} super-class annotations. This strategy + * is only really useful when used with {@link Class} types since the + * {@link Inherited @Inherited} annotation is ignored for all other + * {@link AnnotatedElement annotated elements}. This strategy does not + * search implemented interfaces. + */ + INHERITED_ANNOTATIONS, + + /** + * Find all directly declared and super-class annotations. This strategy + * is similar to {@link #INHERITED_ANNOTATIONS} except the annotations + * do not need to be meta-annotated with {@link Inherited @Inherited}. + * This strategy does not search implemented interfaces. + */ + SUPER_CLASS, + + /** + * Perform a full search of all related elements, include those on any + * super-classes or implemented interfaces. Superclass annotations do + * not need to be meta-annotated with {@link Inherited @Inherited}. + */ + EXHAUSTIVE + + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MissingMergedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/MissingMergedAnnotation.java new file mode 100644 index 000000000000..09188b723cf5 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/MissingMergedAnnotation.java @@ -0,0 +1,161 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.springframework.lang.Nullable; + +/** + * A {@link AbstractMergedAnnotation} used as the implementation of + * {@link MergedAnnotation#missing()}. + * + * @author Phillip Webb + * @since 5.2 + * @param the annotation type + */ +final class MissingMergedAnnotation + extends AbstractMergedAnnotation { + + private static final MissingMergedAnnotation INSTANCE = new MissingMergedAnnotation<>(); + + + private MissingMergedAnnotation() { + } + + + @Override + public String getType() { + throw new NoSuchElementException("Unable to get type for missing annotation"); + } + + @Override + public boolean isPresent() { + return false; + } + + @Override + @Nullable + public Object getSource() { + return null; + } + + @Override + @Nullable + public MergedAnnotation getParent() { + return null; + } + + @Override + public int getDepth() { + return -1; + } + + @Override + public int getAggregateIndex() { + return -1; + } + + public boolean hasNonDefaultValue(String attributeName) { + throw new NoSuchElementException( + "Unable to check non-default value for missing annotation"); + } + + @Override + public boolean hasDefaultValue(String attributeName) { + throw new NoSuchElementException( + "Unable to check default value for missing annotation"); + } + + @Override + public Optional getValue(String attributeName, Class type) { + return Optional.empty(); + } + + @Override + public Optional getDefaultValue(@Nullable String attributeName, Class type) { + return Optional.empty(); + } + + @Override + public MergedAnnotation filterAttributes(Predicate predicate) { + return this; + } + + @Override + public MergedAnnotation withNonMergedAttributes() { + return this; + } + + @Override + public Map asMap(MapValues... options) { + return Collections.emptyMap(); + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public > T asMap( + @Nullable Function, T> factory, MapValues... options) { + if (factory != null) { + return factory.apply(this); + } + return (T) ((Map) Collections.emptyMap()); + } + + @Override + public String toString() { + return "(missing)"; + } + + @Override + public MergedAnnotation getAnnotation(String attributeName, + Class type) throws NoSuchElementException { + + throw new NoSuchElementException( + "Unable to get attribute value for missing annotation"); + } + + @Override + public MergedAnnotation[] getAnnotationArray( + String attributeName, Class type) throws NoSuchElementException { + + throw new NoSuchElementException( + "Unable to get attribute value for missing annotation"); + } + + @Override + protected T getAttributeValue(String attributeName, Class type) { + throw new NoSuchElementException( + "Unable to get attribute value for missing annotation"); + } + + protected A createSynthesized() { + throw new NoSuchElementException("Unable to synthesize missing annotation"); + } + + @SuppressWarnings("unchecked") + static MergedAnnotation getInstance() { + return (MergedAnnotation) INSTANCE; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/PackagesAnnotationFilter.java b/spring-core/src/main/java/org/springframework/core/annotation/PackagesAnnotationFilter.java new file mode 100644 index 000000000000..05f57daf49e9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/PackagesAnnotationFilter.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.util.Arrays; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link AnnotationFilter} implementation used for + * {@link AnnotationFilter#packages(String...)}. + * + * @author Phillip Webb + * @since 5.2 + */ +class PackagesAnnotationFilter implements AnnotationFilter { + + private final String[] prefixes; + + private final int hashCode; + + + PackagesAnnotationFilter(String... packages) { + Assert.notNull(packages, "Packages must not be null"); + this.prefixes = new String[packages.length]; + for (int i = 0; i < packages.length; i++) { + Assert.hasText(packages[i], "Package must not have empty elements"); + this.prefixes[i] = packages[i] + "."; + } + Arrays.sort(this.prefixes); + this.hashCode = Arrays.hashCode(this.prefixes); + } + + + @Override + public boolean matches(@Nullable String annotationType) { + if (annotationType != null) { + for (String prefix : this.prefixes) { + if (annotationType.startsWith(prefix)) { + return true; + } + } + } + return false; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + PackagesAnnotationFilter other = (PackagesAnnotationFilter) obj; + return Arrays.equals(this.prefixes, other.prefixes); + } + + @Override + public int hashCode() { + return this.hashCode; + } + + @Override + public String toString() { + return "Packages annotation filter: " + + StringUtils.arrayToCommaDelimitedString(this.prefixes); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java b/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java new file mode 100644 index 000000000000..ce4732a62f17 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java @@ -0,0 +1,290 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Objects; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Strategy used to determine annotations that act as containers for other + * annotations. The {@link #standardRepeatables()} method provides a default + * strategy that respects Java's {@link Repeatable @Repeatable} support and + * should be suitable for most situations. + *

The {@link #of} method can be used to register relationships for + * annotations that do not wish to use {@link Repeatable @Repeatable}. + * + *

To completely disable repeatable support use {@link #none()}. + * + * @author Phillip Webb + * @since 5.2 + */ +public abstract class RepeatableContainers { + + @Nullable + private final RepeatableContainers parent; + + + private RepeatableContainers(@Nullable RepeatableContainers parent) { + this.parent = parent; + } + + + /** + * Add an additional explicit relationship between a contained and + * repeatable annotation. + * @param container the container type + * @param repeatable the contained repeatable type + * @return a new {@link RepeatableContainers} instance + */ + public RepeatableContainers and(Class container, + Class repeatable) { + + return new ExplicitRepeatableContainer(this, repeatable, container); + } + + @Nullable + Annotation[] findRepeatedAnnotations(Annotation annotation) { + if (this.parent == null) { + return null; + } + return this.parent.findRepeatedAnnotations(annotation); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj == this) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + RepeatableContainers other = (RepeatableContainers) obj; + return Objects.equals(this.parent, other.parent); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.parent); + } + + /** + * Return a {@link RepeatableContainers} instance that searches using Java's + * {@link Repeatable @Repeatable} annotation. + * @return a {@link RepeatableContainers} instance + */ + public static RepeatableContainers standardRepeatables() { + return StandardRepeatableContainers.INSTANCE; + } + + /** + * Return a {@link RepeatableContainers} instance that uses a defined + * container and repeatable type. + * @param repeatable the contained repeatable annotation + * @param container the container annotation or {@code null}. If specified, + * this annotation must declare a {@code value} attribute returning an array + * of repeatable annotations. If not specified, the container will be + * deduced by inspecting the {@code @Repeatable} annotation on + * {@code repeatable}. + * @return a {@link RepeatableContainers} instance + */ + public static RepeatableContainers of(Class repeatable, + @Nullable Class container) { + + return new ExplicitRepeatableContainer(null, repeatable, container); + } + + /** + * Return a {@link RepeatableContainers} instance that does not expand any + * repeatable annotations. + * @return a {@link RepeatableContainers} instance + */ + public static RepeatableContainers none() { + return NoRepeatableContainers.INSTANCE; + } + + + /** + * Standard {@link RepeatableContainers} implementation that searches using + * Java's {@link Repeatable @Repeatable} annotation. + */ + private static class StandardRepeatableContainers extends RepeatableContainers { + + private static final Map, Object> cache = + new ConcurrentReferenceHashMap<>(); + + private static final Object NONE = new Object(); + + private static StandardRepeatableContainers INSTANCE = + new StandardRepeatableContainers(); + + StandardRepeatableContainers() { + super(null); + } + + @Override + @Nullable + Annotation[] findRepeatedAnnotations(Annotation annotation) { + Method method = getRepeatedAnnotationsMethod(annotation.annotationType()); + if (method != null) { + return (Annotation[]) ReflectionUtils.invokeMethod(method, annotation); + } + return super.findRepeatedAnnotations(annotation); + } + + @Nullable + private static Method getRepeatedAnnotationsMethod( + Class annotationType) { + + Object result = cache.computeIfAbsent(annotationType, + StandardRepeatableContainers::computeRepeatedAnnotationsMethod); + return result != NONE ? (Method) result : null; + } + + private static Object computeRepeatedAnnotationsMethod( + Class annotationType) { + + AttributeMethods methods = AttributeMethods.forAnnotationType(annotationType); + if (methods.isOnlyValueAttribute()) { + Method method = methods.get("value"); + if (method == null) { + return NONE; + } + Class returnType = method.getReturnType(); + if (returnType.isArray()) { + Class componentType = returnType.getComponentType(); + if (Annotation.class.isAssignableFrom(componentType) + && componentType.isAnnotationPresent(Repeatable.class)) { + return method; + } + } + } + return NONE; + } + + } + + + /** + * A single explicit mapping. + */ + private static class ExplicitRepeatableContainer extends RepeatableContainers { + + private final Class repeatable; + + private final Class container; + + private final Method valueMethod; + + + ExplicitRepeatableContainer(@Nullable RepeatableContainers parent, + Class repeatable, + @Nullable Class container) { + + super(parent); + Assert.notNull(repeatable, "Repeatable must not be null"); + if (container == null) { + container = deduceContainer(repeatable); + } + Method valueMethod = AttributeMethods.forAnnotationType(container).get("value"); + try { + if (valueMethod == null) { + throw new NoSuchMethodException("No value method found"); + } + Class returnType = valueMethod.getReturnType(); + if (!returnType.isArray() || returnType.getComponentType() != repeatable) { + throw new AnnotationConfigurationException("Container type [" + + container.getName() + + "] must declare a 'value' attribute for an array of type [" + + repeatable.getName() + "]"); + } + } + catch (AnnotationConfigurationException ex) { + throw ex; + } + catch (Throwable ex) { + throw new AnnotationConfigurationException( + "Invalid declaration of container type [" + container.getName() + + "] for repeatable annotation [" + repeatable.getName() + "]", + ex); + } + this.repeatable = repeatable; + this.container = container; + this.valueMethod = valueMethod; + } + + private Class deduceContainer( + Class repeatable) { + + Repeatable annotation = repeatable.getAnnotation(Repeatable.class); + Assert.notNull(annotation, "Annotation type must be a repeatable annotation: " + + "failed to resolve container type for " + repeatable.getName()); + return annotation.value(); + } + + @Override + @Nullable + Annotation[] findRepeatedAnnotations(Annotation annotation) { + if (this.container.isAssignableFrom(annotation.annotationType())) { + return (Annotation[]) ReflectionUtils.invokeMethod(this.valueMethod, annotation); + } + return super.findRepeatedAnnotations(annotation); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (super.equals(obj)) { + ExplicitRepeatableContainer other = (ExplicitRepeatableContainer) obj; + return this.container.equals(other.container) && + this.repeatable.equals(other.repeatable); + } + return false; + } + + @Override + public int hashCode() { + int hashCode = super.hashCode(); + hashCode = 31 * hashCode + this.container.hashCode(); + hashCode = 31 * hashCode + this.repeatable.hashCode(); + return hashCode; + } + + } + + + /** + * No repeatable containers. + */ + private static class NoRepeatableContainers extends RepeatableContainers { + + private static NoRepeatableContainers INSTANCE = new NoRepeatableContainers(); + + NoRepeatableContainers() { + super(null); + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java new file mode 100644 index 000000000000..f21b0b32a348 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java @@ -0,0 +1,211 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.NoSuchElementException; +import java.util.Objects; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +/** + * {@link InvocationHandler} for an {@link Annotation} that Spring has + * synthesized (i.e., wrapped in a dynamic proxy) with additional + * functionality. + * + * @author Sam Brannen + * @author Phillip Webb + * @since 5.2 + * @param the annotation type + * @see Annotation + * @see AnnotationAttributeExtractor + * @see AnnotationUtils#synthesizeAnnotation(Annotation, AnnotatedElement) + */ +class SynthesizedMergedAnnotationInvocationHandler + implements InvocationHandler { + + private final MergedAnnotation annotation; + + private final Class type; + + private final AttributeMethods attributes; + + @Nullable + private volatile Integer hashCode; + + + private SynthesizedMergedAnnotationInvocationHandler(MergedAnnotation annotation, + Class type) { + + Assert.notNull(annotation, "Annotation must not be null"); + Assert.notNull(type, "Type must not be null"); + Assert.isTrue(type.isAnnotation(), "Type must be an annotation"); + this.annotation = annotation; + this.type = type; + this.attributes = AttributeMethods.forAnnotationType(type); + for (int i = 0; i < this.attributes.size(); i++) { + getAttributeValue(this.attributes.get(i)); + } + } + + + @Override + public Object invoke(Object proxy, Method method, Object[] args) { + if (ReflectionUtils.isEqualsMethod(method)) { + return annotationEquals(args[0]); + } + if (ReflectionUtils.isHashCodeMethod(method)) { + return annotationHashCode(); + } + if (ReflectionUtils.isToStringMethod(method)) { + return this.annotation.toString(); + } + if (isAnnotationTypeMethod(method)) { + return this.type; + } + if (this.attributes.indexOf(method.getName()) != -1) { + return getAttributeValue(method); + } + throw new AnnotationConfigurationException(String.format( + "Method [%s] is unsupported for synthesized annotation type [%s]", method, + this.type)); + } + + private boolean isAnnotationTypeMethod(Method method) { + return Objects.equals(method.getName(), "annotationType") + && method.getParameterCount() == 0; + } + + /** + * See {@link Annotation#equals(Object)} for a definition of the required + * algorithm. + * @param other the other object to compare against + */ + private boolean annotationEquals(Object other) { + if (this == other) { + return true; + } + if (!this.type.isInstance(other)) { + return false; + } + for (int i = 0; i < this.attributes.size(); i++) { + Method attribute = this.attributes.get(i); + Object thisValue = getAttributeValue(attribute); + Object otherValue = ReflectionUtils.invokeMethod(attribute, other); + if (!ObjectUtils.nullSafeEquals(thisValue, otherValue)) { + return false; + } + } + return true; + } + + /** + * See {@link Annotation#hashCode()} for a definition of the required + * algorithm. + */ + private int annotationHashCode() { + Integer hashCode = this.hashCode; + if (hashCode == null) { + hashCode = computeHashCode(); + this.hashCode = hashCode; + } + return hashCode; + } + + private Integer computeHashCode() { + int hashCode = 0; + for (int i = 0; i < this.attributes.size(); i++) { + Method attribute = this.attributes.get(i); + Object value = getAttributeValue(attribute); + hashCode += (127 * attribute.getName().hashCode()) ^ getValueHashCode(value); + } + return hashCode; + } + + private int getValueHashCode(Object value) { + // Use Arrays.hashCode since ObjectUtils doesn't comply to to + // Annotation#hashCode() + if (value instanceof boolean[]) { + return Arrays.hashCode((boolean[]) value); + } + if (value instanceof byte[]) { + return Arrays.hashCode((byte[]) value); + } + if (value instanceof char[]) { + return Arrays.hashCode((char[]) value); + } + if (value instanceof double[]) { + return Arrays.hashCode((double[]) value); + } + if (value instanceof float[]) { + return Arrays.hashCode((float[]) value); + } + if (value instanceof int[]) { + return Arrays.hashCode((int[]) value); + } + if (value instanceof long[]) { + return Arrays.hashCode((long[]) value); + } + if (value instanceof short[]) { + return Arrays.hashCode((short[]) value); + } + if (value instanceof Object[]) { + return Arrays.hashCode((Object[]) value); + } + return value.hashCode(); + } + + private Object getAttributeValue(Method method) { + String name = method.getName(); + Class type = ClassUtils.resolvePrimitiveIfNecessary(method.getReturnType()); + return this.annotation.getValue(name, type).orElseThrow( + () -> new NoSuchElementException("No value found for attribute named '" + + name + "' in merged annotation " + this.annotation.getType())); + } + + @SuppressWarnings("unchecked") + static A createProxy(MergedAnnotation annotation, + Class type) { + ClassLoader classLoader = type.getClassLoader(); + InvocationHandler handler = new SynthesizedMergedAnnotationInvocationHandler<>( + annotation, type); + Class[] interfaces = isVisible(classLoader, SynthesizedAnnotation.class) + ? new Class[] { type, SynthesizedAnnotation.class } + : new Class[] { type }; + return (A) Proxy.newProxyInstance(classLoader, interfaces, handler); + } + + private static boolean isVisible(ClassLoader classLoader, Class interfaceClass) { + try { + return Class.forName(interfaceClass.getName(), false, + classLoader) == interfaceClass; + } + catch (ClassNotFoundException ex) { + return false; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java new file mode 100644 index 000000000000..3eaef51cb72f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java @@ -0,0 +1,614 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * {@link MergedAnnotation} that adapts attributes from a root annotation by + * applying the mapping and mirroring rules of an {@link AnnotationTypeMapping}. + * + *

Root attribute values are extracted from a source object using a supplied + * {@code BiFunction}. This allows various different annotation models to be + * supported by the same class. For example, the attributes source might be an + * actual {@link Annotation} instance where methods on the annotation instance + * are {@link ReflectionUtils#invokeMethod(Method, Object) invoked} to extract + * values. Equally, the source could be a simple {@link Map} with values + * extracted using {@link Map#get(Object)}. + * + *

Extracted root attribute values must be compatible with the attribute + * return type, namely: + * + *

+ * + * + * + * + * + * + *
Return TypeExtracted Type
ClassClass or String
Class[]Class[] or String[]
AnnotationAnnotation, Map or Object compatible with the value + * extractor
Annotation[]Annotation[], Map[] or Object[] where elements are + * compatible with the value extractor
Other typesAn exact match or the appropriate primitive wrapper
+ * + * @author Phillip Webb + * @since 5.2 + * @param the annotation type + * @see TypeMappedAnnotations + */ +final class TypeMappedAnnotation extends AbstractMergedAnnotation { + + private final AnnotationTypeMapping mapping; + + @Nullable + private final Object source; + + @Nullable + private final Object rootAttributes; + + private final BiFunction valueExtractor; + + private final int aggregateIndex; + + private final boolean useMergedValues; + + @Nullable + private final Predicate attributeFilter; + + private final int[] resolvedRootMirrors; + + private final int[] resolvedMirrors; + + @Nullable + private String string; + + + private TypeMappedAnnotation(AnnotationTypeMapping mapping, + @Nullable Object source, @Nullable Object rootAttributes, + BiFunction valueExtractor, int aggregateIndex) { + + this(mapping, source, rootAttributes, valueExtractor, aggregateIndex, null); + } + + private TypeMappedAnnotation(AnnotationTypeMapping mapping, + @Nullable Object source, @Nullable Object rootAttributes, + BiFunction valueExtractor, int aggregateIndex, + @Nullable int[] resolvedRootMirrors) { + + this.source = source; + this.rootAttributes = rootAttributes; + this.valueExtractor = valueExtractor; + this.mapping = mapping; + this.aggregateIndex = aggregateIndex; + this.useMergedValues = true; + this.attributeFilter = null; + this.resolvedRootMirrors = resolvedRootMirrors != null ? resolvedRootMirrors + : mapping.getRoot().getMirrorSets().resolve(source, rootAttributes, + this.valueExtractor); + this.resolvedMirrors = getDepth() == 0 ? this.resolvedRootMirrors + : mapping.getMirrorSets().resolve(source, this, + this::getValueForMirrorResolution); + } + + private TypeMappedAnnotation(AnnotationTypeMapping mapping, + @Nullable Object source, @Nullable Object rootAnnotation, + BiFunction valueExtractor, int aggregateIndex, + boolean useMergedValues, @Nullable Predicate attributeFilter, + int[] resolvedRootMirrors, int[] resolvedMirrors) { + + this.source = source; + this.rootAttributes = rootAnnotation; + this.valueExtractor = valueExtractor; + this.mapping = mapping; + this.aggregateIndex = aggregateIndex; + this.useMergedValues = useMergedValues; + this.attributeFilter = attributeFilter; + this.resolvedRootMirrors = resolvedRootMirrors; + this.resolvedMirrors = resolvedMirrors; + } + + + @Nullable + private Object getValueForMirrorResolution(Method attribute, Object annotation) { + int attributeIndex = this.mapping.getAttributes().indexOf(attribute); + boolean valueAttribute = VALUE.equals(attribute.getName()); + return getValue(attributeIndex, !valueAttribute, false); + } + + @Override + public String getType() { + return getAnnotationType().getName(); + } + + @Override + public boolean isPresent() { + return true; + } + + @Override + public int getDepth() { + return this.mapping.getDepth(); + } + + @Override + public int getAggregateIndex() { + return this.aggregateIndex; + } + + @Override + @Nullable + public Object getSource() { + return this.source; + } + + @Override + @Nullable + public MergedAnnotation getParent() { + AnnotationTypeMapping parentMapping = this.mapping.getParent(); + if (parentMapping == null) { + return null; + } + return new TypeMappedAnnotation<>(parentMapping, this.source, this.rootAttributes, + this.valueExtractor, this.aggregateIndex, this.resolvedRootMirrors); + } + + @Override + public boolean hasDefaultValue(String attributeName) { + int attributeIndex = getAttributeIndex(attributeName, true); + Object value = getValue(attributeIndex, true, true); + return value == null || this.mapping.isEquivalentToDefaultValue(attributeIndex, value, + this.valueExtractor); + } + + @Override + @SuppressWarnings("unchecked") + public MergedAnnotation getAnnotation(String attributeName, + Class type) throws NoSuchElementException { + + Assert.notNull(attributeName, "AttributeName must not be null"); + Assert.notNull(type, "Type must not be null"); + int attributeIndex = getAttributeIndex(attributeName, true); + Method attribute = this.mapping.getAttributes().get(attributeIndex); + Assert.isAssignable(type, attribute.getReturnType(), + "Attribute " + attributeName + " type mismatch:"); + return (MergedAnnotation) getRequiredValue(attributeIndex, Object.class); + } + + @Override + @SuppressWarnings("unchecked") + public MergedAnnotation[] getAnnotationArray( + String attributeName, Class type) throws NoSuchElementException { + + Assert.notNull(attributeName, "AttributeName must not be null"); + Assert.notNull(type, "Type must not be null"); + int attributeIndex = getAttributeIndex(attributeName, true); + Method attribute = this.mapping.getAttributes().get(attributeIndex); + Class componentType = attribute.getReturnType().getComponentType(); + Assert.notNull(componentType, () -> "Attribute " + attributeName + " is not an array"); + Assert.isAssignable(type, componentType, "Attribute " + attributeName + " component type mismatch:"); + return (MergedAnnotation[]) getRequiredValue(attributeIndex, Object.class); + } + + @Override + public Optional getDefaultValue(String attributeName, Class type) { + int attributeIndex = getAttributeIndex(attributeName, false); + if (attributeIndex == -1) { + return Optional.empty(); + } + Method attribute = this.mapping.getAttributes().get(attributeIndex); + return Optional.ofNullable(adapt(attribute, attribute.getDefaultValue(), type)); + } + + @Override + public MergedAnnotation filterAttributes(Predicate predicate) { + Assert.notNull(predicate, "Predicate must not be null"); + if (this.attributeFilter != null) { + predicate = this.attributeFilter.and(predicate); + } + return new TypeMappedAnnotation<>(this.mapping, this.source, this.rootAttributes, + this.valueExtractor, this.aggregateIndex, this.useMergedValues, predicate, + this.resolvedRootMirrors, this.resolvedMirrors); + } + + @Override + public MergedAnnotation withNonMergedAttributes() { + return new TypeMappedAnnotation<>(this.mapping, this.source, this.rootAttributes, + this.valueExtractor, this.aggregateIndex, false, this.attributeFilter, + this.resolvedRootMirrors, this.resolvedMirrors); + } + + @Override + @SuppressWarnings("unchecked") + public > T asMap( + @Nullable Function, T> factory, MapValues... options) { + + T map = factory != null ? factory.apply(this) : (T) new LinkedHashMap(); + Assert.state(map != null, + "Factory used to create MergedAnnotation Map must not return null;"); + AttributeMethods attributes = this.mapping.getAttributes(); + for (int i = 0; i < attributes.size(); i++) { + Method attribute = attributes.get(i); + Object value = isFiltered(attribute.getName()) ? + null : + getValue(i, getTypeForMapOptions(attribute, options)); + if (value != null) { + map.put(attribute.getName(), + adaptValueForMapOptions(attribute, value, factory, options)); + } + } + return (factory != null) ? map : (T) Collections.unmodifiableMap(map); + } + + private Class getTypeForMapOptions(Method attribute, MapValues[] options) { + Class attributeType = attribute.getReturnType(); + Class componentType = attributeType.isArray() ? + attributeType.getComponentType() : + attributeType; + if (MapValues.CLASS_TO_STRING.isIn(options) && componentType == Class.class) { + return attributeType.isArray() ? String[].class : String.class; + } + return Object.class; + } + + private > Object adaptValueForMapOptions( + Method attribute, Object value, + @Nullable Function, T> factory, MapValues[] options) { + + if (value instanceof MergedAnnotation) { + MergedAnnotation annotation = (MergedAnnotation) value; + return MapValues.ANNOTATION_TO_MAP.isIn(options) ? + annotation.asMap(factory, options) : + annotation.synthesize(); + } + if (value instanceof MergedAnnotation[]) { + MergedAnnotation[] annotations = (MergedAnnotation[]) value; + if (MapValues.ANNOTATION_TO_MAP.isIn(options)) { + Class componentType = Map.class; + if (factory != null) { + componentType = factory.apply(this).getClass(); + } + Object result = Array.newInstance(componentType, annotations.length); + for (int i = 0; i < annotations.length; i++) { + Array.set(result, i, annotations[i].asMap(factory, options)); + } + return result; + } + Object result = Array.newInstance( + attribute.getReturnType().getComponentType(), annotations.length); + for (int i = 0; i < annotations.length; i++) { + Array.set(result, i, annotations[i].synthesize()); + } + return result; + } + return value; + } + + @Override + protected A createSynthesized() { + return SynthesizedMergedAnnotationInvocationHandler.createProxy(this, getAnnotationType()); + } + + @Override + public String toString() { + String string = this.string; + if (string == null) { + StringBuilder builder = new StringBuilder(); + builder.append("@"); + builder.append(getType()); + builder.append("("); + for (int i = 0; i < this.mapping.getAttributes().size(); i++) { + Method attribute = this.mapping.getAttributes().get(i); + builder.append(i == 0 ? "" : ", "); + builder.append(attribute.getName()); + builder.append("="); + builder.append(toString(getValue(i, Object.class))); + } + builder.append(")"); + string = builder.toString(); + this.string = string; + } + return string; + } + + private Object toString(@Nullable Object value) { + if (value == null) { + return ""; + } + if (value instanceof Class) { + return ((Class) value).getName(); + } + if (value.getClass().isArray()) { + StringBuilder builder = new StringBuilder(); + builder.append("["); + for (int i = 0; i < Array.getLength(value); i++) { + builder.append(i == 0 ? "" : ", "); + builder.append(toString(Array.get(value, i))); + } + builder.append("]"); + return builder.toString(); + } + return String.valueOf(value); + } + + @Nullable + protected T getAttributeValue(String attributeName, Class type) { + int attributeIndex = getAttributeIndex(attributeName, false); + return attributeIndex != -1 ? getValue(attributeIndex, type) : null; + } + + protected final T getRequiredValue(int attributeIndex, Class type) { + T value = getValue(attributeIndex, type); + if (value == null) { + throw new NoSuchElementException( + "No element at attribute index " + attributeIndex); + } + return value; + } + + @Nullable + private T getValue(int attributeIndex, Class type) { + Method attribute = this.mapping.getAttributes().get(attributeIndex); + Object value = getValue(attributeIndex, true, true); + if (value == null) { + value = attribute.getDefaultValue(); + } + return adapt(attribute, value, type); + } + + @Nullable + private Object getValue(int attributeIndex, boolean useConventionMapping, + boolean resolveMirrors) { + AnnotationTypeMapping mapping = this.mapping; + if (this.useMergedValues) { + int mappedIndex = this.mapping.getAliasMapping(attributeIndex); + if (mappedIndex == -1 && useConventionMapping) { + mappedIndex = this.mapping.getConventionMapping(attributeIndex); + } + if (mappedIndex != -1) { + mapping = mapping.getRoot(); + attributeIndex = mappedIndex; + } + } + if (resolveMirrors) { + attributeIndex = (mapping.getDepth() != 0 ? + this.resolvedMirrors : + this.resolvedRootMirrors)[attributeIndex]; + } + if (attributeIndex == -1) { + return null; + } + Method attribute = mapping.getAttributes().get(attributeIndex); + if (mapping.getDepth() == 0) { + return this.valueExtractor.apply(attribute, this.rootAttributes); + } + return getValueFromMetaAnnotation(attribute); + } + + @Nullable + private Object getValueFromMetaAnnotation(Method attribute) { + AnnotationTypeMapping mapping = this.mapping; + if (this.useMergedValues && !VALUE.equals(attribute.getName())) { + AnnotationTypeMapping candidate = mapping; + while (candidate != null && candidate.getDepth() > 0) { + int attributeIndex = candidate.getAttributes().indexOf(attribute.getName()); + if (attributeIndex != -1) { + Method candidateAttribute = candidate.getAttributes().get(attributeIndex); + if (candidateAttribute.getReturnType().equals(attribute.getReturnType())) { + mapping = candidate; + attribute = candidateAttribute; + } + } + candidate = candidate.getParent(); + } + } + return ReflectionUtils.invokeMethod(attribute, mapping.getAnnotation()); + } + + @SuppressWarnings("unchecked") + @Nullable + private T adapt(Method attribute, @Nullable Object value, Class type) { + if (value == null) { + return null; + } + value = adaptForAttribute(attribute, value); + if (type == Object.class) { + type = (Class) getDefaultAdaptType(attribute); + } + else if (value instanceof Class && type == String.class) { + value = ((Class) value).getName(); + } + else if (value instanceof Class[] && type == String[].class) { + Class[] classes = (Class[]) value; + String[] names = new String[classes.length]; + for (int i = 0; i < classes.length; i++) { + names[i] = classes[i].getName(); + } + value = names; + } + else if (value instanceof MergedAnnotation && type.isAnnotation()) { + MergedAnnotation annotation = (MergedAnnotation) value; + value = annotation.synthesize(); + } + else if (value instanceof MergedAnnotation[] && type.isArray() + && type.getComponentType().isAnnotation()) { + MergedAnnotation[] annotations = (MergedAnnotation[]) value; + Object array = Array.newInstance(type.getComponentType(), annotations.length); + for (int i = 0; i < annotations.length; i++) { + Array.set(array, i, annotations[i].synthesize()); + } + value = array; + } + if (!type.isInstance(value)) { + throw new IllegalArgumentException("Unable to adapt value of type " + + value.getClass().getName() + " to " + type.getName()); + } + return (T) value; + } + + @SuppressWarnings("unchecked") + private Object adaptForAttribute(Method attribute, Object value) { + Class attributeType = ClassUtils.resolvePrimitiveIfNecessary(attribute.getReturnType()); + if (attributeType.isArray() && !value.getClass().isArray()) { + Object array = Array.newInstance(value.getClass(), 1); + Array.set(array, 0, value); + return adaptForAttribute(attribute, array); + } + if (attributeType.isAnnotation()) { + return adaptToMergedAnnotation(value,(Class) attributeType); + } + if (attributeType.isArray() && + attributeType.getComponentType().isAnnotation() && + value.getClass().isArray()) { + MergedAnnotation[] result = new MergedAnnotation[Array.getLength(value)]; + for (int i = 0; i < result.length; i++) { + result[i] = adaptToMergedAnnotation(Array.get(value, i), + (Class) attributeType.getComponentType()); + } + return result; + } + if ((attributeType == Class.class && value instanceof String) || + (attributeType == Class[].class && value instanceof String[])) { + return value; + } + if (!attributeType.isInstance(value)) { + throw new IllegalStateException("Attribute '" + attribute.getName() + + "' in annotation " + getType() + " should be compatible with " + + attributeType.getName() + " but a " + value.getClass().getName() + + " value was returned"); + } + return value; + } + + private MergedAnnotation adaptToMergedAnnotation(Object value, + Class annotationType) { + + AnnotationFilter filter = AnnotationFilter.mostAppropriateFor(annotationType); + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType( + annotationType, filter).get(0); + return new TypeMappedAnnotation<>(mapping, this.source, value, + this.getValueExtractor(value), this.aggregateIndex); + } + + private BiFunction getValueExtractor(Object value) { + if (value instanceof Annotation) { + return ReflectionUtils::invokeMethod; + } + if (value instanceof Map) { + return TypeMappedAnnotation::extractFromMap; + } + return this.valueExtractor; + } + + private Class getDefaultAdaptType(Method attribute) { + Class attributeType = attribute.getReturnType(); + if (attributeType.isAnnotation()) { + return MergedAnnotation.class; + } + if (attributeType.isArray() && attributeType.getComponentType().isAnnotation()) { + return MergedAnnotation[].class; + } + return ClassUtils.resolvePrimitiveIfNecessary(attributeType); + } + + private int getAttributeIndex(String attributeName, boolean required) { + Assert.hasText(attributeName, "AttributeName must not be null"); + int attributeIndex = isFiltered(attributeName) ? + -1 : + this.mapping.getAttributes().indexOf(attributeName); + if (attributeIndex == -1 && required) { + throw new NoSuchElementException("No attribute named '" + attributeName + + "' present in merged annotation " + getType()); + } + return attributeIndex; + } + + private boolean isFiltered(String attributeName) { + if (this.attributeFilter != null) { + return !this.attributeFilter.test(attributeName); + } + return false; + } + + @SuppressWarnings("unchecked") + private Class getAnnotationType() { + return (Class) this.mapping.getAnnotationType(); + } + + static MergedAnnotation from(@Nullable Object source, + A annotation) { + + Assert.notNull(annotation, "Annotation must not be null"); + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + annotation.annotationType()); + return new TypeMappedAnnotation<>(mappings.get(0), source, annotation, + ReflectionUtils::invokeMethod, 0); + } + + static MergedAnnotation from(@Nullable Object source, + Class annotationType, @Nullable Map attributes) { + + Assert.notNull(annotationType, "AnnotationType must not be null"); + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(annotationType); + return new TypeMappedAnnotation<>(mappings.get(0), source, attributes, + TypeMappedAnnotation::extractFromMap, 0); + } + + @Nullable + static TypeMappedAnnotation createIfPossible( + AnnotationTypeMapping mapping, @Nullable Object source, Annotation annotation, + int aggregateIndex, IntrospectionFailureLogger logger) { + + try { + return new TypeMappedAnnotation<>(mapping, source, annotation, + ReflectionUtils::invokeMethod, aggregateIndex); + } + catch (Exception ex) { + if (ex instanceof AnnotationConfigurationException) { + throw (AnnotationConfigurationException) ex; + } + if (logger.isEnabled()) { + String type = mapping.getAnnotationType().getName(); + String item = mapping.getDepth() == 0 ? + "annotation " + type : + "meta-annotation " + type + " from " + mapping.getRoot().getAnnotationType().getName(); + logger.log("Failed to introspect " + item, source, ex); + } + return null; + } + } + + @SuppressWarnings("unchecked") + @Nullable + private static Object extractFromMap(Method attribute, @Nullable Object map) { + return map != null ? ((Map) map).get(attribute.getName()) : null; + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java new file mode 100644 index 000000000000..4225edf0a6c4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java @@ -0,0 +1,697 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.lang.Nullable; + +/** + * {@link MergedAnnotations} implementation that searches for and adapts + * annotations and meta-annotations using {@link AnnotationTypeMappings}. + * + * @author Phillip Webb + * @since 5.1 + */ +final class TypeMappedAnnotations implements MergedAnnotations { + + private static final AnnotationFilter FILTER_ALL = annotationType -> true; + + private static final MergedAnnotations NONE = new TypeMappedAnnotations(null, + new Annotation[0], RepeatableContainers.none(), FILTER_ALL); + + + @Nullable + private final Object source; + + @Nullable + private final AnnotatedElement element; + + @Nullable + private final SearchStrategy searchStrategy; + + @Nullable + private final Annotation[] annotations; + + private final RepeatableContainers repeatableContainers; + + private final AnnotationFilter annotationFilter; + + @Nullable + private volatile List aggregates; + + + private TypeMappedAnnotations(AnnotatedElement element, SearchStrategy searchStrategy, + RepeatableContainers repeatableContainers, + AnnotationFilter annotationFilter) { + + this.source = element; + this.element = element; + this.searchStrategy = searchStrategy; + this.annotations = null; + this.repeatableContainers = repeatableContainers; + this.annotationFilter = annotationFilter; + } + + private TypeMappedAnnotations(@Nullable Object source, Annotation[] annotations, + RepeatableContainers repeatableContainers, + AnnotationFilter annotationFilter) { + + this.source = source; + this.element = null; + this.searchStrategy = null; + this.annotations = annotations; + this.repeatableContainers = repeatableContainers; + this.annotationFilter = annotationFilter; + } + + + @Override + public boolean isPresent(@Nullable Class annotationType) { + if (annotationType == null || this.annotationFilter.matches(annotationType)) { + return false; + } + return Boolean.TRUE.equals(scan(annotationType, + IsPresent.get(this.repeatableContainers, this.annotationFilter, false))); + } + + @Override + public boolean isPresent(@Nullable String annotationType) { + if (annotationType == null || this.annotationFilter.matches(annotationType)) { + return false; + } + return Boolean.TRUE.equals(scan(annotationType, + IsPresent.get(this.repeatableContainers, this.annotationFilter, false))); + } + + @Override + public boolean isDirectlyPresent(@Nullable Class annotationType) { + if (annotationType == null || this.annotationFilter.matches(annotationType)) { + return false; + } + return Boolean.TRUE.equals(scan(annotationType, + IsPresent.get(this.repeatableContainers, this.annotationFilter, true))); + } + + @Override + public boolean isDirectlyPresent(@Nullable String annotationType) { + if (annotationType == null || this.annotationFilter.matches(annotationType)) { + return false; + } + return Boolean.TRUE.equals(scan(annotationType, + IsPresent.get(this.repeatableContainers, this.annotationFilter, true))); + } + + @Override + public MergedAnnotation get( + @Nullable Class annotationType) { + + return get(annotationType, null, null); + } + + @Override + public MergedAnnotation get( + @Nullable Class annotationType, + @Nullable Predicate> predicate) { + + return get(annotationType, predicate, null); + } + + @Override + public MergedAnnotation get( + @Nullable Class annotationType, + @Nullable Predicate> predicate, + @Nullable MergedAnnotationSelector selector) { + + if (annotationType == null || this.annotationFilter.matches(annotationType)) { + return MergedAnnotation.missing(); + } + MergedAnnotation result = scan(annotationType, + new MergedAnnotationFinder<>(annotationType, predicate, selector)); + return result != null ? result : MergedAnnotation.missing(); + } + + @Override + public MergedAnnotation get( + @Nullable String annotationType) { + return get(annotationType, null, null); + } + + @Override + public MergedAnnotation get(@Nullable String annotationType, + @Nullable Predicate> predicate) { + return get(annotationType, predicate, null); + } + + @Override + public MergedAnnotation get(@Nullable String annotationType, + @Nullable Predicate> predicate, + @Nullable MergedAnnotationSelector selector) { + + if (annotationType == null || this.annotationFilter.matches(annotationType)) { + return MergedAnnotation.missing(); + } + MergedAnnotation result = scan(annotationType, + new MergedAnnotationFinder<>(annotationType, predicate, selector)); + return result != null ? result : MergedAnnotation.missing(); + } + + @Override + public Stream> stream( + @Nullable Class annotationType) { + + if (this.annotationFilter == FILTER_ALL) { + return Stream.empty(); + } + return StreamSupport.stream(spliterator(annotationType), false); + } + + @Override + public Stream> stream( + @Nullable String annotationType) { + + if (this.annotationFilter == FILTER_ALL) { + return Stream.empty(); + } + return StreamSupport.stream(spliterator(annotationType), false); + } + + @Override + public Stream> stream() { + if (this.annotationFilter == FILTER_ALL) { + return Stream.empty(); + } + return StreamSupport.stream(spliterator(), false); + } + + @Override + public Iterator> iterator() { + if (this.annotationFilter == FILTER_ALL) { + return Collections.emptyIterator(); + } + return Spliterators.iterator(spliterator()); + } + + @Override + public Spliterator> spliterator() { + if (this.annotationFilter == FILTER_ALL) { + return Collections.> emptyList().spliterator(); + } + return spliterator(null); + } + + private Spliterator> spliterator( + @Nullable Object annotationType) { + return new AggregatesSpliterator<>(annotationType, getAggregates()); + } + + private List getAggregates() { + List aggregates = this.aggregates; + if (aggregates == null) { + aggregates = scan(this, new AggregatesCollector()); + if (aggregates == null || aggregates.isEmpty()) { + aggregates = Collections.emptyList(); + } + this.aggregates = aggregates; + } + return aggregates; + } + + @Nullable + private R scan(C criteria, AnnotationsProcessor processor) { + if (this.annotations != null) { + R result = processor.doWithAnnotations(criteria, 0, this.source, this.annotations); + return processor.finish(result); + } + if (this.element != null && this.searchStrategy != null) { + return AnnotationsScanner.scan(criteria, this.element, this.searchStrategy, processor); + } + return null; + } + + static MergedAnnotations from(@Nullable AnnotatedElement element, + SearchStrategy searchStrategy, RepeatableContainers repeatableContainers, + AnnotationFilter annotationFilter) { + + if (element == null || AnnotationsScanner.isKnownEmpty(element, searchStrategy, annotationFilter)) { + return NONE; + } + return new TypeMappedAnnotations(element, searchStrategy, repeatableContainers, + annotationFilter); + } + + static MergedAnnotations from(@Nullable Object source, + Annotation[] annotations, RepeatableContainers repeatableContainers, + AnnotationFilter annotationFilter) { + + if (annotations.length == 0) { + return NONE; + } + return new TypeMappedAnnotations(source, annotations, repeatableContainers, annotationFilter); + } + + private static boolean isMappingForType(@Nullable AnnotationTypeMapping mapping, + AnnotationFilter annotationFilter, @Nullable Object requiredType) { + + if (mapping == null) { + return false; + } + Class actualType = mapping.getAnnotationType(); + return !annotationFilter.matches(actualType) && + (requiredType == null || actualType == requiredType || actualType.getName().equals(requiredType)); + } + + + /** + * {@link AnnotationsProcessor} used to detect if an annotation is directly + * or meta-present. + */ + private static final class IsPresent + implements AnnotationsProcessor { + + /** + * Shared instances that save us needing to create a new processor for + * the common combinations. + */ + private static final IsPresent[] SHARED; + static { + SHARED = new IsPresent[4]; + SHARED[0] = new IsPresent(RepeatableContainers.none(), AnnotationFilter.PLAIN, true); + SHARED[1] = new IsPresent(RepeatableContainers.none(), AnnotationFilter.PLAIN, false); + SHARED[2] = new IsPresent(RepeatableContainers.standardRepeatables(), AnnotationFilter.PLAIN, true); + SHARED[3] = new IsPresent(RepeatableContainers.standardRepeatables(), AnnotationFilter.PLAIN, false); + } + + + private final RepeatableContainers repeatableContainers; + + private final AnnotationFilter annotationFilter; + + private final boolean directOnly; + + + private IsPresent(RepeatableContainers repeatableContainers, + AnnotationFilter annotationFilter, boolean directOnly) { + + this.repeatableContainers = repeatableContainers; + this.annotationFilter = annotationFilter; + this.directOnly = directOnly; + } + + + @Override + @Nullable + public Boolean doWithAnnotations(Object requiredType, int aggregateIndex, + @Nullable Object source, Annotation[] annotations) { + + for (Annotation annotation : annotations) { + if (annotation != null) { + Class type = annotation.annotationType(); + if (type != null && !this.annotationFilter.matches(type)) { + if (type == requiredType || type.getName().equals(requiredType)) { + return Boolean.TRUE; + } + Annotation[] repeatedAnnotations = this.repeatableContainers + .findRepeatedAnnotations(annotation); + if (repeatedAnnotations != null) { + Boolean result = doWithAnnotations(requiredType, aggregateIndex, + source, repeatedAnnotations); + if (result != null) { + return result; + } + } + if (!this.directOnly) { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(type); + for (int i = 0; i < mappings.size(); i++) { + AnnotationTypeMapping mapping = mappings.get(i); + if (isMappingForType(mapping, this.annotationFilter, requiredType)) { + return Boolean.TRUE; + } + } + } + } + } + } + return null; + } + + static IsPresent get(RepeatableContainers repeatableContainers, + AnnotationFilter annotationFilter, boolean directOnly) { + + // Use a single shared instance for common combinations + if (annotationFilter == AnnotationFilter.PLAIN) { + if (repeatableContainers == RepeatableContainers.none()) { + return SHARED[directOnly ? 0 : 1]; + } + if (repeatableContainers == RepeatableContainers.standardRepeatables()) { + return SHARED[directOnly ? 2 : 3]; + } + } + return new IsPresent(repeatableContainers, annotationFilter, directOnly); + } + + } + + + /** + * {@link AnnotationsProcessor} that finds a single + * {@link MergedAnnotation}. + */ + private class MergedAnnotationFinder + implements AnnotationsProcessor> { + + private final Object requiredType; + + @Nullable + private final Predicate> predicate; + + private final MergedAnnotationSelector selector; + + @Nullable + private MergedAnnotation result; + + + MergedAnnotationFinder(Object requiredType, + @Nullable Predicate> predicate, + @Nullable MergedAnnotationSelector selector) { + + this.requiredType = requiredType; + this.predicate = predicate; + this.selector = selector != null ? selector + : MergedAnnotationSelectors.nearest(); + } + + + @Override + @Nullable + public MergedAnnotation doWithAggregate(Object context, int aggregateIndex) { + return this.result; + } + + @Override + @Nullable + public MergedAnnotation doWithAnnotations(Object type, int aggregateIndex, + @Nullable Object source, Annotation[] annotations) { + + for (Annotation annotation : annotations) { + if (annotation != null && + !annotationFilter.matches(annotation)) { + MergedAnnotation result = process(type, aggregateIndex, source, annotation); + if (result != null) { + return result; + } + } + } + return null; + } + + @Nullable + private MergedAnnotation process(Object type, int aggregateIndex, + @Nullable Object source, Annotation annotation) { + Annotation[] repeatedAnnotations = repeatableContainers.findRepeatedAnnotations( + annotation); + if (repeatedAnnotations != null) { + return doWithAnnotations(type, aggregateIndex, source, + repeatedAnnotations); + } + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + annotation.annotationType(), + annotationFilter); + for (int i = 0; i < mappings.size(); i++) { + AnnotationTypeMapping mapping = mappings.get(i); + if (isMappingForType(mapping, annotationFilter, + this.requiredType)) { + MergedAnnotation candidate = TypeMappedAnnotation.createIfPossible( + mapping, source, annotation, aggregateIndex, + IntrospectionFailureLogger.INFO); + if (candidate != null && (this.predicate == null + || this.predicate.test(candidate))) { + if (this.selector.isBestCandidate(candidate)) { + return candidate; + } + updateLastResult(candidate); + } + } + } + return null; + } + + private void updateLastResult(MergedAnnotation candidate) { + MergedAnnotation lastResult = this.result; + this.result = lastResult != null + ? this.selector.select(lastResult, candidate) + : candidate; + } + + @Override + @Nullable + public MergedAnnotation finish(@Nullable MergedAnnotation result) { + return result != null ? result : this.result; + } + + } + + /** + * {@link AnnotationsProcessor} that collects {@link Aggregate} instances. + */ + private class AggregatesCollector + implements AnnotationsProcessor> { + + private final List aggregates = new ArrayList<>(); + + @Override + @Nullable + public List doWithAnnotations(Object criteria, int aggregateIndex, + @Nullable Object source, Annotation[] annotations) { + this.aggregates.add(createAggregate(aggregateIndex, source, annotations)); + return null; + } + + private Aggregate createAggregate(int aggregateIndex, @Nullable Object source, + Annotation[] annotations) { + List aggregateAnnotations = getAggregateAnnotations(annotations); + return new Aggregate(aggregateIndex, source, aggregateAnnotations); + } + + private List getAggregateAnnotations(Annotation[] annotations) { + List result = new ArrayList<>(annotations.length); + addAggregateAnnotations(result, annotations); + return result; + } + + private void addAggregateAnnotations(List aggregateAnnotations, + Annotation[] annotations) { + for (Annotation annotation : annotations) { + if (annotation != null + && !annotationFilter.matches( + annotation)) { + Annotation[] repeatedAnnotations = repeatableContainers.findRepeatedAnnotations( + annotation); + if (repeatedAnnotations != null) { + addAggregateAnnotations(aggregateAnnotations, + repeatedAnnotations); + } + else { + aggregateAnnotations.add(annotation); + } + } + } + } + + @Override + public List finish(@Nullable List processResult) { + return this.aggregates; + } + + } + + private static class Aggregate { + + private final int aggregateIndex; + + @Nullable + private final Object source; + + private final List annotations; + + private final AnnotationTypeMappings[] mappings; + + Aggregate(int aggregateIndex, @Nullable Object source, + List annotations) { + this.aggregateIndex = aggregateIndex; + this.source = source; + this.annotations = annotations; + this.mappings = new AnnotationTypeMappings[annotations.size()]; + for (int i = 0; i < annotations.size(); i++) { + this.mappings[i] = AnnotationTypeMappings.forAnnotationType( + annotations.get(i).annotationType()); + } + } + + int size() { + return this.annotations.size(); + } + + @Nullable + AnnotationTypeMapping getMapping(int annotationIndex, int mappingIndex) { + AnnotationTypeMappings mappings = getMappings(annotationIndex); + return mappingIndex < mappings.size() ? mappings.get(mappingIndex) : null; + } + + AnnotationTypeMappings getMappings(int annotationIndex) { + return this.mappings[annotationIndex]; + } + + @Nullable + MergedAnnotation createMergedAnnotationIfPossible( + int annotationIndex, int mappingIndex, + IntrospectionFailureLogger logger) { + return TypeMappedAnnotation.createIfPossible( + this.mappings[annotationIndex].get(mappingIndex), this.source, + this.annotations.get(annotationIndex), this.aggregateIndex, logger); + } + + } + + /** + * {@link Spliterator} used to consume merged annotations from the + * aggregates in depth fist order. + */ + private class AggregatesSpliterator + implements Spliterator> { + + @Nullable + private final Object requiredType; + + private final List aggregates; + + private int aggregateCursor; + + @Nullable + private int[] mappingCursors; + + AggregatesSpliterator(@Nullable Object requiredType, + List aggregates) { + this.requiredType = requiredType; + this.aggregates = aggregates; + this.aggregateCursor = 0; + } + + public boolean tryAdvance(Consumer> action) { + while (this.aggregateCursor < this.aggregates.size()) { + Aggregate aggregate = this.aggregates.get(this.aggregateCursor); + if (tryAdvance(aggregate, action)) { + return true; + } + this.aggregateCursor++; + this.mappingCursors = null; + } + return false; + } + + private boolean tryAdvance(Aggregate aggregate, + Consumer> action) { + if (this.mappingCursors == null) { + this.mappingCursors = new int[aggregate.size()]; + } + int lowestDepth = Integer.MAX_VALUE; + int annotationResult = -1; + for (int annotationIndex = 0; annotationIndex < aggregate.size(); annotationIndex++) { + AnnotationTypeMapping mapping = getNextSuitableMapping(aggregate, annotationIndex); + if (mapping != null && mapping.getDepth() < lowestDepth) { + annotationResult = annotationIndex; + lowestDepth = mapping.getDepth(); + } + if (lowestDepth == 0) { + break; + } + } + if (annotationResult != -1) { + MergedAnnotation mergedAnnotation = aggregate.createMergedAnnotationIfPossible( + annotationResult, this.mappingCursors[annotationResult], + this.requiredType != null ? IntrospectionFailureLogger.INFO : IntrospectionFailureLogger.DEBUG); + this.mappingCursors[annotationResult]++; + if (mergedAnnotation == null) { + return tryAdvance(aggregate, action); + } + action.accept(mergedAnnotation); + return true; + } + return false; + } + + @Nullable + private AnnotationTypeMapping getNextSuitableMapping(Aggregate aggregate, + int annotationIndex) { + int[] cursors = this.mappingCursors; + if (cursors != null) { + AnnotationTypeMapping mapping; + do { + mapping = aggregate.getMapping(annotationIndex, cursors[annotationIndex]); + if (isMappingForType(mapping, annotationFilter, this.requiredType)) { + return mapping; + } + cursors[annotationIndex]++; + } + while (mapping != null); + } + return null; + } + + @Override + @Nullable + public Spliterator> trySplit() { + return null; + } + + @Override + public long estimateSize() { + int size = 0; + for (int aggregateIndex = this.aggregateCursor; + aggregateIndex < this.aggregates.size(); aggregateIndex++) { + Aggregate aggregate = this.aggregates.get(aggregateIndex); + for (int annotationIndex = 0; annotationIndex < aggregate.size(); annotationIndex++) { + AnnotationTypeMappings mappings = aggregate.getMappings(annotationIndex); + int numberOfMappings = mappings.size(); + if (aggregateIndex == this.aggregateCursor && this.mappingCursors != null) { + numberOfMappings -= Math.min(this.mappingCursors[annotationIndex], mappings.size()); + } + size += numberOfMappings; + } + } + return size; + } + + @Override + public int characteristics() { + return NONNULL | IMMUTABLE; + } + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationBackCompatibiltyTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationBackCompatibiltyTests.java new file mode 100644 index 000000000000..8131dfa0ad33 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationBackCompatibiltyTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests to ensure back-compatibility with Spring Framework 5.1. + * + * @author Phillip Webb + * @since 5.2 + */ +public class AnnotationBackCompatibiltyTests { + + @Test + public void multiplRoutesToMetaAnnotation() { + Class source = WithMetaMetaTestAnnotation1AndMetaTestAnnotation2.class; + // Merged annotation chooses lowest depth + MergedAnnotation mergedAnnotation = MergedAnnotations.from(source).get(TestAnnotation.class); + assertThat(mergedAnnotation.getString("value")).isEqualTo("testAndMetaTest"); + // AnnotatedElementUtils finds first + TestAnnotation previousVersion = AnnotatedElementUtils.getMergedAnnotation(source, TestAnnotation.class); + assertThat(previousVersion.value()).isEqualTo("metaTest"); + } + + @Test + public void defaultValue() { + DefaultValueAnnotation synthesized = MergedAnnotations.from(WithDefaultValue.class).get(DefaultValueAnnotation.class).synthesize(); + assertThat(synthesized).isInstanceOf(SynthesizedAnnotation.class); + Object defaultValue = AnnotationUtils.getDefaultValue(synthesized, "enumValue"); + assertThat(defaultValue).isEqualTo(TestEnum.ONE); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface TestAnnotation { + + String value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @TestAnnotation("metaTest") + @interface MetaTestAnnotation { + + } + + @Retention(RetentionPolicy.RUNTIME) + @TestAnnotation("testAndMetaTest") + @MetaTestAnnotation + @interface TestAndMetaTestAnnotation { + + } + + @Retention(RetentionPolicy.RUNTIME) + @MetaTestAnnotation + @interface MetaMetaTestAnnotation { + } + + @MetaMetaTestAnnotation + @TestAndMetaTestAnnotation + static class WithMetaMetaTestAnnotation1AndMetaTestAnnotation2 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface DefaultValueAnnotation { + + @AliasFor("enumAlais") + TestEnum enumValue() default TestEnum.ONE; + + @AliasFor("enumValue") + TestEnum enumAlais() default TestEnum.ONE; + + } + + @DefaultValueAnnotation + static class WithDefaultValue { + + } + + static enum TestEnum { + + ONE, + + TWO + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationFilterTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationFilterTests.java new file mode 100644 index 000000000000..1e4553e98abd --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationFilterTests.java @@ -0,0 +1,191 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; + +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link AnnotationFilter}. + * + * @author Phillip Webb + */ +public class AnnotationFilterTests { + + private static final AnnotationFilter FILTER = annotationType -> ObjectUtils.nullSafeEquals( + annotationType, TestAnnotation.class.getName()); + + @Test + public void matchesAnnotationWhenAnnotationIsNullReturnsFalse() { + TestAnnotation annotation = null; + assertThat(FILTER.matches(annotation)).isFalse(); + } + + @Test + public void matchesAnnotationWhenMatchReturnsTrue() { + TestAnnotation annotation = WithTestAnnotation.class.getDeclaredAnnotation( + TestAnnotation.class); + assertThat(FILTER.matches(annotation)).isTrue(); + } + + @Test + public void matchesAnnotationWhenNoMatchReturnsFalse() { + OtherAnnotation annotation = WithOtherAnnotation.class.getDeclaredAnnotation( + OtherAnnotation.class); + assertThat(FILTER.matches(annotation)).isFalse(); + } + + @Test + public void matchesAnnotationClassWhenAnnotationClassIsNullReturnsFalse() { + Class annotationType = null; + assertThat(FILTER.matches(annotationType)).isFalse(); + } + + @Test + public void matchesAnnotationClassWhenMatchReturnsTrue() { + Class annotationType = TestAnnotation.class; + assertThat(FILTER.matches(annotationType)).isTrue(); + } + + @Test + public void matchesAnnotationClassWhenNoMatchReturnsFalse() { + Class annotationType = OtherAnnotation.class; + assertThat(FILTER.matches(annotationType)).isFalse(); + } + + @Test + public void plainWhenJavaLangAnnotationReturnsTrue() { + assertThat(AnnotationFilter.PLAIN.matches(Retention.class)).isTrue(); + } + + @Test + public void plainWhenSpringLangAnnotationReturnsTrue() { + assertThat(AnnotationFilter.PLAIN.matches(Nullable.class)).isTrue(); + } + + @Test + public void plainWhenOtherAnnotationReturnsFalse() { + assertThat(AnnotationFilter.PLAIN.matches(TestAnnotation.class)).isFalse(); + } + + @Test + public void javaWhenJavaLangAnnotationReturnsTrue() { + assertThat(AnnotationFilter.JAVA.matches(Retention.class)).isTrue(); + } + + @Test + public void javaWhenSpringLangAnnotationReturnsFalse() { + assertThat(AnnotationFilter.JAVA.matches(Nullable.class)).isFalse(); + } + + @Test + public void javaWhenOtherAnnotationReturnsFalse() { + assertThat(AnnotationFilter.JAVA.matches(TestAnnotation.class)).isFalse(); + } + + @Test + public void noneWhenNonNullReturnsFalse() { + assertThat(AnnotationFilter.NONE.matches(Retention.class)).isFalse(); + assertThat(AnnotationFilter.NONE.matches(Nullable.class)).isFalse(); + assertThat(AnnotationFilter.NONE.matches(TestAnnotation.class)).isFalse(); + assertThat(AnnotationFilter.NONE.matches((Annotation) null)).isFalse(); + assertThat(AnnotationFilter.NONE.matches((Class) null)).isFalse(); + assertThat(AnnotationFilter.NONE.matches((String) null)).isFalse(); + } + + @Test + public void pacakgesReturnsPackagesAnnotationFilter() { + assertThat(AnnotationFilter.packages("com.example")).isInstanceOf( + PackagesAnnotationFilter.class); + } + + @Test + public void mostAppropriateForCollectionWhenAnnotationTypesIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> AnnotationFilter.mostAppropriateFor( + (Collection>) null)).withMessage( + "AnnotationTypes must not be null"); + } + + @Test + public void mostAppropriateForCollectionReturnsPlainWhenPossible() { + AnnotationFilter filter = AnnotationFilter.mostAppropriateFor( + Arrays.asList(TestAnnotation.class, OtherAnnotation.class)); + assertThat(filter).isSameAs(AnnotationFilter.PLAIN); + } + + @Test + public void mostAppropriateForCollectionWhenCantUsePlainReturnsNone() { + AnnotationFilter filter = AnnotationFilter.mostAppropriateFor(Arrays.asList( + TestAnnotation.class, OtherAnnotation.class, Nullable.class)); + assertThat(filter).isSameAs(AnnotationFilter.NONE); + } + + @Test + public void mostAppropriateForArrayWhenAnnotationTypesIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> AnnotationFilter.mostAppropriateFor( + (Class[]) null)).withMessage( + "AnnotationTypes must not be null"); + } + + @Test + public void mostAppropriateForArrayReturnsPlainWhenPossible() { + AnnotationFilter filter = AnnotationFilter.mostAppropriateFor( + TestAnnotation.class, OtherAnnotation.class); + assertThat(filter).isSameAs(AnnotationFilter.PLAIN); + } + + @Test + public void mostAppropriateForArrayWhenCantUsePlainReturnsNone() { + AnnotationFilter filter = AnnotationFilter.mostAppropriateFor( + TestAnnotation.class, OtherAnnotation.class, Nullable.class); + assertThat(filter).isSameAs(AnnotationFilter.NONE); + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface TestAnnotation { + + } + + @TestAnnotation + static class WithTestAnnotation { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface OtherAnnotation { + + } + + @OtherAnnotation + static class WithOtherAnnotation { + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java index 6401b6490337..bf58a51ae664 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationIntrospectionFailureTests.java @@ -78,6 +78,24 @@ public void filteredTypeInMetaAnnotationWhenUsingAnnotatedElementUtilsHandlesExc exampleMetaAnnotationClass)).isTrue(); } + @Test + @SuppressWarnings("unchecked") + public void filteredTypeInMetaAnnotationWhenUsingMergedAnnotationsHandlesException() throws Exception { + FilteringClassLoader classLoader = new FilteringClassLoader( + getClass().getClassLoader()); + Class withExampleMetaAnnotation = ClassUtils.forName( + WithExampleMetaAnnotation.class.getName(), classLoader); + Class exampleAnnotationClass = (Class) ClassUtils.forName( + ExampleAnnotation.class.getName(), classLoader); + Class exampleMetaAnnotationClass = (Class) ClassUtils.forName( + ExampleMetaAnnotation.class.getName(), classLoader); + MergedAnnotations annotations = MergedAnnotations.from(withExampleMetaAnnotation); + assertThat(annotations.get(exampleAnnotationClass).isPresent()).isFalse(); + assertThat(annotations.get(exampleMetaAnnotationClass).isPresent()).isFalse(); + assertThat(annotations.isPresent(exampleMetaAnnotationClass)).isFalse(); + assertThat(annotations.isPresent(exampleAnnotationClass)).isFalse(); + } + static class FilteringClassLoader extends OverridingClassLoader { diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java new file mode 100644 index 000000000000..1f16c1a1282e --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java @@ -0,0 +1,1037 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +import org.junit.Test; + +import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets; +import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet; +import org.springframework.lang.UsesSunMisc; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.*; + +/** + * Tests for {@link AnnotationTypeMappings} and {@link AnnotationTypeMapping}. + * + * @author Phillip Webb + */ +public class AnnotationTypeMappingsTests { + + @Test + public void forAnnotationTypeWhenAnnotationIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> AnnotationTypeMappings.forAnnotationType(null)).withMessage( + "AnnotationType must not be null"); + } + + @Test + public void forAnnotationTypeWhenNoMetaAnnotationsReturnsMappings() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + SimpleAnnotation.class); + assertThat(mappings.size()).isEqualTo(1); + assertThat(mappings.get(0).getAnnotationType()).isEqualTo(SimpleAnnotation.class); + assertThat(getAll(mappings)).flatExtracting( + AnnotationTypeMapping::getAnnotationType).containsExactly( + SimpleAnnotation.class); + } + + @Test + public void forAnnotationWhenHasSpringAnnotationReturnsFilteredMappings() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + WithSpringLangAnnotation.class); + assertThat(mappings.size()).isEqualTo(1); + } + + @Test + public void forAnnotationTypeWhenMetaAnnotationsReturnsMappings() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + MetaAnnotated.class); + assertThat(mappings.size()).isEqualTo(6); + assertThat(getAll(mappings)).flatExtracting( + AnnotationTypeMapping::getAnnotationType).containsExactly( + MetaAnnotated.class, A.class, B.class, AA.class, AB.class, + ABC.class); + } + + @Test + public void forAnnotationTypeWhenHasRepeatingMetaAnnotationReturnsMapping() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + WithRepeatedMetaAnnotations.class); + assertThat(mappings.size()).isEqualTo(3); + assertThat(getAll(mappings)).flatExtracting( + AnnotationTypeMapping::getAnnotationType).containsExactly( + WithRepeatedMetaAnnotations.class, Repeating.class, + Repeating.class); + } + + @Test + public void forAnnotationTypeWhenSelfAnnotatedReturnsMapping() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + SelfAnnotated.class); + assertThat(mappings.size()).isEqualTo(1); + assertThat(getAll(mappings)).flatExtracting( + AnnotationTypeMapping::getAnnotationType).containsExactly( + SelfAnnotated.class); + } + + @Test + public void forAnnotationTypeWhenFormsLoopReturnsMapping() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + LoopA.class); + assertThat(mappings.size()).isEqualTo(2); + assertThat(getAll(mappings)).flatExtracting( + AnnotationTypeMapping::getAnnotationType).containsExactly(LoopA.class, + LoopB.class); + } + + @Test + public void forAnnotationTypeWhenHasAliasForWithBothValueAndAttributeThrowsException() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> AnnotationTypeMappings.forAnnotationType( + AliasForWithBothValueAndAttribute.class)).withMessage( + "In @AliasFor declared on attribute 'test' in annotation [" + + AliasForWithBothValueAndAttribute.class.getName() + + "], attribute 'attribute' and its alias 'value' are present with values of 'foo' and 'bar', but only one is permitted."); + } + + @Test + public void forAnnotationTypeWhenAliasForToSelfNonExistingAttribute() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> AnnotationTypeMappings.forAnnotationType( + AliasForToSelfNonExistingAttribute.class)).withMessage( + "@AliasFor declaration on attribute 'test' in annotation [" + + AliasForToSelfNonExistingAttribute.class.getName() + + "] declares an alias for 'missing' which is not present."); + } + + @Test + public void forAnnotationTypeWhenAliasForToOtherNonExistingAttribute() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> AnnotationTypeMappings.forAnnotationType( + AliasForToOtherNonExistingAttribute.class)).withMessage( + "Attribute 'test' in annotation [" + + AliasForToOtherNonExistingAttribute.class.getName() + + "] is declared as an @AliasFor nonexistent " + + "attribute 'missing' in annotation [" + + AliasForToOtherNonExistingAttributeTarget.class.getName() + + "]."); + } + + @Test + public void forAnnotationTypeWhenAliasForToSelf() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> AnnotationTypeMappings.forAnnotationType( + AliasForToSelf.class)).withMessage( + "@AliasFor declaration on attribute 'test' in annotation [" + + AliasForToSelf.class.getName() + + "] points to itself. Specify 'annotation' to point to " + + "a same-named attribute on a meta-annotation."); + } + + @Test + public void forAnnotationTypeWhenAliasForWithArrayCompatibleReturnTypes() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + AliasForWithArrayCompatibleReturnTypes.class); + AnnotationTypeMapping mapping = getMapping(mappings, + AliasForWithArrayCompatibleReturnTypesTarget.class); + assertThat(getAliasMapping(mapping, 0).getName()).isEqualTo("test"); + } + + @Test + public void forAnnotationTypeWhenAliasForWithIncompatibleReturnTypes() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> AnnotationTypeMappings.forAnnotationType( + AliasForWithIncompatibleReturnTypes.class)).withMessage( + "Misconfigured aliases: attribute 'test' in annotation [" + + AliasForWithIncompatibleReturnTypes.class.getName() + + "] and attribute 'test' in annotation [" + + AliasForWithIncompatibleReturnTypesTarget.class.getName() + + "] must declare the same return type."); + } + + @Test + public void forAnnotationTypeWhenAliasForToSelfNonAnnotatedAttribute() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> AnnotationTypeMappings.forAnnotationType( + AliasForToSelfNonAnnotatedAttribute.class)).withMessage( + "Attribute 'other' in annotation [" + + AliasForToSelfNonAnnotatedAttribute.class.getName() + + "] must be declared as an @AliasFor 'test'."); + } + + @Test + public void forAnnotationTypeWhenAliasForToSelfAnnotatedToOtherAttribute() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> AnnotationTypeMappings.forAnnotationType( + AliasForToSelfAnnotatedToOtherAttribute.class)).withMessage( + "Attribute 'b' in annotation [" + + AliasForToSelfAnnotatedToOtherAttribute.class.getName() + + "] must be declared as an @AliasFor 'a', not 'c'."); + } + + @Test + public void forAnnotationTypeWhenAliasForNonMetaAnnotated() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> AnnotationTypeMappings.forAnnotationType( + AliasForNonMetaAnnotated.class)).withMessage( + "@AliasFor declaration on attribute 'test' in annotation [" + + AliasForNonMetaAnnotated.class.getName() + + "] declares an alias for attribute 'test' in annotation [" + + AliasForNonMetaAnnotatedTarget.class.getName() + + "] which is not meta-present."); + } + + @Test + public void forAnnotationTypeWhenAliasForSelfWithDifferentDefaults() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> AnnotationTypeMappings.forAnnotationType( + AliasForSelfWithDifferentDefaults.class)).withMessage( + "Misconfigured aliases: attribute 'a' in annotation [" + + AliasForSelfWithDifferentDefaults.class.getName() + + "] and attribute 'b' in annotation [" + + AliasForSelfWithDifferentDefaults.class.getName() + + "] must declare the same default value."); + } + + @Test + public void forAnnotationTypeWhenAliasForSelfWithMissingDefault() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> AnnotationTypeMappings.forAnnotationType( + AliasForSelfWithMissingDefault.class)).withMessage( + "Misconfigured aliases: attribute 'a' in annotation [" + + AliasForSelfWithMissingDefault.class.getName() + + "] and attribute 'b' in annotation [" + + AliasForSelfWithMissingDefault.class.getName() + + "] must declare default values."); + } + + @Test + public void forAnnotationTypeWhenAliasWithExplicitMirrorAndDifferentDefaults() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> AnnotationTypeMappings.forAnnotationType( + AliasWithExplicitMirrorAndDifferentDefaults.class)).withMessage( + "Misconfigured aliases: attribute 'a' in annotation [" + + AliasWithExplicitMirrorAndDifferentDefaults.class.getName() + + "] and attribute 'c' in annotation [" + + AliasWithExplicitMirrorAndDifferentDefaults.class.getName() + + "] must declare the same default value."); + } + + @Test + public void getDepthReturnsDepth() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + Mapped.class); + assertThat(mappings.get(0).getDepth()).isEqualTo(0); + assertThat(mappings.get(1).getDepth()).isEqualTo(1); + } + + @Test + public void getAnnotationTypeReturnsAnnotationType() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + Mapped.class); + assertThat(mappings.get(0).getAnnotationType()).isEqualTo(Mapped.class); + assertThat(mappings.get(1).getAnnotationType()).isEqualTo(MappedTarget.class); + } + + @Test + public void getAnnotationWhenRootReturnsNull() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + Mapped.class); + assertThat(mappings.get(0).getAnnotation()).isNull(); + } + + @Test + public void getAnnotationWhenMetaAnnotationReturnsAnnotation() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + Mapped.class); + assertThat(mappings.get(1).getAnnotation()).isEqualTo( + Mapped.class.getAnnotation(MappedTarget.class)); + + } + + @Test + public void getAttributesReturnsAttributes() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType( + Mapped.class).get(0); + AttributeMethods attributes = mapping.getAttributes(); + assertThat(attributes.size()).isEqualTo(2); + assertThat(attributes.get(0).getName()).isEqualTo("alias"); + assertThat(attributes.get(1).getName()).isEqualTo("convention"); + } + + @Test + public void getAliasMappingReturnsAttributes() throws Exception { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType( + Mapped.class).get(1); + assertThat(getAliasMapping(mapping, 0)).isEqualTo( + Mapped.class.getDeclaredMethod("alias")); + } + + @Test + public void getConventionMappingReturnsAttributes() throws Exception { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType( + Mapped.class).get(1); + assertThat(getConventionMapping(mapping, 1)).isEqualTo( + Mapped.class.getDeclaredMethod("convention")); + } + + @Test + public void getMirrorSetWhenAliasPairReturnsMirrors() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType( + AliasPair.class).get(0); + MirrorSets mirrorSets = mapping.getMirrorSets(); + assertThat(mirrorSets.size()).isEqualTo(1); + assertThat(mirrorSets.get(0).size()).isEqualTo(2); + assertThat(mirrorSets.get(0).get(0).getName()).isEqualTo("a"); + assertThat(mirrorSets.get(0).get(1).getName()).isEqualTo("b"); + } + + @Test + public void getMirrorSetWhenImplicitMirrorsReturnsMirrors() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType( + ImplicitMirrors.class).get(0); + MirrorSets mirrorSets = mapping.getMirrorSets(); + assertThat(mirrorSets.size()).isEqualTo(1); + assertThat(mirrorSets.get(0).size()).isEqualTo(2); + assertThat(mirrorSets.get(0).get(0).getName()).isEqualTo("a"); + assertThat(mirrorSets.get(0).get(1).getName()).isEqualTo("b"); + } + + @Test + public void getMirrorSetWhenThreeDeepReturnsMirrors() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + ThreeDeepA.class); + AnnotationTypeMapping mappingA = mappings.get(0); + MirrorSets mirrorSetsA = mappingA.getMirrorSets(); + assertThat(mirrorSetsA.size()).isEqualTo(2); + assertThat(getNames(mirrorSetsA.get(0))).containsExactly("a1", "a2", "a3"); + AnnotationTypeMapping mappingB = mappings.get(1); + MirrorSets mirrorSetsB = mappingB.getMirrorSets(); + assertThat(mirrorSetsB.size()).isEqualTo(1); + assertThat(getNames(mirrorSetsB.get(0))).containsExactly("b1", "b2"); + AnnotationTypeMapping mappingC = mappings.get(2); + MirrorSets mirrorSetsC = mappingC.getMirrorSets(); + assertThat(mirrorSetsC.size()).isEqualTo(0); + } + + @Test + public void getAliasMappingWhenThreeDeepReturnsMappedAttributes() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + ThreeDeepA.class); + AnnotationTypeMapping mappingA = mappings.get(0); + assertThat(getAliasMapping(mappingA, 0)).isNull(); + assertThat(getAliasMapping(mappingA, 1)).isNull(); + assertThat(getAliasMapping(mappingA, 2)).isNull(); + assertThat(getAliasMapping(mappingA, 3)).isNull(); + assertThat(getAliasMapping(mappingA, 4)).isNull(); + AnnotationTypeMapping mappingB = mappings.get(1); + assertThat(getAliasMapping(mappingB, 0).getName()).isEqualTo("a1"); + assertThat(getAliasMapping(mappingB, 1).getName()).isEqualTo("a1"); + AnnotationTypeMapping mappingC = mappings.get(2); + assertThat(getAliasMapping(mappingC, 0).getName()).isEqualTo("a1"); + assertThat(getAliasMapping(mappingC, 1).getName()).isEqualTo("a4"); + } + + @Test + public void getAliasMappingsWhenHasDefinedAttributesReturnsMappedAttributes() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType( + DefinedAttributes.class).get(1); + assertThat(getAliasMapping(mapping, 0)).isNull(); + assertThat(getAliasMapping(mapping, 1).getName()).isEqualTo("value"); + } + + @Test + public void resolveMirrorsWhenAliasPairResolves() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType( + AliasPair.class).get(0); + Method[] resolvedA = resolveMirrorSets(mapping, WithAliasPairA.class, + AliasPair.class); + assertThat(resolvedA[0].getName()).isEqualTo("a"); + assertThat(resolvedA[1].getName()).isEqualTo("a"); + Method[] resolvedB = resolveMirrorSets(mapping, WithAliasPairB.class, + AliasPair.class); + assertThat(resolvedB[0].getName()).isEqualTo("b"); + assertThat(resolvedB[1].getName()).isEqualTo("b"); + } + + @Test + public void resolveMirrorsWhenHasSameValuesUsesFirst() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType( + AliasPair.class).get(0); + Method[] resolved = resolveMirrorSets(mapping, WithSameValueAliasPair.class, + AliasPair.class); + assertThat(resolved[0].getName()).isEqualTo("a"); + assertThat(resolved[1].getName()).isEqualTo("a"); + } + + @Test + public void resolveMirrorsWhenOnlyHasDefaultValuesResolvesNone() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType( + AliasPair.class).get(0); + Method[] resolved = resolveMirrorSets(mapping, WithDefaultValueAliasPair.class, + AliasPair.class); + assertThat(resolved[0]).isNull(); + assertThat(resolved[1]).isNull(); + } + + @Test + public void resolveMirrorsWhenHasDifferentValuesThrowsException() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType( + AliasPair.class).get(0); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> resolveMirrorSets(mapping, WithDifferentValueAliasPair.class, + AliasPair.class)).withMessage( + "Different @AliasFor mirror values for annotation [" + + AliasPair.class.getName() + "] declared on " + + WithDifferentValueAliasPair.class.getName() + + ", attribute 'a' and its alias 'b' are declared with values of [test1] and [test2]."); + } + + @Test + public void resolveMirrorsWhenHasWithMulipleRoutesToAliasReturnsMirrors() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + MulipleRoutesToAliasA.class); + AnnotationTypeMapping mappingsA = getMapping(mappings, + MulipleRoutesToAliasA.class); + assertThat(mappingsA.getMirrorSets().size()).isZero(); + AnnotationTypeMapping mappingsB = getMapping(mappings, + MulipleRoutesToAliasB.class); + assertThat(getNames(mappingsB.getMirrorSets().get(0))).containsExactly("b1", "b2", + "b3"); + AnnotationTypeMapping mappingsC = getMapping(mappings, + MulipleRoutesToAliasC.class); + assertThat(getNames(mappingsC.getMirrorSets().get(0))).containsExactly("c1", + "c2"); + } + + @Test + public void getAliasMappingWhenHasWithMulipleRoutesToAliasReturnsMappedAttributes() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + MulipleRoutesToAliasA.class); + AnnotationTypeMapping mappingsA = getMapping(mappings, + MulipleRoutesToAliasA.class); + assertThat(getAliasMapping(mappingsA, 0)).isNull(); + AnnotationTypeMapping mappingsB = getMapping(mappings, + MulipleRoutesToAliasB.class); + assertThat(getAliasMapping(mappingsB, 0).getName()).isEqualTo("a1"); + assertThat(getAliasMapping(mappingsB, 1).getName()).isEqualTo("a1"); + assertThat(getAliasMapping(mappingsB, 2).getName()).isEqualTo("a1"); + AnnotationTypeMapping mappingsC = getMapping(mappings, + MulipleRoutesToAliasC.class); + assertThat(getAliasMapping(mappingsC, 0).getName()).isEqualTo("a1"); + assertThat(getAliasMapping(mappingsC, 1).getName()).isEqualTo("a1"); + } + + @Test + public void getConventionMappingWhenConventionToExplicitAliasesReturnsMappedAttributes() { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + ConventionToExplicitAliases.class); + AnnotationTypeMapping mapping = getMapping(mappings, + ConventionToExplicitAliasesTarget.class); + assertThat(mapping.getConventionMapping(0)).isEqualTo(0); + assertThat(mapping.getConventionMapping(1)).isEqualTo(0); + } + + @Test + public void isEquivalentToDefaultValueWhenValueAndDefaultAreNullReturnsTrue() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType( + ClassValue.class).get(0); + assertThat(mapping.isEquivalentToDefaultValue(0, null, + ReflectionUtils::invokeMethod)).isTrue(); + } + + @Test + public void isEquivalentToDefaultValueWhenValueAndDefaultMatchReturnsTrue() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType( + ClassValueWithDefault.class).get(0); + assertThat(mapping.isEquivalentToDefaultValue(0, InputStream.class, + ReflectionUtils::invokeMethod)).isTrue(); + } + + @Test + public void isEquivalentToDefaultValueWhenClassAndStringNamesMatchReturnsTrue() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType( + ClassValueWithDefault.class).get(0); + assertThat(mapping.isEquivalentToDefaultValue(0, "java.io.InputStream", + ReflectionUtils::invokeMethod)).isTrue(); + } + + @Test + public void isEquivalentToDefaultValueWhenClassArrayAndStringArrayNamesMatchReturnsTrue() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType( + ClassArrayValueWithDefault.class).get(0); + assertThat(mapping.isEquivalentToDefaultValue(0, + new String[] { "java.io.InputStream", "java.io.OutputStream" }, + ReflectionUtils::invokeMethod)).isTrue(); + } + + @Test + public void isEquivalentToDefaultValueWhenNestedAnnotationAndExtractedValuesMatchReturnsTrue() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType( + NestedValue.class).get(0); + Map value = Collections.singletonMap("value", + "java.io.InputStream"); + assertThat(mapping.isEquivalentToDefaultValue(0, value, + this::extractFromMap)).isTrue(); + } + + @Test + public void isEquivalentToDefaultValueWhenNotMatchingReturnsFalse() { + AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType( + ClassValueWithDefault.class).get(0); + assertThat(mapping.isEquivalentToDefaultValue(0, OutputStream.class, + ReflectionUtils::invokeMethod)).isFalse(); + } + + private Method[] resolveMirrorSets(AnnotationTypeMapping mapping, Class element, + Class annotationClass) { + Annotation annotation = element.getAnnotation(annotationClass); + int[] resolved = mapping.getMirrorSets().resolve(element.getName(), annotation, + ReflectionUtils::invokeMethod); + Method[] result = new Method[resolved.length]; + for (int i = 0; i < resolved.length; i++) { + result[i] = resolved[i] != -1 ? mapping.getAttributes().get(resolved[i]) + : null; + } + return result; + } + + @Nullable + private Method getAliasMapping(AnnotationTypeMapping mapping, int attributeIndex) { + int mapped = mapping.getAliasMapping(attributeIndex); + return mapped != -1 ? mapping.getRoot().getAttributes().get(mapped) : null; + } + + @Nullable + private Method getConventionMapping(AnnotationTypeMapping mapping, + int attributeIndex) { + int mapped = mapping.getConventionMapping(attributeIndex); + return mapped != -1 ? mapping.getRoot().getAttributes().get(mapped) : null; + } + + private AnnotationTypeMapping getMapping(AnnotationTypeMappings mappings, + Class annotationType) { + for (AnnotationTypeMapping candidate : getAll(mappings)) { + if (candidate.getAnnotationType() == annotationType) { + return candidate; + } + } + return null; + } + + private List getAll(AnnotationTypeMappings mappings) { + // AnnotationTypeMappings does not implement Iterable so we don't create + // too many garbage Iterators + List result = new ArrayList<>(mappings.size()); + for (int i = 0; i < mappings.size(); i++) { + result.add(mappings.get(i)); + } + return result; + } + + private List getNames(MirrorSet mirrorSet) { + List names = new ArrayList<>(mirrorSet.size()); + for (int i = 0; i < mirrorSet.size(); i++) { + names.add(mirrorSet.get(i).getName()); + } + return names; + } + + @SuppressWarnings("unchecked") + private Object extractFromMap(Method attribute, Object map) { + return map != null ? ((Map) map).get(attribute.getName()) : null; + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface SimpleAnnotation { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @UsesSunMisc + static @interface WithSpringLangAnnotation { + + } + + @Retention(RetentionPolicy.RUNTIME) + @A + @B + static @interface MetaAnnotated { + + } + + @Retention(RetentionPolicy.RUNTIME) + @AA + @AB + static @interface A { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AA { + + } + + @Retention(RetentionPolicy.RUNTIME) + @ABC + static @interface AB { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ABC { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface B { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Repeating + @Repeating + static @interface WithRepeatedMetaAnnotations { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(Repeatings.class) + static @interface Repeating { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface Repeatings { + + Repeating[] value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @SelfAnnotated + static @interface SelfAnnotated { + + } + + @Retention(RetentionPolicy.RUNTIME) + @LoopB + static @interface LoopA { + + } + + @Retention(RetentionPolicy.RUNTIME) + @LoopA + static @interface LoopB { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForWithBothValueAndAttribute { + + @AliasFor(value = "bar", attribute = "foo") + String test(); + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForToSelfNonExistingAttribute { + + @AliasFor("missing") + String test() default ""; + + String other() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + @AliasForToOtherNonExistingAttributeTarget + static @interface AliasForToOtherNonExistingAttribute { + + @AliasFor(annotation = AliasForToOtherNonExistingAttributeTarget.class, attribute = "missing") + String test() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForToOtherNonExistingAttributeTarget { + + String other() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForToSelf { + + @AliasFor("test") + String test() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + @AliasForWithArrayCompatibleReturnTypesTarget + static @interface AliasForWithArrayCompatibleReturnTypes { + + @AliasFor(annotation = AliasForWithArrayCompatibleReturnTypesTarget.class) + String test() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForWithArrayCompatibleReturnTypesTarget { + + String[] test() default {}; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForWithIncompatibleReturnTypes { + + @AliasFor(annotation = AliasForWithIncompatibleReturnTypesTarget.class) + String[] test() default {}; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForWithIncompatibleReturnTypesTarget { + + String test() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForToSelfNonAnnotatedAttribute { + + @AliasFor("other") + String test() default ""; + + String other() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForToSelfAnnotatedToOtherAttribute { + + @AliasFor("b") + String a() default ""; + + @AliasFor("c") + String b() default ""; + + @AliasFor("a") + String c() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForNonMetaAnnotated { + + @AliasFor(annotation = AliasForNonMetaAnnotatedTarget.class) + String test() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForNonMetaAnnotatedTarget { + + String test() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForSelfWithDifferentDefaults { + + @AliasFor("b") + String a() default "a"; + + @AliasFor("a") + String b() default "b"; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasForSelfWithMissingDefault { + + @AliasFor("b") + String a() default "a"; + + @AliasFor("a") + String b(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @AliasWithExplicitMirrorAndDifferentDefaultsTarget + static @interface AliasWithExplicitMirrorAndDifferentDefaults { + + @AliasFor(annotation = AliasWithExplicitMirrorAndDifferentDefaultsTarget.class, attribute = "a") + String a() default "x"; + + @AliasFor(annotation = AliasWithExplicitMirrorAndDifferentDefaultsTarget.class, attribute = "a") + String b() default "x"; + + @AliasFor(annotation = AliasWithExplicitMirrorAndDifferentDefaultsTarget.class, attribute = "a") + String c() default "y"; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasWithExplicitMirrorAndDifferentDefaultsTarget { + + String a() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + @MappedTarget + static @interface Mapped { + + String convention() default ""; + + @AliasFor(annotation = MappedTarget.class, attribute = "aliasTarget") + String alias() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface MappedTarget { + + String convention() default ""; + + String aliasTarget() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AliasPair { + + @AliasFor("b") + String a() default ""; + + @AliasFor("a") + String b() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + @ImplicitMirrorsTarget + static @interface ImplicitMirrors { + + @AliasFor(annotation = ImplicitMirrorsTarget.class, attribute = "c") + String a() default ""; + + @AliasFor(annotation = ImplicitMirrorsTarget.class, attribute = "c") + String b() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ImplicitMirrorsTarget { + + @AliasFor("d") + String c() default ""; + + @AliasFor("c") + String d() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + @ThreeDeepB + static @interface ThreeDeepA { + + @AliasFor(annotation = ThreeDeepB.class, attribute = "b1") + String a1() default ""; + + @AliasFor(annotation = ThreeDeepB.class, attribute = "b2") + String a2() default ""; + + @AliasFor(annotation = ThreeDeepC.class, attribute = "c1") + String a3() default ""; + + @AliasFor(annotation = ThreeDeepC.class, attribute = "c2") + String a4() default ""; + + @AliasFor(annotation = ThreeDeepC.class, attribute = "c2") + String a5() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + @ThreeDeepC + static @interface ThreeDeepB { + + @AliasFor(annotation = ThreeDeepC.class, attribute = "c1") + String b1() default ""; + + @AliasFor(annotation = ThreeDeepC.class, attribute = "c1") + String b2() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ThreeDeepC { + + String c1() default ""; + + String c2() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + @DefinedAttributesTarget(a = "test") + static @interface DefinedAttributes { + + @AliasFor(annotation = DefinedAttributesTarget.class, attribute = "b") + String value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface DefinedAttributesTarget { + + String a(); + + String b() default ""; + + } + + @AliasPair(a = "test") + static class WithAliasPairA { + + } + + @AliasPair(b = "test") + static class WithAliasPairB { + + } + + @AliasPair(a = "test", b = "test") + static class WithSameValueAliasPair { + + } + + @AliasPair(a = "test1", b = "test2") + static class WithDifferentValueAliasPair { + + } + + @AliasPair + static class WithDefaultValueAliasPair { + + } + + @Retention(RetentionPolicy.RUNTIME) + @MulipleRoutesToAliasB + static @interface MulipleRoutesToAliasA { + + @AliasFor(annotation = MulipleRoutesToAliasB.class, attribute = "b2") + String a1() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + @MulipleRoutesToAliasC + static @interface MulipleRoutesToAliasB { + + @AliasFor(annotation = MulipleRoutesToAliasC.class, attribute = "c2") + String b1() default ""; + + @AliasFor(annotation = MulipleRoutesToAliasC.class, attribute = "c2") + String b2() default ""; + + @AliasFor(annotation = MulipleRoutesToAliasC.class, attribute = "c1") + String b3() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface MulipleRoutesToAliasC { + + @AliasFor("c2") + String c1() default ""; + + @AliasFor("c1") + String c2() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + @ConventionToExplicitAliasesTarget + static @interface ConventionToExplicitAliases { + + String test() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ConventionToExplicitAliasesTarget { + + @AliasFor("test") + String value() default ""; + + @AliasFor("value") + String test() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ClassValue { + + Class value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ClassValueWithDefault { + + Class value() default InputStream.class; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ClassArrayValueWithDefault { + + Class[] value() default { InputStream.class, OutputStream.class }; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface NestedValue { + + ClassValue value() default @ClassValue(InputStream.class); + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java new file mode 100644 index 000000000000..7579dcdc140e --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java @@ -0,0 +1,825 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.Test; + +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.*; + +/** + * Tests for {@link AnnotationsScanner}. + * + * @author Phillip Webb + */ +public class AnnotationsScannerTests { + + @Test + public void directStrategyOnClassWhenNotAnnoatedScansNone() { + Class source = WithNoAnnotations.class; + assertThat(scan(source, SearchStrategy.DIRECT)).isEmpty(); + } + + @Test + public void directStrategyOnClassScansAnnotations() { + Class source = WithSingleAnnotation.class; + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void directStrategyOnClassWhenMultipleAnnotationsScansAnnotations() { + Class source = WithMultipleAnnotations.class; + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( + "0:TestAnnotation1", "0:TestAnnotation2"); + } + + @Test + public void directStrategyOnClassWhenHasSuperclassScansOnlyDirect() { + Class source = WithSingleSuperclass.class; + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void directStrategyOnClassWhenHasInterfaceScansOnlyDirect() { + Class source = WithSingleInterface.class; + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void directStrategyOnClassHierarchyScansInCorrectOrder() { + Class source = WithHierarchy.class; + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void inheritedAnnotationsStrategyOnClassWhenNotAnnoatedScansNone() { + Class source = WithNoAnnotations.class; + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).isEmpty(); + } + + @Test + public void inheritedAnnotationsStrategyOnClassScansAnnotations() { + Class source = WithSingleAnnotation.class; + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void inheritedAnnotationsStrategyOnClassWhenMultipleAnnotationsScansAnnotations() { + Class source = WithMultipleAnnotations.class; + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( + "0:TestAnnotation1", "0:TestAnnotation2"); + } + + @Test + public void inheritedAnnotationsStrategyOnClassWhenHasSuperclassScansOnlyInherited() { + Class source = WithSingleSuperclass.class; + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( + "0:TestAnnotation1", "1:TestInheritedAnnotation2"); + } + + @Test + public void inheritedAnnotationsStrategyOnClassWhenHasInterfaceDoesNotIncludeInterfaces() { + Class source = WithSingleInterface.class; + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void inheritedAnnotationsStrategyOnClassHierarchyScansInCorrectOrder() { + Class source = WithHierarchy.class; + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( + "0:TestAnnotation1", "1:TestInheritedAnnotation2"); + } + + @Test + public void inheritedAnnotationsStrategyOnClassWhenHasAnnotationOnBothClassesIncudesOnlyOne() { + Class source = WithSingleSuperclassAndDoubleInherited.class; + assertThat(Arrays.stream(source.getAnnotations()).map( + Annotation::annotationType).map(Class::getName)).containsExactly( + TestInheritedAnnotation2.class.getName()); + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsOnly( + "0:TestInheritedAnnotation2"); + } + + @Test + public void superclassStrategyOnClassWhenNotAnnoatedScansNone() { + Class source = WithNoAnnotations.class; + assertThat(scan(source, SearchStrategy.SUPER_CLASS)).isEmpty(); + } + + @Test + public void superclassStrategyOnClassScansAnnotations() { + Class source = WithSingleAnnotation.class; + assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void superclassStrategyOnClassWhenMultipleAnnotationsScansAnnotations() { + Class source = WithMultipleAnnotations.class; + assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( + "0:TestAnnotation1", "0:TestAnnotation2"); + } + + @Test + public void superclassStrategyOnClassWhenHasSuperclassScansSuperclass() { + Class source = WithSingleSuperclass.class; + assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); + } + + @Test + public void superclassStrategyOnClassWhenHasInterfaceDoesNotIncludeInterfaces() { + Class source = WithSingleInterface.class; + assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void superclassStrategyOnClassHierarchyScansInCorrectOrder() { + Class source = WithHierarchy.class; + assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2", + "2:TestAnnotation3"); + } + + @Test + public void exhaustiveStrategyOnClassWhenNotAnnoatedScansNone() { + Class source = WithNoAnnotations.class; + assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).isEmpty(); + } + + @Test + public void exhaustiveStrategyOnClassScansAnnotations() { + Class source = WithSingleAnnotation.class; + assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void exhaustiveStrategyOnClassWhenMultipleAnnotationsScansAnnotations() { + Class source = WithMultipleAnnotations.class; + assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( + "0:TestAnnotation1", "0:TestAnnotation2"); + } + + @Test + public void exhaustiveStrategyOnClassWhenHasSuperclassScansSuperclass() { + Class source = WithSingleSuperclass.class; + assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); + } + + @Test + public void exhaustiveStrategyOnClassWhenHasInterfaceDoesNotIncludeInterfaces() { + Class source = WithSingleInterface.class; + assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); + } + + @Test + public void exhaustiveStrategyOnClassHierarchyScansInCorrectOrder() { + Class source = WithHierarchy.class; + assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation5", "1:TestInheritedAnnotation5", + "2:TestAnnotation6", "3:TestAnnotation2", "3:TestInheritedAnnotation2", + "4:TestAnnotation3", "5:TestAnnotation4"); + } + + @Test + public void directStrategyOnMethodWhenNotAnnoatedScansNone() { + Method source = methodFrom(WithNoAnnotations.class); + assertThat(scan(source, SearchStrategy.DIRECT)).isEmpty(); + } + + @Test + public void directStrategyOnMethodScansAnnotations() { + Method source = methodFrom(WithSingleAnnotation.class); + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void directStrategyOnMethodWhenMultipleAnnotationsScansAnnotations() { + Method source = methodFrom(WithMultipleAnnotations.class); + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( + "0:TestAnnotation1", "0:TestAnnotation2"); + } + + @Test + public void directStrategyOnMethodWhenHasSuperclassScansOnlyDirect() { + Method source = methodFrom(WithSingleSuperclass.class); + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void directStrategyOnMethodWhenHasInterfaceScansOnlyDirect() { + Method source = methodFrom(WithSingleInterface.class); + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void directStrategyOnMethodHierarchyScansInCorrectOrder() { + Method source = methodFrom(WithHierarchy.class); + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void inheritedAnnotationsStrategyOnMethodWhenNotAnnoatedScansNone() { + Method source = methodFrom(WithNoAnnotations.class); + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).isEmpty(); + } + + @Test + public void inheritedAnnotationsStrategyOnMethodScansAnnotations() { + Method source = methodFrom(WithSingleAnnotation.class); + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void inheritedAnnotationsStrategyOnMethodWhenMultipleAnnotationsScansAnnotations() { + Method source = methodFrom(WithMultipleAnnotations.class); + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( + "0:TestAnnotation1", "0:TestAnnotation2"); + } + + @Test + public void inheritedAnnotationsMethodOnMethodWhenHasSuperclassIgnoresInherited() { + Method source = methodFrom(WithSingleSuperclass.class); + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void inheritedAnnotationsStrategyOnMethodWhenHasInterfaceDoesNotIncludeInterfaces() { + Method source = methodFrom(WithSingleInterface.class); + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void inheritedAnnotationsStrategyOnMethodHierarchyScansInCorrectOrder() { + Method source = methodFrom(WithHierarchy.class); + assertThat(scan(source, SearchStrategy.INHERITED_ANNOTATIONS)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void superclassStrategyOnMethodWhenNotAnnoatedScansNone() { + Method source = methodFrom(WithNoAnnotations.class); + assertThat(scan(source, SearchStrategy.SUPER_CLASS)).isEmpty(); + } + + @Test + public void superclassStrategyOnMethodScansAnnotations() { + Method source = methodFrom(WithSingleAnnotation.class); + assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void superclassStrategyOnMethodWhenMultipleAnnotationsScansAnnotations() { + Method source = methodFrom(WithMultipleAnnotations.class); + assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( + "0:TestAnnotation1", "0:TestAnnotation2"); + } + + @Test + public void superclassStrategyOnMethodWhenHasSuperclassScansSuperclass() { + Method source = methodFrom(WithSingleSuperclass.class); + assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); + } + + @Test + public void superclassStrategyOnMethodWhenHasInterfaceDoesNotIncludeInterfaces() { + Method source = methodFrom(WithSingleInterface.class); + assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void superclassStrategyOnMethodHierarchyScansInCorrectOrder() { + Method source = methodFrom(WithHierarchy.class); + assertThat(scan(source, SearchStrategy.SUPER_CLASS)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2", + "2:TestAnnotation3"); + } + + @Test + public void exhaustiveStrategyOnMethodWhenNotAnnoatedScansNone() { + Method source = methodFrom(WithNoAnnotations.class); + assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).isEmpty(); + } + + @Test + public void exhaustiveStrategyOnMethodScansAnnotations() { + Method source = methodFrom(WithSingleAnnotation.class); + assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void exhaustiveStrategyOnMethodWhenMultipleAnnotationsScansAnnotations() { + Method source = methodFrom(WithMultipleAnnotations.class); + assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( + "0:TestAnnotation1", "0:TestAnnotation2"); + } + + @Test + public void exhaustiveStrategyOnMethodWhenHasSuperclassScansSuperclass() { + Method source = methodFrom(WithSingleSuperclass.class); + assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); + } + + @Test + public void exhaustiveStrategyOnMethodWhenHasInterfaceDoesNotIncludeInterfaces() { + Method source = methodFrom(WithSingleInterface.class); + assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2", "1:TestInheritedAnnotation2"); + } + + @Test + public void exhaustiveStrategyOnMethodHierarchyScansInCorrectOrder() { + Method source = methodFrom(WithHierarchy.class); + assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation5", "1:TestInheritedAnnotation5", + "2:TestAnnotation6", "3:TestAnnotation2", "3:TestInheritedAnnotation2", + "4:TestAnnotation3", "5:TestAnnotation4"); + } + + @Test + public void exhaustiveStrategyOnBridgeMethodScansAnnotations() throws Exception { + Method source = BridgedMethod.class.getDeclaredMethod("method", Object.class); + assertThat(source.isBridge()).isTrue(); + assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2"); + } + + @Test + public void exhaustiveStrategyOnBridgedMethodScansAnnotations() throws Exception { + Method source = BridgedMethod.class.getDeclaredMethod("method", String.class); + assertThat(source.isBridge()).isFalse(); + assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2"); + } + + @Test + public void directStrategyOnBridgeMethodScansAnnotations() throws Exception { + Method source = BridgedMethod.class.getDeclaredMethod("method", Object.class); + assertThat(source.isBridge()).isTrue(); + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void dirextStrategyOnBridgedMethodScansAnnotations() throws Exception { + Method source = BridgedMethod.class.getDeclaredMethod("method", String.class); + assertThat(source.isBridge()).isFalse(); + assertThat(scan(source, SearchStrategy.DIRECT)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void exhaustiveStrategyOnMethodWithIgnorablesScansAnnotations() + throws Exception { + Method source = methodFrom(Ignoreable.class); + assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void exhaustiveStrategyOnMethodWithMultipleCandidatesScansAnnotations() + throws Exception { + Method source = methodFrom(MultipleMethods.class); + assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void exhaustiveStrategyOnMethodWithGenericParameterOverrideScansAnnotations() + throws Exception { + Method source = ReflectionUtils.findMethod(GenericOverride.class, "method", + String.class); + assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation2"); + } + + @Test + public void exhaustiveStrategyOnMethodWithGenericParameterNonOverrideScansAnnotations() + throws Exception { + Method source = ReflectionUtils.findMethod(GenericNonOverride.class, "method", + StringBuilder.class); + assertThat(scan(source, SearchStrategy.EXHAUSTIVE)).containsExactly( + "0:TestAnnotation1"); + } + + @Test + public void scanWhenProcessorReturnsFromDoWithAggregateExitsEarly() { + String result = AnnotationsScanner.scan(this, WithSingleSuperclass.class, + SearchStrategy.EXHAUSTIVE, new AnnotationsProcessor() { + + @Override + @Nullable + public String doWithAggregate(Object context, int aggregateIndex) { + return ""; + } + + @Override + @Nullable + public String doWithAnnotations(Object context, int aggregateIndex, + Object source, Annotation[] annotations) { + throw new IllegalStateException("Should not call"); + } + + }); + assertThat(result).isEmpty(); + } + + @Test + public void scanWhenProcessorReturnsFromDoWithAnnotationsExitsEarly() { + List indexes = new ArrayList<>(); + String result = AnnotationsScanner.scan(this, WithSingleSuperclass.class, + SearchStrategy.EXHAUSTIVE, + (context, aggregateIndex, source, annotations) -> { + indexes.add(aggregateIndex); + return ""; + }); + assertThat(result).isEmpty(); + assertThat(indexes).containsOnly(0); + } + + @Test + public void scanWhenProcessorHasFinishMethodUsesFinishResult() { + String result = AnnotationsScanner.scan(this, WithSingleSuperclass.class, + SearchStrategy.EXHAUSTIVE, new AnnotationsProcessor() { + + @Override + @Nullable + public String doWithAnnotations(Object context, int aggregateIndex, + Object source, Annotation[] annotations) { + return "K"; + } + + @Override + @Nullable + public String finish(String result) { + return "O" + result; + } + + }); + assertThat(result).isEqualTo("OK"); + } + + private Method methodFrom(Class type) { + return ReflectionUtils.findMethod(type, "method"); + } + + private Stream scan(AnnotatedElement element, SearchStrategy searchStrategy) { + List result = new ArrayList<>(); + AnnotationsScanner.scan(this, element, searchStrategy, + (criteria, aggregateIndex, source, annotations) -> { + for (Annotation annotation : annotations) { + if (annotation != null) { + String name = ClassUtils.getShortName( + annotation.annotationType()); + name = name.substring(name.lastIndexOf(".") + 1); + result.add(aggregateIndex + ":" + name); + } + } + return null; + }); + return result.stream(); + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface TestAnnotation1 { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface TestAnnotation2 { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface TestAnnotation3 { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface TestAnnotation4 { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface TestAnnotation5 { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface TestAnnotation6 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + static @interface TestInheritedAnnotation1 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + static @interface TestInheritedAnnotation2 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + static @interface TestInheritedAnnotation3 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + static @interface TestInheritedAnnotation4 { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + static @interface TestInheritedAnnotation5 { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface OnSuperClass { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface OnInterface { + + } + + static class WithNoAnnotations { + + public void method() { + } + + } + + @TestAnnotation1 + static class WithSingleAnnotation { + + @TestAnnotation1 + public void method() { + } + + } + + @TestAnnotation1 + @TestAnnotation2 + static class WithMultipleAnnotations { + + @TestAnnotation1 + @TestAnnotation2 + public void method() { + } + + } + + @TestAnnotation2 + @TestInheritedAnnotation2 + static class SingleSuperclass { + + @TestAnnotation2 + @TestInheritedAnnotation2 + public void method() { + } + + } + + @TestAnnotation1 + static class WithSingleSuperclass extends SingleSuperclass { + + @TestAnnotation1 + public void method() { + } + + } + + @TestInheritedAnnotation2 + static class WithSingleSuperclassAndDoubleInherited extends SingleSuperclass { + + @TestAnnotation1 + public void method() { + } + + } + + @TestAnnotation1 + static class WithSingleInterface implements SingleInterface { + + @TestAnnotation1 + public void method() { + } + + } + + @TestAnnotation2 + @TestInheritedAnnotation2 + static interface SingleInterface { + + @TestAnnotation2 + @TestInheritedAnnotation2 + public void method(); + + } + + @TestAnnotation1 + static class WithHierarchy extends HierarchySuperclass implements HierarchyInterface { + + @TestAnnotation1 + public void method() { + } + + } + + @TestAnnotation2 + @TestInheritedAnnotation2 + static class HierarchySuperclass extends HierarchySuperSuperclass { + + @TestAnnotation2 + @TestInheritedAnnotation2 + public void method() { + } + + } + + @TestAnnotation3 + static class HierarchySuperSuperclass implements HierarchySuperSuperclassInterface { + + @TestAnnotation3 + public void method() { + } + + } + + @TestAnnotation4 + static interface HierarchySuperSuperclassInterface { + + @TestAnnotation4 + public void method(); + + } + + @TestAnnotation5 + @TestInheritedAnnotation5 + static interface HierarchyInterface extends HierarchyInterfaceInterface { + + @TestAnnotation5 + @TestInheritedAnnotation5 + public void method(); + + } + + @TestAnnotation6 + static interface HierarchyInterfaceInterface { + + @TestAnnotation6 + public void method(); + + } + + static class BridgedMethod implements BridgeMethod { + + @Override + @TestAnnotation1 + public void method(String arg) { + } + + } + + static interface BridgeMethod { + + @TestAnnotation2 + void method(T arg); + + } + + static class Ignoreable implements IgnoreableOverrideInterface1, + IgnoreableOverrideInterface2, Serializable { + + @TestAnnotation1 + public void method() { + } + + } + + static interface IgnoreableOverrideInterface1 { + + @Nullable + public void method(); + + } + + static interface IgnoreableOverrideInterface2 { + + @Nullable + public void method(); + + } + + static abstract class MultipleMethods implements MultipleMethodsInterface { + + @TestAnnotation1 + public void method() { + } + + } + + interface MultipleMethodsInterface { + + @TestAnnotation2 + void method(String arg); + + @TestAnnotation2 + void method1(); + + } + + static class GenericOverride implements GenericOverrideInterface { + + @TestAnnotation1 + public void method(String argument) { + + } + + } + + static interface GenericOverrideInterface { + + @TestAnnotation2 + void method(T argument); + + } + + static abstract class GenericNonOverride + implements GenericNonOverrideInterface { + + @TestAnnotation1 + public void method(StringBuilder argument) { + + } + + } + + static interface GenericNonOverrideInterface { + + @TestAnnotation2 + void method(T argument); + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java new file mode 100644 index 000000000000..43b55f260544 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/AttributeMethodsTests.java @@ -0,0 +1,242 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +/** + * Tests for {@link AttributeMethods}. + * + * @author Phillip Webb + */ +public class AttributeMethodsTests { + + @Test + public void forAnnotationTypeWhenNullReturnsNone() { + AttributeMethods methods = AttributeMethods.forAnnotationType(null); + assertThat(methods).isSameAs(AttributeMethods.NONE); + } + + @Test + public void forAnnotationTypeWhenHasNoAttributesReturnsNone() { + AttributeMethods methods = AttributeMethods.forAnnotationType(NoAttributes.class); + assertThat(methods).isSameAs(AttributeMethods.NONE); + } + + @Test + public void forAnnotationTypeWhenHasMultipleAttributesReturnsAttributes() { + AttributeMethods methods = AttributeMethods.forAnnotationType( + MultipleAttributes.class); + assertThat(methods.get("value").getName()).isEqualTo("value"); + assertThat(methods.get("intValue").getName()).isEqualTo("intValue"); + assertThat(getAll(methods)).flatExtracting(Method::getName).containsExactly( + "intValue", "value"); + } + + @Test + public void isOnlyValueAttributeWhenHasOnlyValueAttributeReturnsTrue() { + AttributeMethods methods = AttributeMethods.forAnnotationType(ValueOnly.class); + assertThat(methods.isOnlyValueAttribute()).isTrue(); + } + + @Test + public void isOnlyValueAttributeWhenHasOnlySingleNonValueAttributeReturnsFalse() { + AttributeMethods methods = AttributeMethods.forAnnotationType(NonValueOnly.class); + assertThat(methods.isOnlyValueAttribute()).isFalse(); + } + + @Test + public void isOnlyValueAttributeWhenHasOnlyMultipleAttributesIncludingValueReturnsFalse() { + AttributeMethods methods = AttributeMethods.forAnnotationType( + MultipleAttributes.class); + assertThat(methods.isOnlyValueAttribute()).isFalse(); + } + + @Test + public void indexOfNameReturnsIndex() { + AttributeMethods methods = AttributeMethods.forAnnotationType( + MultipleAttributes.class); + assertThat(methods.indexOf("value")).isEqualTo(1); + } + + @Test + public void indexOfMethodReturnsIndex() throws Exception { + AttributeMethods methods = AttributeMethods.forAnnotationType( + MultipleAttributes.class); + Method method = MultipleAttributes.class.getDeclaredMethod("value"); + assertThat(methods.indexOf(method)).isEqualTo(1); + } + + @Test + public void sizeReturnsSize() { + AttributeMethods methods = AttributeMethods.forAnnotationType( + MultipleAttributes.class); + assertThat(methods.size()).isEqualTo(2); + } + + @Test + public void canThrowTypeNotPresentExceptionWhenHasClassAttributeReturnsTrue() { + AttributeMethods methods = AttributeMethods.forAnnotationType(ClassValue.class); + assertThat(methods.canThrowTypeNotPresentException(0)).isTrue(); + } + + @Test + public void canThrowTypeNotPresentExceptionWhenHasClassArrayAttributeReturnsTrue() { + AttributeMethods methods = AttributeMethods.forAnnotationType( + ClassArrayValue.class); + assertThat(methods.canThrowTypeNotPresentException(0)).isTrue(); + } + + @Test + public void canThrowTypeNotPresentExceptionWhenNotClassOrClassArrayAttributeReturnsFalse() { + AttributeMethods methods = AttributeMethods.forAnnotationType(ValueOnly.class); + assertThat(methods.canThrowTypeNotPresentException(0)).isFalse(); + } + + @Test + public void hasDefaultValueMethodWhenHasDefaultValueMethodReturnsTrue() { + AttributeMethods methods = AttributeMethods.forAnnotationType( + DefaultValueAttribute.class); + assertThat(methods.hasDefaultValueMethod()).isTrue(); + } + + @Test + public void hasDefaultValueMethodWhenHasNoDefaultValueMethodsReturnsFalse() { + AttributeMethods methods = AttributeMethods.forAnnotationType( + MultipleAttributes.class); + assertThat(methods.hasDefaultValueMethod()).isFalse(); + } + + @Test + public void isValidWhenHasTypeNotPresentExceptionReturnsFalse() { + ClassValue annotation = mockAnnotation(ClassValue.class); + given(annotation.value()).willThrow(TypeNotPresentException.class); + AttributeMethods attributes = AttributeMethods.forAnnotationType( + annotation.annotationType()); + assertThat(attributes.isValid(annotation)).isFalse(); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void isValidWhenDoesNotHaveTypeNotPresentExceptionReturnsTrue() { + ClassValue annotation = mock(ClassValue.class); + given(annotation.value()).willReturn((Class) InputStream.class); + AttributeMethods attributes = AttributeMethods.forAnnotationType( + annotation.annotationType()); + assertThat(attributes.isValid(annotation)).isTrue(); + } + + @Test + public void validateWhenHasTypeNotPresentExceptionThrowsException() { + ClassValue annotation = mockAnnotation(ClassValue.class); + given(annotation.value()).willThrow(TypeNotPresentException.class); + AttributeMethods attributes = AttributeMethods.forAnnotationType( + annotation.annotationType()); + assertThatIllegalStateException().isThrownBy( + () -> attributes.validate(annotation)); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void validateWhenDoesNotHaveTypeNotPresentExceptionThrowsNothing() { + ClassValue annotation = mockAnnotation(ClassValue.class); + given(annotation.value()).willReturn((Class) InputStream.class); + AttributeMethods attributes = AttributeMethods.forAnnotationType( + annotation.annotationType()); + attributes.validate(annotation); + } + + private List getAll(AttributeMethods attributes) { + List result = new ArrayList<>(attributes.size()); + for (int i = 0; i < attributes.size(); i++) { + result.add(attributes.get(i)); + } + return result; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private A mockAnnotation(Class annotationType) { + A annotation = mock(annotationType); + given(annotation.annotationType()).willReturn((Class) annotationType); + return annotation; + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface NoAttributes { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface MultipleAttributes { + + int intValue(); + + String value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ValueOnly { + + String value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface NonValueOnly { + + String test(); + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ClassValue { + + Class value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ClassArrayValue { + + Class[] value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface DefaultValueAttribute { + + String one(); + + String two(); + + String three() default "3"; + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationClassLoaderTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationClassLoaderTests.java new file mode 100644 index 000000000000..0a44e4db015f --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationClassLoaderTests.java @@ -0,0 +1,172 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; + +import org.junit.Test; + +import org.springframework.core.OverridingClassLoader; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MergedAnnotation} to ensure the correct class loader is + * used. + * + * @author Phillip Webb + * @since 5.2 + */ +public class MergedAnnotationClassLoaderTests { + + private static final String TEST_ANNOTATION = TestAnnotation.class.getName(); + + private static final String TEST_META_ANNOTATION = TestMetaAnnotation.class.getName(); + + private static final String WITH_TEST_ANNOTATION = WithTestAnnotation.class.getName(); + + private static final String TEST_REFERENCE = TestReference.class.getName(); + + @Test + public void synthesizedUsesCorrectClassLoader() throws Exception { + ClassLoader parent = getClass().getClassLoader(); + TestClassLoader child = new TestClassLoader(parent); + Class source = child.loadClass(WITH_TEST_ANNOTATION); + Annotation annotation = getDeclaredAnnotation(source, TEST_ANNOTATION); + Annotation metaAnnotation = getDeclaredAnnotation(annotation.annotationType(), + TEST_META_ANNOTATION); + // We should have loaded the source and initial annotation from child + assertThat(source.getClassLoader()).isEqualTo(child); + assertThat(annotation.getClass().getClassLoader()).isEqualTo(child); + assertThat(annotation.annotationType().getClassLoader()).isEqualTo(child); + // The meta-annotation should have been loaded by the parent + assertThat(metaAnnotation.getClass().getClassLoader()).isEqualTo(parent); + assertThat(metaAnnotation.getClass().getClassLoader()).isEqualTo(parent); + assertThat( + getEnumAttribute(metaAnnotation).getClass().getClassLoader()).isEqualTo( + parent); + assertThat(getClassAttribute(metaAnnotation).getClassLoader()).isEqualTo(child); + // MergedAnnotation should follow the same class loader logic + MergedAnnotations mergedAnnotations = MergedAnnotations.from(source); + Annotation synthesized = mergedAnnotations.get(TEST_ANNOTATION).synthesize(); + Annotation synthesizedMeta = mergedAnnotations.get( + TEST_META_ANNOTATION).synthesize(); + assertThat(synthesized.getClass().getClassLoader()).isEqualTo(child); + assertThat(synthesized.annotationType().getClassLoader()).isEqualTo(child); + assertThat(synthesizedMeta.getClass().getClassLoader()).isEqualTo(parent); + assertThat(synthesizedMeta.getClass().getClassLoader()).isEqualTo(parent); + assertThat(getClassAttribute(synthesizedMeta).getClassLoader()).isEqualTo(child); + assertThat( + getEnumAttribute(synthesizedMeta).getClass().getClassLoader()).isEqualTo( + parent); + assertThat(synthesized).isEqualTo(annotation); + assertThat(synthesizedMeta).isEqualTo(metaAnnotation); + // Also check utils version + Annotation utilsMeta = AnnotatedElementUtils.getMergedAnnotation(source, + TestMetaAnnotation.class); + assertThat(utilsMeta.getClass().getClassLoader()).isEqualTo(parent); + assertThat(utilsMeta.getClass().getClassLoader()).isEqualTo(parent); + assertThat(getClassAttribute(utilsMeta).getClassLoader()).isEqualTo(child); + assertThat(getEnumAttribute(utilsMeta).getClass().getClassLoader()).isEqualTo( + parent); + assertThat(utilsMeta).isEqualTo(metaAnnotation); + } + + private Class getClassAttribute(Annotation annotation) throws Exception { + return (Class) getAttributeValue(annotation, "classValue"); + } + + private Enum getEnumAttribute(Annotation annotation) throws Exception { + return (Enum) getAttributeValue(annotation, "enumValue"); + } + + private Object getAttributeValue(Annotation annotation, String name) + throws Exception { + Method classValueMethod = annotation.annotationType().getDeclaredMethod(name); + classValueMethod.setAccessible(true); + return classValueMethod.invoke(annotation); + } + + private Annotation getDeclaredAnnotation(Class element, String annotationType) { + for (Annotation annotation : element.getDeclaredAnnotations()) { + if (annotation.annotationType().getName().equals(annotationType)) { + return annotation; + } + } + return null; + } + + private static class TestClassLoader extends OverridingClassLoader { + + public TestClassLoader(ClassLoader parent) { + super(parent); + } + + @Override + protected boolean isEligibleForOverriding(String className) { + return WITH_TEST_ANNOTATION.equals(className) + || TEST_ANNOTATION.equals(className) + || TEST_REFERENCE.equals(className); + } + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface TestMetaAnnotation { + + @AliasFor("d") + String c() default ""; + + @AliasFor("c") + String d() default ""; + + Class classValue(); + + TestEnum enumValue(); + + } + + @TestMetaAnnotation(classValue = TestReference.class, enumValue = TestEnum.TWO) + @Retention(RetentionPolicy.RUNTIME) + static @interface TestAnnotation { + + @AliasFor("b") + String a() default ""; + + @AliasFor("a") + String b() default ""; + + } + + @TestAnnotation + static class WithTestAnnotation { + + } + + static class TestReference { + + } + + static enum TestEnum { + + ONE, TWO, THREE + + } +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationCollectorsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationCollectorsTests.java new file mode 100644 index 000000000000..8ecf4b1f7aad --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationCollectorsTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.junit.Test; + +import org.springframework.core.annotation.MergedAnnotation.MapValues; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link MergedAnnotationCollectors}. + * + * @author Phillip Webb + */ +public class MergedAnnotationCollectorsTests { + + @Test + public void toAnnotationSetCollectsLinkedHashSetWithSynthesizedAnnotations() { + Set set = stream().collect( + MergedAnnotationCollectors.toAnnotationSet()); + assertThat(set).isInstanceOf(LinkedHashSet.class).flatExtracting( + TestAnnotation::value).containsExactly("a", "b", "c"); + assertThat(set).allMatch(SynthesizedAnnotation.class::isInstance); + } + + @Test + public void toAnnotationArrayCollectsAnnotationArrayWithSynthesizedAnnotations() { + Annotation[] array = stream().collect( + MergedAnnotationCollectors.toAnnotationArray()); + assertThat(Arrays.stream(array).map( + annotation -> ((TestAnnotation) annotation).value())).containsExactly("a", + "b", "c"); + assertThat(array).allMatch(SynthesizedAnnotation.class::isInstance); + } + + @Test + public void toSuppliedAnnotationArrayCollectsAnnotationArrayWithSynthesizedAnnotations() { + TestAnnotation[] array = stream().collect( + MergedAnnotationCollectors.toAnnotationArray(TestAnnotation[]::new)); + assertThat(Arrays.stream(array).map(TestAnnotation::value)).containsExactly("a", + "b", "c"); + assertThat(array).allMatch(SynthesizedAnnotation.class::isInstance); + } + + @Test + public void toMultiValueMapCollectsMultiValueMap() { + MultiValueMap map = stream().map( + MergedAnnotation::filterDefaultValues).collect( + MergedAnnotationCollectors.toMultiValueMap( + MapValues.CLASS_TO_STRING)); + assertThat(map.get("value")).containsExactly("a", "b", "c"); + assertThat(map.get("extra")).containsExactly("java.lang.String", + "java.lang.Integer"); + } + + @Test + public void toFinishedMultiValueMapCollectsMultiValueMap() { + MultiValueMap map = stream().collect( + MergedAnnotationCollectors.toMultiValueMap(result -> { + result.add("finished", true); + return result; + })); + assertThat(map.get("value")).containsExactly("a", "b", "c"); + assertThat(map.get("extra")).containsExactly(void.class, String.class, + Integer.class); + assertThat(map.get("finished")).containsExactly(true); + } + + @Test + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void toFinishedMultiValueMapWhenFinisherIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> stream().collect( + MergedAnnotationCollectors.toMultiValueMap((Function) null))).withMessage( + "Finisher must not be null"); + } + + private Stream> stream() { + return MergedAnnotations.from(WithTestAnnotations.class).stream( + TestAnnotation.class); + } + + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(TestAnnotations.class) + @interface TestAnnotation { + + @AliasFor("name") + String value() default ""; + + @AliasFor("value") + String name() default ""; + + Class extra() default void.class; + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface TestAnnotations { + + TestAnnotation[] value(); + + } + + @TestAnnotation("a") + @TestAnnotation(name = "b", extra = String.class) + @TestAnnotation(name = "c", extra = Integer.class) + static class WithTestAnnotations { + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationPredicatesTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationPredicatesTests.java new file mode 100644 index 000000000000..4dbefa836b8e --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationPredicatesTests.java @@ -0,0 +1,197 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link MergedAnnotationPredicates}. + * + * @author Phillip Webb + */ +public class MergedAnnotationPredicatesTests { + + @Test + public void typeInStringArrayWhenNameMatchesAccepts() { + MergedAnnotation annotation = MergedAnnotations.from( + WithTestAnnotation.class).get(TestAnnotation.class); + assertThat(MergedAnnotationPredicates.typeIn( + TestAnnotation.class.getName())).accepts(annotation); + } + + @Test + public void typeInStringArrayWhenNameDoesNotMatchRejects() { + MergedAnnotation annotation = MergedAnnotations.from( + WithTestAnnotation.class).get(TestAnnotation.class); + assertThat(MergedAnnotationPredicates.typeIn( + MissingAnnotation.class.getName())).rejects(annotation); + } + + @Test + public void typeInStringArrayWhenStringArraysIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> MergedAnnotationPredicates.typeIn((String[]) null)).withMessage( + "TypeNames must not be null"); + } + + @Test + public void typeInClassArrayWhenNameMatchesAccepts() { + MergedAnnotation annotation = MergedAnnotations.from( + WithTestAnnotation.class).get(TestAnnotation.class); + assertThat(MergedAnnotationPredicates.typeIn(TestAnnotation.class)).accepts( + annotation); + } + + @Test + public void typeInClassArrayWhenNameDoesNotMatchRejects() { + MergedAnnotation annotation = MergedAnnotations.from( + WithTestAnnotation.class).get(TestAnnotation.class); + assertThat(MergedAnnotationPredicates.typeIn(MissingAnnotation.class)).rejects( + annotation); + } + + @Test + public void typeInClassArrayWhenClassArraysIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> MergedAnnotationPredicates.typeIn( + (Class[]) null)).withMessage( + "Types must not be null"); + } + + @Test + public void typeInCollectionWhenMatchesStringInCollectionAccepts() { + MergedAnnotation annotation = MergedAnnotations.from( + WithTestAnnotation.class).get(TestAnnotation.class); + assertThat(MergedAnnotationPredicates.typeIn( + Collections.singleton(TestAnnotation.class.getName()))).accepts( + annotation); + } + + @Test + public void typeInCollectionWhenMatchesClassInCollectionAccepts() { + MergedAnnotation annotation = MergedAnnotations.from( + WithTestAnnotation.class).get(TestAnnotation.class); + assertThat(MergedAnnotationPredicates.typeIn( + Collections.singleton(TestAnnotation.class))).accepts(annotation); + } + + @Test + public void typeInCollectionWhenDoesNotMatchAnyRejects() { + MergedAnnotation annotation = MergedAnnotations.from( + WithTestAnnotation.class).get(TestAnnotation.class); + assertThat(MergedAnnotationPredicates.typeIn(Arrays.asList( + MissingAnnotation.class.getName(), MissingAnnotation.class))).rejects( + annotation); + } + + @Test + public void typeInCollectionWhenCollectionIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> MergedAnnotationPredicates.typeIn( + (Collection) null)).withMessage("Types must not be null"); + } + + @Test + public void firstRunOfAcceptsOnlyFirstRun() { + List> filtered = MergedAnnotations.from( + WithMultipleTestAnnotation.class).stream(TestAnnotation.class).filter( + MergedAnnotationPredicates.firstRunOf( + this::firstCharOfValue)).collect(Collectors.toList()); + assertThat(filtered.stream().map( + annotation -> annotation.getString("value"))).containsExactly("a1", "a2", + "a3"); + } + + @Test + public void firstRunOfWhenValueExtractorIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> MergedAnnotationPredicates.firstRunOf(null)).withMessage( + "ValueExtractor must not be null"); + } + + @Test + public void uniqueAcceptsUniquely() { + List> filtered = MergedAnnotations.from( + WithMultipleTestAnnotation.class).stream(TestAnnotation.class).filter( + MergedAnnotationPredicates.unique( + this::firstCharOfValue)).collect(Collectors.toList()); + assertThat(filtered.stream().map( + annotation -> annotation.getString("value"))).containsExactly("a1", "b1", + "c1"); + } + + @Test + public void uniqueWhenKeyExtractorIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> MergedAnnotationPredicates.unique(null)).withMessage( + "KeyExtractor must not be null"); + } + + private char firstCharOfValue(MergedAnnotation annotation) { + return annotation.getString("value").charAt(0); + } + + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(TestAnnotations.class) + static @interface TestAnnotation { + + String value() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface TestAnnotations { + + TestAnnotation[] value(); + + } + + static @interface MissingAnnotation { + + } + + @TestAnnotation("test") + static class WithTestAnnotation { + + } + + @TestAnnotation("a1") + @TestAnnotation("a2") + @TestAnnotation("a3") + @TestAnnotation("b1") + @TestAnnotation("b2") + @TestAnnotation("b3") + @TestAnnotation("c1") + @TestAnnotation("c2") + @TestAnnotation("c3") + static class WithMultipleTestAnnotation { + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsComposedOnSingleAnnotatedElementTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsComposedOnSingleAnnotatedElementTests.java new file mode 100644 index 000000000000..a738165ea6bd --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsComposedOnSingleAnnotatedElementTests.java @@ -0,0 +1,306 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.Test; + +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.*; + +/** + * Unit tests that verify support for finding multiple composed annotations on a single + * annotated element. + * + * @author Phillip Webb + * @author Sam Brannen + */ +public class MergedAnnotationsComposedOnSingleAnnotatedElementTests { + + // See SPR-13486 + + @Test + public void inheritedStrategyMultipleComposedAnnotationsOnClass() { + assertInheritedStrategyBehavior(MultipleComposedCachesClass.class); + } + + @Test + public void inheritedStrategyMultipleInheritedComposedAnnotationsOnSuperclass() { + assertInheritedStrategyBehavior(SubMultipleComposedCachesClass.class); + } + + @Test + public void inheritedStrategyMultipleNoninheritedComposedAnnotationsOnClass() { + MergedAnnotations annotations = MergedAnnotations.from( + MultipleNoninheritedComposedCachesClass.class, + SearchStrategy.INHERITED_ANNOTATIONS); + assertThat(stream(annotations, "value")).containsExactly("noninheritedCache1", + "noninheritedCache2"); + } + + @Test + public void inheritedStrategyMultipleNoninheritedComposedAnnotationsOnSuperclass() { + MergedAnnotations annotations = MergedAnnotations.from( + SubMultipleNoninheritedComposedCachesClass.class, + SearchStrategy.INHERITED_ANNOTATIONS); + assertThat(annotations.stream(Cacheable.class)).isEmpty(); + } + + @Test + public void inheritedStrategyComposedPlusLocalAnnotationsOnClass() { + assertInheritedStrategyBehavior(ComposedPlusLocalCachesClass.class); + } + + @Test + public void inheritedStrategyMultipleComposedAnnotationsOnInterface() { + MergedAnnotations annotations = MergedAnnotations.from( + MultipleComposedCachesOnInterfaceClass.class, + SearchStrategy.INHERITED_ANNOTATIONS); + assertThat(annotations.stream(Cacheable.class)).isEmpty(); + } + + @Test + public void inheritedStrategyMultipleComposedAnnotationsOnMethod() throws Exception { + assertInheritedStrategyBehavior( + getClass().getDeclaredMethod("multipleComposedCachesMethod")); + } + + @Test + public void inheritedStrategyComposedPlusLocalAnnotationsOnMethod() throws Exception { + assertInheritedStrategyBehavior( + getClass().getDeclaredMethod("composedPlusLocalCachesMethod")); + } + + private void assertInheritedStrategyBehavior(AnnotatedElement element) { + MergedAnnotations annotations = MergedAnnotations.from(element, + SearchStrategy.INHERITED_ANNOTATIONS); + assertThat(stream(annotations, "key")).containsExactly("fooKey", "barKey"); + assertThat(stream(annotations, "value")).containsExactly("fooCache", "barCache"); + } + + @Test + public void exhaustiveStrategyMultipleComposedAnnotationsOnClass() { + assertExhaustiveStrategyBehavior(MultipleComposedCachesClass.class); + } + + @Test + public void exhaustiveStrategyMultipleInheritedComposedAnnotationsOnSuperclass() { + assertExhaustiveStrategyBehavior(SubMultipleComposedCachesClass.class); + } + + @Test + public void exhaustiveStrategyMultipleNoninheritedComposedAnnotationsOnClass() { + MergedAnnotations annotations = MergedAnnotations.from( + MultipleNoninheritedComposedCachesClass.class, SearchStrategy.EXHAUSTIVE); + assertThat(stream(annotations, "value")).containsExactly("noninheritedCache1", + "noninheritedCache2"); + } + + @Test + public void exhaustiveStrategyMultipleNoninheritedComposedAnnotationsOnSuperclass() { + MergedAnnotations annotations = MergedAnnotations.from( + SubMultipleNoninheritedComposedCachesClass.class, + SearchStrategy.EXHAUSTIVE); + assertThat(stream(annotations, "value")).containsExactly("noninheritedCache1", + "noninheritedCache2"); + } + + @Test + public void exhaustiveStrategyComposedPlusLocalAnnotationsOnClass() { + assertExhaustiveStrategyBehavior(ComposedPlusLocalCachesClass.class); + } + + @Test + public void exhaustiveStrategyMultipleComposedAnnotationsOnInterface() { + assertExhaustiveStrategyBehavior(MultipleComposedCachesOnInterfaceClass.class); + } + + @Test + public void exhaustiveStrategyComposedCacheOnInterfaceAndLocalCacheOnClass() { + assertExhaustiveStrategyBehavior( + ComposedCacheOnInterfaceAndLocalCacheClass.class); + } + + @Test + public void exhaustiveStrategyMultipleComposedAnnotationsOnMethod() throws Exception { + assertExhaustiveStrategyBehavior( + getClass().getDeclaredMethod("multipleComposedCachesMethod")); + } + + @Test + public void exhaustiveStrategyComposedPlusLocalAnnotationsOnMethod() + throws Exception { + assertExhaustiveStrategyBehavior( + getClass().getDeclaredMethod("composedPlusLocalCachesMethod")); + } + + @Test + public void exhaustiveStrategyMultipleComposedAnnotationsOnBridgeMethod() + throws Exception { + assertExhaustiveStrategyBehavior(getBridgeMethod()); + } + + private void assertExhaustiveStrategyBehavior(AnnotatedElement element) { + MergedAnnotations annotations = MergedAnnotations.from(element, + SearchStrategy.EXHAUSTIVE); + assertThat(stream(annotations, "key")).containsExactly("fooKey", "barKey"); + assertThat(stream(annotations, "value")).containsExactly("fooCache", "barCache"); + } + + public Method getBridgeMethod() throws NoSuchMethodException { + List methods = new ArrayList<>(); + ReflectionUtils.doWithLocalMethods(StringGenericParameter.class, method -> { + if ("getFor".equals(method.getName())) { + methods.add(method); + } + }); + Method bridgeMethod = methods.get(0).getReturnType().equals(Object.class) + ? methods.get(0) + : methods.get(1); + assertThat(bridgeMethod.isBridge()).isTrue(); + return bridgeMethod; + } + + private Stream stream(MergedAnnotations annotations, String attributeName) { + return annotations.stream(Cacheable.class).map( + annotation -> annotation.getString(attributeName)); + } + + // @formatter:off + + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface Cacheable { + @AliasFor("cacheName") + String value() default ""; + @AliasFor("value") + String cacheName() default ""; + String key() default ""; + } + + @Cacheable("fooCache") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface FooCache { + @AliasFor(annotation = Cacheable.class) + String key() default ""; + } + + @Cacheable("barCache") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface BarCache { + @AliasFor(annotation = Cacheable.class) + String key(); + } + + @Cacheable("noninheritedCache1") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @interface NoninheritedCache1 { + @AliasFor(annotation = Cacheable.class) + String key() default ""; + } + + @Cacheable("noninheritedCache2") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @interface NoninheritedCache2 { + @AliasFor(annotation = Cacheable.class) + String key() default ""; + } + + @FooCache(key = "fooKey") + @BarCache(key = "barKey") + private static class MultipleComposedCachesClass { + } + + private static class SubMultipleComposedCachesClass + extends MultipleComposedCachesClass { + } + + @NoninheritedCache1 + @NoninheritedCache2 + private static class MultipleNoninheritedComposedCachesClass { + } + + private static class SubMultipleNoninheritedComposedCachesClass + extends MultipleNoninheritedComposedCachesClass { + } + + @Cacheable(cacheName = "fooCache", key = "fooKey") + @BarCache(key = "barKey") + private static class ComposedPlusLocalCachesClass { + } + + @FooCache(key = "fooKey") + @BarCache(key = "barKey") + private interface MultipleComposedCachesInterface { + } + + private static class MultipleComposedCachesOnInterfaceClass implements MultipleComposedCachesInterface { + } + + @BarCache(key = "barKey") + private interface ComposedCacheInterface { + } + + @Cacheable(cacheName = "fooCache", key = "fooKey") + private static class ComposedCacheOnInterfaceAndLocalCacheClass implements ComposedCacheInterface { + } + + @FooCache(key = "fooKey") + @BarCache(key = "barKey") + private void multipleComposedCachesMethod() { + } + + @Cacheable(cacheName = "fooCache", key = "fooKey") + @BarCache(key = "barKey") + private void composedPlusLocalCachesMethod() { + } + + public interface GenericParameter { + T getFor(Class cls); + } + + @SuppressWarnings("unused") + private static class StringGenericParameter implements GenericParameter { + @FooCache(key = "fooKey") + @BarCache(key = "barKey") + @Override + public String getFor(Class cls) { return "foo"; } + public String getFor(Integer integer) { return "foo"; } + } + + // @formatter:on + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java new file mode 100644 index 000000000000..935a6e412071 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java @@ -0,0 +1,424 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedElement; +import java.util.Set; + +import org.assertj.core.api.ThrowableTypeAssert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link MergedAnnotations} and {@link RepeatableContainers} that + * verify support for repeatable annotations. + * + * @author Phillip Webb + * @author Sam Brannen + */ +public class MergedAnnotationsRepeatableAnnotationTests { + + // See SPR-13973 + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void inheritedAnnotationsWhenNonRepeatableThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> getAnnotations(null, NonRepeatable.class, + SearchStrategy.INHERITED_ANNOTATIONS, getClass())).satisfies( + this::nonRepeatableRequirements); + } + + @Test + public void inheritedAnnotationsWhenContainerMissingValueAttributeThrowsException() { + assertThatAnnotationConfigurationException().isThrownBy( + () -> getAnnotations(ContainerMissingValueAttribute.class, + InvalidRepeatable.class, SearchStrategy.INHERITED_ANNOTATIONS, + getClass())).satisfies(this::missingValueAttributeRequirements); + } + + @Test + public void inheritedAnnotationsWhenWhenNonArrayValueAttributeThrowsException() { + assertThatAnnotationConfigurationException().isThrownBy( + () -> getAnnotations(ContainerWithNonArrayValueAttribute.class, + InvalidRepeatable.class, SearchStrategy.INHERITED_ANNOTATIONS, + getClass())).satisfies(this::nonArrayValueAttributeRequirements); + } + + @Test + public void inheritedAnnotationsWhenWrongComponentTypeThrowsException() { + assertThatAnnotationConfigurationException().isThrownBy(() -> getAnnotations( + ContainerWithArrayValueAttributeButWrongComponentType.class, + InvalidRepeatable.class, SearchStrategy.INHERITED_ANNOTATIONS, + getClass())).satisfies(this::wrongComponentTypeRequirements); + } + + @Test + public void inheritedAnnotationsWhenOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.INHERITED_ANNOTATIONS, RepeatableClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + public void inheritedAnnotationsWhenWhenOnSuperclassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.INHERITED_ANNOTATIONS, SubRepeatableClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + public void inheritedAnnotationsWhenComposedOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.INHERITED_ANNOTATIONS, ComposedRepeatableClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + public void inheritedAnnotationsWhenComposedMixedWithContainerOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.INHERITED_ANNOTATIONS, + ComposedRepeatableMixedWithContainerClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + public void inheritedAnnotationsWhenComposedContainerForRepeatableOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.INHERITED_ANNOTATIONS, ComposedContainerClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + public void inheritedAnnotationsWhenNoninheritedComposedRepeatableOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, Noninherited.class, + SearchStrategy.INHERITED_ANNOTATIONS, NoninheritedRepeatableClass.class); + assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A", + "B", "C"); + } + + @Test + public void inheritedAnnotationsWhenNoninheritedComposedRepeatableOnSuperclassReturnsAnnotations() { + Set annotations = getAnnotations(null, Noninherited.class, + SearchStrategy.INHERITED_ANNOTATIONS, + SubNoninheritedRepeatableClass.class); + assertThat(annotations).isEmpty(); + } + + @Test + public void exhaustiveWhenNonRepeatableThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> getAnnotations(null, + NonRepeatable.class, SearchStrategy.EXHAUSTIVE, getClass())).satisfies( + this::nonRepeatableRequirements); + } + + @Test + public void exhaustiveWhenContainerMissingValueAttributeThrowsException() { + assertThatAnnotationConfigurationException().isThrownBy( + () -> getAnnotations(ContainerMissingValueAttribute.class, + InvalidRepeatable.class, SearchStrategy.EXHAUSTIVE, + getClass())).satisfies(this::missingValueAttributeRequirements); + } + + @Test + public void exhaustiveWhenWhenNonArrayValueAttributeThrowsException() { + assertThatAnnotationConfigurationException().isThrownBy( + () -> getAnnotations(ContainerWithNonArrayValueAttribute.class, + InvalidRepeatable.class, SearchStrategy.EXHAUSTIVE, + getClass())).satisfies(this::nonArrayValueAttributeRequirements); + } + + @Test + public void exhaustiveWhenWrongComponentTypeThrowsException() { + assertThatAnnotationConfigurationException().isThrownBy(() -> getAnnotations( + ContainerWithArrayValueAttributeButWrongComponentType.class, + InvalidRepeatable.class, SearchStrategy.EXHAUSTIVE, + getClass())).satisfies(this::wrongComponentTypeRequirements); + } + + @Test + public void exhaustiveWhenOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.EXHAUSTIVE, RepeatableClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + public void exhaustiveWhenWhenOnSuperclassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.EXHAUSTIVE, SubRepeatableClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + public void exhaustiveWhenComposedOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.EXHAUSTIVE, ComposedRepeatableClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + public void exhaustiveWhenComposedMixedWithContainerOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.EXHAUSTIVE, + ComposedRepeatableMixedWithContainerClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + public void exhaustiveWhenComposedContainerForRepeatableOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, PeteRepeat.class, + SearchStrategy.EXHAUSTIVE, ComposedContainerClass.class); + assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B", + "C"); + } + + @Test + public void exhaustiveAnnotationsWhenNoninheritedComposedRepeatableOnClassReturnsAnnotations() { + Set annotations = getAnnotations(null, Noninherited.class, + SearchStrategy.EXHAUSTIVE, NoninheritedRepeatableClass.class); + assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A", + "B", "C"); + } + + @Test + public void exhaustiveAnnotationsWhenNoninheritedComposedRepeatableOnSuperclassReturnsAnnotations() { + Set annotations = getAnnotations(null, Noninherited.class, + SearchStrategy.EXHAUSTIVE, SubNoninheritedRepeatableClass.class); + assertThat(annotations.stream().map(Noninherited::value)).containsExactly("A", + "B", "C"); + } + + private Set getAnnotations( + Class container, Class repeatable, + SearchStrategy searchStrategy, AnnotatedElement element) { + RepeatableContainers containers = RepeatableContainers.of(repeatable, container); + MergedAnnotations annotations = MergedAnnotations.from(element, + searchStrategy, containers, AnnotationFilter.PLAIN); + return annotations.stream(repeatable).collect( + MergedAnnotationCollectors.toAnnotationSet()); + } + + private void nonRepeatableRequirements(Exception ex) { + assertThat(ex.getMessage()).startsWith( + "Annotation type must be a repeatable annotation").contains( + "failed to resolve container type for", + NonRepeatable.class.getName()); + } + + private void missingValueAttributeRequirements(Exception ex) { + ex.printStackTrace(); + assertThat(ex.getMessage()).startsWith( + "Invalid declaration of container type").contains( + ContainerMissingValueAttribute.class.getName(), + "for repeatable annotation", InvalidRepeatable.class.getName()); + assertThat(ex).hasCauseInstanceOf(NoSuchMethodException.class); + } + + private void nonArrayValueAttributeRequirements(Exception ex) { + assertThat(ex.getMessage()).startsWith("Container type").contains( + ContainerWithNonArrayValueAttribute.class.getName(), + "must declare a 'value' attribute for an array of type", + InvalidRepeatable.class.getName()); + } + + private void wrongComponentTypeRequirements(Exception ex) { + assertThat(ex.getMessage()).startsWith("Container type").contains( + ContainerWithArrayValueAttributeButWrongComponentType.class.getName(), + "must declare a 'value' attribute for an array of type", + InvalidRepeatable.class.getName()); + } + + private static ThrowableTypeAssert assertThatAnnotationConfigurationException() { + return assertThatExceptionOfType(AnnotationConfigurationException.class); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface NonRepeatable { + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ContainerMissingValueAttribute { + + // InvalidRepeatable[] value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ContainerWithNonArrayValueAttribute { + + InvalidRepeatable value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ContainerWithArrayValueAttributeButWrongComponentType { + + String[] value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface InvalidRepeatable { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface PeteRepeats { + + PeteRepeat[] value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @Repeatable(PeteRepeats.class) + @interface PeteRepeat { + + String value(); + + } + + @PeteRepeat("shadowed") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface ForPetesSake { + + @AliasFor(annotation = PeteRepeat.class) + String value(); + + } + + @PeteRepeat("shadowed") + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface ForTheLoveOfFoo { + + @AliasFor(annotation = PeteRepeat.class) + String value(); + + } + + @PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") }) + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface ComposedContainer { + + } + + @PeteRepeat("A") + @PeteRepeats({ @PeteRepeat("B"), @PeteRepeat("C") }) + static class RepeatableClass { + + } + + static class SubRepeatableClass extends RepeatableClass { + + } + + @ForPetesSake("B") + @ForTheLoveOfFoo("C") + @PeteRepeat("A") + static class ComposedRepeatableClass { + + } + + @ForPetesSake("C") + @PeteRepeats(@PeteRepeat("A")) + @PeteRepeat("B") + static class ComposedRepeatableMixedWithContainerClass { + + } + + @PeteRepeat("A") + @ComposedContainer + static class ComposedContainerClass { + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @interface Noninheriteds { + + Noninherited[] value(); + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(Noninheriteds.class) + @interface Noninherited { + + @AliasFor("name") + String value() default ""; + + @AliasFor("value") + String name() default ""; + + } + + @Noninherited(name = "shadowed") + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @interface ComposedNoninherited { + + @AliasFor(annotation = Noninherited.class) + String name() default ""; + + } + + @ComposedNoninherited(name = "C") + @Noninheriteds({ @Noninherited(value = "A"), @Noninherited(name = "B") }) + static class NoninheritedRepeatableClass { + + } + + static class SubNoninheritedRepeatableClass extends NoninheritedRepeatableClass { + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java new file mode 100644 index 000000000000..4c14ba0785a0 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java @@ -0,0 +1,3395 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.annotation.Resource; + +import org.junit.Test; +import org.junit.internal.ArrayComparisonFailure; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.MergedAnnotation.MapValues; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.core.annotation.subpackage.NonPublicAnnotatedClass; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Indexed; +import org.springframework.util.ClassUtils; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.*; + +/** + * Tests for {@link MergedAnnotations} and {@link MergedAnnotation}. These tests + * cover common usage scenarios and were mainly ported from the original + * {@code AnnotationUtils} and {@code AnnotatedElementUtils} tests. + * + * @author Phillip Webb + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + * @author Chris Beams + * @author Oleg Zhurakousky + * @author Rossen Stoyanchev + * @see MergedAnnotationsRepeatableAnnotationTests + * @see MergedAnnotationClassLoaderTests + */ +public class MergedAnnotationsTests { + + @Test + public void streamWhenFromNonAnnotatedClass() { + assertThat(MergedAnnotations.from(NonAnnotatedClass.class).stream( + TransactionalComponent.class)).isEmpty(); + } + + @Test + public void streamWhenFromClassWithMetaDepth1() { + Stream names = MergedAnnotations.from( + TransactionalComponent.class).stream().map(MergedAnnotation::getType); + assertThat(names).containsExactly(Transactional.class.getName(), + Component.class.getName(), Indexed.class.getName()); + } + + @Test + public void streamWhenFromClassWithMetaDepth2() { + Stream names = MergedAnnotations.from( + ComposedTransactionalComponent.class).stream().map( + MergedAnnotation::getType); + assertThat(names).containsExactly(TransactionalComponent.class.getName(), + Transactional.class.getName(), Component.class.getName(), + Indexed.class.getName()); + } + + @Test + public void isPresentWhenFromNonAnnotatedClass() { + assertThat(MergedAnnotations.from(NonAnnotatedClass.class).isPresent( + Transactional.class)).isFalse(); + } + + @Test + public void isPresentWhenFromAnnotationClassWithMetaDepth0() { + assertThat(MergedAnnotations.from(TransactionalComponent.class).isPresent( + TransactionalComponent.class)).isFalse(); + } + + @Test + public void isPresentWhenFromAnnotationClassWithMetaDepth1() { + MergedAnnotations annotations = MergedAnnotations.from( + TransactionalComponent.class); + assertThat(annotations.isPresent(Transactional.class)).isTrue(); + assertThat(annotations.isPresent(Component.class)).isTrue(); + } + + @Test + public void isPresentWhenFromAnnotationClassWithMetaDepth2() { + MergedAnnotations annotations = MergedAnnotations.from( + ComposedTransactionalComponent.class); + assertThat(annotations.isPresent(Transactional.class)).isTrue(); + assertThat(annotations.isPresent(Component.class)).isTrue(); + assertThat(annotations.isPresent(ComposedTransactionalComponent.class)).isFalse(); + } + + @Test + public void isPresentWhenFromClassWithMetaDepth0() { + assertThat(MergedAnnotations.from(TransactionalComponentClass.class).isPresent( + TransactionalComponent.class)).isTrue(); + } + + @Test + public void isPresentWhenFromSubclassWithMetaDepth0() { + assertThat(MergedAnnotations.from(SubTransactionalComponentClass.class).isPresent( + TransactionalComponent.class)).isFalse(); + } + + @Test + public void isPresentWhenFromClassWithMetaDepth1() { + MergedAnnotations annotations = MergedAnnotations.from( + TransactionalComponentClass.class); + assertThat(annotations.isPresent(Transactional.class)).isTrue(); + assertThat(annotations.isPresent(Component.class)).isTrue(); + } + + @Test + public void isPresentWhenFromClassWithMetaDepth2() { + MergedAnnotations annotations = MergedAnnotations.from( + ComposedTransactionalComponentClass.class); + assertThat(annotations.isPresent(Transactional.class)).isTrue(); + assertThat(annotations.isPresent(Component.class)).isTrue(); + assertThat(annotations.isPresent(ComposedTransactionalComponent.class)).isTrue(); + } + + @Test + public void collectMultiValueMapFromNonAnnotatedClass() { + MultiValueMap map = MergedAnnotations.from( + NonAnnotatedClass.class).stream(Transactional.class).collect( + MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).isEmpty(); + } + + @Test + public void collectMultiValueMapFromClassWithLocalAnnotation() { + MultiValueMap map = MergedAnnotations.from(TxConfig.class).stream( + Transactional.class).collect( + MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains(entry("value", Arrays.asList("TxConfig"))); + } + + @Test + public void collectMultiValueMapFromClassWithLocalComposedAnnotationAndInheritedAnnotation() { + MultiValueMap map = MergedAnnotations.from( + SubClassWithInheritedAnnotation.class, + SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( + MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains( + entry("qualifier", Arrays.asList("composed2", "transactionManager"))); + } + + @Test + public void collectMultiValueMapFavorsInheritedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { + MultiValueMap map = MergedAnnotations.from( + SubSubClassWithInheritedAnnotation.class, + SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( + MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains(entry("qualifier", Arrays.asList("transactionManager"))); + } + + @Test + public void collectMultiValueMapFavorsInheritedComposedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { + MultiValueMap map = MergedAnnotations.from( + SubSubClassWithInheritedComposedAnnotation.class, + SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( + MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains(entry("qualifier", Arrays.asList("composed1"))); + } + + /** + * If the "value" entry contains both "DerivedTxConfig" AND "TxConfig", then + * the algorithm is accidentally picking up shadowed annotations of the same + * type within the class hierarchy. Such undesirable behavior would cause + * the logic in + * {@link org.springframework.context.annotation.ProfileCondition} to fail. + * + * @see org.springframework.core.env.EnvironmentSystemIntegrationTests#mostSpecificDerivedClassDrivesEnvironment_withDevEnvAndDerivedDevConfigClass + */ + @Test + public void collectMultiValueMapFromClassWithLocalAnnotationThatShadowsAnnotationFromSuperclass() { + MultiValueMap map = MergedAnnotations.from(DerivedTxConfig.class, + SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( + MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains(entry("value", Arrays.asList("DerivedTxConfig"))); + } + + /** + * Note: this functionality is required by + * {@link org.springframework.context.annotation.ProfileCondition}. + * + * @see org.springframework.core.env.EnvironmentSystemIntegrationTests + */ + @Test + public void collectMultiValueMapFromClassWithMultipleComposedAnnotations() { + MultiValueMap map = MergedAnnotations.from( + TxFromMultipleComposedAnnotations.class, + SearchStrategy.INHERITED_ANNOTATIONS).stream(Transactional.class).collect( + MergedAnnotationCollectors.toMultiValueMap()); + assertThat(map).contains( + entry("value", Arrays.asList("TxInheritedComposed", "TxComposed"))); + } + + @Test + public void getWithInheritedAnnotationsFromClassWithLocalAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from(TxConfig.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + assertThat(annotation.getString("value")).isEqualTo("TxConfig"); + } + + @Test + public void getWithInheritedAnnotationsFromClassWithLocalAnnotationThatShadowsAnnotationFromSuperclass() { + MergedAnnotation annotation = MergedAnnotations.from(DerivedTxConfig.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + assertThat(annotation.getString("value")).isEqualTo("DerivedTxConfig"); + } + + @Test + public void getWithInheritedAnnotationsFromMetaCycleAnnotatedClassWithMissingTargetMetaAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from( + MetaCycleAnnotatedClass.class, SearchStrategy.INHERITED_ANNOTATIONS).get( + Transactional.class); + assertThat(annotation.isPresent()).isFalse(); + } + + @Test + public void getWithInheritedAnnotationsFavorsLocalComposedAnnotationOverInheritedAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from( + SubClassWithInheritedAnnotation.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + assertThat(annotation.getBoolean("readOnly")).isTrue(); + } + + @Test + public void getWithInheritedAnnotationsFavorsInheritedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { + MergedAnnotation annotation = MergedAnnotations.from( + SubSubClassWithInheritedAnnotation.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + assertThat(annotation.getBoolean("readOnly")).isFalse(); + } + + @Test + public void getWithInheritedAnnotationsFavorsInheritedComposedAnnotationsOverMoreLocallyDeclaredComposedAnnotations() { + MergedAnnotation annotation = MergedAnnotations.from( + SubSubClassWithInheritedComposedAnnotation.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + assertThat(annotation.getBoolean("readOnly")).isFalse(); + } + + @Test + public void getWithInheritedAnnotationsFromInterfaceImplementedBySuperclass() { + MergedAnnotation annotation = MergedAnnotations.from( + ConcreteClassWithInheritedAnnotation.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + assertThat(annotation.isPresent()).isFalse(); + } + + @Test + public void getWithInheritedAnnotationsFromInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + InheritedAnnotationInterface.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(Transactional.class); + assertThat(annotation.isPresent()).isTrue(); + } + + @Test + public void getWithInheritedAnnotationsFromNonInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + NonInheritedAnnotationInterface.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(Order.class); + assertThat(annotation.isPresent()).isTrue(); + } + + @Test + public void getWithInheritedAnnotationsAttributesWithConventionBasedComposedAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from( + ConventionBasedComposedContextConfigurationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getStringArray("locations")).containsExactly( + "explicitDeclaration"); + assertThat(annotation.getStringArray("value")).containsExactly( + "explicitDeclaration"); + } + + @Test + public void getWithInheritedAnnotationsFromHalfConventionBasedAndHalfAliasedComposedAnnotation1() { + // SPR-13554: convention mapping mixed with AlaisFor annotations + // xmlConfigFiles can be used because it has an AlaisFor annotation + MergedAnnotation annotation = MergedAnnotations.from( + HalfConventionBasedAndHalfAliasedComposedContextConfigurationClass1.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + assertThat(annotation.getStringArray("locations")).containsExactly( + "explicitDeclaration"); + assertThat(annotation.getStringArray("value")).containsExactly( + "explicitDeclaration"); + } + + @Test + public void WithInheritedAnnotationsFromHalfConventionBasedAndHalfAliasedComposedAnnotation2() { + // SPR-13554: convention mapping mixed with AlaisFor annotations + // locations doesn't apply because it has no AlaisFor annotation + MergedAnnotation annotation = MergedAnnotations.from( + HalfConventionBasedAndHalfAliasedComposedContextConfigurationClass2.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + assertThat(annotation.getStringArray("locations")).isEmpty(); + assertThat(annotation.getStringArray("value")).isEmpty(); + } + + @Test + public void WithInheritedAnnotationsFromAliasedComposedAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from( + AliasedComposedContextConfigurationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + assertThat(annotation.getStringArray("value")).containsExactly("test.xml"); + assertThat(annotation.getStringArray("locations")).containsExactly("test.xml"); + } + + @Test + public void WithInheritedAnnotationsFromAliasedValueComposedAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from( + AliasedValueComposedContextConfigurationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + assertThat(annotation.getStringArray("value")).containsExactly("test.xml"); + assertThat(annotation.getStringArray("locations")).containsExactly("test.xml"); + } + + @Test + public void getWithInheritedAnnotationsFromImplicitAliasesInMetaAnnotationOnComposedAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from( + ComposedImplicitAliasesContextConfigurationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + ImplicitAliasesContextConfiguration.class); + assertThat(annotation.getStringArray("groovyScripts")).containsExactly("A.xml", + "B.xml"); + assertThat(annotation.getStringArray("xmlFiles")).containsExactly("A.xml", + "B.xml"); + assertThat(annotation.getStringArray("locations")).containsExactly("A.xml", + "B.xml"); + assertThat(annotation.getStringArray("value")).containsExactly("A.xml", "B.xml"); + } + + @Test + public void getWithInheritedAnnotationsFromAliasedValueComposedAnnotation() { + testGetWithInherited(AliasedValueComposedContextConfigurationClass.class, + "test.xml"); + } + + @Test + public void getWithInheritedAnnotationsFromImplicitAliasesForSameAttributeInComposedAnnotation() { + testGetWithInherited(ImplicitAliasesContextConfigurationClass1.class, "foo.xml"); + testGetWithInherited(ImplicitAliasesContextConfigurationClass2.class, "bar.xml"); + testGetWithInherited(ImplicitAliasesContextConfigurationClass3.class, "baz.xml"); + } + + @Test + public void getWithInheritedAnnotationsFromTransitiveImplicitAliases() { + testGetWithInherited(TransitiveImplicitAliasesContextConfigurationClass.class, + "test.groovy"); + } + + @Test + public void getWithInheritedAnnotationsFromTransitiveImplicitAliasesWithSingleElementOverridingAnArrayViaAliasFor() { + testGetWithInherited( + SingleLocationTransitiveImplicitAliasesContextConfigurationClass.class, + "test.groovy"); + } + + @Test + public void getWithInheritedAnnotationsFromTransitiveImplicitAliasesWithSkippedLevel() { + testGetWithInherited( + TransitiveImplicitAliasesWithSkippedLevelContextConfigurationClass.class, + "test.xml"); + } + + @Test + public void getWithInheritedAnnotationsFromTransitiveImplicitAliasesWithSkippedLevelWithSingleElementOverridingAnArrayViaAliasFor() { + testGetWithInherited( + SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfigurationClass.class, + "test.xml"); + } + + private void testGetWithInherited(Class element, String... expected) { + MergedAnnotation annotation = MergedAnnotations.from(element, + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + assertThat(annotation.getStringArray("locations")).isEqualTo(expected); + assertThat(annotation.getStringArray("value")).isEqualTo(expected); + assertThat(annotation.getClassArray("classes")).isEmpty(); + } + + @Test + public void getWithInheritedAnnotationsFromInvalidConventionBasedComposedAnnotation() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotations.from( + InvalidConventionBasedComposedContextConfigurationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + ContextConfiguration.class)); + } + + @Test + public void getWithInheritedAnnotationsFromShadowedAliasComposedAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from( + ShadowedAliasComposedContextConfigurationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); + assertThat(annotation.getStringArray("locations")).containsExactly("test.xml"); + assertThat(annotation.getStringArray("value")).containsExactly("test.xml"); + } + + @Test + public void getWithExhaustiveFromInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + InheritedAnnotationInterface.class, SearchStrategy.EXHAUSTIVE).get( + Transactional.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(0); + } + + @Test + public void getWithExhaustiveFromSubInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + SubInheritedAnnotationInterface.class, SearchStrategy.EXHAUSTIVE).get( + Transactional.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(1); + } + + @Test + public void getWithExhaustiveFromSubSubInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + SubSubInheritedAnnotationInterface.class, SearchStrategy.EXHAUSTIVE).get( + Transactional.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(2); + } + + @Test + public void getWithExhaustiveFromNonInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + NonInheritedAnnotationInterface.class, SearchStrategy.EXHAUSTIVE).get( + Order.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(0); + } + + @Test + public void getWithExhaustiveFromSubNonInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + SubNonInheritedAnnotationInterface.class, SearchStrategy.EXHAUSTIVE).get( + Order.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(1); + } + + @Test + public void getWithExhaustiveFromSubSubNonInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + SubSubNonInheritedAnnotationInterface.class, + SearchStrategy.EXHAUSTIVE).get(Order.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(2); + } + + @Test + public void getWithExhaustiveInheritedFromInterfaceMethod() + throws NoSuchMethodException { + Method method = ConcreteClassWithInheritedAnnotation.class.getMethod( + "handleFromInterface"); + MergedAnnotation annotation = MergedAnnotations.from(method, + SearchStrategy.EXHAUSTIVE).get(Order.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(1); + } + + @Test + public void getWithExhaustiveInheritedFromAbstractMethod() + throws NoSuchMethodException { + Method method = ConcreteClassWithInheritedAnnotation.class.getMethod("handle"); + MergedAnnotation annotation = MergedAnnotations.from(method, + SearchStrategy.EXHAUSTIVE).get(Transactional.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(1); + } + + @Test + public void getWithExhaustiveInheritedFromBridgedMethod() + throws NoSuchMethodException { + Method method = ConcreteClassWithInheritedAnnotation.class.getMethod( + "handleParameterized", String.class); + MergedAnnotation annotation = MergedAnnotations.from(method, + SearchStrategy.EXHAUSTIVE).get(Transactional.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(1); + } + + @Test + public void getWithExhaustiveFromBridgeMethod() { + List methods = new ArrayList<>(); + ReflectionUtils.doWithLocalMethods(StringGenericParameter.class, method -> { + if ("getFor".equals(method.getName())) { + methods.add(method); + } + }); + Method bridgeMethod = methods.get(0).getReturnType().equals(Object.class) + ? methods.get(0) + : methods.get(1); + Method bridgedMethod = methods.get(0).getReturnType().equals(Object.class) + ? methods.get(1) + : methods.get(0); + assertThat(bridgeMethod.isBridge()).isTrue(); + assertThat(bridgedMethod.isBridge()).isFalse(); + MergedAnnotation annotation = MergedAnnotations.from(bridgeMethod, + SearchStrategy.EXHAUSTIVE).get(Order.class); + assertThat(annotation.isPresent()).isTrue(); + assertThat(annotation.getAggregateIndex()).isEqualTo(0); + } + + @Test + public void getWithExhaustiveFromClassWithMetaAndLocalTxConfig() { + MergedAnnotation annotation = MergedAnnotations.from( + MetaAndLocalTxConfigClass.class, SearchStrategy.EXHAUSTIVE).get( + Transactional.class); + assertThat(annotation.getString("qualifier")).isEqualTo("localTxMgr"); + } + + @Test + public void getWithExhaustiveFromClassWithAttributeAliasesInTargetAnnotation() { + MergedAnnotation mergedAnnotation = MergedAnnotations.from( + AliasedTransactionalComponentClass.class, SearchStrategy.EXHAUSTIVE).get( + AliasedTransactional.class); + AliasedTransactional synthesizedAnnotation = mergedAnnotation.synthesize(); + String qualifier = "aliasForQualifier"; + assertThat(mergedAnnotation.getString("value")).isEqualTo(qualifier); + assertThat(mergedAnnotation.getString("qualifier")).isEqualTo(qualifier); + assertThat(synthesizedAnnotation.value()).isEqualTo(qualifier); + assertThat(synthesizedAnnotation.qualifier()).isEqualTo(qualifier); + } + + @Test + public void getWithExhaustiveFromClassWithAttributeAliasInComposedAnnotationAndNestedAnnotationsInTargetAnnotation() { + MergedAnnotation annotation = testGetWithExhaustive( + TestComponentScanClass.class, "com.example.app.test"); + MergedAnnotation[] excludeFilters = annotation.getAnnotationArray( + "excludeFilters", Filter.class); + assertThat(Arrays.stream(excludeFilters).map( + filter -> filter.getString("pattern"))).containsExactly("*Test", + "*Tests"); + } + + @Test + public void getWithExhaustiveFromClassWithBothAttributesOfAnAliasPairDeclared() { + testGetWithExhaustive(ComponentScanWithBasePackagesAndValueAliasClass.class, + "com.example.app.test"); + } + + @Test + public void getWithExhaustiveWithSingleElementOverridingAnArrayViaConvention() { + testGetWithExhaustive(ConventionBasedSinglePackageComponentScanClass.class, + "com.example.app.test"); + } + + @Test + public void getWithExhaustiveWithSingleElementOverridingAnArrayViaAliasFor() { + testGetWithExhaustive(AliasForBasedSinglePackageComponentScanClass.class, + "com.example.app.test"); + } + + private MergedAnnotation testGetWithExhaustive(Class element, + String... expected) { + MergedAnnotation annotation = MergedAnnotations.from(element, + SearchStrategy.EXHAUSTIVE).get(ComponentScan.class); + assertThat(annotation.getStringArray("value")).containsExactly(expected); + assertThat(annotation.getStringArray("basePackages")).containsExactly(expected); + return annotation; + } + + @Test + public void getWithExhaustiveWhenMultipleMetaAnnotationsHaveClashingAttributeNames() { + MergedAnnotations annotations = MergedAnnotations.from( + AliasedComposedContextConfigurationAndTestPropertySourceClass.class, + SearchStrategy.EXHAUSTIVE); + MergedAnnotation contextConfig = annotations.get(ContextConfiguration.class); + assertThat(contextConfig.getStringArray("locations")).containsExactly("test.xml"); + assertThat(contextConfig.getStringArray("value")).containsExactly("test.xml"); + MergedAnnotation testPropSource = annotations.get(TestPropertySource.class); + assertThat(testPropSource.getStringArray("locations")).containsExactly( + "test.properties"); + assertThat(testPropSource.getStringArray("value")).containsExactly( + "test.properties"); + } + + @Test + public void getWithExhaustiveWithLocalAliasesThatConflictWithAttributesInMetaAnnotationByConvention() { + MergedAnnotation annotation = MergedAnnotations.from( + SpringApplicationConfigurationClass.class, SearchStrategy.EXHAUSTIVE).get( + ContextConfiguration.class); + assertThat(annotation.getStringArray("locations")).isEmpty(); + assertThat(annotation.getStringArray("value")).isEmpty(); + assertThat(annotation.getClassArray("classes")).containsExactly(Number.class); + } + + @Test + public void getWithExhaustiveOnMethodWithSingleElementOverridingAnArrayViaConvention() + throws Exception { + testGetWithExhaustiveWebMapping( + WebController.class.getMethod("postMappedWithPathAttribute")); + } + + @Test + public void getWithExhaustiveOnMethodWithSingleElementOverridingAnArrayViaAliasFor() + throws Exception { + testGetWithExhaustiveWebMapping( + WebController.class.getMethod("getMappedWithValueAttribute")); + testGetWithExhaustiveWebMapping( + WebController.class.getMethod("getMappedWithPathAttribute")); + } + + private void testGetWithExhaustiveWebMapping(AnnotatedElement element) + throws ArrayComparisonFailure { + MergedAnnotation annotation = MergedAnnotations.from(element, + SearchStrategy.EXHAUSTIVE).get(RequestMapping.class); + assertThat(annotation.getStringArray("value")).containsExactly("/test"); + assertThat(annotation.getStringArray("path")).containsExactly("/test"); + } + + @Test + public void getDirectWithJavaLangAnnotationType() throws Exception { + Constructor deprecatedConstructor = Date.class.getConstructor(String.class); + MergedAnnotation annotation = MergedAnnotations.from(deprecatedConstructor, + SearchStrategy.DIRECT, RepeatableContainers.standardRepeatables(), + AnnotationFilter.NONE).get(Deprecated.class); + assertThat(annotation.isPresent()).isTrue(); + } + + @Test + public void getDirectWithJavaxAnnotationType() throws Exception { + assertThat(MergedAnnotations.from(ResourceHolder.class).get( + Resource.class).getString("name")).isEqualTo("x"); + } + + @Test + public void streamInheritedFromClassWithInterface() throws Exception { + Method method = TransactionalServiceImpl.class.getMethod("doIt"); + assertThat(MergedAnnotations.from(method, + SearchStrategy.INHERITED_ANNOTATIONS).stream( + Transactional.class)).isEmpty(); + } + + @Test + public void streamExhaustiveFromClassWithInterface() throws Exception { + Method method = TransactionalServiceImpl.class.getMethod("doIt"); + assertThat(MergedAnnotations.from(method, SearchStrategy.EXHAUSTIVE).stream( + Transactional.class)).hasSize(1); + } + + @Test + public void getFromMethodWithMethodAnnotationOnLeaf() throws Exception { + Method method = Leaf.class.getMethod("annotatedOnLeaf"); + assertThat(method.getAnnotation(Order.class)).isNotNull(); + assertThat(MergedAnnotations.from(method).get(Order.class).getDepth()).isEqualTo( + 0); + assertThat(MergedAnnotations.from(method, SearchStrategy.EXHAUSTIVE).get( + Order.class).getDepth()).isEqualTo(0); + } + + @Test + public void getFromMethodWithAnnotationOnMethodInInterface() throws Exception { + Method method = Leaf.class.getMethod("fromInterfaceImplementedByRoot"); + assertThat(method.getAnnotation(Order.class)).isNull(); + assertThat(MergedAnnotations.from(method).get(Order.class).getDepth()).isEqualTo( + -1); + assertThat(MergedAnnotations.from(method, SearchStrategy.EXHAUSTIVE).get( + Order.class).getDepth()).isEqualTo(0); + } + + @Test + public void getFromMethodWithMetaAnnotationOnLeaf() throws Exception { + Method method = Leaf.class.getMethod("metaAnnotatedOnLeaf"); + assertThat(method.getAnnotation(Order.class)).isNull(); + assertThat(MergedAnnotations.from(method).get(Order.class).getDepth()).isEqualTo( + 1); + assertThat(MergedAnnotations.from(method, SearchStrategy.EXHAUSTIVE).get( + Order.class).getDepth()).isEqualTo(1); + } + + @Test + public void getFromMethodWithMetaMetaAnnotationOnLeaf() throws Exception { + Method method = Leaf.class.getMethod("metaMetaAnnotatedOnLeaf"); + assertThat(method.getAnnotation(Component.class)).isNull(); + assertThat( + MergedAnnotations.from(method).get(Component.class).getDepth()).isEqualTo( + 2); + assertThat(MergedAnnotations.from(method, SearchStrategy.EXHAUSTIVE).get( + Component.class).getDepth()).isEqualTo(2); + } + + @Test + public void getWithAnnotationOnRoot() throws Exception { + Method method = Leaf.class.getMethod("annotatedOnRoot"); + assertThat(method.getAnnotation(Order.class)).isNotNull(); + assertThat(MergedAnnotations.from(method).get(Order.class).getDepth()).isEqualTo( + 0); + assertThat(MergedAnnotations.from(method, SearchStrategy.EXHAUSTIVE).get( + Order.class).getDepth()).isEqualTo(0); + } + + @Test + public void getFromMethodWithMetaAnnotationOnRoot() throws Exception { + Method method = Leaf.class.getMethod("metaAnnotatedOnRoot"); + assertThat(method.getAnnotation(Order.class)).isNull(); + assertThat(MergedAnnotations.from(method).get(Order.class).getDepth()).isEqualTo( + 1); + assertThat(MergedAnnotations.from(method, SearchStrategy.EXHAUSTIVE).get( + Order.class).getDepth()).isEqualTo(1); + } + + @Test + public void getFromMethodWithOnRootButOverridden() throws Exception { + Method method = Leaf.class.getMethod("overrideWithoutNewAnnotation"); + assertThat(method.getAnnotation(Order.class)).isNull(); + assertThat(MergedAnnotations.from(method).get(Order.class).getDepth()).isEqualTo( + -1); + assertThat(MergedAnnotations.from(method, SearchStrategy.EXHAUSTIVE).get( + Order.class).getDepth()).isEqualTo(0); + } + + @Test + public void getFromMethodWithNotAnnotated() throws Exception { + Method method = Leaf.class.getMethod("notAnnotated"); + assertThat(method.getAnnotation(Order.class)).isNull(); + assertThat(MergedAnnotations.from(method).get(Order.class).getDepth()).isEqualTo( + -1); + assertThat(MergedAnnotations.from(method, SearchStrategy.EXHAUSTIVE).get( + Order.class).getDepth()).isEqualTo(-1); + } + + @Test + public void getFromMethodWithBridgeMethod() throws Exception { + Method method = TransactionalStringGeneric.class.getMethod("something", + Object.class); + assertThat(method.isBridge()).isTrue(); + assertThat(method.getAnnotation(Order.class)).isNull(); + assertThat(MergedAnnotations.from(method).get(Order.class).getDepth()).isEqualTo( + -1); + assertThat(MergedAnnotations.from(method, SearchStrategy.EXHAUSTIVE).get( + Order.class).getDepth()).isEqualTo(0); + boolean runningInEclipse = Arrays.stream( + new Exception().getStackTrace()).anyMatch( + element -> element.getClassName().startsWith("org.eclipse.jdt")); + // As of JDK 8, invoking getAnnotation() on a bridge method actually + // finds an + // annotation on its 'bridged' method [1]; however, the Eclipse compiler + // will not + // support this until Eclipse 4.9 [2]. Thus, we effectively ignore the + // following + // assertion if the test is currently executing within the Eclipse IDE. + // [1] https://bugs.openjdk.java.net/browse/JDK-6695379 + // [2] https://bugs.eclipse.org/bugs/show_bug.cgi?id=495396 + if (!runningInEclipse) { + assertThat(method.getAnnotation(Transactional.class)).isNotNull(); + } + assertThat(MergedAnnotations.from(method).get( + Transactional.class).getDepth()).isEqualTo(0); + assertThat(MergedAnnotations.from(method, SearchStrategy.EXHAUSTIVE).get( + Transactional.class).getDepth()).isEqualTo(0); + } + + @Test + public void getFromMethodWithBridgedMethod() throws Exception { + Method method = TransactionalStringGeneric.class.getMethod("something", + String.class); + assertThat(method.isBridge()).isFalse(); + assertThat(method.getAnnotation(Order.class)).isNull(); + assertThat(MergedAnnotations.from(method).get(Order.class).getDepth()).isEqualTo( + -1); + assertThat(MergedAnnotations.from(method, SearchStrategy.EXHAUSTIVE).get( + Order.class).getDepth()).isEqualTo(0); + assertThat(method.getAnnotation(Transactional.class)).isNotNull(); + assertThat(MergedAnnotations.from(method).get( + Transactional.class).getDepth()).isEqualTo(0); + assertThat(MergedAnnotations.from(method, SearchStrategy.EXHAUSTIVE).get( + Transactional.class).getDepth()).isEqualTo(0); + } + + @Test + public void getFromMethodWithInterface() throws Exception { + Method method = ImplementsInterfaceWithAnnotatedMethod.class.getMethod("foo"); + assertThat(MergedAnnotations.from(method, SearchStrategy.EXHAUSTIVE).get( + Order.class).getDepth()).isEqualTo(0); + } + + @Test // SPR-16060 + public void getFromMethodWithGenericInterface() throws Exception { + Method method = ImplementsInterfaceWithGenericAnnotatedMethod.class.getMethod( + "foo", String.class); + assertThat(MergedAnnotations.from(method, SearchStrategy.EXHAUSTIVE).get( + Order.class).getDepth()).isEqualTo(0); + } + + @Test // SPR-17146 + public void getFromMethodWithGenericSuperclass() throws Exception { + Method method = ExtendsBaseClassWithGenericAnnotatedMethod.class.getMethod("foo", + String.class); + assertThat(MergedAnnotations.from(method, SearchStrategy.EXHAUSTIVE).get( + Order.class).getDepth()).isEqualTo(0); + } + + @Test + public void getFromMethodWithInterfaceOnSuper() throws Exception { + Method method = SubOfImplementsInterfaceWithAnnotatedMethod.class.getMethod( + "foo"); + assertThat(MergedAnnotations.from(method, SearchStrategy.EXHAUSTIVE).get( + Order.class).getDepth()).isEqualTo(0); + } + + @Test + public void getFromMethodWhenInterfaceWhenSuperDoesNotImplementMethod() + throws Exception { + Method method = SubOfAbstractImplementsInterfaceWithAnnotatedMethod.class.getMethod( + "foo"); + assertThat(MergedAnnotations.from(method, SearchStrategy.EXHAUSTIVE).get( + Order.class).getDepth()).isEqualTo(0); + } + + @Test + public void getDirectFromClassFavorsMoreLocallyDeclaredComposedAnnotationsOverAnnotationsOnInterfaces() { + MergedAnnotation annotation = MergedAnnotations.from( + ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface.class, + SearchStrategy.EXHAUSTIVE).get(Component.class); + assertThat(annotation.getString("value")).isEqualTo("meta2"); + } + + @Test + public void getDirectFromClassFavorsMoreLocallyDeclaredComposedAnnotationsOverInheritedAnnotations() { + MergedAnnotation annotation = MergedAnnotations.from( + SubSubClassWithInheritedAnnotation.class, SearchStrategy.EXHAUSTIVE).get( + Transactional.class); + assertThat(annotation.getBoolean("readOnly")).isTrue(); + } + + @Test + public void getDirectFromClassFavorsMoreLocallyDeclaredComposedAnnotationsOverInheritedComposedAnnotations() { + MergedAnnotation annotation = MergedAnnotations.from( + SubSubClassWithInheritedMetaAnnotation.class, + SearchStrategy.EXHAUSTIVE).get(Component.class); + assertThat(annotation.getString("value")).isEqualTo("meta2"); + } + + @Test + public void getDirectFromClassgetDirectFromClassMetaMetaAnnotatedClass() { + MergedAnnotation annotation = MergedAnnotations.from( + MetaMetaAnnotatedClass.class, SearchStrategy.EXHAUSTIVE).get( + Component.class); + assertThat(annotation.getString("value")).isEqualTo("meta2"); + } + + @Test + public void getDirectFromClassWithMetaMetaMetaAnnotatedClass() { + MergedAnnotation annotation = MergedAnnotations.from( + MetaMetaMetaAnnotatedClass.class, SearchStrategy.EXHAUSTIVE).get( + Component.class); + assertThat(annotation.getString("value")).isEqualTo("meta2"); + } + + @Test + public void getDirectFromClassWithAnnotatedClassWithMissingTargetMetaAnnotation() { + // TransactionalClass is NOT annotated or meta-annotated with @Component + MergedAnnotation annotation = MergedAnnotations.from(TransactionalClass.class, + SearchStrategy.EXHAUSTIVE).get(Component.class); + assertThat(annotation.isPresent()).isFalse(); + } + + @Test + public void getDirectFromClassWithMetaCycleAnnotatedClassWithMissingTargetMetaAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from( + MetaCycleAnnotatedClass.class, SearchStrategy.EXHAUSTIVE).get( + Component.class); + assertThat(annotation.isPresent()).isFalse(); + } + + @Test + public void getDirectFromClassWithInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + InheritedAnnotationInterface.class, SearchStrategy.EXHAUSTIVE).get( + Transactional.class); + assertThat(annotation.getAggregateIndex()).isEqualTo(0); + } + + @Test + public void getDirectFromClassWithSubInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + SubInheritedAnnotationInterface.class, SearchStrategy.EXHAUSTIVE).get( + Transactional.class); + assertThat(annotation.getAggregateIndex()).isEqualTo(1); + } + + @Test + public void getDirectFromClassWithSubSubInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + SubSubInheritedAnnotationInterface.class, SearchStrategy.EXHAUSTIVE).get( + Transactional.class); + assertThat(annotation.getAggregateIndex()).isEqualTo(2); + } + + @Test + public void getDirectFromClassWithNonInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + NonInheritedAnnotationInterface.class, SearchStrategy.EXHAUSTIVE).get( + Order.class); + assertThat(annotation.getAggregateIndex()).isEqualTo(0); + } + + @Test + public void getDirectFromClassWithSubNonInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + SubNonInheritedAnnotationInterface.class, SearchStrategy.EXHAUSTIVE).get( + Order.class); + assertThat(annotation.getAggregateIndex()).isEqualTo(1); + } + + @Test + public void getDirectFromClassWithSubSubNonInheritedAnnotationInterface() { + MergedAnnotation annotation = MergedAnnotations.from( + SubSubNonInheritedAnnotationInterface.class, + SearchStrategy.EXHAUSTIVE).get(Order.class); + assertThat(annotation.getAggregateIndex()).isEqualTo(2); + } + + @Test + public void getSuperClassForAllScenarios() { + // no class-level annotation + assertThat(MergedAnnotations.from(NonAnnotatedInterface.class, + SearchStrategy.SUPER_CLASS).get( + Transactional.class).getSource()).isNull(); + assertThat(MergedAnnotations.from(NonAnnotatedClass.class, + SearchStrategy.SUPER_CLASS).get( + Transactional.class).getSource()).isNull(); + // inherited class-level annotation; note: @Transactional is inherited + assertThat(MergedAnnotations.from(InheritedAnnotationInterface.class, + SearchStrategy.SUPER_CLASS).get( + Transactional.class).getSource()).isEqualTo( + InheritedAnnotationInterface.class); + assertThat(MergedAnnotations.from(SubInheritedAnnotationInterface.class, + SearchStrategy.SUPER_CLASS).get( + Transactional.class).getSource()).isNull(); + assertThat(MergedAnnotations.from(InheritedAnnotationClass.class, + SearchStrategy.SUPER_CLASS).get( + Transactional.class).getSource()).isEqualTo( + InheritedAnnotationClass.class); + assertThat(MergedAnnotations.from(SubInheritedAnnotationClass.class, + SearchStrategy.SUPER_CLASS).get( + Transactional.class).getSource()).isEqualTo( + InheritedAnnotationClass.class); + // non-inherited class-level annotation; note: @Order is not inherited, + // but we should still find it on classes. + assertThat(MergedAnnotations.from(NonInheritedAnnotationInterface.class, + SearchStrategy.SUPER_CLASS).get(Order.class).getSource()).isEqualTo( + NonInheritedAnnotationInterface.class); + assertThat(MergedAnnotations.from(SubNonInheritedAnnotationInterface.class, + SearchStrategy.SUPER_CLASS).get(Order.class).getSource()).isNull(); + assertThat(MergedAnnotations.from(NonInheritedAnnotationClass.class, + SearchStrategy.SUPER_CLASS).get(Order.class).getSource()).isEqualTo( + NonInheritedAnnotationClass.class); + assertThat(MergedAnnotations.from(SubNonInheritedAnnotationClass.class, + SearchStrategy.SUPER_CLASS).get(Order.class).getSource()).isEqualTo( + NonInheritedAnnotationClass.class); + } + + @Test + public void getSuperClassSourceForTypesWithSingleCandidateType() { + // no class-level annotation + List> transactionalCandidateList = Collections.singletonList( + Transactional.class); + assertThat(getSuperClassSourceWithTypeIn(NonAnnotatedInterface.class, + transactionalCandidateList)).isNull(); + assertThat(getSuperClassSourceWithTypeIn(NonAnnotatedClass.class, + transactionalCandidateList)).isNull(); + // inherited class-level annotation; note: @Transactional is inherited + assertThat(getSuperClassSourceWithTypeIn(InheritedAnnotationInterface.class, + transactionalCandidateList)).isEqualTo( + InheritedAnnotationInterface.class); + assertThat(getSuperClassSourceWithTypeIn(SubInheritedAnnotationInterface.class, + transactionalCandidateList)).isNull(); + assertThat(getSuperClassSourceWithTypeIn(InheritedAnnotationClass.class, + transactionalCandidateList)).isEqualTo(InheritedAnnotationClass.class); + assertThat(getSuperClassSourceWithTypeIn(SubInheritedAnnotationClass.class, + transactionalCandidateList)).isEqualTo(InheritedAnnotationClass.class); + // non-inherited class-level annotation; note: @Order is not inherited, + // but should still find it on classes. + List> orderCandidateList = Collections.singletonList( + Order.class); + assertThat(getSuperClassSourceWithTypeIn(NonInheritedAnnotationInterface.class, + orderCandidateList)).isEqualTo(NonInheritedAnnotationInterface.class); + assertThat(getSuperClassSourceWithTypeIn(SubNonInheritedAnnotationInterface.class, + orderCandidateList)).isNull(); + assertThat(getSuperClassSourceWithTypeIn(NonInheritedAnnotationClass.class, + orderCandidateList)).isEqualTo(NonInheritedAnnotationClass.class); + assertThat(getSuperClassSourceWithTypeIn(SubNonInheritedAnnotationClass.class, + orderCandidateList)).isEqualTo(NonInheritedAnnotationClass.class); + } + + @Test + public void getSuperClassSourceForTypesWithMultipleCandidateTypes() { + List> candidates = Arrays.asList(Transactional.class, + Order.class); + // no class-level annotation + assertThat(getSuperClassSourceWithTypeIn(NonAnnotatedInterface.class, + candidates)).isNull(); + assertThat(getSuperClassSourceWithTypeIn(NonAnnotatedClass.class, + candidates)).isNull(); + // inherited class-level annotation; note: @Transactional is inherited + assertThat(getSuperClassSourceWithTypeIn(InheritedAnnotationInterface.class, + candidates)).isEqualTo(InheritedAnnotationInterface.class); + assertThat(getSuperClassSourceWithTypeIn(SubInheritedAnnotationInterface.class, + candidates)).isNull(); + assertThat(getSuperClassSourceWithTypeIn(InheritedAnnotationClass.class, + candidates)).isEqualTo(InheritedAnnotationClass.class); + assertThat(getSuperClassSourceWithTypeIn(SubInheritedAnnotationClass.class, + candidates)).isEqualTo(InheritedAnnotationClass.class); + // non-inherited class-level annotation; note: @Order is not inherited, + // but findAnnotationDeclaringClassForTypes() should still find it on + // classes. + assertThat(getSuperClassSourceWithTypeIn(NonInheritedAnnotationInterface.class, + candidates)).isEqualTo(NonInheritedAnnotationInterface.class); + assertThat(getSuperClassSourceWithTypeIn(SubNonInheritedAnnotationInterface.class, + candidates)).isNull(); + assertThat(getSuperClassSourceWithTypeIn(NonInheritedAnnotationClass.class, + candidates)).isEqualTo(NonInheritedAnnotationClass.class); + assertThat(getSuperClassSourceWithTypeIn(SubNonInheritedAnnotationClass.class, + candidates)).isEqualTo(NonInheritedAnnotationClass.class); + // class hierarchy mixed with @Transactional and @Order declarations + assertThat(getSuperClassSourceWithTypeIn(TransactionalClass.class, + candidates)).isEqualTo(TransactionalClass.class); + assertThat(getSuperClassSourceWithTypeIn(TransactionalAndOrderedClass.class, + candidates)).isEqualTo(TransactionalAndOrderedClass.class); + assertThat(getSuperClassSourceWithTypeIn(SubTransactionalAndOrderedClass.class, + candidates)).isEqualTo(TransactionalAndOrderedClass.class); + } + + private Object getSuperClassSourceWithTypeIn(Class clazz, + List> annotationTypes) { + return MergedAnnotations.from(clazz, SearchStrategy.SUPER_CLASS).stream().filter( + MergedAnnotationPredicates.typeIn(annotationTypes).and( + MergedAnnotation::isDirectlyPresent)).map( + MergedAnnotation::getSource).findFirst().orElse(null); + } + + @Test + public void isDirectlyPresentForAllScenarios() throws Exception { + // no class-level annotation + assertThat(MergedAnnotations.from(NonAnnotatedInterface.class).get( + Transactional.class).isDirectlyPresent()).isFalse(); + assertThat(MergedAnnotations.from(NonAnnotatedInterface.class).isDirectlyPresent( + Transactional.class)).isFalse(); + assertThat(MergedAnnotations.from(NonAnnotatedClass.class).get( + Transactional.class).isDirectlyPresent()).isFalse(); + assertThat(MergedAnnotations.from(NonAnnotatedClass.class).isDirectlyPresent( + Transactional.class)).isFalse(); + // inherited class-level annotation; note: @Transactional is inherited + assertThat(MergedAnnotations.from(InheritedAnnotationInterface.class).get( + Transactional.class).isDirectlyPresent()).isTrue(); + assertThat(MergedAnnotations.from( + InheritedAnnotationInterface.class).isDirectlyPresent( + Transactional.class)).isTrue(); + assertThat(MergedAnnotations.from(SubInheritedAnnotationInterface.class).get( + Transactional.class).isDirectlyPresent()).isFalse(); + assertThat(MergedAnnotations.from( + SubInheritedAnnotationInterface.class).isDirectlyPresent( + Transactional.class)).isFalse(); + assertThat(MergedAnnotations.from(InheritedAnnotationClass.class).get( + Transactional.class).isDirectlyPresent()).isTrue(); + assertThat( + MergedAnnotations.from(InheritedAnnotationClass.class).isDirectlyPresent( + Transactional.class)).isTrue(); + assertThat(MergedAnnotations.from(SubInheritedAnnotationClass.class).get( + Transactional.class).isDirectlyPresent()).isFalse(); + assertThat(MergedAnnotations.from( + SubInheritedAnnotationClass.class).isDirectlyPresent( + Transactional.class)).isFalse(); + // non-inherited class-level annotation; note: @Order is not inherited + assertThat(MergedAnnotations.from(NonInheritedAnnotationInterface.class).get( + Order.class).isDirectlyPresent()).isTrue(); + assertThat(MergedAnnotations.from( + NonInheritedAnnotationInterface.class).isDirectlyPresent( + Order.class)).isTrue(); + assertThat(MergedAnnotations.from(SubNonInheritedAnnotationInterface.class).get( + Order.class).isDirectlyPresent()).isFalse(); + assertThat(MergedAnnotations.from( + SubNonInheritedAnnotationInterface.class).isDirectlyPresent( + Order.class)).isFalse(); + assertThat(MergedAnnotations.from(NonInheritedAnnotationClass.class).get( + Order.class).isDirectlyPresent()).isTrue(); + assertThat(MergedAnnotations.from( + NonInheritedAnnotationClass.class).isDirectlyPresent( + Order.class)).isTrue(); + assertThat(MergedAnnotations.from(SubNonInheritedAnnotationClass.class).get( + Order.class).isDirectlyPresent()).isFalse(); + assertThat(MergedAnnotations.from( + SubNonInheritedAnnotationClass.class).isDirectlyPresent( + Order.class)).isFalse(); + } + + @Test + public void getAggregateIndexForAllScenarios() { + // no class-level annotation + assertThat(MergedAnnotations.from(NonAnnotatedInterface.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Transactional.class).getAggregateIndex()).isEqualTo(-1); + assertThat(MergedAnnotations.from(NonAnnotatedClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Transactional.class).getAggregateIndex()).isEqualTo(-1); + // inherited class-level annotation; note: @Transactional is inherited + assertThat(MergedAnnotations.from(InheritedAnnotationInterface.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Transactional.class).getAggregateIndex()).isEqualTo(0); + // Since we're not traversing interface hierarchies the following, + // though perhaps + // counter intuitive, must be false: + assertThat(MergedAnnotations.from(SubInheritedAnnotationInterface.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Transactional.class).getAggregateIndex()).isEqualTo(-1); + assertThat(MergedAnnotations.from(InheritedAnnotationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Transactional.class).getAggregateIndex()).isEqualTo(0); + assertThat(MergedAnnotations.from(SubInheritedAnnotationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Transactional.class).getAggregateIndex()).isEqualTo(1); + // non-inherited class-level annotation; note: @Order is not inherited + assertThat(MergedAnnotations.from(NonInheritedAnnotationInterface.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Order.class).getAggregateIndex()).isEqualTo(0); + assertThat(MergedAnnotations.from(SubNonInheritedAnnotationInterface.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Order.class).getAggregateIndex()).isEqualTo(-1); + assertThat(MergedAnnotations.from(NonInheritedAnnotationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Order.class).getAggregateIndex()).isEqualTo(0); + assertThat(MergedAnnotations.from(SubNonInheritedAnnotationClass.class, + SearchStrategy.INHERITED_ANNOTATIONS).get( + Order.class).getAggregateIndex()).isEqualTo(-1); + } + + @Test + public void getDirectWithoutAttributeAliases() { + MergedAnnotation annotation = MergedAnnotations.from(WebController.class).get( + Component.class); + assertThat(annotation.getString("value")).isEqualTo("webController"); + } + + @Test + public void getDirectWithNestedAnnotations() { + MergedAnnotation annotation = MergedAnnotations.from( + ComponentScanClass.class).get(ComponentScan.class); + MergedAnnotation[] filters = annotation.getAnnotationArray( + "excludeFilters", Filter.class); + assertThat(Arrays.stream(filters).map( + filter -> filter.getString("pattern"))).containsExactly("*Foo", "*Bar"); + } + + @Test + public void getDirectWithAttributeAliases1() throws Exception { + Method method = WebController.class.getMethod("handleMappedWithValueAttribute"); + MergedAnnotation annotation = MergedAnnotations.from(method).get( + RequestMapping.class); + assertThat(annotation.getString("name")).isEqualTo("foo"); + assertThat(annotation.getStringArray("value")).containsExactly("/test"); + assertThat(annotation.getStringArray("path")).containsExactly("/test"); + } + + @Test + public void getDirectWithAttributeAliases2() throws Exception { + Method method = WebController.class.getMethod("handleMappedWithPathAttribute"); + MergedAnnotation annotation = MergedAnnotations.from(method).get( + RequestMapping.class); + assertThat(annotation.getString("name")).isEqualTo("bar"); + assertThat(annotation.getStringArray("value")).containsExactly("/test"); + assertThat(annotation.getStringArray("path")).containsExactly("/test"); + } + + @Test + public void getDirectWithAttributeAliasesWithDifferentValues() throws Exception { + Method method = WebController.class.getMethod( + "handleMappedWithDifferentPathAndValueAttributes"); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotations.from(method).get( + RequestMapping.class)).withMessageContaining( + "attribute 'path' and its alias 'value'").withMessageContaining( + "values of [{/test}] and [{/enigma}]"); + } + + @Test + public void getValueFromAnnotation() throws Exception { + Method method = TransactionalStringGeneric.class.getMethod("something", + Object.class); + MergedAnnotation annotation = MergedAnnotations.from(method, + SearchStrategy.EXHAUSTIVE).get(Order.class); + assertThat(annotation.getInt("value")).isEqualTo(1); + } + + @Test + public void getValueFromNonPublicAnnotation() throws Exception { + Annotation[] declaredAnnotations = NonPublicAnnotatedClass.class.getDeclaredAnnotations(); + assertThat(declaredAnnotations).hasSize(1); + Annotation annotation = declaredAnnotations[0]; + MergedAnnotation mergedAnnotation = MergedAnnotation.from(annotation); + assertThat(mergedAnnotation.getType()).contains("NonPublicAnnotation"); + assertThat( + mergedAnnotation.synthesize().annotationType().getSimpleName()).isEqualTo( + "NonPublicAnnotation"); + assertThat(mergedAnnotation.getInt("value")).isEqualTo(42); + } + + @Test + public void getDefaultValueFromAnnotation() throws Exception { + Method method = TransactionalStringGeneric.class.getMethod("something", + Object.class); + MergedAnnotation annotation = MergedAnnotations.from(method, + SearchStrategy.EXHAUSTIVE).get(Order.class); + assertThat(annotation.getDefaultValue("value")).contains( + Ordered.LOWEST_PRECEDENCE); + } + + @Test + public void getDefaultValueFromNonPublicAnnotation() { + Annotation[] declaredAnnotations = NonPublicAnnotatedClass.class.getDeclaredAnnotations(); + assertThat(declaredAnnotations).hasSize(1); + Annotation declaredAnnotation = declaredAnnotations[0]; + MergedAnnotation annotation = MergedAnnotation.from(declaredAnnotation); + assertThat(annotation.getType()).isEqualTo( + "org.springframework.core.annotation.subpackage.NonPublicAnnotation"); + assertThat(annotation.getDefaultValue("value")).contains(-1); + } + + @Test + public void getDefaultValueFromAnnotationType() { + MergedAnnotation annotation = MergedAnnotation.from(Order.class); + assertThat(annotation.getDefaultValue("value")).contains( + Ordered.LOWEST_PRECEDENCE); + } + + @Test + public void getRepeatableOnComposedAnnotation() { + MergedAnnotation annotation = MergedAnnotations.from(MyRepeatableMeta1.class, + SearchStrategy.EXHAUSTIVE, RepeatableContainers.none(), + AnnotationFilter.NONE).get(Repeatable.class); + assertThat(annotation.getClass("value")).isEqualTo(MyRepeatableContainer.class); + } + + @Test + public void getRepeatableDeclaredOnMethod() throws Exception { + Method method = InterfaceWithRepeated.class.getMethod("foo"); + Stream> annotations = MergedAnnotations.from( + method, SearchStrategy.EXHAUSTIVE).stream(MyRepeatable.class); + Stream values = annotations.map( + annotation -> annotation.getString("value")); + assertThat(values).containsExactly("A", "B", "C", "meta1"); + } + + @Test + public void getRepeatableDeclaredOnClassWithMissingAttributeAliasDeclaration() + throws Exception { + RepeatableContainers containers = RepeatableContainers.of( + BrokenContextConfiguration.class, BrokenHierarchy.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotations.from(BrokenHierarchyClass.class, + SearchStrategy.EXHAUSTIVE, containers, + AnnotationFilter.PLAIN).get( + BrokenHierarchy.class)).withMessageStartingWith( + "Attribute 'value' in").withMessageContaining( + BrokenContextConfiguration.class.getName()).withMessageContaining( + "@AliasFor 'location'"); + } + + @Test + public void getRepeatableDeclaredOnClassWithAttributeAliases() { + assertThat(MergedAnnotations.from(HierarchyClass.class).stream( + TestConfiguration.class)).isEmpty(); + RepeatableContainers containers = RepeatableContainers.of(TestConfiguration.class, + Hierarchy.class); + MergedAnnotations annotations = MergedAnnotations.from(HierarchyClass.class, + SearchStrategy.DIRECT, containers, AnnotationFilter.NONE); + assertThat(annotations.stream(TestConfiguration.class).map( + annotation -> annotation.getString("location"))).containsExactly("A", + "B"); + assertThat(annotations.stream(TestConfiguration.class).map( + annotation -> annotation.getString("value"))).containsExactly("A", "B"); + } + + @Test + public void getRepeatableDeclaredOnClass() { + Class element = MyRepeatableClass.class; + String[] expectedValuesJava = { "A", "B", "C" }; + String[] expectedValuesSpring = { "A", "B", "C", "meta1" }; + testRepeatables(SearchStrategy.SUPER_CLASS, element, expectedValuesJava, + expectedValuesSpring); + } + + @Test + public void getRepeatableDeclaredOnSuperclass() { + Class element = SubMyRepeatableClass.class; + String[] expectedValuesJava = { "A", "B", "C" }; + String[] expectedValuesSpring = { "A", "B", "C", "meta1" }; + testRepeatables(SearchStrategy.SUPER_CLASS, element, expectedValuesJava, + expectedValuesSpring); + } + + @Test + public void getRepeatableDeclaredOnClassAndSuperclass() { + Class element = SubMyRepeatableWithAdditionalLocalDeclarationsClass.class; + String[] expectedValuesJava = { "X", "Y", "Z" }; + String[] expectedValuesSpring = { "X", "Y", "Z", "meta2" }; + testRepeatables(SearchStrategy.SUPER_CLASS, element, expectedValuesJava, + expectedValuesSpring); + } + + @Test + public void getRepeatableDeclaredOnMultipleSuperclasses() { + Class element = SubSubMyRepeatableWithAdditionalLocalDeclarationsClass.class; + String[] expectedValuesJava = { "X", "Y", "Z" }; + String[] expectedValuesSpring = { "X", "Y", "Z", "meta2" }; + testRepeatables(SearchStrategy.SUPER_CLASS, element, expectedValuesJava, + expectedValuesSpring); + } + + @Test + public void getDirectRepeatablesDeclaredOnClass() { + Class element = MyRepeatableClass.class; + String[] expectedValuesJava = { "A", "B", "C" }; + String[] expectedValuesSpring = { "A", "B", "C", "meta1" }; + testRepeatables(SearchStrategy.DIRECT, element, expectedValuesJava, + expectedValuesSpring); + } + + @Test + public void getDirectRepeatablesDeclaredOnSuperclass() { + Class element = SubMyRepeatableClass.class; + String[] expectedValuesJava = {}; + String[] expectedValuesSpring = {}; + testRepeatables(SearchStrategy.DIRECT, element, expectedValuesJava, + expectedValuesSpring); + } + + private void testRepeatables(SearchStrategy searchStrategy, Class element, + String[] expectedValuesJava, String[] expectedValuesSpring) { + testJavaRepeatables(searchStrategy, element, expectedValuesJava); + testExplicitRepeatables(searchStrategy, element, expectedValuesSpring); + testStandardRepeatables(searchStrategy, element, expectedValuesSpring); + } + + private void testJavaRepeatables(SearchStrategy searchStrategy, Class element, + String[] expected) { + MyRepeatable[] annotations = searchStrategy == SearchStrategy.DIRECT + ? element.getDeclaredAnnotationsByType(MyRepeatable.class) + : element.getAnnotationsByType(MyRepeatable.class); + assertThat(Arrays.stream(annotations).map(MyRepeatable::value)).containsExactly( + expected); + } + + private void testExplicitRepeatables(SearchStrategy searchStrategy, Class element, + String[] expected) { + MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy, + RepeatableContainers.of(MyRepeatable.class, MyRepeatableContainer.class), + AnnotationFilter.PLAIN); + assertThat(annotations.stream(MyRepeatable.class).filter( + MergedAnnotationPredicates.firstRunOf( + MergedAnnotation::getAggregateIndex)).map( + annotation -> annotation.getString( + "value"))).containsExactly(expected); + } + + private void testStandardRepeatables(SearchStrategy searchStrategy, Class element, + String[] expected) { + MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy); + assertThat(annotations.stream(MyRepeatable.class).filter( + MergedAnnotationPredicates.firstRunOf( + MergedAnnotation::getAggregateIndex)).map( + annotation -> annotation.getString( + "value"))).containsExactly(expected); + } + + @Test + public void synthesizeWithoutAttributeAliases() throws Exception { + Component component = WebController.class.getAnnotation(Component.class); + assertThat(component).isNotNull(); + Component synthesizedComponent = MergedAnnotation.from(component).synthesize(); + assertThat(synthesizedComponent).isNotNull(); + assertThat(synthesizedComponent).isEqualTo(component); + assertThat(synthesizedComponent.value()).isEqualTo("webController"); + } + + @Test + public void synthesizeAlreadySynthesized() throws Exception { + Method method = WebController.class.getMethod("handleMappedWithValueAttribute"); + RequestMapping webMapping = method.getAnnotation(RequestMapping.class); + assertThat(webMapping).isNotNull(); + RequestMapping synthesizedWebMapping = MergedAnnotation.from( + webMapping).synthesize(); + RequestMapping synthesizedAgainWebMapping = MergedAnnotation.from( + synthesizedWebMapping).synthesize(); + assertThat(synthesizedWebMapping).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesizedAgainWebMapping).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesizedWebMapping).isEqualTo(synthesizedAgainWebMapping); + assertThat(synthesizedWebMapping.name()).isEqualTo("foo"); + assertThat(synthesizedWebMapping.path()).containsExactly("/test"); + assertThat(synthesizedWebMapping.value()).containsExactly("/test"); + } + + @Test + public void synthesizeWhenAliasForIsMissingAttributeDeclaration() throws Exception { + AliasForWithMissingAttributeDeclaration annotation = AliasForWithMissingAttributeDeclarationClass.class.getAnnotation( + AliasForWithMissingAttributeDeclaration.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)).withMessageStartingWith( + "@AliasFor declaration on attribute 'foo' in annotation").withMessageContaining( + AliasForWithMissingAttributeDeclaration.class.getName()).withMessageContaining( + "points to itself"); + } + + @Test + public void synthesizeWhenAliasForHasDuplicateAttributeDeclaration() + throws Exception { + AliasForWithDuplicateAttributeDeclaration annotation = AliasForWithDuplicateAttributeDeclarationClass.class.getAnnotation( + AliasForWithDuplicateAttributeDeclaration.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)).withMessageStartingWith( + "In @AliasFor declared on attribute 'foo' in annotation").withMessageContaining( + AliasForWithDuplicateAttributeDeclaration.class.getName()).withMessageContaining( + "attribute 'attribute' and its alias 'value' are present with values of 'baz' and 'bar'"); + } + + @Test + public void synthesizeWhenAttributeAliasForNonexistentAttribute() throws Exception { + AliasForNonexistentAttribute annotation = AliasForNonexistentAttributeClass.class.getAnnotation( + AliasForNonexistentAttribute.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)).withMessageStartingWith( + "@AliasFor declaration on attribute 'foo' in annotation").withMessageContaining( + AliasForNonexistentAttribute.class.getName()).withMessageContaining( + "declares an alias for 'bar' which is not present"); + } + + @Test + public void synthesizeWhenAttributeAliasWithoutMirroredAliasFor() throws Exception { + AliasForWithoutMirroredAliasFor annotation = AliasForWithoutMirroredAliasForClass.class.getAnnotation( + AliasForWithoutMirroredAliasFor.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)).withMessageStartingWith( + "Attribute 'bar' in").withMessageContaining( + AliasForWithoutMirroredAliasFor.class.getName()).withMessageContaining( + "@AliasFor 'foo'"); + } + + @Test + public void synthesizeWhenAttributeAliasWithMirroredAliasForWrongAttribute() + throws Exception { + AliasForWithMirroredAliasForWrongAttribute annotation = AliasForWithMirroredAliasForWrongAttributeClass.class.getAnnotation( + AliasForWithMirroredAliasForWrongAttribute.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)).withMessage( + "@AliasFor declaration on attribute 'bar' in annotation [" + + AliasForWithMirroredAliasForWrongAttribute.class.getName() + + "] declares an alias for 'quux' which is not present."); + } + + @Test + public void synthesizeWhenAttributeAliasForAttributeOfDifferentType() + throws Exception { + AliasForAttributeOfDifferentType annotation = AliasForAttributeOfDifferentTypeClass.class.getAnnotation( + AliasForAttributeOfDifferentType.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)).withMessageStartingWith( + "Misconfigured aliases").withMessageContaining( + AliasForAttributeOfDifferentType.class.getName()).withMessageContaining( + "attribute 'foo'").withMessageContaining( + "attribute 'bar'").withMessageContaining( + "same return type"); + } + + @Test + public void synthesizeWhenAttributeAliasForWithMissingDefaultValues() + throws Exception { + AliasForWithMissingDefaultValues annotation = AliasForWithMissingDefaultValuesClass.class.getAnnotation( + AliasForWithMissingDefaultValues.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)).withMessageStartingWith( + "Misconfigured aliases").withMessageContaining( + AliasForWithMissingDefaultValues.class.getName()).withMessageContaining( + "attribute 'foo' in annotation").withMessageContaining( + "attribute 'bar' in annotation").withMessageContaining( + "default values"); + } + + @Test + public void synthesizeWhenAttributeAliasForAttributeWithDifferentDefaultValue() + throws Exception { + AliasForAttributeWithDifferentDefaultValue annotation = AliasForAttributeWithDifferentDefaultValueClass.class.getAnnotation( + AliasForAttributeWithDifferentDefaultValue.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)).withMessageStartingWith( + "Misconfigured aliases").withMessageContaining( + AliasForAttributeWithDifferentDefaultValue.class.getName()).withMessageContaining( + "attribute 'foo' in annotation").withMessageContaining( + "attribute 'bar' in annotation").withMessageContaining( + "same default value"); + } + + @Test + public void synthesizeWhenAttributeAliasForMetaAnnotationThatIsNotMetaPresent() + throws Exception { + AliasedComposedTestConfigurationNotMetaPresent annotation = AliasedComposedTestConfigurationNotMetaPresentClass.class.getAnnotation( + AliasedComposedTestConfigurationNotMetaPresent.class); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(annotation)).withMessageStartingWith( + "@AliasFor declaration on attribute 'xmlConfigFile' in annotation").withMessageContaining( + AliasedComposedTestConfigurationNotMetaPresent.class.getName()).withMessageContaining( + "declares an alias for attribute 'location' in annotation").withMessageContaining( + TestConfiguration.class.getName()).withMessageContaining( + "not meta-present"); + } + + @Test + public void synthesizeWithImplicitAliases() throws Exception { + testSynthesisWithImplicitAliases(ValueImplicitAliasesTestConfigurationClass.class, + "value"); + testSynthesisWithImplicitAliases( + Location1ImplicitAliasesTestConfigurationClass.class, "location1"); + testSynthesisWithImplicitAliases(XmlImplicitAliasesTestConfigurationClass.class, + "xmlFile"); + testSynthesisWithImplicitAliases( + GroovyImplicitAliasesSimpleTestConfigurationClass.class, "groovyScript"); + } + + private void testSynthesisWithImplicitAliases(Class clazz, String expected) + throws Exception { + ImplicitAliasesTestConfiguration config = clazz.getAnnotation( + ImplicitAliasesTestConfiguration.class); + assertThat(config).isNotNull(); + ImplicitAliasesTestConfiguration synthesized = MergedAnnotation.from( + config).synthesize(); + assertThat(synthesized).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesized.value()).isEqualTo(expected); + assertThat(synthesized.location1()).isEqualTo(expected); + assertThat(synthesized.xmlFile()).isEqualTo(expected); + assertThat(synthesized.groovyScript()).isEqualTo(expected); + } + + @Test + public void synthesizeWithImplicitAliasesWithImpliedAliasNamesOmitted() + throws Exception { + testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted( + ValueImplicitAliasesWithImpliedAliasNamesOmittedTestConfigurationClass.class, + "value"); + testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted( + LocationsImplicitAliasesWithImpliedAliasNamesOmittedTestConfigurationClass.class, + "location"); + testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted( + XmlFilesImplicitAliasesWithImpliedAliasNamesOmittedTestConfigurationClass.class, + "xmlFile"); + } + + private void testSynthesisWithImplicitAliasesWithImpliedAliasNamesOmitted( + Class clazz, String expected) { + ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration config = clazz.getAnnotation( + ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration.class); + assertThat(config).isNotNull(); + ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration synthesized = MergedAnnotation.from( + config).synthesize(); + assertThat(synthesized).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesized.value()).isEqualTo(expected); + assertThat(synthesized.location()).isEqualTo(expected); + assertThat(synthesized.xmlFile()).isEqualTo(expected); + } + + @Test + public void synthesizeWithImplicitAliasesForAliasPair() throws Exception { + ImplicitAliasesForAliasPairTestConfiguration config = ImplicitAliasesForAliasPairTestConfigurationClass.class.getAnnotation( + ImplicitAliasesForAliasPairTestConfiguration.class); + ImplicitAliasesForAliasPairTestConfiguration synthesized = MergedAnnotation.from( + config).synthesize(); + assertThat(synthesized).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesized.xmlFile()).isEqualTo("test.xml"); + assertThat(synthesized.groovyScript()).isEqualTo("test.xml"); + } + + @Test + public void synthesizeWithTransitiveImplicitAliases() throws Exception { + TransitiveImplicitAliasesTestConfiguration config = TransitiveImplicitAliasesTestConfigurationClass.class.getAnnotation( + TransitiveImplicitAliasesTestConfiguration.class); + TransitiveImplicitAliasesTestConfiguration synthesized = MergedAnnotation.from( + config).synthesize(); + assertThat(synthesized).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesized.xml()).isEqualTo("test.xml"); + assertThat(synthesized.groovy()).isEqualTo("test.xml"); + } + + @Test + public void synthesizeWithTransitiveImplicitAliasesForAliasPair() throws Exception { + TransitiveImplicitAliasesForAliasPairTestConfiguration config = TransitiveImplicitAliasesForAliasPairTestConfigurationClass.class.getAnnotation( + TransitiveImplicitAliasesForAliasPairTestConfiguration.class); + TransitiveImplicitAliasesForAliasPairTestConfiguration synthesized = MergedAnnotation.from( + config).synthesize(); + assertThat(synthesized).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesized.xml()).isEqualTo("test.xml"); + assertThat(synthesized.groovy()).isEqualTo("test.xml"); + } + + @Test + public void synthesizeWithImplicitAliasesWithMissingDefaultValues() throws Exception { + Class clazz = ImplicitAliasesWithMissingDefaultValuesTestConfigurationClass.class; + Class annotationType = ImplicitAliasesWithMissingDefaultValuesTestConfiguration.class; + ImplicitAliasesWithMissingDefaultValuesTestConfiguration config = clazz.getAnnotation( + annotationType); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(clazz, config)).withMessageStartingWith( + "Misconfigured aliases:").withMessageContaining( + "attribute 'location1' in annotation [" + + annotationType.getName() + + "]").withMessageContaining( + "attribute 'location2' in annotation [" + + annotationType.getName() + + "]").withMessageContaining( + "default values"); + } + + @Test + public void synthesizeWithImplicitAliasesWithDifferentDefaultValues() + throws Exception { + Class clazz = ImplicitAliasesWithDifferentDefaultValuesTestConfigurationClass.class; + Class annotationType = ImplicitAliasesWithDifferentDefaultValuesTestConfiguration.class; + ImplicitAliasesWithDifferentDefaultValuesTestConfiguration config = clazz.getAnnotation( + annotationType); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(clazz, config)).withMessageStartingWith( + "Misconfigured aliases:").withMessageContaining( + "attribute 'location1' in annotation [" + + annotationType.getName() + + "]").withMessageContaining( + "attribute 'location2' in annotation [" + + annotationType.getName() + + "]").withMessageContaining( + "same default value"); + } + + @Test + public void synthesizeWithImplicitAliasesWithDuplicateValues() throws Exception { + Class clazz = ImplicitAliasesWithDuplicateValuesTestConfigurationClass.class; + Class annotationType = ImplicitAliasesWithDuplicateValuesTestConfiguration.class; + ImplicitAliasesWithDuplicateValuesTestConfiguration config = clazz.getAnnotation( + annotationType); + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(clazz, config)).withMessageStartingWith( + "Different @AliasFor mirror values for annotation").withMessageContaining( + annotationType.getName()).withMessageContaining( + "declared on class").withMessageContaining( + clazz.getName()).withMessageContaining( + "are declared with values of"); + } + + @Test + public void synthesizeFromMapWithoutAttributeAliases() throws Exception { + Component component = WebController.class.getAnnotation(Component.class); + assertThat(component).isNotNull(); + Map map = Collections.singletonMap("value", "webController"); + MergedAnnotation annotation = MergedAnnotation.from(Component.class, + map); + Component synthesizedComponent = annotation.synthesize(); + assertThat(synthesizedComponent).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesizedComponent.value()).isEqualTo("webController"); + } + + @Test + @SuppressWarnings("unchecked") + public void synthesizeFromMapWithNestedMap() throws Exception { + ComponentScanSingleFilter componentScan = ComponentScanSingleFilterClass.class.getAnnotation( + ComponentScanSingleFilter.class); + assertThat(componentScan).isNotNull(); + assertThat(componentScan.value().pattern()).isEqualTo("*Foo"); + Map map = MergedAnnotation.from(componentScan).asMap( + annotation -> new LinkedHashMap(), + MapValues.ANNOTATION_TO_MAP); + Map filterMap = (Map) map.get("value"); + assertThat(filterMap.get("pattern")).isEqualTo("*Foo"); + filterMap.put("pattern", "newFoo"); + filterMap.put("enigma", 42); + MergedAnnotation annotation = MergedAnnotation.from( + ComponentScanSingleFilter.class, map); + ComponentScanSingleFilter synthesizedComponentScan = annotation.synthesize(); + assertThat(synthesizedComponentScan).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesizedComponentScan.value().pattern()).isEqualTo("newFoo"); + } + + @Test + @SuppressWarnings("unchecked") + public void synthesizeFromMapWithNestedArrayOfMaps() throws Exception { + ComponentScan componentScan = ComponentScanClass.class.getAnnotation( + ComponentScan.class); + assertThat(componentScan).isNotNull(); + Map map = MergedAnnotation.from(componentScan).asMap( + annotation -> new LinkedHashMap(), + MapValues.ANNOTATION_TO_MAP); + Map[] filters = (Map[]) map.get("excludeFilters"); + List patterns = Arrays.stream(filters).map( + m -> (String) m.get("pattern")).collect(Collectors.toList()); + assertThat(patterns).containsExactly("*Foo", "*Bar"); + filters[0].put("pattern", "newFoo"); + filters[0].put("enigma", 42); + filters[1].put("pattern", "newBar"); + filters[1].put("enigma", 42); + MergedAnnotation annotation = MergedAnnotation.from( + ComponentScan.class, map); + ComponentScan synthesizedComponentScan = annotation.synthesize(); + assertThat(synthesizedComponentScan).isInstanceOf(SynthesizedAnnotation.class); + assertThat(Arrays.stream(synthesizedComponentScan.excludeFilters()).map( + Filter::pattern)).containsExactly("newFoo", "newBar"); + } + + @Test + public void synthesizeFromDefaultsWithoutAttributeAliases() throws Exception { + MergedAnnotation annotation = MergedAnnotation.from( + AnnotationWithDefaults.class); + AnnotationWithDefaults synthesized = annotation.synthesize(); + assertThat(synthesized.text()).isEqualTo("enigma"); + assertThat(synthesized.predicate()).isTrue(); + assertThat(synthesized.characters()).containsExactly('a', 'b', 'c'); + } + + @Test + public void synthesizeFromDefaultsWithAttributeAliases() throws Exception { + MergedAnnotation annotation = MergedAnnotation.from( + TestConfiguration.class); + TestConfiguration synthesized = annotation.synthesize(); + assertThat(synthesized.value()).isEqualTo(""); + assertThat(synthesized.location()).isEqualTo(""); + } + + @Test + public void synthesizeWhenAttributeAliasesWithDifferentValues() throws Exception { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> MergedAnnotation.from(TestConfigurationMismatch.class.getAnnotation( + TestConfiguration.class)).synthesize()); + } + + @Test + public void synthesizeFromMapWithMinimalAttributesWithAttributeAliases() + throws Exception { + Map map = Collections.singletonMap("location", "test.xml"); + MergedAnnotation annotation = MergedAnnotation.from( + TestConfiguration.class, map); + TestConfiguration synthesized = annotation.synthesize(); + assertThat(synthesized.value()).isEqualTo("test.xml"); + assertThat(synthesized.location()).isEqualTo("test.xml"); + } + + @Test + public void synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements() + throws Exception { + synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements( + Collections.singletonMap("value", "/foo")); + synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements( + Collections.singletonMap("path", "/foo")); + } + + private void synthesizeFromMapWithAttributeAliasesThatOverrideArraysWithSingleElements( + Map map) { + MergedAnnotation annotation = MergedAnnotation.from(GetMapping.class, + map); + GetMapping synthesized = annotation.synthesize(); + assertThat(synthesized.value()).isEqualTo("/foo"); + assertThat(synthesized.path()).isEqualTo("/foo"); + } + + @Test + public void synthesizeFromMapWithImplicitAttributeAliases() throws Exception { + testSynthesisFromMapWithImplicitAliases("value"); + testSynthesisFromMapWithImplicitAliases("location1"); + testSynthesisFromMapWithImplicitAliases("location2"); + testSynthesisFromMapWithImplicitAliases("location3"); + testSynthesisFromMapWithImplicitAliases("xmlFile"); + testSynthesisFromMapWithImplicitAliases("groovyScript"); + } + + private void testSynthesisFromMapWithImplicitAliases(String attributeNameAndValue) + throws Exception { + Map map = Collections.singletonMap(attributeNameAndValue, + attributeNameAndValue); + MergedAnnotation annotation = MergedAnnotation.from( + ImplicitAliasesTestConfiguration.class, map); + ImplicitAliasesTestConfiguration synthesized = annotation.synthesize(); + assertThat(synthesized.value()).isEqualTo(attributeNameAndValue); + assertThat(synthesized.location1()).isEqualTo(attributeNameAndValue); + assertThat(synthesized.location2()).isEqualTo(attributeNameAndValue); + assertThat(synthesized.location2()).isEqualTo(attributeNameAndValue); + assertThat(synthesized.xmlFile()).isEqualTo(attributeNameAndValue); + assertThat(synthesized.groovyScript()).isEqualTo(attributeNameAndValue); + } + + @Test + public void synthesizeFromMapWithMissingAttributeValue() throws Exception { + testMissingTextAttribute(Collections.emptyMap()); + } + + @Test + public void synthesizeFromMapWithNullAttributeValue() throws Exception { + Map map = Collections.singletonMap("text", null); + assertThat(map).containsKey("text"); + testMissingTextAttribute(map); + } + + private void testMissingTextAttribute(Map attributes) { + assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() -> { + MergedAnnotation annotation = MergedAnnotation.from( + AnnotationWithoutDefaults.class, attributes); + annotation.synthesize(); + }).withMessage("No value found for attribute named 'text' in merged annotation " + + AnnotationWithoutDefaults.class.getName()); + } + + @Test + public void synthesizeFromMapWithAttributeOfIncorrectType() throws Exception { + Map map = Collections.singletonMap("value", 42L); + MergedAnnotation annotation = MergedAnnotation.from(Component.class, + map); + // annotation.synthesize(); + assertThatIllegalStateException().isThrownBy( + () -> annotation.synthesize()).withMessage( + "Attribute 'value' in annotation org.springframework.stereotype.Component " + + "should be compatible with java.lang.String but a java.lang.Long value was returned"); + } + + @Test + public void synthesizeFromAnnotationAttributesWithoutAttributeAliases() + throws Exception { + Component component = WebController.class.getAnnotation(Component.class); + assertThat(component).isNotNull(); + Map attributes = MergedAnnotation.from(component).asMap(); + Component synthesized = MergedAnnotation.from(Component.class, + attributes).synthesize(); + assertThat(synthesized).isInstanceOf(SynthesizedAnnotation.class); + assertThat(synthesized).isEqualTo(component); + } + + @Test + public void toStringForSynthesizedAnnotations() throws Exception { + Method methodWithPath = WebController.class.getMethod( + "handleMappedWithPathAttribute"); + RequestMapping webMappingWithAliases = methodWithPath.getAnnotation( + RequestMapping.class); + assertThat(webMappingWithAliases).isNotNull(); + Method methodWithPathAndValue = WebController.class.getMethod( + "handleMappedWithSamePathAndValueAttributes"); + RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation( + RequestMapping.class); + assertThat(methodWithPathAndValue).isNotNull(); + RequestMapping synthesizedWebMapping1 = MergedAnnotation.from( + webMappingWithAliases).synthesize(); + RequestMapping synthesizedWebMapping2 = MergedAnnotation.from( + webMappingWithPathAndValue).synthesize(); + assertThat(webMappingWithAliases.toString()).isNotEqualTo( + synthesizedWebMapping1.toString()); + assertToStringForWebMappingWithPathAndValue(synthesizedWebMapping1); + assertToStringForWebMappingWithPathAndValue(synthesizedWebMapping2); + } + + private void assertToStringForWebMappingWithPathAndValue(RequestMapping webMapping) { + String prefix = "@" + RequestMapping.class.getName() + "("; + assertThat(webMapping.toString()).startsWith(prefix).contains("value=[/test]", + "path=[/test]", "name=bar", "method=", "[GET, POST]").endsWith(")"); + } + + @Test + public void equalsForSynthesizedAnnotations() throws Exception { + Method methodWithPath = WebController.class.getMethod( + "handleMappedWithPathAttribute"); + RequestMapping webMappingWithAliases = methodWithPath.getAnnotation( + RequestMapping.class); + assertThat(webMappingWithAliases).isNotNull(); + Method methodWithPathAndValue = WebController.class.getMethod( + "handleMappedWithSamePathAndValueAttributes"); + RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation( + RequestMapping.class); + assertThat(webMappingWithPathAndValue).isNotNull(); + RequestMapping synthesizedWebMapping1 = MergedAnnotation.from( + webMappingWithAliases).synthesize(); + RequestMapping synthesizedWebMapping2 = MergedAnnotation.from( + webMappingWithPathAndValue).synthesize(); + // Equality amongst standard annotations + assertThat(webMappingWithAliases).isEqualTo(webMappingWithAliases); + assertThat(webMappingWithPathAndValue).isEqualTo(webMappingWithPathAndValue); + // Inequality amongst standard annotations + assertThat(webMappingWithAliases).isNotEqualTo(webMappingWithPathAndValue); + assertThat(webMappingWithPathAndValue).isNotEqualTo(webMappingWithAliases); + // Equality amongst synthesized annotations + assertThat(synthesizedWebMapping1).isEqualTo(synthesizedWebMapping1); + assertThat(synthesizedWebMapping2).isEqualTo(synthesizedWebMapping2); + assertThat(synthesizedWebMapping1).isEqualTo(synthesizedWebMapping2); + assertThat(synthesizedWebMapping2).isEqualTo(synthesizedWebMapping1); + // Equality between standard and synthesized annotations + assertThat(synthesizedWebMapping1).isEqualTo(webMappingWithPathAndValue); + assertThat(webMappingWithPathAndValue).isEqualTo(synthesizedWebMapping1); + // Inequality between standard and synthesized annotations + assertThat(synthesizedWebMapping1).isNotEqualTo(webMappingWithAliases); + assertThat(webMappingWithAliases).isNotEqualTo(synthesizedWebMapping1); + } + + @Test + public void hashCodeForSynthesizedAnnotations() throws Exception { + Method methodWithPath = WebController.class.getMethod( + "handleMappedWithPathAttribute"); + RequestMapping webMappingWithAliases = methodWithPath.getAnnotation( + RequestMapping.class); + assertThat(webMappingWithAliases).isNotNull(); + Method methodWithPathAndValue = WebController.class.getMethod( + "handleMappedWithSamePathAndValueAttributes"); + RequestMapping webMappingWithPathAndValue = methodWithPathAndValue.getAnnotation( + RequestMapping.class); + assertThat(webMappingWithPathAndValue).isNotNull(); + RequestMapping synthesizedWebMapping1 = MergedAnnotation.from( + webMappingWithAliases).synthesize(); + assertThat(synthesizedWebMapping1).isNotNull(); + RequestMapping synthesizedWebMapping2 = MergedAnnotation.from( + webMappingWithPathAndValue).synthesize(); + assertThat(synthesizedWebMapping2).isNotNull(); + // Equality amongst standard annotations + assertThat(webMappingWithAliases.hashCode()).isEqualTo( + webMappingWithAliases.hashCode()); + assertThat(webMappingWithPathAndValue.hashCode()).isEqualTo( + webMappingWithPathAndValue.hashCode()); + // Inequality amongst standard annotations + assertThat(webMappingWithAliases.hashCode()).isNotEqualTo( + webMappingWithPathAndValue.hashCode()); + assertThat(webMappingWithPathAndValue.hashCode()).isNotEqualTo( + webMappingWithAliases.hashCode()); + // Equality amongst synthesized annotations + assertThat(synthesizedWebMapping1.hashCode()).isEqualTo( + synthesizedWebMapping1.hashCode()); + assertThat(synthesizedWebMapping2.hashCode()).isEqualTo( + synthesizedWebMapping2.hashCode()); + assertThat(synthesizedWebMapping1.hashCode()).isEqualTo( + synthesizedWebMapping2.hashCode()); + assertThat(synthesizedWebMapping2.hashCode()).isEqualTo( + synthesizedWebMapping1.hashCode()); + // Equality between standard and synthesized annotations + assertThat(synthesizedWebMapping1.hashCode()).isEqualTo( + webMappingWithPathAndValue.hashCode()); + assertThat(webMappingWithPathAndValue.hashCode()).isEqualTo( + synthesizedWebMapping1.hashCode()); + // Inequality between standard and synthesized annotations + assertThat(synthesizedWebMapping1.hashCode()).isNotEqualTo( + webMappingWithAliases.hashCode()); + assertThat(webMappingWithAliases.hashCode()).isNotEqualTo( + synthesizedWebMapping1.hashCode()); + } + + /** + * Fully reflection-based test that verifies support for synthesizing + * annotations across packages with non-public visibility of user types + * (e.g., a non-public annotation that uses {@code @AliasFor}). + */ + @Test + @SuppressWarnings("unchecked") + public void synthesizeNonPublicWithAttributeAliasesFromDifferentPackage() + throws Exception { + Class type = ClassUtils.forName( + "org.springframework.core.annotation.subpackage.NonPublicAliasedAnnotatedClass", + null); + Class annotationType = (Class) ClassUtils.forName( + "org.springframework.core.annotation.subpackage.NonPublicAliasedAnnotation", + null); + Annotation annotation = type.getAnnotation(annotationType); + assertThat(annotation).isNotNull(); + MergedAnnotation mergedAnnotation = MergedAnnotation.from(annotation); + Annotation synthesizedAnnotation = mergedAnnotation.synthesize(); + assertThat(synthesizedAnnotation).isInstanceOf(SynthesizedAnnotation.class); + assertThat(mergedAnnotation.getString("name")).isEqualTo("test"); + assertThat(mergedAnnotation.getString("path")).isEqualTo("/test"); + assertThat(mergedAnnotation.getString("value")).isEqualTo("/test"); + } + + @Test + public void synthesizeWithAttributeAliasesInNestedAnnotations() throws Exception { + Hierarchy hierarchy = HierarchyClass.class.getAnnotation(Hierarchy.class); + assertThat(hierarchy).isNotNull(); + Hierarchy synthesizedHierarchy = MergedAnnotation.from(hierarchy).synthesize(); + assertThat(synthesizedHierarchy).isInstanceOf(SynthesizedAnnotation.class); + TestConfiguration[] configs = synthesizedHierarchy.value(); + assertThat(configs).isNotNull(); + assertThat(configs).allMatch(SynthesizedAnnotation.class::isInstance); + assertThat( + Arrays.stream(configs).map(TestConfiguration::location)).containsExactly( + "A", "B"); + assertThat(Arrays.stream(configs).map(TestConfiguration::value)).containsExactly( + "A", "B"); + } + + @Test + public void synthesizeWithArrayOfAnnotations() throws Exception { + Hierarchy hierarchy = HierarchyClass.class.getAnnotation(Hierarchy.class); + assertThat(hierarchy).isNotNull(); + Hierarchy synthesizedHierarchy = MergedAnnotation.from(hierarchy).synthesize(); + assertThat(synthesizedHierarchy).isInstanceOf(SynthesizedAnnotation.class); + TestConfiguration contextConfig = TestConfigurationClass.class.getAnnotation( + TestConfiguration.class); + assertThat(contextConfig).isNotNull(); + TestConfiguration[] configs = synthesizedHierarchy.value(); + assertThat( + Arrays.stream(configs).map(TestConfiguration::location)).containsExactly( + "A", "B"); + // Alter array returned from synthesized annotation + configs[0] = contextConfig; + // Re-retrieve the array from the synthesized annotation + configs = synthesizedHierarchy.value(); + assertThat( + Arrays.stream(configs).map(TestConfiguration::location)).containsExactly( + "A", "B"); + } + + @Test + public void synthesizeWithArrayOfChars() throws Exception { + CharsContainer charsContainer = GroupOfCharsClass.class.getAnnotation( + CharsContainer.class); + assertThat(charsContainer).isNotNull(); + CharsContainer synthesizedCharsContainer = MergedAnnotation.from( + charsContainer).synthesize(); + assertThat(synthesizedCharsContainer).isInstanceOf(SynthesizedAnnotation.class); + char[] chars = synthesizedCharsContainer.chars(); + assertThat(chars).containsExactly('x', 'y', 'z'); + // Alter array returned from synthesized annotation + chars[0] = '?'; + // Re-retrieve the array from the synthesized annotation + chars = synthesizedCharsContainer.chars(); + assertThat(chars).containsExactly('x', 'y', 'z'); + } + + @Test + public void getValueWhenHasDefaultOverride() { + MergedAnnotation annotation = MergedAnnotations.from( + DefaultOverrideClass.class).get(DefaultOverrideRoot.class); + assertThat(annotation.getString("text")).isEqualTo("metameta"); + } + + // @formatter:off + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Inherited + @interface Transactional { + + String value() default ""; + + String qualifier() default "transactionManager"; + + boolean readOnly() default false; + } + + @Transactional + @Component + @Retention(RetentionPolicy.RUNTIME) + @interface TransactionalComponent { + } + + @TransactionalComponent + @Retention(RetentionPolicy.RUNTIME) + @interface ComposedTransactionalComponent { + } + + static class NonAnnotatedClass { + } + + static interface NonAnnotatedInterface { + } + + @TransactionalComponent + static class TransactionalComponentClass { + } + + static class SubTransactionalComponentClass extends TransactionalComponentClass { + } + + @ComposedTransactionalComponent + static class ComposedTransactionalComponentClass { + } + + @AliasedTransactionalComponent + static class AliasedTransactionalComponentClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Inherited + @interface AliasedTransactional { + + @AliasFor(attribute = "qualifier") + String value() default ""; + + @AliasFor(attribute = "value") + String qualifier() default ""; + } + + @Transactional(qualifier = "composed1") + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Inherited + @interface InheritedComposed { + } + + @Transactional(qualifier = "composed2", readOnly = true) + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @interface Composed { + } + + @Transactional + @Retention(RetentionPolicy.RUNTIME) + @interface TxComposedWithOverride { + + String qualifier() default "txMgr"; + } + + @Transactional("TxInheritedComposed") + @Retention(RetentionPolicy.RUNTIME) + @interface TxInheritedComposed { + } + + @Transactional("TxComposed") + @Retention(RetentionPolicy.RUNTIME) + @interface TxComposed { + } + + @AliasedTransactional(value = "aliasForQualifier") + @Component + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedTransactionalComponent { + } + + @TxComposedWithOverride + // Override default "txMgr" from @TxComposedWithOverride with "localTxMgr" + @Transactional(qualifier = "localTxMgr") + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @interface MetaAndLocalTxConfig { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface TestPropertySource { + + @AliasFor("locations") + String[] value() default {}; + + @AliasFor("value") + String[] locations() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ContextConfiguration { + + @AliasFor(attribute = "locations") + String[] value() default {}; + + @AliasFor(attribute = "value") + String[] locations() default {}; + + Class[] classes() default {}; + } + + @ContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface ConventionBasedComposedContextConfiguration { + + String[] locations() default {}; + } + + @ContextConfiguration(value = "duplicateDeclaration") + @Retention(RetentionPolicy.RUNTIME) + @interface InvalidConventionBasedComposedContextConfiguration { + + String[] locations(); + } + + /** + * This hybrid approach for annotation attribute overrides with transitive implicit + * aliases is unsupported. See SPR-13554 for details. + */ + @ContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface HalfConventionBasedAndHalfAliasedComposedContextConfiguration { + + String[] locations() default {}; + + @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") + String[] xmlConfigFiles() default {}; + } + + @ContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedComposedContextConfiguration { + + @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") + String[] xmlConfigFiles(); + } + + @ContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedValueComposedContextConfiguration { + + @AliasFor(annotation = ContextConfiguration.class, attribute = "value") + String[] locations(); + } + + @ContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesContextConfiguration { + + @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") + String[] groovyScripts() default {}; + + @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") + String[] xmlFiles() default {}; + + // intentionally omitted: attribute = "locations" + @AliasFor(annotation = ContextConfiguration.class) + String[] locations() default {}; + + // intentionally omitted: attribute = "locations" (SPR-14069) + @AliasFor(annotation = ContextConfiguration.class) + String[] value() default {}; + } + + @ImplicitAliasesContextConfiguration(xmlFiles = { "A.xml", "B.xml" }) + @Retention(RetentionPolicy.RUNTIME) + @interface ComposedImplicitAliasesContextConfiguration { + } + + @ImplicitAliasesContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesContextConfiguration { + + @AliasFor(annotation = ImplicitAliasesContextConfiguration.class, attribute = "xmlFiles") + String[] xml() default {}; + + @AliasFor(annotation = ImplicitAliasesContextConfiguration.class, attribute = "groovyScripts") + String[] groovy() default {}; + } + + @ImplicitAliasesContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface SingleLocationTransitiveImplicitAliasesContextConfiguration { + + @AliasFor(annotation = ImplicitAliasesContextConfiguration.class, attribute = "xmlFiles") + String xml() default ""; + + @AliasFor(annotation = ImplicitAliasesContextConfiguration.class, attribute = "groovyScripts") + String groovy() default ""; + } + + @ImplicitAliasesContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesWithSkippedLevelContextConfiguration { + + @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") + String[] xml() default {}; + + @AliasFor(annotation = ImplicitAliasesContextConfiguration.class, attribute = "groovyScripts") + String[] groovy() default {}; + } + + @ImplicitAliasesContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfiguration { + + @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") + String xml() default ""; + + @AliasFor(annotation = ImplicitAliasesContextConfiguration.class, attribute = "groovyScripts") + String groovy() default ""; + } + + /** + * Although the configuration declares an explicit value for 'value' and requires a + * value for the aliased 'locations', this does not result in an error since + * 'locations' effectively shadows the 'value' attribute (which cannot be set via the + * composed annotation anyway). If 'value' were not shadowed, such a declaration would + * not make sense. + */ + @ContextConfiguration(value = "duplicateDeclaration") + @Retention(RetentionPolicy.RUNTIME) + @interface ShadowedAliasComposedContextConfiguration { + + @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") + String[] xmlConfigFiles(); + } + + @ContextConfiguration(locations = "shadowed.xml") + @TestPropertySource(locations = "test.properties") + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedComposedContextConfigurationAndTestPropertySource { + + @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") + String[] xmlConfigFiles() default "default.xml"; + } + + @ContextConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface SpringApplicationConfiguration { + + @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") + String[] locations() default {}; + + @AliasFor("value") + Class[] classes() default {}; + + @AliasFor("classes") + Class[] value() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ComponentScan { + + @AliasFor("basePackages") + String[] value() default {}; + + @AliasFor("value") + String[] basePackages() default {}; + + Filter[] excludeFilters() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({}) + @interface Filter { + + String pattern(); + } + + @ComponentScan(excludeFilters = { @Filter(pattern = "*Test"), + @Filter(pattern = "*Tests") }) + @Retention(RetentionPolicy.RUNTIME) + @interface TestComponentScan { + + @AliasFor(attribute = "basePackages", annotation = ComponentScan.class) + String[] packages(); + } + + @ComponentScan + @Retention(RetentionPolicy.RUNTIME) + @interface ConventionBasedSinglePackageComponentScan { + + String basePackages(); + } + + @ComponentScan + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForBasedSinglePackageComponentScan { + + @AliasFor(attribute = "basePackages", annotation = ComponentScan.class) + String pkg(); + } + + @Transactional + static class ClassWithInheritedAnnotation { + } + + @Composed + static class SubClassWithInheritedAnnotation extends ClassWithInheritedAnnotation { + } + + static class SubSubClassWithInheritedAnnotation + extends SubClassWithInheritedAnnotation { + } + + @InheritedComposed + static class ClassWithInheritedComposedAnnotation { + } + + @Composed + static class SubClassWithInheritedComposedAnnotation + extends ClassWithInheritedComposedAnnotation { + } + + static class SubSubClassWithInheritedComposedAnnotation + extends SubClassWithInheritedComposedAnnotation { + } + + @MetaAndLocalTxConfig + static class MetaAndLocalTxConfigClass { + } + + @Transactional("TxConfig") + static class TxConfig { + } + + @Transactional("DerivedTxConfig") + static class DerivedTxConfig extends TxConfig { + } + + @TxInheritedComposed + @TxComposed + static class TxFromMultipleComposedAnnotations { + } + + @Transactional + static interface InterfaceWithInheritedAnnotation { + + @Order + void handleFromInterface(); + } + + static abstract class AbstractClassWithInheritedAnnotation + implements InterfaceWithInheritedAnnotation { + + @Transactional + public abstract void handle(); + + @Transactional + public void handleParameterized(T t) { + } + } + + static class ConcreteClassWithInheritedAnnotation + extends AbstractClassWithInheritedAnnotation { + + @Override + public void handle() { + } + + @Override + public void handleParameterized(String s) { + } + + @Override + public void handleFromInterface() { + } + } + + public interface GenericParameter { + + T getFor(Class cls); + } + + @SuppressWarnings("unused") + private static class StringGenericParameter implements GenericParameter { + + @Order + @Override + public String getFor(Class cls) { + return "foo"; + } + + public String getFor(Integer integer) { + return "foo"; + } + } + + @Transactional + public interface InheritedAnnotationInterface { + } + + public interface SubInheritedAnnotationInterface + extends InheritedAnnotationInterface { + } + + public interface SubSubInheritedAnnotationInterface + extends SubInheritedAnnotationInterface { + } + + @Order + public interface NonInheritedAnnotationInterface { + } + + public interface SubNonInheritedAnnotationInterface + extends NonInheritedAnnotationInterface { + } + + public interface SubSubNonInheritedAnnotationInterface + extends SubNonInheritedAnnotationInterface { + } + + @ConventionBasedComposedContextConfiguration(locations = "explicitDeclaration") + static class ConventionBasedComposedContextConfigurationClass { + } + + @InvalidConventionBasedComposedContextConfiguration(locations = "requiredLocationsDeclaration") + static class InvalidConventionBasedComposedContextConfigurationClass { + } + + @HalfConventionBasedAndHalfAliasedComposedContextConfiguration(xmlConfigFiles = "explicitDeclaration") + static class HalfConventionBasedAndHalfAliasedComposedContextConfigurationClass1 { + } + + @HalfConventionBasedAndHalfAliasedComposedContextConfiguration(locations = "explicitDeclaration") + static class HalfConventionBasedAndHalfAliasedComposedContextConfigurationClass2 { + } + + @AliasedComposedContextConfiguration(xmlConfigFiles = "test.xml") + static class AliasedComposedContextConfigurationClass { + } + + @AliasedValueComposedContextConfiguration(locations = "test.xml") + static class AliasedValueComposedContextConfigurationClass { + } + + @ImplicitAliasesContextConfiguration("foo.xml") + static class ImplicitAliasesContextConfigurationClass1 { + } + + @ImplicitAliasesContextConfiguration(locations = "bar.xml") + static class ImplicitAliasesContextConfigurationClass2 { + } + + @ImplicitAliasesContextConfiguration(xmlFiles = "baz.xml") + static class ImplicitAliasesContextConfigurationClass3 { + } + + @TransitiveImplicitAliasesContextConfiguration(groovy = "test.groovy") + static class TransitiveImplicitAliasesContextConfigurationClass { + } + + @SingleLocationTransitiveImplicitAliasesContextConfiguration(groovy = "test.groovy") + static class SingleLocationTransitiveImplicitAliasesContextConfigurationClass { + } + + @TransitiveImplicitAliasesWithSkippedLevelContextConfiguration(xml = "test.xml") + static class TransitiveImplicitAliasesWithSkippedLevelContextConfigurationClass { + } + + @SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfiguration(xml = "test.xml") + static class SingleLocationTransitiveImplicitAliasesWithSkippedLevelContextConfigurationClass { + } + + @ComposedImplicitAliasesContextConfiguration + static class ComposedImplicitAliasesContextConfigurationClass { + } + + @ShadowedAliasComposedContextConfiguration(xmlConfigFiles = "test.xml") + static class ShadowedAliasComposedContextConfigurationClass { + } + + @AliasedComposedContextConfigurationAndTestPropertySource(xmlConfigFiles = "test.xml") + static class AliasedComposedContextConfigurationAndTestPropertySourceClass { + } + + @ComponentScan(value = "com.example.app.test", basePackages = "com.example.app.test") + static class ComponentScanWithBasePackagesAndValueAliasClass { + } + + @TestComponentScan(packages = "com.example.app.test") + static class TestComponentScanClass { + } + + @ConventionBasedSinglePackageComponentScan(basePackages = "com.example.app.test") + static class ConventionBasedSinglePackageComponentScanClass { + } + + @AliasForBasedSinglePackageComponentScan(pkg = "com.example.app.test") + static class AliasForBasedSinglePackageComponentScanClass { + } + + @SpringApplicationConfiguration(Number.class) + static class SpringApplicationConfigurationClass { + } + + @Resource(name = "x") + static class ResourceHolder { + } + + interface TransactionalService { + + @Transactional + void doIt(); + } + + class TransactionalServiceImpl implements TransactionalService { + + @Override + public void doIt() { + } + } + + @Component("meta1") + @Order + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface Meta1 { + } + + @Component("meta2") + @Transactional(readOnly = true) + @Retention(RetentionPolicy.RUNTIME) + @interface Meta2 { + } + + @Meta2 + @Retention(RetentionPolicy.RUNTIME) + @interface MetaMeta { + } + + @MetaMeta + @Retention(RetentionPolicy.RUNTIME) + @interface MetaMetaMeta { + } + + @MetaCycle3 + @Retention(RetentionPolicy.RUNTIME) + @interface MetaCycle1 { + } + + @MetaCycle1 + @Retention(RetentionPolicy.RUNTIME) + @interface MetaCycle2 { + } + + @MetaCycle2 + @Retention(RetentionPolicy.RUNTIME) + @interface MetaCycle3 { + } + + @Meta1 + interface InterfaceWithMetaAnnotation { + } + + @Meta2 + static class ClassWithLocalMetaAnnotationAndMetaAnnotatedInterface + implements InterfaceWithMetaAnnotation { + } + + @Meta1 + static class ClassWithInheritedMetaAnnotation { + } + + @Meta2 + static class SubClassWithInheritedMetaAnnotation + extends ClassWithInheritedMetaAnnotation { + } + + static class SubSubClassWithInheritedMetaAnnotation + extends SubClassWithInheritedMetaAnnotation { + } + + @MetaMeta + static class MetaMetaAnnotatedClass { + } + + @MetaMetaMeta + static class MetaMetaMetaAnnotatedClass { + } + + @MetaCycle3 + static class MetaCycleAnnotatedClass { + } + + interface AnnotatedInterface { + + @Order(0) + void fromInterfaceImplementedByRoot(); + } + + interface NullableAnnotatedInterface { + + @Nullable + void fromInterfaceImplementedByRoot(); + } + + static class Root implements AnnotatedInterface { + + @Order(27) + public void annotatedOnRoot() { + } + + @Meta1 + public void metaAnnotatedOnRoot() { + } + + public void overrideToAnnotate() { + } + + @Order(27) + public void overrideWithoutNewAnnotation() { + } + + public void notAnnotated() { + } + + @Override + public void fromInterfaceImplementedByRoot() { + } + } + + public static class Leaf extends Root { + + @Order(25) + public void annotatedOnLeaf() { + } + + @Meta1 + public void metaAnnotatedOnLeaf() { + } + + @MetaMeta + public void metaMetaAnnotatedOnLeaf() { + } + + @Override + @Order(1) + public void overrideToAnnotate() { + } + + @Override + public void overrideWithoutNewAnnotation() { + } + } + + public static abstract class SimpleGeneric { + + @Order(1) + public abstract void something(T arg); + + } + + public static class TransactionalStringGeneric extends SimpleGeneric { + + @Override + @Transactional + public void something(final String arg) { + } + } + + @Transactional + public static class InheritedAnnotationClass { + } + + public static class SubInheritedAnnotationClass extends InheritedAnnotationClass { + } + + @Order + public static class NonInheritedAnnotationClass { + } + + public static class SubNonInheritedAnnotationClass + extends NonInheritedAnnotationClass { + } + + @Transactional + public static class TransactionalClass { + } + + @Order + public static class TransactionalAndOrderedClass extends TransactionalClass { + } + + public static class SubTransactionalAndOrderedClass + extends TransactionalAndOrderedClass { + } + + public interface InterfaceWithAnnotatedMethod { + + @Order + void foo(); + } + + public static class ImplementsInterfaceWithAnnotatedMethod + implements InterfaceWithAnnotatedMethod { + + @Override + public void foo() { + } + } + + public static class SubOfImplementsInterfaceWithAnnotatedMethod + extends ImplementsInterfaceWithAnnotatedMethod { + + @Override + public void foo() { + } + } + + public abstract static class AbstractDoesNotImplementInterfaceWithAnnotatedMethod + implements InterfaceWithAnnotatedMethod { + } + + public static class SubOfAbstractImplementsInterfaceWithAnnotatedMethod + extends AbstractDoesNotImplementInterfaceWithAnnotatedMethod { + + @Override + public void foo() { + } + } + + public interface InterfaceWithGenericAnnotatedMethod { + + @Order + void foo(T t); + } + + public static class ImplementsInterfaceWithGenericAnnotatedMethod + implements InterfaceWithGenericAnnotatedMethod { + + public void foo(String t) { + } + } + + public static abstract class BaseClassWithGenericAnnotatedMethod { + + @Order + abstract void foo(T t); + } + + public static class ExtendsBaseClassWithGenericAnnotatedMethod + extends BaseClassWithGenericAnnotatedMethod { + + public void foo(String t) { + } + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @interface MyRepeatableContainer { + + MyRepeatable[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @Repeatable(MyRepeatableContainer.class) + @interface MyRepeatable { + + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @MyRepeatable("meta1") + @interface MyRepeatableMeta1 { + } + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @MyRepeatable("meta2") + @interface MyRepeatableMeta2 { + } + + interface InterfaceWithRepeated { + + @MyRepeatable("A") + @MyRepeatableContainer({ @MyRepeatable("B"), @MyRepeatable("C") }) + @MyRepeatableMeta1 + void foo(); + } + + @MyRepeatable("A") + @MyRepeatableContainer({ @MyRepeatable("B"), @MyRepeatable("C") }) + @MyRepeatableMeta1 + static class MyRepeatableClass { + } + + static class SubMyRepeatableClass extends MyRepeatableClass { + } + + @MyRepeatable("X") + @MyRepeatableContainer({ @MyRepeatable("Y"), @MyRepeatable("Z") }) + @MyRepeatableMeta2 + static class SubMyRepeatableWithAdditionalLocalDeclarationsClass + extends MyRepeatableClass { + } + + static class SubSubMyRepeatableWithAdditionalLocalDeclarationsClass + extends SubMyRepeatableWithAdditionalLocalDeclarationsClass { + } + + enum RequestMethod { + GET, POST + } + + @Retention(RetentionPolicy.RUNTIME) + @interface RequestMapping { + + String name(); + + @AliasFor("path") + String[] value() default ""; + + @AliasFor(attribute = "value") + String[] path() default ""; + + RequestMethod[] method() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @RequestMapping(method = RequestMethod.GET, name = "") + @interface GetMapping { + + @AliasFor(annotation = RequestMapping.class) + String value() default ""; + + @AliasFor(annotation = RequestMapping.class) + String path() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @RequestMapping(method = RequestMethod.POST, name = "") + @interface PostMapping { + + String path() default ""; + } + + @Component("webController") + static class WebController { + + @RequestMapping(value = "/test", name = "foo") + public void handleMappedWithValueAttribute() { + } + + @RequestMapping(path = "/test", name = "bar", method = { RequestMethod.GET, + RequestMethod.POST }) + public void handleMappedWithPathAttribute() { + } + + @GetMapping("/test") + public void getMappedWithValueAttribute() { + } + + @GetMapping(path = "/test") + public void getMappedWithPathAttribute() { + } + + @PostMapping(path = "/test") + public void postMappedWithPathAttribute() { + } + + @RequestMapping(value = "/test", path = "/test", name = "bar", method = { + RequestMethod.GET, RequestMethod.POST }) + public void handleMappedWithSamePathAndValueAttributes() { + } + + @RequestMapping(value = "/enigma", path = "/test", name = "baz") + public void handleMappedWithDifferentPathAndValueAttributes() { + } + } + + @Retention(RetentionPolicy.RUNTIME) + @interface BrokenContextConfiguration { + + String value() default ""; + + @AliasFor("value") + String location() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface TestConfiguration { + + @AliasFor("location") + String value() default ""; + + @AliasFor("value") + String location() default ""; + + Class configClass() default Object.class; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Hierarchy { + + TestConfiguration[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface BrokenHierarchy { + + BrokenContextConfiguration[] value(); + } + + @Hierarchy({ @TestConfiguration("A"), @TestConfiguration(location = "B") }) + static class HierarchyClass { + } + + @BrokenHierarchy(@BrokenContextConfiguration) + static class BrokenHierarchyClass { + } + + @TestConfiguration("simple.xml") + static class TestConfigurationClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface CharsContainer { + + @AliasFor(attribute = "chars") + char[] value() default {}; + + @AliasFor(attribute = "value") + char[] chars() default {}; + } + + @CharsContainer(chars = { 'x', 'y', 'z' }) + static class GroupOfCharsClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithMissingAttributeDeclaration { + + @AliasFor + String foo() default ""; + } + + @AliasForWithMissingAttributeDeclaration + static class AliasForWithMissingAttributeDeclarationClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithDuplicateAttributeDeclaration { + + @AliasFor(value = "bar", attribute = "baz") + String foo() default ""; + } + + @AliasForWithDuplicateAttributeDeclaration + static class AliasForWithDuplicateAttributeDeclarationClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForNonexistentAttribute { + + @AliasFor("bar") + String foo() default ""; + } + + @AliasForNonexistentAttribute + static class AliasForNonexistentAttributeClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithoutMirroredAliasFor { + + @AliasFor("bar") + String foo() default ""; + + String bar() default ""; + } + + @AliasForWithoutMirroredAliasFor + static class AliasForWithoutMirroredAliasForClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithMirroredAliasForWrongAttribute { + + @AliasFor(attribute = "bar") + String[] foo() default ""; + + @AliasFor(attribute = "quux") + String[] bar() default ""; + } + + @AliasForWithMirroredAliasForWrongAttribute + static class AliasForWithMirroredAliasForWrongAttributeClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForAttributeOfDifferentType { + + @AliasFor("bar") + String[] foo() default ""; + + @AliasFor("foo") + boolean bar() default true; + } + + @AliasForAttributeOfDifferentType + static class AliasForAttributeOfDifferentTypeClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForWithMissingDefaultValues { + + @AliasFor(attribute = "bar") + String foo(); + + @AliasFor(attribute = "foo") + String bar(); + } + + @AliasForWithMissingDefaultValues(foo = "foo", bar = "bar") + static class AliasForWithMissingDefaultValuesClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasForAttributeWithDifferentDefaultValue { + + @AliasFor("bar") + String foo() default "X"; + + @AliasFor("foo") + String bar() default "Z"; + } + + @AliasForAttributeWithDifferentDefaultValue + static class AliasForAttributeWithDifferentDefaultValueClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedComposedTestConfigurationNotMetaPresent { + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String xmlConfigFile(); + } + + @AliasedComposedTestConfigurationNotMetaPresent(xmlConfigFile = "test.xml") + static class AliasedComposedTestConfigurationNotMetaPresentClass { + } + + @TestConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface AliasedComposedTestConfiguration { + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String xmlConfigFile(); + } + + @TestConfiguration + @Retention(RetentionPolicy.RUNTIME) + public @interface ImplicitAliasesTestConfiguration { + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String xmlFile() default ""; + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String groovyScript() default ""; + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String value() default ""; + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String location1() default ""; + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String location2() default ""; + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String location3() default ""; + + @AliasFor(annotation = TestConfiguration.class, attribute = "configClass") + Class configClass() default Object.class; + + String nonAliasedAttribute() default ""; + } + + @ImplicitAliasesTestConfiguration(groovyScript = "groovyScript") + static class GroovyImplicitAliasesSimpleTestConfigurationClass { + } + + @ImplicitAliasesTestConfiguration(xmlFile = "xmlFile") + static class XmlImplicitAliasesTestConfigurationClass { + } + + @ImplicitAliasesTestConfiguration("value") + static class ValueImplicitAliasesTestConfigurationClass { + } + + @ImplicitAliasesTestConfiguration(location1 = "location1") + static class Location1ImplicitAliasesTestConfigurationClass { + } + + @ImplicitAliasesTestConfiguration(location2 = "location2") + static class Location2ImplicitAliasesTestConfigurationClass { + } + + @ImplicitAliasesTestConfiguration(location3 = "location3") + static class Location3ImplicitAliasesTestConfigurationClass { + } + + @TestConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration { + + @AliasFor(annotation = TestConfiguration.class) + String value() default ""; + + @AliasFor(annotation = TestConfiguration.class) + String location() default ""; + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String xmlFile() default ""; + } + + @ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration { + + @AliasFor(annotation = ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration.class, attribute = "xmlFile") + String xml() default ""; + + @AliasFor(annotation = ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration.class, attribute = "location") + String groovy() default ""; + } + + @ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration("value") + static class ValueImplicitAliasesWithImpliedAliasNamesOmittedTestConfigurationClass { + } + + @ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration(location = "location") + static class LocationsImplicitAliasesWithImpliedAliasNamesOmittedTestConfigurationClass { + } + + @ImplicitAliasesWithImpliedAliasNamesOmittedTestConfiguration(xmlFile = "xmlFile") + static class XmlFilesImplicitAliasesWithImpliedAliasNamesOmittedTestConfigurationClass { + } + + @TestConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesWithMissingDefaultValuesTestConfiguration { + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String location1(); + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String location2(); + } + + @ImplicitAliasesWithMissingDefaultValuesTestConfiguration(location1 = "1", location2 = "2") + static class ImplicitAliasesWithMissingDefaultValuesTestConfigurationClass { + } + + @TestConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesWithDifferentDefaultValuesTestConfiguration { + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String location1() default "foo"; + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String location2() default "bar"; + } + + @ImplicitAliasesWithDifferentDefaultValuesTestConfiguration(location1 = "1", location2 = "2") + static class ImplicitAliasesWithDifferentDefaultValuesTestConfigurationClass { + } + + @TestConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesWithDuplicateValuesTestConfiguration { + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String location1() default ""; + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String location2() default ""; + } + + @ImplicitAliasesWithDuplicateValuesTestConfiguration(location1 = "1", location2 = "2") + static class ImplicitAliasesWithDuplicateValuesTestConfigurationClass { + } + + @TestConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface ImplicitAliasesForAliasPairTestConfiguration { + + @AliasFor(annotation = TestConfiguration.class, attribute = "location") + String xmlFile() default ""; + + @AliasFor(annotation = TestConfiguration.class, value = "value") + String groovyScript() default ""; + } + + @ImplicitAliasesForAliasPairTestConfiguration(xmlFile = "test.xml") + static class ImplicitAliasesForAliasPairTestConfigurationClass { + } + + @ImplicitAliasesTestConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesTestConfiguration { + + @AliasFor(annotation = ImplicitAliasesTestConfiguration.class, attribute = "xmlFile") + String xml() default ""; + + @AliasFor(annotation = ImplicitAliasesTestConfiguration.class, attribute = "groovyScript") + String groovy() default ""; + } + + @TransitiveImplicitAliasesTestConfiguration(xml = "test.xml") + static class TransitiveImplicitAliasesTestConfigurationClass { + } + + @ImplicitAliasesForAliasPairTestConfiguration + @Retention(RetentionPolicy.RUNTIME) + @interface TransitiveImplicitAliasesForAliasPairTestConfiguration { + + @AliasFor(annotation = ImplicitAliasesForAliasPairTestConfiguration.class, attribute = "xmlFile") + String xml() default ""; + + @AliasFor(annotation = ImplicitAliasesForAliasPairTestConfiguration.class, attribute = "groovyScript") + String groovy() default ""; + } + + @TransitiveImplicitAliasesForAliasPairTestConfiguration(xml = "test.xml") + static class TransitiveImplicitAliasesForAliasPairTestConfigurationClass { + } + + @ComponentScan(excludeFilters = { @Filter(pattern = "*Foo"), + @Filter(pattern = "*Bar") }) + static class ComponentScanClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface ComponentScanSingleFilter { + + Filter value(); + } + + @ComponentScanSingleFilter(@Filter(pattern = "*Foo")) + static class ComponentScanSingleFilterClass { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AnnotationWithDefaults { + + String text() default "enigma"; + + boolean predicate() default true; + + char[] characters() default { 'a', 'b', 'c' }; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface AnnotationWithoutDefaults { + + String text(); + } + + @TestConfiguration(value = "foo", location = "bar") + interface TestConfigurationMismatch { + } + + @Retention(RetentionPolicy.RUNTIME) + @interface DefaultOverrideRoot { + + String text() default "root"; + + } + + @Retention(RetentionPolicy.RUNTIME) + @DefaultOverrideRoot + @interface DefaultOverrideMeta { + + } + + @Retention(RetentionPolicy.RUNTIME) + @DefaultOverrideMeta + @interface DefaultOverrideMetaMeta { + + String text() default "metameta"; + + } + + @Retention(RetentionPolicy.RUNTIME) + @DefaultOverrideMetaMeta + @interface DefaultOverrideMetaMetaMeta { + + } + + @DefaultOverrideMetaMetaMeta + static class DefaultOverrideClass { + + } + + // @formatter:on + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/PackagesAnnotationFilterTests.java b/spring-core/src/test/java/org/springframework/core/annotation/PackagesAnnotationFilterTests.java new file mode 100644 index 000000000000..052a85e45828 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/PackagesAnnotationFilterTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link PackagesAnnotationFilter}. + * + * @author Phillip Webb + */ +public class PackagesAnnotationFilterTests { + + @Test + public void createWhenPackagesIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> new PackagesAnnotationFilter((String[]) null)).withMessage( + "Packages must not be null"); + } + + @Test + public void createWhenPackagesContainsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> new PackagesAnnotationFilter((String) null)).withMessage( + "Package must not have empty elements"); + } + + @Test + public void createWhenPackagesContainsEmptyTextThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> new PackagesAnnotationFilter("")).withMessage( + "Package must not have empty elements"); + } + + @Test + public void matchesWhenInPackageReturnsTrue() { + PackagesAnnotationFilter filter = new PackagesAnnotationFilter("com.example"); + assertThat(filter.matches("com.example.Component")).isTrue(); + } + + @Test + public void matchesWhenNotInPackageReturnsFalse() { + PackagesAnnotationFilter filter = new PackagesAnnotationFilter("com.example"); + assertThat(filter.matches("org.springframework.sterotype.Component")).isFalse(); + } + + @Test + public void matchesWhenInSimilarPackageReturnsFalse() { + PackagesAnnotationFilter filter = new PackagesAnnotationFilter("com.example"); + assertThat(filter.matches("com.examples.Component")).isFalse(); + } + + @Test + public void equalsAndHashCode() { + PackagesAnnotationFilter filter1 = new PackagesAnnotationFilter("com.example", + "org.springframework"); + PackagesAnnotationFilter filter2 = new PackagesAnnotationFilter( + "org.springframework", "com.example"); + PackagesAnnotationFilter filter3 = new PackagesAnnotationFilter("com.examples"); + assertThat(filter1.hashCode()).isEqualTo(filter2.hashCode()); + assertThat(filter1).isEqualTo(filter1).isEqualTo(filter2).isNotEqualTo(filter3); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/RepeatableContainersTests.java b/spring-core/src/test/java/org/springframework/core/annotation/RepeatableContainersTests.java new file mode 100644 index 000000000000..ba2fdcf6fb1a --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/RepeatableContainersTests.java @@ -0,0 +1,283 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.*; + +/** + * Tests for {@link RepeatableContainers}. + * + * @author Phillip Webb + */ +public class RepeatableContainersTests { + + @Test + public void standardRepeatablesWhenNonRepeatableReturnsNull() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.standardRepeatables(), WithNonRepeatable.class, + NonRepeatable.class); + assertThat(values).isNull(); + } + + @Test + public void standardRepeatablesWhenSingleReturnsNull() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.standardRepeatables(), + WithSingleStandardRepeatable.class, StandardRepeatable.class); + assertThat(values).isNull(); + } + + @Test + public void standardRepeatablesWhenContainerReturnsRepeats() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.standardRepeatables(), WithStandardRepeatables.class, + StandardContainer.class); + assertThat(values).containsExactly("a", "b"); + } + + @Test + public void standardRepeatablesWhenContainerButNotRepeatableReturnsNull() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.standardRepeatables(), WithExplicitRepeatables.class, + ExplicitContainer.class); + assertThat(values).isNull(); + } + + @Test + public void ofExplicitWhenNonRepeatableReturnsNull() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.of(ExplicitRepeatable.class, + ExplicitContainer.class), + WithNonRepeatable.class, NonRepeatable.class); + assertThat(values).isNull(); + } + + @Test + public void ofExplicitWhenStandardRepeatableContainerReturnsNull() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.of(ExplicitRepeatable.class, + ExplicitContainer.class), + WithStandardRepeatables.class, StandardContainer.class); + assertThat(values).isNull(); + } + + @Test + public void ofExplicitWhenContainerReturnsRepeats() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.of(ExplicitRepeatable.class, + ExplicitContainer.class), + WithExplicitRepeatables.class, ExplicitContainer.class); + assertThat(values).containsExactly("a", "b"); + } + + @Test + public void ofExplicitWhenHasNoValueThrowsException() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> RepeatableContainers.of(ExplicitRepeatable.class, + InvalidNoValue.class)).withMessageContaining( + "Invalid declaration of container type [" + + InvalidNoValue.class.getName() + + "] for repeatable annotation [" + + ExplicitRepeatable.class.getName() + "]"); + } + + @Test + public void ofExplicitWhenValueIsNotArrayThrowsException() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> RepeatableContainers.of(ExplicitRepeatable.class, + InvalidNotArray.class)).withMessage("Container type [" + + InvalidNotArray.class.getName() + + "] must declare a 'value' attribute for an array of type [" + + ExplicitRepeatable.class.getName() + "]"); + } + + @Test + public void ofExplicitWhenValueIsArrayOfWrongTypeThrowsException() { + assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( + () -> RepeatableContainers.of(ExplicitRepeatable.class, + InvalidWrongArrayType.class)).withMessage("Container type [" + + InvalidWrongArrayType.class.getName() + + "] must declare a 'value' attribute for an array of type [" + + ExplicitRepeatable.class.getName() + "]"); + } + + @Test + public void ofExplicitWhenAnnotationIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> RepeatableContainers.of(null, null)).withMessage( + "Repeatable must not be null"); + } + + @Test + public void ofExplicitWhenContainerIsNullDeducesContainer() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.of(StandardRepeatable.class, null), + WithStandardRepeatables.class, StandardContainer.class); + assertThat(values).containsExactly("a", "b"); + } + + @Test + public void ofExplicitWhenContainerIsNullAndNotRepeatableThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> RepeatableContainers.of( + ExplicitRepeatable.class, null)).withMessage( + "Annotation type must be a repeatable annotation: " + + "failed to resolve container type for " + + ExplicitRepeatable.class.getName()); + } + + @Test + public void standardAndExplicitReturnsRepeats() { + RepeatableContainers repeatableContainers = RepeatableContainers.standardRepeatables().and( + ExplicitContainer.class, ExplicitRepeatable.class); + assertThat(findRepeatedAnnotationValues(repeatableContainers, + WithStandardRepeatables.class, StandardContainer.class)).containsExactly( + "a", "b"); + assertThat(findRepeatedAnnotationValues(repeatableContainers, + WithExplicitRepeatables.class, ExplicitContainer.class)).containsExactly( + "a", "b"); + } + + @Test + public void noneAlwaysReturnsNull() { + Object[] values = findRepeatedAnnotationValues( + RepeatableContainers.none(), WithStandardRepeatables.class, + StandardContainer.class); + assertThat(values).isNull(); + } + + @Test + public void equalsAndHashcode() { + RepeatableContainers c1 = RepeatableContainers.of(ExplicitRepeatable.class, + ExplicitContainer.class); + RepeatableContainers c2 = RepeatableContainers.of(ExplicitRepeatable.class, + ExplicitContainer.class); + RepeatableContainers c3 = RepeatableContainers.standardRepeatables(); + RepeatableContainers c4 = RepeatableContainers.standardRepeatables().and( + ExplicitContainer.class, ExplicitRepeatable.class); + assertThat(c1.hashCode()).isEqualTo(c2.hashCode()); + assertThat(c1).isEqualTo(c1).isEqualTo(c2); + assertThat(c1).isNotEqualTo(c3).isNotEqualTo(c4); + } + + private Object[] findRepeatedAnnotationValues(RepeatableContainers containers, + Class element, Class annotationType) { + Annotation[] annotations = containers.findRepeatedAnnotations( + element.getAnnotation(annotationType)); + return extractValues(annotations); + } + + private Object[] extractValues(Annotation[] annotations) { + try { + if (annotations == null) { + return null; + } + Object[] result = new String[annotations.length]; + for (int i = 0; i < annotations.length; i++) { + result[i] = annotations[i].annotationType().getMethod("value").invoke( + annotations[i]); + } + return result; + } + catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface NonRepeatable { + + String value() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(StandardContainer.class) + static @interface StandardRepeatable { + + String value() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface StandardContainer { + + StandardRepeatable[] value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ExplicitRepeatable { + + String value() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ExplicitContainer { + + ExplicitRepeatable[] value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface InvalidNoValue { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface InvalidNotArray { + + int value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface InvalidWrongArrayType { + + StandardRepeatable[] value(); + + } + + @NonRepeatable("a") + static class WithNonRepeatable { + + } + + @StandardRepeatable("a") + static class WithSingleStandardRepeatable { + + } + + @StandardRepeatable("a") + @StandardRepeatable("b") + static class WithStandardRepeatables { + + } + + @ExplicitContainer({ @ExplicitRepeatable("a"), @ExplicitRepeatable("b") }) + static class WithExplicitRepeatables { + + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/TypeMappedAnnotationTests.java b/spring-core/src/test/java/org/springframework/core/annotation/TypeMappedAnnotationTests.java new file mode 100644 index 000000000000..2784fc9429a4 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/TypeMappedAnnotationTests.java @@ -0,0 +1,163 @@ +/* + * Copyright 2002-2019 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 + * + * 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. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.*; + +/** + * Tests for {@link TypeMappedAnnotation}. See also + * {@link MergedAnnotationsTests} for a much more extensive collection of tests. + * + * @author Phillip Webb + */ +public class TypeMappedAnnotationTests { + + @Test + public void mappingWhenMirroredReturnsMirroredValues() { + testExplicitMirror(WithExplicitMirrorA.class); + testExplicitMirror(WithExplicitMirrorB.class); + } + + private void testExplicitMirror(Class annotatedClass) { + TypeMappedAnnotation annotation = getTypeMappedAnnotation( + annotatedClass, ExplicitMirror.class); + assertThat(annotation.getString("a")).isEqualTo("test"); + assertThat(annotation.getString("b")).isEqualTo("test"); + } + + @Test + public void mappingExplicitAliasToMetaAnnotationReturnsMappedValues() { + TypeMappedAnnotation annotation = getTypeMappedAnnotation( + WithExplicitAliasToMetaAnnotation.class, + ExplicitAliasToMetaAnnotation.class, + ExplicitAliasMetaAnnotationTarget.class); + assertThat(annotation.getString("aliased")).isEqualTo("aliased"); + assertThat(annotation.getString("nonAliased")).isEqualTo("nonAliased"); + } + + @Test + public void mappingConventionAliasToMetaAnnotationReturnsMappedValues() { + TypeMappedAnnotation annotation = getTypeMappedAnnotation( + WithConventionAliasToMetaAnnotation.class, + ConventionAliasToMetaAnnotation.class, + ConventionAliasMetaAnnotationTarget.class); + assertThat(annotation.getString("value")).isEqualTo(""); + assertThat(annotation.getString("convention")).isEqualTo("convention"); + } + + private TypeMappedAnnotation getTypeMappedAnnotation( + Class source, Class annotationType) { + return getTypeMappedAnnotation(source, annotationType, annotationType); + } + + private TypeMappedAnnotation getTypeMappedAnnotation( + Class source, Class rootAnnotationType, + Class annotationType) { + Annotation rootAnnotation = source.getAnnotation(rootAnnotationType); + AnnotationTypeMapping mapping = getMapping(rootAnnotation, annotationType); + return TypeMappedAnnotation.createIfPossible(mapping, source, rootAnnotation, 0, IntrospectionFailureLogger.INFO); + } + + private AnnotationTypeMapping getMapping(Annotation annotation, + Class mappedAnnotationType) { + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( + annotation.annotationType()); + for (int i = 0; i < mappings.size(); i++) { + AnnotationTypeMapping candidate = mappings.get(i); + if (candidate.getAnnotationType().equals(mappedAnnotationType)) { + return candidate; + } + } + throw new IllegalStateException( + "No mapping from " + annotation + " to " + mappedAnnotationType); + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ExplicitMirror { + + @AliasFor("b") + String a() default ""; + + @AliasFor("a") + String b() default ""; + + } + + @ExplicitMirror(a = "test") + static class WithExplicitMirrorA { + + } + + @ExplicitMirror(b = "test") + static class WithExplicitMirrorB { + + } + + @Retention(RetentionPolicy.RUNTIME) + @ExplicitAliasMetaAnnotationTarget(nonAliased = "nonAliased") + static @interface ExplicitAliasToMetaAnnotation { + + @AliasFor(annotation = ExplicitAliasMetaAnnotationTarget.class) + String aliased() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ExplicitAliasMetaAnnotationTarget { + + String aliased() default ""; + + String nonAliased() default ""; + + } + + @ExplicitAliasToMetaAnnotation(aliased = "aliased") + private static class WithExplicitAliasToMetaAnnotation { + + } + + @Retention(RetentionPolicy.RUNTIME) + @ConventionAliasMetaAnnotationTarget + static @interface ConventionAliasToMetaAnnotation { + + String value() default ""; + + String convention() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface ConventionAliasMetaAnnotationTarget { + + String value() default ""; + + String convention() default ""; + + } + + @ConventionAliasToMetaAnnotation(value = "value", convention = "convention") + private static class WithConventionAliasToMetaAnnotation { + + } + +}