Revise RepeatableContainers API to better guide developers
Build and Deploy Snapshot / Build and Deploy Snapshot (push) Waiting to run Details
Build and Deploy Snapshot / Verify (push) Blocked by required conditions Details
Deploy Docs / Dispatch docs deployment (push) Waiting to run Details

Historically, the Spring Framework first had support for repeatable
annotations based on convention and later added explicit support for
Java 8's @⁠Repeatable facility. Consequently, the support for both
types of repeatable annotations has grown a bit intertwined over the
years. However, modern Java applications typically make use of
@⁠Repeatable, and convention-based repeatable annotations have become
more of a niche.

The RepeatableContainers API supports both types of repeatable
annotations with @⁠Repeatable support being the default. However,
RepeatableContainers.of() makes it very easy to enable support for
convention-based repeatable annotations while accidentally disabling
support for @⁠Repeatable, which can lead to subtle bugs – for example,
if convention-based annotations are combined with @⁠Repeatable
annotations. In addition, it is not readily clear how to combine
@⁠Repeatable support with convention-based repeatable annotations.

In light of the above, this commit revises the RepeatableContainers API
to better guide developers to use @⁠Repeatable support for almost all
use cases while still supporting convention-based repeatable
annotations for special use cases.

Specifically:

- RepeatableContainers.of() is now deprecated in favor of the new
  RepeatableContainers.explicitRepeatable() method.

- RepeatableContainers.and() is now deprecated in favor of the new
  RepeatableContainers.plus() method which declares the repeatable and
  container arguments in the same order as the rest of Spring
  Framework's repeated annotation APIs.

For example, instead of the following confusing mixture of
repeatable/container and container/repeatable:

RepeatableContainers.of(A.class, A.Container.class)
    .and(B.Container.class, B.class)

Developers are now be able to use:

RepeatableContainers.explicitRepeatable(A.class, A.Container.class)
    .plus(B.class, B.Container.class)

This commit also overhauls the Javadoc for RepeatableContainers and
explicitly points out that the following is the recommended approach to
support convention-based repeatable annotations while retaining support
for @⁠Repeatable.

RepeatableContainers.standardRepeatables()
    .plus(MyRepeatable1.class, MyContainer1.class)
    .plus(MyRepeatable2.class, MyContainer2.class)

See gh-20279
Closes gh-34637
This commit is contained in:
Sam Brannen 2025-03-21 17:42:16 +01:00
parent 7d5b3892c4
commit 274a689a10
7 changed files with 181 additions and 75 deletions

View File

