Skip to content

Add support for repeatable annotations #1670

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
29b9861
Create AnnotationHolder.java
YangSiJun528 Jul 8, 2025
ebf3cf8
Implement AnnotationHolder in AnnotationContainer
YangSiJun528 Jul 8, 2025
5f339cb
Create MultipleAnnotationContainer.java
YangSiJun528 Jul 8, 2025
ee7f144
Create MultipleAnnotationContainerTests.java
YangSiJun528 Jul 8, 2025
18eb26f
Update Annotatable interface to use AnnotationHolder
YangSiJun528 Jul 8, 2025
cd472db
Format code to match style guide
YangSiJun528 Jul 8, 2025
cf4734c
Format code to match style guide
YangSiJun528 Jul 8, 2025
99401a3
Update comments
YangSiJun528 Jul 8, 2025
12ccae5
Add constructor for AnnotationHolder
YangSiJun528 Jul 9, 2025
cc900e2
Streamline constructor for AnnotationHolder
YangSiJun528 Jul 9, 2025
84bcecf
Add type declaration creation with annotations
YangSiJun528 Jul 9, 2025
f0444b9
Fix writeProperty to write annotations
YangSiJun528 Jul 9, 2025
870857b
Add tests for multiple annotations
YangSiJun528 Jul 9, 2025
c7a3a99
Chore
YangSiJun528 Jul 9, 2025
d268330
Format code to match style guide
YangSiJun528 Jul 9, 2025
4df38dd
Revert accidental changes
YangSiJun528 Jul 9, 2025
e8e0272
Chore
YangSiJun528 Jul 10, 2025
15cf12e
Revert changes
YangSiJun528 Jul 15, 2025
2d94c84
Allow multiple annotations of the same type
YangSiJun528 Jul 15, 2025
557687b
Add tests of multiple annotations of the same type
YangSiJun528 Jul 15, 2025
d3d2dd8
Fix writeProperty to write annotations
YangSiJun528 Jul 15, 2025
15b5cf4
Add tests about repeatable annotations
YangSiJun528 Jul 15, 2025
78cd91a
Chore
YangSiJun528 Jul 15, 2025
2bfa2e7
Improve AnnotationContainerTests assertion
YangSiJun528 Jul 15, 2025
6a68ce0
Separate single and repeatable annotation handling
YangSiJun528 Jul 21, 2025
5974472
Deprecate AnnotationContainer#add
YangSiJun528 Jul 21, 2025
3435d7f
Add tests
YangSiJun528 Jul 21, 2025
8327127
Chore
YangSiJun528 Jul 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ public class SourceCodeProjectGenerationConfiguration {
@Bean
public MainApplicationTypeCustomizer<TypeDeclaration> springBootApplicationAnnotator() {
return (typeDeclaration) -> typeDeclaration.annotations()
.add(ClassName.of("org.springframework.boot.autoconfigure.SpringBootApplication"));
.addSingle(ClassName.of("org.springframework.boot.autoconfigure.SpringBootApplication"));
}

@Bean
public TestApplicationTypeCustomizer<TypeDeclaration> junitJupiterSpringBootTestTypeCustomizer() {
return (typeDeclaration) -> typeDeclaration.annotations()
.add(ClassName.of("org.springframework.boot.test.context.SpringBootTest"));
.addSingle(ClassName.of("org.springframework.boot.test.context.SpringBootTest"));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ TestApplicationTypeCustomizer<GroovyTypeDeclaration> junitJupiterTestMethodContr
GroovyMethodDeclaration method = GroovyMethodDeclaration.method("contextLoads")
.returning("void")
.body(CodeBlock.of(""));
method.annotations().add(ClassName.of("org.junit.jupiter.api.Test"));
method.annotations().addSingle(ClassName.of("org.junit.jupiter.api.Test"));
typeDeclaration.addMethodDeclaration(method);
};
}
Expand All @@ -93,7 +93,7 @@ ServletInitializerCustomizer<GroovyTypeDeclaration> javaServletInitializerCustom
.parameters(
Parameter.of("application", "org.springframework.boot.builder.SpringApplicationBuilder"))
.body(CodeBlock.ofStatement("application.sources($L)", description.getApplicationName()));
configure.annotations().add(ClassName.of(Override.class));
configure.annotations().addSingle(ClassName.of(Override.class));
typeDeclaration.addMethodDeclaration(configure);
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ TestApplicationTypeCustomizer<JavaTypeDeclaration> junitJupiterTestMethodContrib
JavaMethodDeclaration method = JavaMethodDeclaration.method("contextLoads")
.returning("void")
.body(CodeBlock.of(""));
method.annotations().add(ClassName.of("org.junit.jupiter.api.Test"));
method.annotations().addSingle(ClassName.of("org.junit.jupiter.api.Test"));
typeDeclaration.addMethodDeclaration(method);
};
}
Expand All @@ -85,7 +85,7 @@ ServletInitializerCustomizer<JavaTypeDeclaration> javaServletInitializerCustomiz
Parameter.of("application", "org.springframework.boot.builder.SpringApplicationBuilder"))
.body(CodeBlock.ofStatement("return application.sources($L.class)",
description.getApplicationName()));
configure.annotations().add(ClassName.of(Override.class));
configure.annotations().addSingle(ClassName.of(Override.class));
typeDeclaration.addMethodDeclaration(configure);
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ TestApplicationTypeCustomizer<KotlinTypeDeclaration> junitJupiterTestMethodContr
return (typeDeclaration) -> {
KotlinFunctionDeclaration function = KotlinFunctionDeclaration.function("contextLoads")
.body(CodeBlock.of(""));
function.annotations().add(ClassName.of("org.junit.jupiter.api.Test"));
function.annotations().addSingle(ClassName.of("org.junit.jupiter.api.Test"));
typeDeclaration.addFunctionDeclaration(function);
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,91 +16,233 @@

package io.spring.initializr.generator.language;

import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Stream;

import io.spring.initializr.generator.language.Annotation.Builder;

import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

/**
* A container for {@linkplain Annotation annotations} defined on an annotated element.
* A container for annotations defined on an annotated element.
* <p>
* Supports both single and repeatable annotations. Single annotations can be customized
* even after they have been added.
*
* @author Stephane Nicoll
* @author Moritz Halbritter
*/
public class AnnotationContainer {

private final Map<ClassName, Builder> annotations;
private final Map<ClassName, Builder> singleAnnotations;

private final MultiValueMap<ClassName, Builder> repeatableAnnotations;

public AnnotationContainer() {
this(new LinkedHashMap<>());
this(new LinkedHashMap<>(), new LinkedMultiValueMap<>());
}

private AnnotationContainer(Map<ClassName, Builder> annotations) {
this.annotations = annotations;
private AnnotationContainer(Map<ClassName, Builder> singleAnnotations,
MultiValueMap<ClassName, Builder> repeatableAnnotations) {
this.singleAnnotations = singleAnnotations;
this.repeatableAnnotations = repeatableAnnotations;
}

/**
* Specify if this container is empty.
* @return {@code true} if no annotation is registered
*/
public boolean isEmpty() {
return this.annotations.isEmpty();
return this.singleAnnotations.isEmpty() && this.repeatableAnnotations.isEmpty();
}

/**
* Specify if this container has a an annotation with the specified {@link ClassName}.
* Specify if this container has an annotation with the specified class name.
* Considers both single and repeatable annotations.
* @param className the class name of an annotation
* @return {@code true} if the annotation with the specified class name exists
*/
public boolean has(ClassName className) {
return this.annotations.containsKey(className);
return this.singleAnnotations.containsKey(className) || this.repeatableAnnotations.containsKey(className);
}

/**
* Whether this container has a single annotation with the specified class name.
* @param className the class name of an annotation
* @return whether this container has the annotation
*/
public boolean hasSingle(ClassName className) {
return this.singleAnnotations.containsKey(className);
}

/**
* Whether this container has repeatable annotations with the specified class name.
* @param className the class name of an annotation
* @return whether this container has the annotation
*/
public boolean hasRepeatable(ClassName className) {
return this.repeatableAnnotations.containsKey(className);
}

/**
* Return the {@link Annotation annotations}.
* Return the annotations. Returns both single and repeatable annotations.
* @return the annotations
*/
public Stream<Annotation> values() {
return this.annotations.values().stream().map(Builder::build);
return Stream
.concat(this.singleAnnotations.values().stream(),
this.repeatableAnnotations.values().stream().flatMap(Collection::stream))
.map(Builder::build);
}

/**
* Add a single annotation with the specified class name. Does nothing If the
* annotation has already been added.
* @param className the class name of an annotation
* @deprecated in favor of {@link #addSingle(ClassName)} and
* {@link #addRepeatable(ClassName)}
*/
@Deprecated(forRemoval = true)
public void add(ClassName className) {
add(className, null);
}

/**
* Add a single {@link Annotation} with the specified class name and {@link Consumer}
* to customize it. If the annotation has already been added, the consumer can be used
* to further tune attributes
* to further tune attributes.
* @param className the class name of an annotation
* @param annotation a {@link Consumer} to customize the {@link Annotation}
* @deprecated in favor of {@link #addSingle(ClassName, Consumer)} and
* {@link #addRepeatable(ClassName)}
*/
@Deprecated(forRemoval = true)
public void add(ClassName className, Consumer<Builder> annotation) {
Builder builder = this.annotations.computeIfAbsent(className, (key) -> new Builder(className));
if (hasRepeatable(className)) {
throw new IllegalArgumentException(
"%s has already been used for repeatable annotations".formatted(className));
}
Builder builder = this.singleAnnotations.computeIfAbsent(className, (key) -> new Builder(className));
if (annotation != null) {
annotation.accept(builder);
}
}

/**
* Add a single {@link Annotation} with the specified class name. Does nothing If the
* annotation has already been added.
* Add a single annotation.
* @param className the class name of an annotation
* @return whether the annotation has been added
* @throws IllegalStateException if the annotation has already been used for
* repeatable annotations
*/
public void add(ClassName className) {
add(className, null);
public boolean addSingle(ClassName className) {
return addSingle(className, null);
}

/**
* Add a single annotation with the specified class name. If the annotation already
* exists, this method does nothing.
* @param className the class name of an annotation
* @param annotation a {@link Consumer} to customize the annotation
* @return whether the annotation has been added
* @throws IllegalStateException if the annotation has already been used for
* repeatable annotations
*/
public boolean addSingle(ClassName className, Consumer<Builder> annotation) {
if (hasSingle(className)) {
return false;
}
if (hasRepeatable(className)) {
throw new IllegalStateException("%s has already been used for repeatable annotations".formatted(className));
}
Builder builder = new Builder(className);
if (annotation != null) {
annotation.accept(builder);
}
this.singleAnnotations.put(className, builder);
return true;
}

/**
* Customize a single annotation if it exists. This method does nothing if the
* annotation doesn't exist.
* @param className the class name of an annotation
* @param customizer the customizer for the annotation
*/
public void customizeSingle(ClassName className, Consumer<Builder> customizer) {
Builder builder = this.singleAnnotations.get(className);
if (builder != null) {
customizer.accept(builder);
}
}

/**
* Add a repeatable annotation.
* @param className the class name of an annotation
* @throws IllegalStateException if the annotation has already been added as a single
* annotation
*/
public void addRepeatable(ClassName className) {
addRepeatable(className, null);
}

/**
* Add a repeatable annotation.
* @param className the class name of an annotation
* @param annotation a {@link Consumer} to customize the annotation
* @throws IllegalStateException if the annotation has already been added as a single
* annotation
*/
public void addRepeatable(ClassName className, Consumer<Builder> annotation) {
if (hasSingle(className)) {
throw new IllegalStateException("%s has already been added as a single annotation".formatted(className));
}
Builder builder = new Builder(className);
if (annotation != null) {
annotation.accept(builder);
}
this.repeatableAnnotations.add(className, builder);
}

/**
* Remove the annotation with the specified {@link ClassName}.
* Remove the annotation with the specified classname from either the single
* annotation or the repeatable annotations.
* @param className the class name of the annotation
* @return {@code true} if such an annotation exists, {@code false} otherwise
*/
public boolean remove(ClassName className) {
return this.annotations.remove(className) != null;
return this.singleAnnotations.remove(className) != null || this.repeatableAnnotations.remove(className) != null;
}

/**
* Remove a single with the specified classname.
* @param className the class name of an annotation
* @return whether the annotation has been removed
*/
public boolean removeSingle(ClassName className) {
return this.singleAnnotations.remove(className) != null;
}

/**
* Remove all repeatable annotations with the specified classname.
* @param className the class name of an annotation
* @return whether any annotation has been removed
*/
public boolean removeAllRepeatable(ClassName className) {
return this.repeatableAnnotations.remove(className) != null;
}

public AnnotationContainer deepCopy() {
Map<ClassName, Builder> copy = new LinkedHashMap<>();
this.annotations.forEach((className, builder) -> copy.put(className, new Builder(builder)));
return new AnnotationContainer(copy);
Map<ClassName, Builder> singleAnnotations = new LinkedHashMap<>();
this.singleAnnotations.forEach((className, builder) -> singleAnnotations.put(className, new Builder(builder)));
MultiValueMap<ClassName, Builder> repeatableAnnotations = new LinkedMultiValueMap<>();
this.repeatableAnnotations.forEach((className, builders) -> builders
.forEach((builder) -> repeatableAnnotations.add(className, new Builder(builder))));
return new AnnotationContainer(singleAnnotations, repeatableAnnotations);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,10 @@ public Builder type(String type) {
* Annotate the parameter with the specified annotation.
* @param className the class of the annotation
* @return this for method chaining
* @deprecated in favor of {@link #singleAnnotate(ClassName)} and
* {@link #repeatableAnnotate(ClassName)}
*/
@Deprecated(forRemoval = true)
public Builder annotate(ClassName className) {
return annotate(className, null);
}
Expand All @@ -156,12 +159,58 @@ public Builder annotate(ClassName className) {
* @param className the class of the annotation
* @param annotation a consumer of the builder
* @return this for method chaining
* @deprecated in favor of {@link #singleAnnotate(ClassName, Consumer)} and
* {@link #singleAnnotate(ClassName)}
*/
@Deprecated(forRemoval = true)
@SuppressWarnings("removal")
public Builder annotate(ClassName className, Consumer<Annotation.Builder> annotation) {
this.annotations.add(className, annotation);
return this;
}

/**
* Annotate the parameter with the specified single annotation.
* @param className the class of the annotation
* @return this for method chaining
*/
public Builder singleAnnotate(ClassName className) {
return singleAnnotate(className, null);
}

/**
* Annotate the parameter with the specified single annotation, customized by the
* specified consumer.
* @param className the class of the annotation
* @param annotation a consumer of the builder
* @return this for method chaining
*/
public Builder singleAnnotate(ClassName className, Consumer<Annotation.Builder> annotation) {
this.annotations.addSingle(className, annotation);
return this;
}

/**
* Annotate the parameter with the specified repeatable annotation.
* @param className the class of the annotation
* @return this for method chaining
*/
public Builder repeatableAnnotate(ClassName className) {
return repeatableAnnotate(className, null);
}

/**
* Annotate the parameter with the specified repeatable annotation, customized by
* the specified consumer.
* @param className the class of the annotation
* @param annotation a consumer of the builder
* @return this for method chaining
*/
public Builder repeatableAnnotate(ClassName className, Consumer<Annotation.Builder> annotation) {
this.annotations.addRepeatable(className, annotation);
return this;
}

public Parameter build() {
return new Parameter(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ public AccessorBuilder<?> withAnnotation(ClassName className) {
* @return this for method chaining
*/
public AccessorBuilder<?> withAnnotation(ClassName className, Consumer<Annotation.Builder> annotation) {
this.annotations.add(className, annotation);
this.annotations.addSingle(className, annotation);
return this;
}

Expand Down
Loading