Skip to content

Commit b11839d

Browse files
committed
Separate single and repeatable annotation handling
1 parent 2d345f5 commit b11839d

File tree

1 file changed

+151
-51
lines changed

1 file changed

+151
-51
lines changed

initializr-generator/src/main/java/io/spring/initializr/generator/language/AnnotationContainer.java

Lines changed: 151 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -16,89 +16,99 @@
1616

1717
package io.spring.initializr.generator.language;
1818

19+
import java.util.Collection;
1920
import java.util.LinkedHashMap;
2021
import java.util.Map;
2122
import java.util.function.Consumer;
2223
import java.util.stream.Stream;
2324

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

27+
import org.springframework.util.LinkedMultiValueMap;
28+
import org.springframework.util.MultiValueMap;
29+
2630
/**
27-
* A container for {@linkplain Annotation annotations} defined on an annotated element.
31+
* A container for annotations defined on an annotated element.
32+
* <p>
33+
* Supports both single and repeatable annotations. Single annotations can be customized
34+
* even after they have been added.
2835
*
2936
* @author Stephane Nicoll
30-
* @author Sijun Yang
37+
* @author Moritz Halbritter
3138
*/
3239
public class AnnotationContainer {
3340

34-
private final Map<String, Builder> annotations;
41+
private final Map<ClassName, Builder> singleAnnotations;
42+
43+
private final MultiValueMap<ClassName, Builder> repeatableAnnotations;
3544

3645
public AnnotationContainer() {
37-
this(new LinkedHashMap<>());
46+
this(new LinkedHashMap<>(), new LinkedMultiValueMap<>());
3847
}
3948

40-
private AnnotationContainer(Map<String, Builder> annotations) {
41-
this.annotations = annotations;
49+
private AnnotationContainer(Map<ClassName, Builder> singleAnnotations,
50+
MultiValueMap<ClassName, Builder> repeatableAnnotations) {
51+
this.singleAnnotations = singleAnnotations;
52+
this.repeatableAnnotations = repeatableAnnotations;
4253
}
4354

4455
/**
4556
* Specify if this container is empty.
4657
* @return {@code true} if no annotation is registered
4758
*/
4859
public boolean isEmpty() {
49-
return this.annotations.isEmpty();
60+
return this.singleAnnotations.isEmpty() && this.repeatableAnnotations.isEmpty();
5061
}
5162

5263
/**
53-
* Specify if this container has an annotation with the specified {@link ClassName}.
64+
* Specify if this container has an annotation with the specified class name.
65+
* Considers both single and repeatable annotations.
5466
* @param className the class name of an annotation
5567
* @return {@code true} if the annotation with the specified class name exists
5668
*/
5769
public boolean has(ClassName className) {
58-
return this.annotations.containsKey(className.getCanonicalName());
70+
return this.singleAnnotations.containsKey(className) || this.repeatableAnnotations.containsKey(className);
5971
}
6072

6173
/**
62-
* Specify if this instance contains an annotation with the specified name.
63-
* @param name the name of the annotation
64-
* @return {@code true} if an annotation with the specified name is present, otherwise
65-
* {@code false}
74+
* Whether this container has a single annotation with the specified class name.
75+
* @param className the class name of an annotation
76+
* @return whether this container has the annotation
6677
*/
67-
public boolean has(String name) {
68-
return this.annotations.containsKey(name);
78+
public boolean hasSingle(ClassName className) {
79+
return this.singleAnnotations.containsKey(className);
6980
}
7081

7182
/**
72-
* Return the {@link Annotation annotations}.
73-
* @return the annotations
83+
* Whether this container has repeatable annotations with the specified class name.
84+
* @param className the class name of an annotation
85+
* @return whether this container has the annotation
7486
*/
75-
public Stream<Annotation> values() {
76-
return this.annotations.values().stream().map(Builder::build);
87+
public boolean hasRepeatable(ClassName className) {
88+
return this.repeatableAnnotations.containsKey(className);
7789
}
7890

7991
/**
80-
* Add an {@link Annotation} with the specific name and {@link Consumer} to customize
81-
* it. If an annotation with that name already exists, the consumer can be used to
82-
* further tune its attributes.
83-
* @param name the name of the annotation
84-
* @param className the class name of the annotation
85-
* @param annotation a {@link Consumer} to further configure the annotation
92+
* Return the annotations. Returns both single and repeatable annotations.
93+
* @return the annotations
8694
*/
87-
public void add(String name, ClassName className, Consumer<Builder> annotation) {
88-
Builder builder = this.annotations.computeIfAbsent(name, (key) -> new Builder(className));
89-
if (annotation != null) {
90-
annotation.accept(builder);
91-
}
95+
public Stream<Annotation> values() {
96+
return Stream
97+
.concat(this.singleAnnotations.values().stream(),
98+
this.repeatableAnnotations.values().stream().flatMap(Collection::stream))
99+
.map(Builder::build);
92100
}
93101

94102
/**
95-
* Add an {@link Annotation} with the specific name. Does nothing If an annotation
96-
* with that name already exists.
97-
* @param name the name of the annotation
98-
* @param className the class name of the annotation
103+
* Add a single annotation with the specified class name. Does nothing If the
104+
* annotation has already been added.
105+
* @param className the class name of an annotation
106+
* @deprecated in favor of {@link #addSingle(ClassName)} and
107+
* {@link #addRepeatable(ClassName)}
99108
*/
100-
public void add(String name, ClassName className) {
101-
add(name, className, null);
109+
@Deprecated(forRemoval = true)
110+
public void add(ClassName className) {
111+
add(className, null);
102112
}
103113

104114
/**
@@ -107,42 +117,132 @@ public void add(String name, ClassName className) {
107117
* to further tune attributes.
108118
* @param className the class name of an annotation
109119
* @param annotation a {@link Consumer} to customize the {@link Annotation}
120+
* @deprecated in favor of {@link #addSingle(ClassName, Consumer)} and
121+
* {@link #addRepeatable(ClassName)}
110122
*/
123+
@Deprecated(forRemoval = true)
111124
public void add(ClassName className, Consumer<Builder> annotation) {
112-
add(className.getCanonicalName(), className, annotation);
125+
if (hasRepeatable(className)) {
126+
throw new IllegalArgumentException(
127+
"%s has already been used for repeatable annotations".formatted(className));
128+
}
129+
Builder builder = this.singleAnnotations.computeIfAbsent(className, (key) -> new Builder(className));
130+
if (annotation != null) {
131+
annotation.accept(builder);
132+
}
113133
}
114134

115135
/**
116-
* Add a single {@link Annotation} with the specified class name. Does nothing If the
117-
* annotation has already been added.
136+
* Add a single annotation.
118137
* @param className the class name of an annotation
138+
* @return whether the annotation has been added
139+
* @throws IllegalStateException if the annotation has already been used for
140+
* repeatable annotations
119141
*/
120-
public void add(ClassName className) {
121-
add(className.getCanonicalName(), className, null);
142+
public boolean addSingle(ClassName className) {
143+
return addSingle(className, null);
144+
}
145+
146+
/**
147+
* Add a single annotation with the specified class name. If the annotation already
148+
* exists, this method does nothing.
149+
* @param className the class name of an annotation
150+
* @param annotation a {@link Consumer} to customize the annotation
151+
* @return whether the annotation has been added
152+
* @throws IllegalStateException if the annotation has already been used for
153+
* repeatable annotations
154+
*/
155+
public boolean addSingle(ClassName className, Consumer<Builder> annotation) {
156+
if (hasSingle(className)) {
157+
return false;
158+
}
159+
if (hasRepeatable(className)) {
160+
throw new IllegalStateException("%s has already been used for repeatable annotations".formatted(className));
161+
}
162+
Builder builder = new Builder(className);
163+
if (annotation != null) {
164+
annotation.accept(builder);
165+
}
166+
this.singleAnnotations.put(className, builder);
167+
return true;
122168
}
123169

124170
/**
125-
* Remove the annotation with the specified {@link ClassName}.
171+
* Customize a single annotation if it exists. This method does nothing if the
172+
* annotation doesn't exist.
173+
* @param className the class name of an annotation
174+
* @param customizer the customizer for the annotation
175+
*/
176+
public void customizeSingle(ClassName className, Consumer<Builder> customizer) {
177+
Builder builder = this.singleAnnotations.get(className);
178+
if (builder != null) {
179+
customizer.accept(builder);
180+
}
181+
}
182+
183+
/**
184+
* Add a repeatable annotation.
185+
* @param className the class name of an annotation
186+
* @throws IllegalStateException if the annotation has already been added as a single
187+
* annotation
188+
*/
189+
public void addRepeatable(ClassName className) {
190+
addRepeatable(className, null);
191+
}
192+
193+
/**
194+
* Add a repeatable annotation.
195+
* @param className the class name of an annotation
196+
* @param annotation a {@link Consumer} to customize the annotation
197+
* @throws IllegalStateException if the annotation has already been added as a single
198+
* annotation
199+
*/
200+
public void addRepeatable(ClassName className, Consumer<Builder> annotation) {
201+
if (hasSingle(className)) {
202+
throw new IllegalStateException("%s has already been added as a single annotation".formatted(className));
203+
}
204+
Builder builder = new Builder(className);
205+
if (annotation != null) {
206+
annotation.accept(builder);
207+
}
208+
this.repeatableAnnotations.add(className, builder);
209+
}
210+
211+
/**
212+
* Remove the annotation with the specified classname from either the single or the
213+
* repeatable annotations.
126214
* @param className the class name of the annotation
127215
* @return {@code true} if such an annotation exists, {@code false} otherwise
128216
*/
129217
public boolean remove(ClassName className) {
130-
return this.annotations.remove(className.getCanonicalName()) != null;
218+
return this.singleAnnotations.remove(className) != null || this.repeatableAnnotations.remove(className) != null;
131219
}
132220

133221
/**
134-
* Remove the annotation with the specified name.
135-
* @param name the name of the annotation to remove
136-
* @return {@code true} if the annotation was removed, {@code false} otherwise
222+
* Remove a single with the specified classname.
223+
* @param className the class name of an annotation
224+
* @return whether the annotation has been removed
225+
*/
226+
public boolean removeSingle(ClassName className) {
227+
return this.singleAnnotations.remove(className) != null;
228+
}
229+
230+
/**
231+
* Remove all repeatable annotations with the specified classname.
232+
* @param className the class name of an annotation
233+
* @return whether any annotation has been removed
137234
*/
138-
public boolean remove(String name) {
139-
return this.annotations.remove(name) != null;
235+
public boolean removeAllRepeatable(ClassName className) {
236+
return this.repeatableAnnotations.remove(className) != null;
140237
}
141238

142239
public AnnotationContainer deepCopy() {
143-
Map<String, Builder> copy = new LinkedHashMap<>();
144-
this.annotations.forEach((name, builder) -> copy.put(name, new Builder(builder)));
145-
return new AnnotationContainer(copy);
240+
Map<ClassName, Builder> singleAnnotations = new LinkedHashMap<>();
241+
this.singleAnnotations.forEach((className, builder) -> singleAnnotations.put(className, new Builder(builder)));
242+
MultiValueMap<ClassName, Builder> repeatableAnnotations = new LinkedMultiValueMap<>();
243+
this.repeatableAnnotations.forEach((className, builders) -> builders
244+
.forEach((builder) -> repeatableAnnotations.add(className, new Builder(builder))));
245+
return new AnnotationContainer(singleAnnotations, repeatableAnnotations);
146246
}
147247

148248
}

0 commit comments

Comments
 (0)