@ -443,13 +443,17 @@ public abstract class AnnotatedElementUtils {
* support such a use case, favor {@link #getMergedRepeatableAnnotations(AnnotatedElement, Class)} * support such a use case, favor {@link #getMergedRepeatableAnnotations(AnnotatedElement, Class)}
* over this method or alternatively use the {@link MergedAnnotations} API * over this method or alternatively use the {@link MergedAnnotations} API
* directly in conjunction with {@link RepeatableContainers} that are * directly in conjunction with {@link RepeatableContainers} that are
* {@linkplain RepeatableContainers#and(Class, Class) composed} to support * {@linkplain RepeatableContainers#plus(Class, Class) composed} to support
* multiple repeatable annotation types. * multiple repeatable annotation types — for example:
* <pre class="code">
* RepeatableContainers.standardRepeatables()
* .plus(MyRepeatable1.class, MyContainer1.class)
* .plus(MyRepeatable2.class, MyContainer2.class);</pre>
* @param element the annotated element (never {@code null}) * @param element the annotated element (never {@code null})
* @param annotationType the annotation type to find (never {@code null}) * @param annotationType the repeatable annotation type to find (never {@code null})
* @param containerType the type of the container that holds the annotations; * @param containerType the type of the container that holds the repeatable
* may be {@code null} if the container type should be looked up via * annotations; may be {@code null} if the container type should be looked up
* {@link java.lang.annotation.Repeatable} * via {@link java.lang.annotation.Repeatable @Repeatable}
* @return the set of all merged repeatable {@code Annotations} found, * @return the set of all merged repeatable {@code Annotations} found,
* or an empty set if none were found * or an empty set if none were found
* @throws IllegalArgumentException if the {@code element} or {@code annotationType} * @throws IllegalArgumentException if the {@code element} or {@code annotationType}
@ -740,13 +744,17 @@ public abstract class AnnotatedElementUtils {
* support such a use case, favor {@link #findMergedRepeatableAnnotations(AnnotatedElement, Class)} * support such a use case, favor {@link #findMergedRepeatableAnnotations(AnnotatedElement, Class)}
* over this method or alternatively use the {@link MergedAnnotations} API * over this method or alternatively use the {@link MergedAnnotations} API
* directly in conjunction with {@link RepeatableContainers} that are * directly in conjunction with {@link RepeatableContainers} that are
* {@linkplain RepeatableContainers#and(Class, Class) composed} to support * {@linkplain RepeatableContainers#plus(Class, Class) composed} to support
* multiple repeatable annotation types. * multiple repeatable annotation types &mdash; for example:
* <pre class="code">
* RepeatableContainers.standardRepeatables()
* .plus(MyRepeatable1.class, MyContainer1.class)
* .plus(MyRepeatable2.class, MyContainer2.class);</pre>
* @param element the annotated element (never {@code null}) * @param element the annotated element (never {@code null})
* @param annotationType the annotation type to find (never {@code null}) * @param annotationType the repeatable annotation type to find (never {@code null})
* @param containerType the type of the container that holds the annotations; * @param containerType the type of the container that holds the repeatable
* may be {@code null} if the container type should be looked up via * annotations; may be {@code null} if the container type should be looked up
* {@link java.lang.annotation.Repeatable} * via {@link java.lang.annotation.Repeatable @Repeatable}
* @return the set of all merged repeatable {@code Annotations} found, * @return the set of all merged repeatable {@code Annotations} found,
* or an empty set if none were found * or an empty set if none were found
* @throws IllegalArgumentException if the {@code element} or {@code annotationType} * @throws IllegalArgumentException if the {@code element} or {@code annotationType}
@ -775,7 +783,7 @@ public abstract class AnnotatedElementUtils {
RepeatableContainers repeatableContainers; RepeatableContainers repeatableContainers;
if (containerType == null) { if (containerType == null) {
// Invoke RepeatableContainers.of() in order to adhere to the contract of // Invoke RepeatableContainers.explicitRepeatable() in order to adhere to the contract of
// getMergedRepeatableAnnotations() which states that an IllegalArgumentException // getMergedRepeatableAnnotations() which states that an IllegalArgumentException
// will be thrown if the container cannot be resolved. // will be thrown if the container cannot be resolved.
// //
@ -784,11 +792,11 @@ public abstract class AnnotatedElementUtils {
// annotation types). // annotation types).
// //
// See https://github.com/spring-projects/spring-framework/issues/20279 // See https://github.com/spring-projects/spring-framework/issues/20279
RepeatableContainers.of(annotationType, null); RepeatableContainers.explicitRepeatable(annotationType, null);
repeatableContainers = RepeatableContainers.standardRepeatables(); repeatableContainers = RepeatableContainers.standardRepeatables();
} }
else { else {
repeatableContainers = RepeatableContainers.of(annotationType, containerType); repeatableContainers = RepeatableContainers.explicitRepeatable(annotationType, containerType);
} }
return MergedAnnotations.from(element, SearchStrategy.INHERITED_ANNOTATIONS, repeatableContainers); return MergedAnnotations.from(element, SearchStrategy.INHERITED_ANNOTATIONS, repeatableContainers);
} }
@ -802,7 +810,7 @@ public abstract class AnnotatedElementUtils {
RepeatableContainers repeatableContainers; RepeatableContainers repeatableContainers;
if (containerType == null) { if (containerType == null) {
// Invoke RepeatableContainers.of() in order to adhere to the contract of // Invoke RepeatableContainers.explicitRepeatable() in order to adhere to the contract of
// findMergedRepeatableAnnotations() which states that an IllegalArgumentException // findMergedRepeatableAnnotations() which states that an IllegalArgumentException
// will be thrown if the container cannot be resolved. // will be thrown if the container cannot be resolved.
// //
@ -811,11 +819,11 @@ public abstract class AnnotatedElementUtils {
// annotation types). // annotation types).
// //
// See https://github.com/spring-projects/spring-framework/issues/20279 // See https://github.com/spring-projects/spring-framework/issues/20279
RepeatableContainers.of(annotationType, null); RepeatableContainers.explicitRepeatable(annotationType, null);
repeatableContainers = RepeatableContainers.standardRepeatables(); repeatableContainers = RepeatableContainers.standardRepeatables();
} }
else { else {
repeatableContainers = RepeatableContainers.of(annotationType, containerType); repeatableContainers = RepeatableContainers.explicitRepeatable(annotationType, containerType);
} }
return MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY, repeatableContainers); return MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY, repeatableContainers);
} }

View File

@ -370,7 +370,7 @@ public abstract class AnnotationUtils {
Class<A> annotationType, @Nullable Class<? extends Annotation> containerAnnotationType) { Class<A> annotationType, @Nullable Class<? extends Annotation> containerAnnotationType) {
RepeatableContainers repeatableContainers = (containerAnnotationType != null ? RepeatableContainers repeatableContainers = (containerAnnotationType != null ?
RepeatableContainers.of(annotationType, containerAnnotationType) : RepeatableContainers.explicitRepeatable(annotationType, containerAnnotationType) :
RepeatableContainers.standardRepeatables()); RepeatableContainers.standardRepeatables());
return MergedAnnotations.from(annotatedElement, SearchStrategy.SUPERCLASS, repeatableContainers) return MergedAnnotations.from(annotatedElement, SearchStrategy.SUPERCLASS, repeatableContainers)
@ -451,7 +451,7 @@ public abstract class AnnotationUtils {
Class<A> annotationType, @Nullable Class<? extends Annotation> containerAnnotationType) { Class<A> annotationType, @Nullable Class<? extends Annotation> containerAnnotationType) {
RepeatableContainers repeatableContainers = containerAnnotationType != null ? RepeatableContainers repeatableContainers = containerAnnotationType != null ?
RepeatableContainers.of(annotationType, containerAnnotationType) : RepeatableContainers.explicitRepeatable(annotationType, containerAnnotationType) :
RepeatableContainers.standardRepeatables(); RepeatableContainers.standardRepeatables();
return MergedAnnotations.from(annotatedElement, SearchStrategy.DIRECT, repeatableContainers) return MergedAnnotations.from(annotatedElement, SearchStrategy.DIRECT, repeatableContainers)

View File

@ -30,15 +30,38 @@ import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
/** /**
* Strategy used to determine annotations that act as containers for other * Strategy used to find repeatable annotations within container annotations.
* annotations. The {@link #standardRepeatables()} method provides a default
* strategy that respects Java's {@link Repeatable @Repeatable} support and
* should be suitable for most situations.
* *
* <p>The {@link #of} method can be used to register relationships for * <p>{@link #standardRepeatables() RepeatableContainers.standardRepeatables()}
* annotations that do not wish to use {@link Repeatable @Repeatable}. * provides a default strategy that respects Java's {@link Repeatable @Repeatable}
* support and is suitable for most situations.
* *
* <p>To completely disable repeatable support use {@link #none()}. * <p>If you need to register repeatable annotation types that do not make use of
* {@code @Repeatable}, you should typically use {@code standardRepeatables()}
* combined with {@link #plus(Class, Class)}. Note that multiple invocations of
* {@code plus()} can be chained together to register multiple repeatable/container
* type pairs. For example:
*
* <pre class="code">
* RepeatableContainers repeatableContainers =
* RepeatableContainers.standardRepeatables()
* .plus(MyRepeatable1.class, MyContainer1.class)
* .plus(MyRepeatable2.class, MyContainer2.class);</pre>
*
* <p>For special use cases where you are certain that you do not need Java's
* {@code @Repeatable} support, you can use {@link #explicitRepeatable(Class, Class)
* RepeatableContainers.explicitRepeatable()} to create an instance of
* {@code RepeatableContainers} that only supports explicit repeatable/container
* type pairs. As with {@code standardRepeatables()}, {@code plus()} can be used
* to register additional repeatable/container type pairs. For example:
*
* <pre class="code">
* RepeatableContainers repeatableContainers =
* RepeatableContainers.explicitRepeatable(MyRepeatable1.class, MyContainer1.class)
* .plus(MyRepeatable2.class, MyContainer2.class);</pre>
*
* <p>To completely disable repeatable annotation support use
* {@link #none() RepeatableContainers.none()}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Sam Brannen * @author Sam Brannen
@ -55,22 +78,46 @@ public abstract class RepeatableContainers {
this.parent = parent; this.parent = parent;
} }
/** /**
* Add an additional explicit relationship between a container and * Register a pair of repeatable and container annotation types.
* repeatable annotation. * <p>See the {@linkplain RepeatableContainers class-level javadoc} for examples.
* <p>WARNING: the arguments supplied to this method are in the reverse order
* of those supplied to {@link #of(Class, Class)}.
* @param container the container annotation type
* @param repeatable the repeatable annotation type * @param repeatable the repeatable annotation type
* @return a new {@link RepeatableContainers} instance * @param container the container annotation type
* @return a new {@code RepeatableContainers} instance that is chained to
* the current instance
* @since 7.0
*/ */
public RepeatableContainers and(Class<? extends Annotation> container, public final RepeatableContainers plus(Class<? extends Annotation> repeatable,
Class<? extends Annotation> repeatable) { Class<? extends Annotation> container) {
return new ExplicitRepeatableContainer(this, repeatable, container); return new ExplicitRepeatableContainer(this, repeatable, container);
} }
/**
* Register a pair of container and repeatable annotation types.
* <p><strong>WARNING</strong>: The arguments supplied to this method are in
* the reverse order of those supplied to {@link #plus(Class, Class)},
* {@link #explicitRepeatable(Class, Class)}, and {@link #of(Class, Class)}.
* @param container the container annotation type
* @param repeatable the repeatable annotation type
* @return a new {@code RepeatableContainers} instance that is chained to
* the current instance
* @deprecated as of Spring Framework 7.0, in favor of {@link #plus(Class, Class)}
*/
@Deprecated(since = "7.0")
public RepeatableContainers and(Class<? extends Annotation> container,
Class<? extends Annotation> repeatable) {
return plus(repeatable, container);
}
/**
* Find repeated annotations contained in the supplied {@code annotation}.
* @param annotation the candidate container annotation
* @return the repeated annotations found in the supplied container annotation
* (potentially an empty array), or {@code null} if the supplied annotation is
* not a supported container annotation
*/
Annotation @Nullable [] findRepeatedAnnotations(Annotation annotation) { Annotation @Nullable [] findRepeatedAnnotations(Annotation annotation) {
if (this.parent == null) { if (this.parent == null) {
return null; return null;
@ -98,41 +145,92 @@ public abstract class RepeatableContainers {
/** /**
* Create a {@link RepeatableContainers} instance that searches using Java's * Create a {@link RepeatableContainers} instance that searches for repeated
* {@link Repeatable @Repeatable} annotation. * annotations according to the semantics of Java's {@link Repeatable @Repeatable}
* @return a {@link RepeatableContainers} instance * annotation.
* <p>See the {@linkplain RepeatableContainers class-level javadoc} for examples.
* @return a {@code RepeatableContainers} instance that supports {@code @Repeatable}
* @see #plus(Class, Class)
*/ */
public static RepeatableContainers standardRepeatables() { public static RepeatableContainers standardRepeatables() {
return StandardRepeatableContainers.INSTANCE; return StandardRepeatableContainers.INSTANCE;
} }
/** /**
* Create a {@link RepeatableContainers} instance that uses predefined * Create a {@link RepeatableContainers} instance that searches for repeated
* repeatable and container types. * annotations by taking into account the supplied repeatable and container
* <p>WARNING: the arguments supplied to this method are in the reverse order * annotation types.
* of those supplied to {@link #and(Class, Class)}. * <p><strong>WARNING</strong>: The {@code RepeatableContainers} instance
* returned by this factory method does <strong>not</strong> respect Java's
* {@link Repeatable @Repeatable} support. Use {@link #standardRepeatables()}
* for standard {@code @Repeatable} support, optionally combined with
* {@link #plus(Class, Class)}.
* <p>If the supplied container annotation type is not {@code null}, it must
* declare a {@code value} attribute returning an array of repeatable
* annotations. If the supplied container annotation type is {@code null}, the
* container will be deduced by inspecting the {@code @Repeatable} annotation
* on the {@code repeatable} annotation type.
* <p>See the {@linkplain RepeatableContainers class-level javadoc} for examples.
* @param repeatable the repeatable annotation type * @param repeatable the repeatable annotation type
* @param container the container annotation type or {@code null}. If specified, * @param container the container annotation type or {@code null}
* this annotation must declare a {@code value} attribute returning an array * @return a {@code RepeatableContainers} instance that does not support
* of repeatable annotations. If not specified, the container will be * {@link Repeatable @Repeatable}
* deduced by inspecting the {@code @Repeatable} annotation on
* {@code repeatable}.
* @return a {@link RepeatableContainers} instance
* @throws IllegalArgumentException if the supplied container type is * @throws IllegalArgumentException if the supplied container type is
* {@code null} and the annotation type is not a repeatable annotation * {@code null} and the annotation type is not a repeatable annotation
* @throws AnnotationConfigurationException if the supplied container type * @throws AnnotationConfigurationException if the supplied container type
* is not a properly configured container for a repeatable annotation * is not a properly configured container for a repeatable annotation
* @since 7.0
* @see #standardRepeatables()
* @see #plus(Class, Class)
*/ */
public static RepeatableContainers of( public static RepeatableContainers explicitRepeatable(
Class<? extends Annotation> repeatable, @Nullable Class<? extends Annotation> container) { Class<? extends Annotation> repeatable, @Nullable Class<? extends Annotation> container) {
return new ExplicitRepeatableContainer(null, repeatable, container); return new ExplicitRepeatableContainer(null, repeatable, container);
} }
/**
* Create a {@link RepeatableContainers} instance that searches for repeated
* annotations by taking into account the supplied repeatable and container
* annotation types.
* <p><strong>WARNING</strong>: The {@code RepeatableContainers} instance
* returned by this factory method does <strong>not</strong> respect Java's
* {@link Repeatable @Repeatable} support. Use {@link #standardRepeatables()}
* for standard {@code @Repeatable} support, optionally combined with
* {@link #plus(Class, Class)}.
* <p><strong>WARNING</strong>: The arguments supplied to this method are in
* the reverse order of those supplied to {@link #and(Class, Class)}.
* <p>If the supplied container annotation type is not {@code null}, it must
* declare a {@code value} attribute returning an array of repeatable
* annotations. If the supplied container annotation type is {@code null}, the
* container will be deduced by inspecting the {@code @Repeatable} annotation
* on the {@code repeatable} annotation type.
* @param repeatable the repeatable annotation type
* @param container the container annotation type or {@code null}
* @return a {@code RepeatableContainers} instance that does not support
* {@link Repeatable @Repeatable}
* @throws IllegalArgumentException if the supplied container type is
* {@code null} and the annotation type is not a repeatable annotation
* @throws AnnotationConfigurationException if the supplied container type
* is not a properly configured container for a repeatable annotation
* @deprecated as of Spring Framework 7.0, in favor of {@link #explicitRepeatable(Class, Class)}
*/
@Deprecated(since = "7.0")
public static RepeatableContainers of(
Class<? extends Annotation> repeatable, @Nullable Class<? extends Annotation> container) {
return explicitRepeatable(repeatable, container);
}
/** /**
* Create a {@link RepeatableContainers} instance that does not support any * Create a {@link RepeatableContainers} instance that does not support any
* repeatable annotations. * repeatable annotations.
* @return a {@link RepeatableContainers} instance * <p>Note, however, that {@link #plus(Class, Class)} may still be invoked on
* the {@code RepeatableContainers} instance returned from this method.
* <p>See the {@linkplain RepeatableContainers class-level javadoc} for examples
* and further details.
* @return a {@code RepeatableContainers} instance that does not support
* repeatable annotations
*/ */
public static RepeatableContainers none() { public static RepeatableContainers none() {
return NoRepeatableContainers.INSTANCE; return NoRepeatableContainers.INSTANCE;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2024 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -274,7 +274,7 @@ class MergedAnnotationsRepeatableAnnotationTests {
private <A extends Annotation> Set<A> getAnnotations(Class<? extends Annotation> container, private <A extends Annotation> Set<A> getAnnotations(Class<? extends Annotation> container,
Class<A> repeatable, SearchStrategy searchStrategy, AnnotatedElement element, AnnotationFilter annotationFilter) { Class<A> repeatable, SearchStrategy searchStrategy, AnnotatedElement element, AnnotationFilter annotationFilter) {
RepeatableContainers containers = RepeatableContainers.of(repeatable, container); RepeatableContainers containers = RepeatableContainers.explicitRepeatable(repeatable, container);
MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy, containers, annotationFilter); MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy, containers, annotationFilter);
return annotations.stream(repeatable).collect(MergedAnnotationCollectors.toAnnotationSet()); return annotations.stream(repeatable).collect(MergedAnnotationCollectors.toAnnotationSet());
} }

View File

@ -136,7 +136,7 @@ class MergedAnnotationsTests {
@Test @Test
void searchFromClassWithCustomRepeatableContainers() { void searchFromClassWithCustomRepeatableContainers() {
assertThat(MergedAnnotations.from(HierarchyClass.class).stream(TestConfiguration.class)).isEmpty(); assertThat(MergedAnnotations.from(HierarchyClass.class).stream(TestConfiguration.class)).isEmpty();
RepeatableContainers containers = RepeatableContainers.of(TestConfiguration.class, Hierarchy.class); RepeatableContainers containers = RepeatableContainers.explicitRepeatable(TestConfiguration.class, Hierarchy.class);
MergedAnnotations annotations = MergedAnnotations.search(SearchStrategy.DIRECT) MergedAnnotations annotations = MergedAnnotations.search(SearchStrategy.DIRECT)
.withRepeatableContainers(containers) .withRepeatableContainers(containers)
@ -1364,7 +1364,7 @@ class MergedAnnotationsTests {
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
void streamRepeatableDeclaredOnClassWithAttributeAliases() { void streamRepeatableDeclaredOnClassWithAttributeAliases() {
assertThat(MergedAnnotations.from(HierarchyClass.class).stream(TestConfiguration.class)).isEmpty(); assertThat(MergedAnnotations.from(HierarchyClass.class).stream(TestConfiguration.class)).isEmpty();
RepeatableContainers containers = RepeatableContainers.of(TestConfiguration.class, Hierarchy.class); RepeatableContainers containers = RepeatableContainers.explicitRepeatable(TestConfiguration.class, Hierarchy.class);
MergedAnnotations annotations = MergedAnnotations.from(HierarchyClass.class, MergedAnnotations annotations = MergedAnnotations.from(HierarchyClass.class,
SearchStrategy.DIRECT, containers, AnnotationFilter.NONE); SearchStrategy.DIRECT, containers, AnnotationFilter.NONE);
assertThat(annotations.stream(TestConfiguration.class) assertThat(annotations.stream(TestConfiguration.class)
@ -1440,7 +1440,7 @@ class MergedAnnotationsTests {
private void testExplicitRepeatables(SearchStrategy searchStrategy, Class<?> element, String[] expected) { private void testExplicitRepeatables(SearchStrategy searchStrategy, Class<?> element, String[] expected) {
MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy, MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy,
RepeatableContainers.of(MyRepeatable.class, MyRepeatableContainer.class)); RepeatableContainers.explicitRepeatable(MyRepeatable.class, MyRepeatableContainer.class));
Stream<String> values = annotations.stream(MyRepeatable.class) Stream<String> values = annotations.stream(MyRepeatable.class)
.filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex)) .filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex))
.map(annotation -> annotation.getString("value")); .map(annotation -> annotation.getString("value"));

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2024 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -109,7 +109,7 @@ class NestedRepeatableAnnotationsTests {
@Test @Test
void streamRepeatableAnnotationsWithExplicitRepeatables_MergedAnnotationsApi() { void streamRepeatableAnnotationsWithExplicitRepeatables_MergedAnnotationsApi() {
RepeatableContainers repeatableContainers = RepeatableContainers repeatableContainers =
RepeatableContainers.of(A.class, A.Container.class).and(B.Container.class, B.class); RepeatableContainers.explicitRepeatable(A.class, A.Container.class).plus(B.class, B.Container.class);
Set<A> annotations = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY, repeatableContainers) Set<A> annotations = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY, repeatableContainers)
.stream(A.class).collect(MergedAnnotationCollectors.toAnnotationSet()); .stream(A.class).collect(MergedAnnotationCollectors.toAnnotationSet());
// Merged, so we expect to find @A twice with values coming from @B(5) and @B(10). // Merged, so we expect to find @A twice with values coming from @B(5) and @B(10).
@ -127,8 +127,8 @@ class NestedRepeatableAnnotationsTests {
void findMergedRepeatableAnnotationsWithExplicitContainer_AnnotatedElementUtils() { void findMergedRepeatableAnnotationsWithExplicitContainer_AnnotatedElementUtils() {
Set<A> annotations = AnnotatedElementUtils.findMergedRepeatableAnnotations(method, A.class, A.Container.class); Set<A> annotations = AnnotatedElementUtils.findMergedRepeatableAnnotations(method, A.class, A.Container.class);
// When findMergedRepeatableAnnotations(...) is invoked with an explicit container // When findMergedRepeatableAnnotations(...) is invoked with an explicit container
// type, it uses RepeatableContainers.of(...) which limits the repeatable annotation // type, it uses RepeatableContainers.explicitRepeatable(...) which limits the
// support to a single container type. // repeatable annotation support to a single container type.
// //
// In this test case, we are therefore limiting the support to @A.Container, which // In this test case, we are therefore limiting the support to @A.Container, which
// means that @B.Container is unsupported and effectively ignored as a repeatable // means that @B.Container is unsupported and effectively ignored as a repeatable
@ -149,8 +149,8 @@ class NestedRepeatableAnnotationsTests {
void getMergedRepeatableAnnotationsWithExplicitContainer_AnnotatedElementUtils() { void getMergedRepeatableAnnotationsWithExplicitContainer_AnnotatedElementUtils() {
Set<A> annotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(method, A.class, A.Container.class); Set<A> annotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(method, A.class, A.Container.class);
// When getMergedRepeatableAnnotations(...) is invoked with an explicit container // When getMergedRepeatableAnnotations(...) is invoked with an explicit container
// type, it uses RepeatableContainers.of(...) which limits the repeatable annotation // type, it uses RepeatableContainers.explicitRepeatable(...) which limits the
// support to a single container type. // repeatable annotation support to a single container type.
// //
// In this test case, we are therefore limiting the support to @A.Container, which // In this test case, we are therefore limiting the support to @A.Container, which
// means that @B.Container is unsupported and effectively ignored as a repeatable // means that @B.Container is unsupported and effectively ignored as a repeatable

View File

@ -87,7 +87,7 @@ class RepeatableContainersTests {
@Test @Test
void ofExplicitWhenNonRepeatableReturnsNull() { void ofExplicitWhenNonRepeatableReturnsNull() {
Object[] values = findRepeatedAnnotationValues( Object[] values = findRepeatedAnnotationValues(
RepeatableContainers.of(ExplicitRepeatable.class, ExplicitContainer.class), RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, ExplicitContainer.class),
NonRepeatableTestCase.class, NonRepeatable.class); NonRepeatableTestCase.class, NonRepeatable.class);
assertThat(values).isNull(); assertThat(values).isNull();
} }
@ -95,7 +95,7 @@ class RepeatableContainersTests {
@Test @Test
void ofExplicitWhenStandardRepeatableContainerReturnsNull() { void ofExplicitWhenStandardRepeatableContainerReturnsNull() {
Object[] values = findRepeatedAnnotationValues( Object[] values = findRepeatedAnnotationValues(
RepeatableContainers.of(ExplicitRepeatable.class, ExplicitContainer.class), RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, ExplicitContainer.class),
StandardRepeatablesTestCase.class, StandardContainer.class); StandardRepeatablesTestCase.class, StandardContainer.class);
assertThat(values).isNull(); assertThat(values).isNull();
} }
@ -103,14 +103,14 @@ class RepeatableContainersTests {
@Test @Test
void ofExplicitWhenContainerReturnsRepeats() { void ofExplicitWhenContainerReturnsRepeats() {
Object[] values = findRepeatedAnnotationValues( Object[] values = findRepeatedAnnotationValues(
RepeatableContainers.of(ExplicitRepeatable.class, ExplicitContainer.class), RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, ExplicitContainer.class),
ExplicitRepeatablesTestCase.class, ExplicitContainer.class); ExplicitRepeatablesTestCase.class, ExplicitContainer.class);
assertThat(values).containsExactly("a", "b"); assertThat(values).containsExactly("a", "b");
} }
@Test @Test
void ofExplicitWhenContainerIsNullDeducesContainer() { void ofExplicitWhenContainerIsNullDeducesContainer() {
Object[] values = findRepeatedAnnotationValues(RepeatableContainers.of(StandardRepeatable.class, null), Object[] values = findRepeatedAnnotationValues(RepeatableContainers.explicitRepeatable(StandardRepeatable.class, null),
StandardRepeatablesTestCase.class, StandardContainer.class); StandardRepeatablesTestCase.class, StandardContainer.class);
assertThat(values).containsExactly("a", "b"); assertThat(values).containsExactly("a", "b");
} }
@ -118,7 +118,7 @@ class RepeatableContainersTests {
@Test @Test
void ofExplicitWhenHasNoValueThrowsException() { void ofExplicitWhenHasNoValueThrowsException() {
assertThatExceptionOfType(AnnotationConfigurationException.class) assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> RepeatableContainers.of(ExplicitRepeatable.class, InvalidNoValue.class)) .isThrownBy(() -> RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, InvalidNoValue.class))
.withMessageContaining("Invalid declaration of container type [%s] for repeatable annotation [%s]", .withMessageContaining("Invalid declaration of container type [%s] for repeatable annotation [%s]",
InvalidNoValue.class.getName(), ExplicitRepeatable.class.getName()); InvalidNoValue.class.getName(), ExplicitRepeatable.class.getName());
} }
@ -126,7 +126,7 @@ class RepeatableContainersTests {
@Test @Test
void ofExplicitWhenValueIsNotArrayThrowsException() { void ofExplicitWhenValueIsNotArrayThrowsException() {
assertThatExceptionOfType(AnnotationConfigurationException.class) assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> RepeatableContainers.of(ExplicitRepeatable.class, InvalidNotArray.class)) .isThrownBy(() -> RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, InvalidNotArray.class))
.withMessage("Container type [%s] must declare a 'value' attribute for an array of type [%s]", .withMessage("Container type [%s] must declare a 'value' attribute for an array of type [%s]",
InvalidNotArray.class.getName(), ExplicitRepeatable.class.getName()); InvalidNotArray.class.getName(), ExplicitRepeatable.class.getName());
} }
@ -134,7 +134,7 @@ class RepeatableContainersTests {
@Test @Test
void ofExplicitWhenValueIsArrayOfWrongTypeThrowsException() { void ofExplicitWhenValueIsArrayOfWrongTypeThrowsException() {
assertThatExceptionOfType(AnnotationConfigurationException.class) assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> RepeatableContainers.of(ExplicitRepeatable.class, InvalidWrongArrayType.class)) .isThrownBy(() -> RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, InvalidWrongArrayType.class))
.withMessage("Container type [%s] must declare a 'value' attribute for an array of type [%s]", .withMessage("Container type [%s] must declare a 'value' attribute for an array of type [%s]",
InvalidWrongArrayType.class.getName(), ExplicitRepeatable.class.getName()); InvalidWrongArrayType.class.getName(), ExplicitRepeatable.class.getName());
} }
@ -142,14 +142,14 @@ class RepeatableContainersTests {
@Test @Test
void ofExplicitWhenAnnotationIsNullThrowsException() { void ofExplicitWhenAnnotationIsNullThrowsException() {
assertThatIllegalArgumentException() assertThatIllegalArgumentException()
.isThrownBy(() -> RepeatableContainers.of(null, null)) .isThrownBy(() -> RepeatableContainers.explicitRepeatable(null, null))
.withMessage("Repeatable must not be null"); .withMessage("Repeatable must not be null");
} }
@Test @Test
void ofExplicitWhenContainerIsNullAndNotRepeatableThrowsException() { void ofExplicitWhenContainerIsNullAndNotRepeatableThrowsException() {
assertThatIllegalArgumentException() assertThatIllegalArgumentException()
.isThrownBy(() -> RepeatableContainers.of(ExplicitRepeatable.class, null)) .isThrownBy(() -> RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, null))
.withMessage("Annotation type must be a repeatable annotation: failed to resolve container type for %s", .withMessage("Annotation type must be a repeatable annotation: failed to resolve container type for %s",
ExplicitRepeatable.class.getName()); ExplicitRepeatable.class.getName());
} }
@ -159,7 +159,7 @@ class RepeatableContainersTests {
@Test @Test
void standardAndExplicitReturnsRepeats() { void standardAndExplicitReturnsRepeats() {
RepeatableContainers repeatableContainers = RepeatableContainers.standardRepeatables() RepeatableContainers repeatableContainers = RepeatableContainers.standardRepeatables()
.and(ExplicitContainer.class, ExplicitRepeatable.class); .plus(ExplicitRepeatable.class, ExplicitContainer.class);
assertThat(findRepeatedAnnotationValues(repeatableContainers, StandardRepeatablesTestCase.class, StandardContainer.class)) assertThat(findRepeatedAnnotationValues(repeatableContainers, StandardRepeatablesTestCase.class, StandardContainer.class))
.containsExactly("a", "b"); .containsExactly("a", "b");
assertThat(findRepeatedAnnotationValues(repeatableContainers, ExplicitRepeatablesTestCase.class, ExplicitContainer.class)) assertThat(findRepeatedAnnotationValues(repeatableContainers, ExplicitRepeatablesTestCase.class, ExplicitContainer.class))
@ -175,10 +175,10 @@ class RepeatableContainersTests {
@Test @Test
void equalsAndHashcode() { void equalsAndHashcode() {
RepeatableContainers c1 = RepeatableContainers.of(ExplicitRepeatable.class, ExplicitContainer.class); RepeatableContainers c1 = RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, ExplicitContainer.class);
RepeatableContainers c2 = RepeatableContainers.of(ExplicitRepeatable.class, ExplicitContainer.class); RepeatableContainers c2 = RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, ExplicitContainer.class);
RepeatableContainers c3 = RepeatableContainers.standardRepeatables(); RepeatableContainers c3 = RepeatableContainers.standardRepeatables();
RepeatableContainers c4 = RepeatableContainers.standardRepeatables().and(ExplicitContainer.class, ExplicitRepeatable.class); RepeatableContainers c4 = RepeatableContainers.standardRepeatables().plus(ExplicitRepeatable.class, ExplicitContainer.class);
assertThat(c1).hasSameHashCodeAs(c2); assertThat(c1).hasSameHashCodeAs(c2);
assertThat(c1).isEqualTo(c1).isEqualTo(c2); assertThat(c1).isEqualTo(c1).isEqualTo(c2);
assertThat(c1).isNotEqualTo(c3).isNotEqualTo(c4); assertThat(c1).isNotEqualTo(c3).isNotEqualTo(c4);