diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/code/SourceCodeProjectGenerationConfiguration.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/code/SourceCodeProjectGenerationConfiguration.java index 5da16990f3..2a84b3acf1 100644 --- a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/code/SourceCodeProjectGenerationConfiguration.java +++ b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/code/SourceCodeProjectGenerationConfiguration.java @@ -39,13 +39,13 @@ public class SourceCodeProjectGenerationConfiguration { @Bean public MainApplicationTypeCustomizer springBootApplicationAnnotator() { return (typeDeclaration) -> typeDeclaration.annotations() - .add(ClassName.of("org.springframework.boot.autoconfigure.SpringBootApplication")); + .addSingle(ClassName.of("org.springframework.boot.autoconfigure.SpringBootApplication")); } @Bean public TestApplicationTypeCustomizer junitJupiterSpringBootTestTypeCustomizer() { return (typeDeclaration) -> typeDeclaration.annotations() - .add(ClassName.of("org.springframework.boot.test.context.SpringBootTest")); + .addSingle(ClassName.of("org.springframework.boot.test.context.SpringBootTest")); } /** diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/code/groovy/GroovyProjectGenerationDefaultContributorsConfiguration.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/code/groovy/GroovyProjectGenerationDefaultContributorsConfiguration.java index eecbb3bd5c..660d649ddf 100644 --- a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/code/groovy/GroovyProjectGenerationDefaultContributorsConfiguration.java +++ b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/code/groovy/GroovyProjectGenerationDefaultContributorsConfiguration.java @@ -66,7 +66,7 @@ TestApplicationTypeCustomizer 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); }; } @@ -93,7 +93,7 @@ ServletInitializerCustomizer 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); }; } diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/code/java/JavaProjectGenerationDefaultContributorsConfiguration.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/code/java/JavaProjectGenerationDefaultContributorsConfiguration.java index e09332ffdb..71685307e4 100644 --- a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/code/java/JavaProjectGenerationDefaultContributorsConfiguration.java +++ b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/code/java/JavaProjectGenerationDefaultContributorsConfiguration.java @@ -61,7 +61,7 @@ TestApplicationTypeCustomizer 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); }; } @@ -85,7 +85,7 @@ ServletInitializerCustomizer 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); }; } diff --git a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/code/kotlin/KotlinProjectGenerationDefaultContributorsConfiguration.java b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/code/kotlin/KotlinProjectGenerationDefaultContributorsConfiguration.java index dd8dc57926..27fdc33e5e 100644 --- a/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/code/kotlin/KotlinProjectGenerationDefaultContributorsConfiguration.java +++ b/initializr-generator-spring/src/main/java/io/spring/initializr/generator/spring/code/kotlin/KotlinProjectGenerationDefaultContributorsConfiguration.java @@ -57,7 +57,7 @@ TestApplicationTypeCustomizer 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); }; } diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/AnnotationContainer.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/AnnotationContainer.java index 8526797715..0e52e43fe2 100644 --- a/initializr-generator/src/main/java/io/spring/initializr/generator/language/AnnotationContainer.java +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/AnnotationContainer.java @@ -16,6 +16,7 @@ package io.spring.initializr.generator.language; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Consumer; @@ -23,21 +24,32 @@ 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. + *

+ * 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 annotations; + private final Map singleAnnotations; + + private final MultiValueMap repeatableAnnotations; public AnnotationContainer() { - this(new LinkedHashMap<>()); + this(new LinkedHashMap<>(), new LinkedMultiValueMap<>()); } - private AnnotationContainer(Map annotations) { - this.annotations = annotations; + private AnnotationContainer(Map singleAnnotations, + MultiValueMap repeatableAnnotations) { + this.singleAnnotations = singleAnnotations; + this.repeatableAnnotations = repeatableAnnotations; } /** @@ -45,62 +57,192 @@ private AnnotationContainer(Map annotations) { * @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 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 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 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 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 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 copy = new LinkedHashMap<>(); - this.annotations.forEach((className, builder) -> copy.put(className, new Builder(builder))); - return new AnnotationContainer(copy); + Map singleAnnotations = new LinkedHashMap<>(); + this.singleAnnotations.forEach((className, builder) -> singleAnnotations.put(className, new Builder(builder))); + MultiValueMap repeatableAnnotations = new LinkedMultiValueMap<>(); + this.repeatableAnnotations.forEach((className, builders) -> builders + .forEach((builder) -> repeatableAnnotations.add(className, new Builder(builder)))); + return new AnnotationContainer(singleAnnotations, repeatableAnnotations); } } diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/Parameter.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/Parameter.java index af1acbfcd1..e333e95203 100644 --- a/initializr-generator/src/main/java/io/spring/initializr/generator/language/Parameter.java +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/Parameter.java @@ -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); } @@ -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) { 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) { + 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) { + this.annotations.addRepeatable(className, annotation); + return this; + } + public Parameter build() { return new Parameter(this); } diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinPropertyDeclaration.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinPropertyDeclaration.java index 3e2a364fbc..4f9610e029 100644 --- a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinPropertyDeclaration.java +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinPropertyDeclaration.java @@ -281,7 +281,7 @@ public AccessorBuilder withAnnotation(ClassName className) { * @return this for method chaining */ public AccessorBuilder withAnnotation(ClassName className, Consumer annotation) { - this.annotations.add(className, annotation); + this.annotations.addSingle(className, annotation); return this; } diff --git a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriter.java b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriter.java index 7fc79adc1a..ff46ef9f08 100644 --- a/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriter.java +++ b/initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriter.java @@ -162,6 +162,7 @@ private String escapeKotlinKeywords(String packageName) { private void writeProperty(IndentingWriter writer, KotlinPropertyDeclaration propertyDeclaration) { writer.println(); + writeAnnotations(writer, propertyDeclaration); writeModifiers(writer, propertyDeclaration.getModifiers()); if (propertyDeclaration.isVal()) { writer.print("val "); diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/language/AnnotationContainerTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/language/AnnotationContainerTests.java index 5a892004de..144af01f8f 100644 --- a/initializr-generator/src/test/java/io/spring/initializr/generator/language/AnnotationContainerTests.java +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/language/AnnotationContainerTests.java @@ -19,11 +19,13 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link AnnotationContainer}. * * @author Stephane Nicoll + * @author Sijun Yang */ class AnnotationContainerTests { @@ -31,6 +33,8 @@ class AnnotationContainerTests { private static final ClassName NESTED_CLASS_NAME = ClassName.of("com.example.Nested"); + private static final ClassName ANOTHER_CLASS_NAME = ClassName.of("com.example.Another"); + @Test void isEmptyWithEmptyContainer() { AnnotationContainer container = new AnnotationContainer(); @@ -38,6 +42,7 @@ void isEmptyWithEmptyContainer() { } @Test + @SuppressWarnings("removal") void isEmptyWithAnnotation() { AnnotationContainer container = new AnnotationContainer(); container.add(TEST_CLASS_NAME, (annotation) -> annotation.add("value", "test")); @@ -45,6 +50,21 @@ void isEmptyWithAnnotation() { } @Test + void isEmptyWithSingleAnnotation() { + AnnotationContainer container = new AnnotationContainer(); + container.addSingle(TEST_CLASS_NAME); + assertThat(container.isEmpty()).isFalse(); + } + + @Test + void isEmptyWithRepeatableAnnotation() { + AnnotationContainer container = new AnnotationContainer(); + container.addRepeatable(TEST_CLASS_NAME); + assertThat(container.isEmpty()).isFalse(); + } + + @Test + @SuppressWarnings("removal") void hasWithMatchingAnnotation() { AnnotationContainer container = new AnnotationContainer(); container.add(TEST_CLASS_NAME, (annotation) -> annotation.add("value", "test")); @@ -52,6 +72,7 @@ void hasWithMatchingAnnotation() { } @Test + @SuppressWarnings("removal") void hasWithNonMatchingAnnotation() { AnnotationContainer container = new AnnotationContainer(); container.add(TEST_CLASS_NAME, (annotation) -> annotation.add("value", "test")); @@ -59,6 +80,28 @@ void hasWithNonMatchingAnnotation() { } @Test + void hasWithMatchingSingleAnnotation() { + AnnotationContainer container = new AnnotationContainer(); + container.addSingle(TEST_CLASS_NAME); + assertThat(container.has(TEST_CLASS_NAME)).isTrue(); + } + + @Test + void hasWithMatchingRepeatableAnnotation() { + AnnotationContainer container = new AnnotationContainer(); + container.addRepeatable(TEST_CLASS_NAME); + assertThat(container.has(TEST_CLASS_NAME)).isTrue(); + } + + @Test + void hasWithNonMatchingSingleAnnotation() { + AnnotationContainer container = new AnnotationContainer(); + container.addSingle(TEST_CLASS_NAME); + assertThat(container.has(ANOTHER_CLASS_NAME)).isFalse(); + } + + @Test + @SuppressWarnings("removal") void valuesWithSimpleAnnotation() { AnnotationContainer container = new AnnotationContainer(); container.add(TEST_CLASS_NAME, (annotation) -> annotation.add("value", "test")); @@ -73,6 +116,36 @@ void valuesWithSimpleAnnotation() { } @Test + void valuesWithSingleAnnotation() { + AnnotationContainer container = new AnnotationContainer(); + container.addSingle(TEST_CLASS_NAME, (builder) -> builder.add("value", "test")); + assertThat(container.values()).singleElement().satisfies((annotation) -> { + assertThat(annotation.getClassName()).isEqualTo(TEST_CLASS_NAME); + assertThat(annotation.getAttributes()).singleElement().satisfies((attribute) -> { + assertThat(attribute.getName()).isEqualTo("value"); + assertThat(attribute.getValues()).containsExactly("test"); + }); + }); + } + + @Test + void valuesWithRepeatableAnnotations() { + AnnotationContainer container = new AnnotationContainer(); + container.addRepeatable(TEST_CLASS_NAME, (builder) -> builder.add("value", "test1")); + container.addRepeatable(TEST_CLASS_NAME, (builder) -> builder.add("value", "test2")); + assertThat(container.values()).hasSize(2); + } + + @Test + void valuesWithMixedAnnotations() { + AnnotationContainer container = new AnnotationContainer(); + container.addSingle(TEST_CLASS_NAME); + container.addRepeatable(ANOTHER_CLASS_NAME); + assertThat(container.values()).hasSize(2); + } + + @Test + @SuppressWarnings("removal") void addAnnotationSeveralTimeReuseConfiguration() { AnnotationContainer container = new AnnotationContainer(); container.add(TEST_CLASS_NAME, (annotation) -> annotation.add("value", "test")); @@ -88,6 +161,7 @@ void addAnnotationSeveralTimeReuseConfiguration() { } @Test + @SuppressWarnings("removal") void addAnnotationSeveralTimeCanReplaceAttribute() { AnnotationContainer container = new AnnotationContainer(); container.add(TEST_CLASS_NAME, @@ -111,6 +185,7 @@ void addAnnotationSeveralTimeCanReplaceAttribute() { } @Test + @SuppressWarnings("removal") void removeWithMatchingAnnotation() { AnnotationContainer container = new AnnotationContainer(); container.add(TEST_CLASS_NAME, (annotation) -> annotation.add("value", "test")); @@ -119,6 +194,7 @@ void removeWithMatchingAnnotation() { } @Test + @SuppressWarnings("removal") void removeWithNonMatchingAnnotation() { AnnotationContainer container = new AnnotationContainer(); container.add(TEST_CLASS_NAME, (annotation) -> annotation.add("value", "test")); @@ -126,4 +202,227 @@ void removeWithNonMatchingAnnotation() { assertThat(container.isEmpty()).isFalse(); } + @Test + void removeWithSingleAnnotation() { + AnnotationContainer container = new AnnotationContainer(); + container.addSingle(TEST_CLASS_NAME); + assertThat(container.remove(TEST_CLASS_NAME)).isTrue(); + assertThat(container.isEmpty()).isTrue(); + } + + @Test + void removeWithRepeatableAnnotation() { + AnnotationContainer container = new AnnotationContainer(); + container.addRepeatable(TEST_CLASS_NAME); + assertThat(container.remove(TEST_CLASS_NAME)).isTrue(); + assertThat(container.isEmpty()).isTrue(); + } + + @Test + void removeWithNonMatchingSingleAnnotation() { + AnnotationContainer container = new AnnotationContainer(); + container.addSingle(TEST_CLASS_NAME); + assertThat(container.remove(ANOTHER_CLASS_NAME)).isFalse(); + assertThat(container.isEmpty()).isFalse(); + } + + @Test + void removeWithNonMatchingRepeatableAnnotation() { + AnnotationContainer container = new AnnotationContainer(); + container.addRepeatable(TEST_CLASS_NAME); + assertThat(container.remove(ANOTHER_CLASS_NAME)).isFalse(); + assertThat(container.isEmpty()).isFalse(); + } + + @Test + void hasSingleWithSingleAnnotation() { + AnnotationContainer container = new AnnotationContainer(); + container.addSingle(TEST_CLASS_NAME); + assertThat(container.hasSingle(TEST_CLASS_NAME)).isTrue(); + assertThat(container.hasSingle(ANOTHER_CLASS_NAME)).isFalse(); + } + + @Test + void hasSingleWithRepeatableAnnotation() { + AnnotationContainer container = new AnnotationContainer(); + container.addRepeatable(TEST_CLASS_NAME); + assertThat(container.hasSingle(TEST_CLASS_NAME)).isFalse(); + } + + @Test + void addSingle() { + AnnotationContainer container = new AnnotationContainer(); + assertThat(container.addSingle(TEST_CLASS_NAME)).isTrue(); + assertThat(container.hasSingle(TEST_CLASS_NAME)).isTrue(); + } + + @Test + void addSingleTwiceReturnsFalse() { + AnnotationContainer container = new AnnotationContainer(); + container.addSingle(TEST_CLASS_NAME); + assertThat(container.addSingle(TEST_CLASS_NAME)).isFalse(); + } + + @Test + void addSingleWithBuilder() { + AnnotationContainer container = new AnnotationContainer(); + container.addSingle(TEST_CLASS_NAME, (builder) -> builder.add("value", 123)); + assertThat(container.values()).singleElement().satisfies((annotation) -> { + assertThat(annotation.getClassName()).isEqualTo(TEST_CLASS_NAME); + assertThat(annotation.getAttributes()).singleElement().satisfies((attribute) -> { + assertThat(attribute.getName()).isEqualTo("value"); + assertThat(attribute.getValues()).containsExactly(123); + }); + }); + } + + @Test + void customizeSingle() { + AnnotationContainer container = new AnnotationContainer(); + container.addSingle(TEST_CLASS_NAME, (builder) -> builder.add("value", "test")); + container.customizeSingle(TEST_CLASS_NAME, (builder) -> builder.add("value", "another")); + assertThat(container.values()).singleElement() + .satisfies((annotation) -> assertThat(annotation.getAttributes().get(0).getValues()).containsExactly("test", + "another")); + } + + @Test + void customizeSingleCanReplaceAttribute() { + AnnotationContainer container = new AnnotationContainer(); + container.addSingle(TEST_CLASS_NAME, + (builder) -> builder.add("value", Annotation.of(NESTED_CLASS_NAME).add("counter", 42).build())); + container.customizeSingle(TEST_CLASS_NAME, + (builder) -> builder.set("value", Annotation.of(NESTED_CLASS_NAME).add("counter", 24).build())); + assertThat(container.values()).singleElement().satisfies((annotation) -> { + assertThat(annotation.getAttributes()).singleElement().satisfies((attribute) -> { + assertThat(attribute.getName()).isEqualTo("value"); + assertThat(attribute.getValues()).singleElement().isInstanceOfSatisfying(Annotation.class, (nested) -> { + assertThat(nested.getClassName()).isEqualTo(NESTED_CLASS_NAME); + assertThat(nested.getAttributes()).singleElement().satisfies((nestedAttribute) -> { + assertThat(nestedAttribute.getName()).isEqualTo("counter"); + assertThat(nestedAttribute.getValues()).containsExactly(24); + }); + }); + }); + }); + } + + @Test + void customizeSingleOnNonMatchingAnnotationDoesNothing() { + AnnotationContainer container = new AnnotationContainer(); + container.addSingle(NESTED_CLASS_NAME); + container.customizeSingle(ANOTHER_CLASS_NAME, (builder) -> builder.add("value", "test")); + assertThat(container.values()).singleElement() + .satisfies((annotation) -> assertThat(annotation.getAttributes()).isEmpty()); + } + + @Test + void removeSingle() { + AnnotationContainer container = new AnnotationContainer(); + container.addSingle(TEST_CLASS_NAME); + assertThat(container.removeSingle(TEST_CLASS_NAME)).isTrue(); + assertThat(container.isEmpty()).isTrue(); + } + + @Test + void removeSingleWithNonMatchingAnnotation() { + AnnotationContainer container = new AnnotationContainer(); + container.addSingle(TEST_CLASS_NAME); + assertThat(container.removeSingle(ANOTHER_CLASS_NAME)).isFalse(); + assertThat(container.isEmpty()).isFalse(); + } + + @Test + void hasRepeatableWithSingleAnnotation() { + AnnotationContainer container = new AnnotationContainer(); + container.addSingle(TEST_CLASS_NAME); + assertThat(container.hasRepeatable(TEST_CLASS_NAME)).isFalse(); + } + + @Test + void hasRepeatableWithRepeatableAnnotation() { + AnnotationContainer container = new AnnotationContainer(); + container.addRepeatable(TEST_CLASS_NAME); + assertThat(container.hasRepeatable(TEST_CLASS_NAME)).isTrue(); + assertThat(container.hasRepeatable(ANOTHER_CLASS_NAME)).isFalse(); + } + + @Test + void addRepeatable() { + AnnotationContainer container = new AnnotationContainer(); + container.addRepeatable(TEST_CLASS_NAME); + assertThat(container.hasRepeatable(TEST_CLASS_NAME)).isTrue(); + assertThat(container.values()).hasSize(1); + } + + @Test + void addRepeatableTwice() { + AnnotationContainer container = new AnnotationContainer(); + container.addRepeatable(TEST_CLASS_NAME); + container.addRepeatable(TEST_CLASS_NAME); + assertThat(container.hasRepeatable(TEST_CLASS_NAME)).isTrue(); + assertThat(container.values()).hasSize(2); + } + + @Test + void addRepeatableWithBuilder() { + AnnotationContainer container = new AnnotationContainer(); + container.addRepeatable(TEST_CLASS_NAME, (builder) -> builder.add("value", "test1")); + container.addRepeatable(TEST_CLASS_NAME, (builder) -> builder.add("value", "test2")); + assertThat(container.values()).satisfiesExactly( + (annotation) -> assertThat(annotation.getAttributes().get(0).getValues()).containsExactly("test1"), + (annotation) -> assertThat(annotation.getAttributes().get(0).getValues()).containsExactly("test2")); + } + + @Test + void removeAllRepeatable() { + AnnotationContainer container = new AnnotationContainer(); + container.addRepeatable(TEST_CLASS_NAME); + container.addRepeatable(TEST_CLASS_NAME); + assertThat(container.removeAllRepeatable(TEST_CLASS_NAME)).isTrue(); + assertThat(container.isEmpty()).isTrue(); + } + + @Test + void removeAllRepeatableWithNonMatchingAnnotation() { + AnnotationContainer container = new AnnotationContainer(); + container.addRepeatable(TEST_CLASS_NAME); + assertThat(container.removeAllRepeatable(ANOTHER_CLASS_NAME)).isFalse(); + assertThat(container.isEmpty()).isFalse(); + } + + @Test + void addSingleWhenAlreadyRepeatable() { + AnnotationContainer container = new AnnotationContainer(); + container.addRepeatable(TEST_CLASS_NAME); + assertThatIllegalStateException() + .isThrownBy(() -> container.addSingle(TEST_CLASS_NAME, (builder) -> builder.add("value", "test"))) + .withMessageContaining("has already been used for repeatable annotations"); + } + + @Test + void addRepeatableWhenAlreadySingle() { + AnnotationContainer container = new AnnotationContainer(); + container.addSingle(TEST_CLASS_NAME); + assertThatIllegalStateException() + .isThrownBy(() -> container.addRepeatable(TEST_CLASS_NAME, (builder) -> builder.add("value", "test"))) + .withMessageContaining("has already been added as a single annotation"); + } + + @Test + void deepCopy() { + AnnotationContainer original = new AnnotationContainer(); + original.addSingle(TEST_CLASS_NAME, (builder) -> builder.add("value", "test")); + original.addRepeatable(ANOTHER_CLASS_NAME, (builder) -> builder.add("value", "test")); + AnnotationContainer copy = original.deepCopy(); + assertThat(copy).isNotSameAs(original); + assertThat(copy.values()).hasSize(2); + original.customizeSingle(TEST_CLASS_NAME, (builder) -> builder.add("value", "another")); + original.addRepeatable(ANOTHER_CLASS_NAME); + assertThat(copy.values()).hasSize(2); + assertThat(copy.values()).satisfiesExactly( + (annotation) -> assertThat(annotation.getAttributes().get(0).getValues()).containsExactly("test"), + (annotation) -> assertThat(annotation.getAttributes().get(0).getValues()).containsExactly("test")); + } + } diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/language/groovy/GroovySourceCodeWriterTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/language/groovy/GroovySourceCodeWriterTests.java index c960402ef5..6476e2830b 100644 --- a/initializr-generator/src/test/java/io/spring/initializr/generator/language/groovy/GroovySourceCodeWriterTests.java +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/language/groovy/GroovySourceCodeWriterTests.java @@ -167,7 +167,7 @@ void springBootApplication() throws IOException { GroovySourceCode sourceCode = new GroovySourceCode(); GroovyCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test"); GroovyTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); - test.annotations().add(ClassName.of("org.springframework.boot.autoconfigure.SpringBootApplication")); + test.annotations().addSingle(ClassName.of("org.springframework.boot.autoconfigure.SpringBootApplication")); test.addMethodDeclaration(GroovyMethodDeclaration.method("main") .modifiers(Modifier.PUBLIC | Modifier.STATIC) .returning("void") @@ -244,7 +244,7 @@ void fieldAnnotation() throws IOException { GroovyCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test"); GroovyTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); GroovyFieldDeclaration field = GroovyFieldDeclaration.field("testString").returning("java.lang.String"); - field.annotations().add(ClassName.of("org.springframework.beans.factory.annotation.Autowired")); + field.annotations().addSingle(ClassName.of("org.springframework.beans.factory.annotation.Autowired")); test.addFieldDeclaration(field); List lines = writeSingleType(sourceCode, "com/example/Test.groovy"); assertThat(lines).containsExactly("package com.example", "", @@ -283,7 +283,7 @@ private List writeClassAnnotation(String annotationClassName, Consumer lines = writeSingleType(sourceCode, "com/example/Test.groovy"); assertThat(lines).containsExactly("package com.example", "", "import com.example.test.TestAnnotation", "", @@ -312,7 +312,7 @@ void methodWithParameterAnnotation() throws IOException { .returning("void") .parameters(Parameter.builder("service") .type(ClassName.of("com.example.another.MyService")) - .annotate(ClassName.of("com.example.stereotype.Service")) + .singleAnnotate(ClassName.of("com.example.stereotype.Service")) .build()) .body(CodeBlock.of(""))); List lines = writeSingleType(sourceCode, "com/example/Test.groovy"); @@ -321,6 +321,48 @@ void methodWithParameterAnnotation() throws IOException { " void something(@Service MyService service) {", " }", "", "}"); } + @Test + void repeatableClassAnnotations() throws IOException { + GroovySourceCode sourceCode = new GroovySourceCode(); + GroovyCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test"); + GroovyTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + test.annotations().addRepeatable(ClassName.of("com.example.Repeatable")); + test.annotations().addRepeatable(ClassName.of("com.example.Repeatable")); + List lines = writeSingleType(sourceCode, "com/example/Test.groovy"); + assertThat(lines).containsExactly("package com.example", "", "@Repeatable", "@Repeatable", "class Test {", "", + "}"); + } + + @Test + void repeatableFieldAnnotations() throws IOException { + GroovySourceCode sourceCode = new GroovySourceCode(); + GroovyCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test"); + GroovyTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + GroovyFieldDeclaration field = GroovyFieldDeclaration.field("myField").returning("java.lang.String"); + field.annotations().addRepeatable(ClassName.of("com.example.Repeatable")); + field.annotations().addRepeatable(ClassName.of("com.example.Repeatable")); + test.addFieldDeclaration(field); + List lines = writeSingleType(sourceCode, "com/example/Test.groovy"); + assertThat(lines).containsExactly("package com.example", "", "class Test {", "", " @Repeatable", + " @Repeatable", " String myField", "", "}"); + } + + @Test + void repeatableMethodAnnotations() throws IOException { + GroovySourceCode sourceCode = new GroovySourceCode(); + GroovyCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test"); + GroovyTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + GroovyMethodDeclaration method = GroovyMethodDeclaration.method("myMethod") + .returning("void") + .body(CodeBlock.of("")); + method.annotations().addRepeatable(ClassName.of("com.example.Repeatable")); + method.annotations().addRepeatable(ClassName.of("com.example.Repeatable")); + test.addMethodDeclaration(method); + List lines = writeSingleType(sourceCode, "com/example/Test.groovy"); + assertThat(lines).containsExactly("package com.example", "", "class Test {", "", " " + "@Repeatable", + " @Repeatable", " void myMethod() {", " }", "", "}"); + } + private List writeSingleType(GroovySourceCode sourceCode, String location) throws IOException { Path source = writeSourceCode(sourceCode).resolve(location); try (InputStream stream = Files.newInputStream(source)) { diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/language/java/JavaSourceCodeWriterTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/language/java/JavaSourceCodeWriterTests.java index 66ffaf0a76..6a2e18544c 100644 --- a/initializr-generator/src/test/java/io/spring/initializr/generator/language/java/JavaSourceCodeWriterTests.java +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/language/java/JavaSourceCodeWriterTests.java @@ -182,7 +182,7 @@ void fieldAnnotation() throws IOException { JavaFieldDeclaration field = JavaFieldDeclaration.field("testString") .modifiers(Modifier.PRIVATE) .returning("java.lang.String"); - field.annotations().add(ClassName.of("org.springframework.beans.factory.annotation.Autowired")); + field.annotations().addSingle(ClassName.of("org.springframework.beans.factory.annotation.Autowired")); test.addFieldDeclaration(field); List lines = writeSingleType(sourceCode, "com/example/Test.java"); assertThat(lines).containsExactly("package com.example;", "", @@ -242,7 +242,7 @@ void springBootApplication() throws IOException { JavaSourceCode sourceCode = new JavaSourceCode(); JavaCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test"); JavaTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); - test.annotations().add(ClassName.of("org.springframework.boot.autoconfigure.SpringBootApplication")); + test.annotations().addSingle(ClassName.of("org.springframework.boot.autoconfigure.SpringBootApplication")); test.addMethodDeclaration(JavaMethodDeclaration.method("main") .modifiers(Modifier.PUBLIC | Modifier.STATIC) .returning("void") @@ -289,7 +289,7 @@ private List writeClassAnnotation(String annotationClassName, Consumer lines = writeSingleType(sourceCode, "com/example/Test.java"); assertThat(lines).containsExactly("package com.example;", "", "import com.example.test.TestAnnotation;", "", @@ -318,7 +318,7 @@ void methodWithParameterAnnotation() throws IOException { .returning("void") .parameters(Parameter.builder("service") .type(ClassName.of("com.example.another.MyService")) - .annotate(ClassName.of("com.example.stereotype.Service")) + .singleAnnotate(ClassName.of("com.example.stereotype.Service")) .build()) .body(CodeBlock.of(""))); List lines = writeSingleType(sourceCode, "com/example/Test.java"); @@ -327,6 +327,48 @@ void methodWithParameterAnnotation() throws IOException { " void something(@Service MyService service) {", " }", "", "}"); } + @Test + void repeatableClassAnnotations() throws IOException { + JavaSourceCode sourceCode = new JavaSourceCode(); + JavaCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test"); + JavaTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + test.annotations().addRepeatable(ClassName.of("com.example.Repeatable")); + test.annotations().addRepeatable(ClassName.of("com.example.Repeatable")); + List lines = writeSingleType(sourceCode, "com/example/Test.java"); + assertThat(lines).containsExactly("package com.example;", "", "@Repeatable", "@Repeatable", "class Test {", "", + "}"); + } + + @Test + void repeatableFieldAnnotations() throws IOException { + JavaSourceCode sourceCode = new JavaSourceCode(); + JavaCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test"); + JavaTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + JavaFieldDeclaration field = JavaFieldDeclaration.field("myField").returning("java.lang.String"); + field.annotations().addRepeatable(ClassName.of("com.example.Repeatable")); + field.annotations().addRepeatable(ClassName.of("com.example.Repeatable")); + test.addFieldDeclaration(field); + List lines = writeSingleType(sourceCode, "com/example/Test.java"); + assertThat(lines).containsExactly("package com.example;", "", "class Test {", "", " @Repeatable", + " @Repeatable", " String myField;", "", "}"); + } + + @Test + void repeatableMethodAnnotations() throws IOException { + JavaSourceCode sourceCode = new JavaSourceCode(); + JavaCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test"); + JavaTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + JavaMethodDeclaration method = JavaMethodDeclaration.method("myMethod") + .returning("void") + .body(CodeBlock.of("")); + method.annotations().addRepeatable(ClassName.of("com.example.Repeatable")); + method.annotations().addRepeatable(ClassName.of("com.example.Repeatable")); + test.addMethodDeclaration(method); + List lines = writeSingleType(sourceCode, "com/example/Test.java"); + assertThat(lines).containsExactly("package com.example;", "", "class Test {", "", " @Repeatable", + " @Repeatable", " void myMethod() {", " }", "", "}"); + } + private List writeSingleType(JavaSourceCode sourceCode, String location) throws IOException { Path source = writeSourceCode(sourceCode).resolve(location); try (InputStream stream = Files.newInputStream(source)) { diff --git a/initializr-generator/src/test/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriterTests.java b/initializr-generator/src/test/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriterTests.java index 431508048d..d1e3a811a2 100644 --- a/initializr-generator/src/test/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriterTests.java +++ b/initializr-generator/src/test/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriterTests.java @@ -310,7 +310,7 @@ void springBootApplication() throws IOException { KotlinSourceCode sourceCode = new KotlinSourceCode(); KotlinCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test"); KotlinTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); - test.annotations().add(ClassName.of("org.springframework.boot.autoconfigure.SpringBootApplication")); + test.annotations().addSingle(ClassName.of("org.springframework.boot.autoconfigure.SpringBootApplication")); compilationUnit.addTopLevelFunction(KotlinFunctionDeclaration.function("main") .parameters(Parameter.of("args", "Array")) .body(CodeBlock.ofStatement("$T<$L>(*args)", "org.springframework.boot.runApplication", "Test"))); @@ -352,7 +352,7 @@ private List writeClassAnnotation(String annotationClassName, Consumer lines = writeSingleType(sourceCode, "com/example/Test.kt"); assertThat(lines).containsExactly("package com.example", "", "import com.example.test.TestAnnotation", "", @@ -377,7 +377,7 @@ void functionWithParameterAnnotation() throws IOException { test.addFunctionDeclaration(KotlinFunctionDeclaration.function("something") .parameters(Parameter.builder("service") .type(ClassName.of("com.example.another.MyService")) - .annotate(ClassName.of("com.example.stereotype.Service")) + .singleAnnotate(ClassName.of("com.example.stereotype.Service")) .build()) .body(CodeBlock.of(""))); List lines = writeSingleType(sourceCode, "com/example/Test.kt"); @@ -434,6 +434,47 @@ void reservedJavaKeywordsEndPackageName() throws IOException { assertThat(lines).containsExactly("package com.example.`package`"); } + @Test + void repeatableClassAnnotations() throws IOException { + KotlinSourceCode sourceCode = new KotlinSourceCode(); + KotlinCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test"); + KotlinTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + test.annotations().addRepeatable(ClassName.of("com.example.Repeatable")); + test.annotations().addRepeatable(ClassName.of("com.example.Repeatable")); + List lines = writeSingleType(sourceCode, "com/example/Test.kt"); + assertThat(lines).containsExactly("package com.example", "", "@Repeatable", "@Repeatable", "class Test"); + } + + @Test + void repeatablePropertyAnnotations() throws IOException { + KotlinSourceCode sourceCode = new KotlinSourceCode(); + KotlinCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test"); + KotlinTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + KotlinPropertyDeclaration property = KotlinPropertyDeclaration.var("myProperty") + .returning("java.lang.String") + .empty(); + property.annotations().addRepeatable(ClassName.of("com.example.Repeatable")); + property.annotations().addRepeatable(ClassName.of("com.example.Repeatable")); + test.addPropertyDeclaration(property); + List lines = writeSingleType(sourceCode, "com/example/Test.kt"); + assertThat(lines).containsExactly("package com.example", "", "class Test {", "", " @Repeatable", + " @Repeatable", " var myProperty: String", "", "}"); + } + + @Test + void repeatableMethodAnnotations() throws IOException { + KotlinSourceCode sourceCode = new KotlinSourceCode(); + KotlinCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test"); + KotlinTypeDeclaration test = compilationUnit.createTypeDeclaration("Test"); + KotlinFunctionDeclaration method = KotlinFunctionDeclaration.function("myMethod").body(CodeBlock.of("")); + method.annotations().addRepeatable(ClassName.of("com.example.Repeatable")); + method.annotations().addRepeatable(ClassName.of("com.example.Repeatable")); + test.addFunctionDeclaration(method); + List lines = writeSingleType(sourceCode, "com/example/Test.kt"); + assertThat(lines).containsExactly("package com.example", "", "class Test {", "", " @Repeatable", + " @Repeatable", " fun myMethod() {", " }", "", "}"); + } + private List writeSingleType(KotlinSourceCode sourceCode, String location) throws IOException { Path source = writeSourceCode(sourceCode).resolve(location); try (InputStream stream = Files.newInputStream(source)) {