Refine MergedAnnotation.asMap

Add a convenience method that allows a `MergedAnnotation` to be
converted into an `AnnotationAttributes` instance. Also rename
the `MapValues` enum to `Adapt` which generally seems to read
better.

Closes gh-22738
This commit is contained in:
Phillip Webb 2019-04-05 11:10:17 -07:00
parent ef151f578d
commit e905f1712e
10 changed files with 105 additions and 68 deletions

View File

@ -166,6 +166,11 @@ abstract class AbstractMergedAnnotation<A extends Annotation> implements MergedA
return filterAttributes(this::hasNonDefaultValue);
}
@Override
public AnnotationAttributes asAnnotationAttributes(Adapt... adaptations) {
return asMap(mergedAnnotation -> new AnnotationAttributes(getType()), adaptations);
}
@Override
public Optional<A> synthesize(Predicate<? super MergedAnnotation<A>> condition)
throws NoSuchElementException {

View File

@ -25,7 +25,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.annotation.MergedAnnotation.MapValues;
import org.springframework.core.annotation.MergedAnnotation.Adapt;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.lang.Nullable;
import org.springframework.util.MultiValueMap;
@ -500,11 +500,11 @@ public abstract class AnnotatedElementUtils {
public static MultiValueMap<String, Object> getAllAnnotationAttributes(AnnotatedElement element,
String annotationName, final boolean classValuesAsString, final boolean nestedAnnotationsAsMap) {
MapValues[] mapValues = MapValues.of(classValuesAsString, nestedAnnotationsAsMap);
Adapt[] adaptations = Adapt.values(classValuesAsString, nestedAnnotationsAsMap);
return getAnnotations(element).stream(annotationName)
.filter(MergedAnnotationPredicates.unique(AnnotatedElementUtils::parentAndType))
.map(MergedAnnotation::withNonMergedAttributes)
.collect(MergedAnnotationCollectors.toMultiValueMap(AnnotatedElementUtils::nullIfEmpty, mapValues));
.collect(MergedAnnotationCollectors.toMultiValueMap(AnnotatedElementUtils::nullIfEmpty, adaptations));
}
/**
@ -799,8 +799,8 @@ public abstract class AnnotatedElementUtils {
if (!annotation.isPresent()) {
return null;
}
return annotation.asMap(mergedAnnotation -> new AnnotationAttributes(mergedAnnotation.getType()),
MapValues.of(classValuesAsString, nestedAnnotationsAsMap));
return annotation.asAnnotationAttributes(
Adapt.values(classValuesAsString, nestedAnnotationsAsMap));
}

View File

@ -29,11 +29,10 @@ import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.Function;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet;
import org.springframework.core.annotation.MergedAnnotation.MapValues;
import org.springframework.core.annotation.MergedAnnotation.Adapt;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.lang.Nullable;
import org.springframework.util.ConcurrentReferenceHashMap;
@ -857,14 +856,11 @@ public abstract class AnnotationUtils {
@Nullable AnnotatedElement annotatedElement, Annotation annotation,
boolean classValuesAsString, boolean nestedAnnotationsAsMap) {
MapValues[] mapValues = MapValues.of(classValuesAsString, nestedAnnotationsAsMap);
Adapt[] adaptations = Adapt.values(classValuesAsString, nestedAnnotationsAsMap);
return MergedAnnotation.from(annotatedElement, annotation)
.withNonMergedAttributes()
.asMap(getAnnotationAttributesFactory(), mapValues);
}
private static Function<MergedAnnotation<?>, AnnotationAttributes> getAnnotationAttributesFactory() {
return (annotation -> new AnnotationAttributes(annotation.getType(), true));
.asMap(mergedAnnotation ->
new AnnotationAttributes(mergedAnnotation.getType(), true), adaptations);
}
/**
@ -910,7 +906,8 @@ public abstract class AnnotationUtils {
else {
// If we have nested annotations, we need them as nested maps
AnnotationAttributes attributes = MergedAnnotation.from(annotationType)
.asMap(getAnnotationAttributesFactory(), MapValues.ANNOTATION_TO_MAP);
.asMap(annotation ->
new AnnotationAttributes(annotation.getType(), true), Adapt.ANNOTATION_TO_MAP);
for (Map.Entry<String, Object> element : attributes.entrySet()) {
result.put(element.getKey(), new DefaultValueHolder(element.getValue()));
}

View File

@ -422,21 +422,32 @@ public interface MergedAnnotation<A extends Annotation> {
MergedAnnotation<A> withNonMergedAttributes();
/**
* Create an immutable {@link Map} that contains all the annotation attributes.
* <p>The {@link MapValues options} may be used to change the way that values are added.
* @param options map value options
* Create a new mutable {@link AnnotationAttributes} instance from this
* merged annotation.
* <p>The {@link Adapt adaptations} may be used to change the way that values
* are added.
* @param adaptations adaptations that should be applied to the annotation values
* @return an immutable map containing the attributes and values
*/
Map<String, Object> asMap(MapValues... options);
AnnotationAttributes asAnnotationAttributes(Adapt... adaptations);
/**
* Create a {@link Map} of the given type that contains all the annotation attributes.
* <p>The {@link MapValues options} may be used to change the way that values are added.
* Return an immutable {@link Map} that contains all the annotation attributes.
* <p>The {@link Adapt adaptations} may be used to change the way that values are added.
* @param adaptations adaptations that should be applied to the annotation values
* @return an immutable map containing the attributes and values
*/
Map<String, Object> asMap(Adapt... adaptations);
/**
* Create a new {@link Map} instance of the given type that contains all the annotation
* attributes.
* <p>The {@link Adapt adaptations} may be used to change the way that values are added.
* @param factory a map factory
* @param options map value options
* @param adaptations adaptations that should be applied to the annotation values
* @return a map containing the attributes and values
*/
<T extends Map<String, Object>> T asMap(Function<MergedAnnotation<?>, T> factory, MapValues... options);
<T extends Map<String, Object>> T asMap(Function<MergedAnnotation<?>, T> factory, Adapt... adaptations);
/**
* Create a type-safe synthesized version of this annotation that can be
@ -539,24 +550,25 @@ public interface MergedAnnotation<A extends Annotation> {
/**
* Options that effect the way map values are
* {@linkplain MergedAnnotation#asMap(MapValues...) converted}.
* Adaptations that can be applied to attributes values when creating
* {@linkplain MergedAnnotation#asMap(Adapt...) Maps} or
* {@link MergedAnnotation#asAnnotationAttributes(Adapt...) AnnotationAttributes}.
*/
enum MapValues {
enum Adapt {
/**
* Add class or class array attributes as strings.
* Adapt class or class array attributes to strings.
*/
CLASS_TO_STRING,
/**
* Convert any nested annotation or annotation arrays to maps rather
* Adapt 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) {
protected final boolean isIn(Adapt... adaptations) {
for (Adapt candidate : adaptations) {
if (candidate == this) {
return true;
}
@ -565,16 +577,16 @@ public interface MergedAnnotation<A extends Annotation> {
}
/**
* 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
* Factory method to create a {@link Adapt} array from a set of boolean flags.
* @param classToString if {@link Adapt#CLASS_TO_STRING} is included
* @param annotationsToMap if {@link Adapt#ANNOTATION_TO_MAP} is included
* @return a new {@link Adapt} array
*/
public static MapValues[] of(boolean classToString, boolean annotationsToMap) {
EnumSet<MapValues> 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]);
public static Adapt[] values(boolean classToString, boolean annotationsToMap) {
EnumSet<Adapt> result = EnumSet.noneOf(Adapt.class);
addIfTrue(result, Adapt.CLASS_TO_STRING, classToString);
addIfTrue(result, Adapt.ANNOTATION_TO_MAP, annotationsToMap);
return result.toArray(new Adapt[0]);
}
private static <T> void addIfTrue(Set<T> result, T value, boolean test) {

View File

@ -26,7 +26,7 @@ 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.core.annotation.MergedAnnotation.Adapt;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@ -97,39 +97,39 @@ public abstract class MergedAnnotationCollectors {
* Create a new {@link Collector} that accumulates merged annotations to an
* {@link MultiValueMap} with items {@linkplain MultiValueMap#add(Object, Object)
* added} from each merged annotation
* {@link MergedAnnotation#asMap(MapValues...) as a map}.
* {@link MergedAnnotation#asMap(Adapt...) as a map}.
* @param <A> the annotation type
* @param options the map conversion options
* @param adaptations adaptations that should be applied to the annotation values
* @return a {@link Collector} which collects and synthesizes the
* annotations into a {@link LinkedMultiValueMap}
* @see #toMultiValueMap(Function, MergedAnnotation.MapValues...)
* @see #toMultiValueMap(Function, MergedAnnotation.Adapt...)
*/
public static <A extends Annotation> Collector<MergedAnnotation<A>, ?, MultiValueMap<String, Object>> toMultiValueMap(
MapValues... options) {
Adapt... adaptations) {
return toMultiValueMap(Function.identity(), options);
return toMultiValueMap(Function.identity(), adaptations);
}
/**
* Create a new {@link Collector} that accumulates merged annotations to an
* {@link MultiValueMap} with items {@linkplain MultiValueMap#add(Object, Object)
* added} from each merged annotation
* {@link MergedAnnotation#asMap(MapValues...) as a map}.
* {@link MergedAnnotation#asMap(Adapt...) as a map}.
* @param <A> the annotation type
* @param options the map conversion options
* @param adaptations adaptations that should be applied to the annotation values
* @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(MergedAnnotation.MapValues...)
* @see #toMultiValueMap(MergedAnnotation.Adapt...)
*/
public static <A extends Annotation> Collector<MergedAnnotation<A>, ?, MultiValueMap<String, Object>> toMultiValueMap(
Function<MultiValueMap<String, Object>, MultiValueMap<String, Object>> finisher,
MapValues... options) {
Adapt... adaptations) {
Characteristics[] characteristics = (isSameInstance(finisher, Function.identity()) ?
IDENTITY_FINISH_CHARACTERISTICS : NO_CHARACTERISTICS);
return Collector.of(LinkedMultiValueMap::new,
(map, annotation) -> annotation.asMap(options).forEach(map::add),
(map, annotation) -> annotation.asMap(adaptations).forEach(map::add),
MergedAnnotationCollectors::merge, finisher, characteristics);
}

View File

@ -108,12 +108,17 @@ final class MissingMergedAnnotation<A extends Annotation> extends AbstractMerged
}
@Override
public Map<String, Object> asMap(MapValues... options) {
public AnnotationAttributes asAnnotationAttributes(Adapt... adaptations) {
return new AnnotationAttributes();
}
@Override
public Map<String, Object> asMap(Adapt... adaptations) {
return Collections.emptyMap();
}
@Override
public <T extends Map<String, Object>> T asMap(Function<MergedAnnotation<?>, T> factory, MapValues... options) {
public <T extends Map<String, Object>> T asMap(Function<MergedAnnotation<?>, T> factory, Adapt... adaptations) {
return factory.apply(this);
}

View File

@ -233,50 +233,50 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
}
@Override
public Map<String, Object> asMap(MapValues... options) {
return Collections.unmodifiableMap(asMap(mergedAnnotation -> new LinkedHashMap<>(), options));
public Map<String, Object> asMap(Adapt... adaptations) {
return Collections.unmodifiableMap(asMap(mergedAnnotation -> new LinkedHashMap<>(), adaptations));
}
@Override
public <T extends Map<String, Object>> T asMap(Function<MergedAnnotation<?>, T> factory, MapValues... options) {
public <T extends Map<String, Object>> T asMap(Function<MergedAnnotation<?>, T> factory, Adapt... adaptations) {
T map = factory.apply(this);
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)));
getValue(i, getTypeForMapOptions(attribute, adaptations)));
if (value != null) {
map.put(attribute.getName(),
adaptValueForMapOptions(attribute, value, map.getClass(), factory, options));
adaptValueForMapOptions(attribute, value, map.getClass(), factory, adaptations));
}
}
return map;
}
private Class<?> getTypeForMapOptions(Method attribute, MapValues[] options) {
private Class<?> getTypeForMapOptions(Method attribute, Adapt[] adaptations) {
Class<?> attributeType = attribute.getReturnType();
Class<?> componentType = (attributeType.isArray() ? attributeType.getComponentType() : attributeType);
if (MapValues.CLASS_TO_STRING.isIn(options) && componentType == Class.class) {
if (Adapt.CLASS_TO_STRING.isIn(adaptations) && componentType == Class.class) {
return (attributeType.isArray() ? String[].class : String.class);
}
return Object.class;
}
private <T extends Map<String, Object>> Object adaptValueForMapOptions(Method attribute, Object value,
Class<?> mapType, Function<MergedAnnotation<?>, T> factory, MapValues[] options) {
Class<?> mapType, Function<MergedAnnotation<?>, T> factory, Adapt[] adaptations) {
if (value instanceof MergedAnnotation) {
MergedAnnotation<?> annotation = (MergedAnnotation<?>) value;
return (MapValues.ANNOTATION_TO_MAP.isIn(options) ?
annotation.asMap(factory, options) : annotation.synthesize());
return (Adapt.ANNOTATION_TO_MAP.isIn(adaptations) ?
annotation.asMap(factory, adaptations) : annotation.synthesize());
}
if (value instanceof MergedAnnotation[]) {
MergedAnnotation<?>[] annotations = (MergedAnnotation<?>[]) value;
if (MapValues.ANNOTATION_TO_MAP.isIn(options)) {
if (Adapt.ANNOTATION_TO_MAP.isIn(adaptations)) {
Object result = Array.newInstance(mapType, annotations.length);
for (int i = 0; i < annotations.length; i++) {
Array.set(result, i, annotations[i].asMap(factory, options));
Array.set(result, i, annotations[i].asMap(factory, adaptations));
}
return result;
}

View File

@ -27,7 +27,7 @@ import java.util.stream.Stream;
import org.junit.Test;
import org.springframework.core.annotation.MergedAnnotation.MapValues;
import org.springframework.core.annotation.MergedAnnotation.Adapt;
import org.springframework.util.MultiValueMap;
import static org.assertj.core.api.Assertions.*;
@ -72,7 +72,7 @@ public class MergedAnnotationCollectorsTests {
MultiValueMap<String, Object> map = stream().map(
MergedAnnotation::filterDefaultValues).collect(
MergedAnnotationCollectors.toMultiValueMap(
MapValues.CLASS_TO_STRING));
Adapt.CLASS_TO_STRING));
assertThat(map.get("value")).containsExactly("a", "b", "c");
assertThat(map.get("extra")).containsExactly("java.lang.String",
"java.lang.Integer");

View File

@ -40,7 +40,7 @@ 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.MergedAnnotation.Adapt;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.core.annotation.subpackage.NonPublicAnnotatedClass;
import org.springframework.lang.Nullable;
@ -1665,7 +1665,7 @@ public class MergedAnnotationsTests {
assertThat(componentScan.value().pattern()).isEqualTo("*Foo");
Map<String, Object> map = MergedAnnotation.from(componentScan).asMap(
annotation -> new LinkedHashMap<String, Object>(),
MapValues.ANNOTATION_TO_MAP);
Adapt.ANNOTATION_TO_MAP);
Map<String, Object> filterMap = (Map<String, Object>) map.get("value");
assertThat(filterMap.get("pattern")).isEqualTo("*Foo");
filterMap.put("pattern", "newFoo");
@ -1685,7 +1685,7 @@ public class MergedAnnotationsTests {
assertThat(componentScan).isNotNull();
Map<String, Object> map = MergedAnnotation.from(componentScan).asMap(
annotation -> new LinkedHashMap<String, Object>(),
MapValues.ANNOTATION_TO_MAP);
Adapt.ANNOTATION_TO_MAP);
Map<String, Object>[] filters = (Map[]) map.get("excludeFilters");
List<String> patterns = Arrays.stream(filters).map(
m -> (String) m.get("pattern")).collect(Collectors.toList());
@ -2053,6 +2053,17 @@ public class MergedAnnotationsTests {
"FromValueAttributeMeta");
}
@Test
public void asAnnotationAttributesReturnsPopulatedAnnotationAttributes() {
MergedAnnotation<?> annotation = MergedAnnotations.from(
SpringApplicationConfigurationClass.class).get(
SpringApplicationConfiguration.class);
AnnotationAttributes attributes = annotation.asAnnotationAttributes(
Adapt.CLASS_TO_STRING);
assertThat(attributes).containsEntry("classes", new String[] { Number.class.getName() });
assertThat(attributes.annotationType()).isEqualTo(SpringApplicationConfiguration.class);
}
// @formatter:off
@Retention(RetentionPolicy.RUNTIME)

View File

@ -264,6 +264,13 @@ public class MissingMergedAnnotationTests {
assertThat(this.missing.toString()).isEqualTo("(missing)");
}
@Test
public void asAnnotationAttributesReturnsNewAnnotationAttributes() {
AnnotationAttributes attributes = this.missing.asAnnotationAttributes();
assertThat(attributes).isEmpty();
assertThat(this.missing.asAnnotationAttributes()).isNotSameAs(attributes);
}
@Test
public void asMapReturnsEmptyMap() {
Map<String, Object> map = this.missing.asMap();