Skip to content

Commit 2386966

Browse files
committed
Support interface implementation in SourceCodeWriter
Closes gh-1617
1 parent 95200d2 commit 2386966

File tree

8 files changed

+204
-2
lines changed

8 files changed

+204
-2
lines changed

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

+35
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@
1616

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

19+
import java.util.Collection;
20+
import java.util.Collections;
21+
import java.util.List;
22+
1923
/**
2024
* A type declared in a {@link CompilationUnit}.
2125
*
2226
* @author Andy Wilkinson
27+
* @author Moritz Halbritter
2328
*/
2429
public class TypeDeclaration implements Annotatable {
2530

@@ -29,14 +34,40 @@ public class TypeDeclaration implements Annotatable {
2934

3035
private String extendedClassName;
3136

37+
private List<String> implementsClassNames = Collections.emptyList();
38+
39+
/**
40+
* Creates a new instance.
41+
* @param name the type name
42+
*/
3243
public TypeDeclaration(String name) {
3344
this.name = name;
3445
}
3546

47+
/**
48+
* Extend the class with the given name.
49+
* @param name the name of the class to extend
50+
*/
3651
public void extend(String name) {
3752
this.extendedClassName = name;
3853
}
3954

55+
/**
56+
* Implement the given interfaces.
57+
* @param names the names of the interfaces to implement
58+
*/
59+
public void implement(Collection<String> names) {
60+
this.implementsClassNames = List.copyOf(names);
61+
}
62+
63+
/**
64+
* Implement the given interfaces.
65+
* @param names the names of the interfaces to implement
66+
*/
67+
public void implement(String... names) {
68+
this.implementsClassNames = List.of(names);
69+
}
70+
4071
@Override
4172
public AnnotationContainer annotations() {
4273
return this.annotations;
@@ -50,4 +81,8 @@ public String getExtends() {
5081
return this.extendedClassName;
5182
}
5283

84+
public List<String> getImplements() {
85+
return this.implementsClassNames;
86+
}
87+
5388
}

initializr-generator/src/main/java/io/spring/initializr/generator/language/groovy/GroovySourceCodeWriter.java

+18
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
import io.spring.initializr.generator.language.SourceCodeWriter;
4949
import io.spring.initializr.generator.language.SourceStructure;
5050

51+
import org.springframework.util.CollectionUtils;
52+
5153
/**
5254
* A {@link SourceCodeWriter} that writes {@link SourceCode} in Groovy.
5355
*
@@ -125,6 +127,9 @@ private void writeTo(SourceStructure structure, GroovyCompilationUnit compilatio
125127
if (type.getExtends() != null) {
126128
writer.print(" extends " + getUnqualifiedName(type.getExtends()));
127129
}
130+
if (!CollectionUtils.isEmpty(type.getImplements())) {
131+
writeImplements(type, writer);
132+
}
128133
writer.println(" {");
129134
writer.println();
130135
List<GroovyFieldDeclaration> fieldDeclarations = type.getFieldDeclarations();
@@ -148,6 +153,18 @@ private void writeTo(SourceStructure structure, GroovyCompilationUnit compilatio
148153
}
149154
}
150155

156+
private void writeImplements(GroovyTypeDeclaration type, IndentingWriter writer) {
157+
writer.print(" implements ");
158+
Iterator<String> iterator = type.getImplements().iterator();
159+
while (iterator.hasNext()) {
160+
String name = iterator.next();
161+
writer.print(getUnqualifiedName(name));
162+
if (iterator.hasNext()) {
163+
writer.print(", ");
164+
}
165+
}
166+
}
167+
151168
private void writeAnnotations(IndentingWriter writer, Annotatable annotatable, Runnable separator) {
152169
annotatable.annotations().values().forEach((annotation) -> {
153170
annotation.write(writer, FORMATTING_OPTIONS);
@@ -216,6 +233,7 @@ private Set<String> determineImports(GroovyCompilationUnit compilationUnit) {
216233
List<String> imports = new ArrayList<>();
217234
for (GroovyTypeDeclaration typeDeclaration : compilationUnit.getTypeDeclarations()) {
218235
imports.add(typeDeclaration.getExtends());
236+
imports.addAll(typeDeclaration.getImplements());
219237
imports.addAll(appendImports(typeDeclaration.annotations().values(), Annotation::getImports));
220238
for (GroovyFieldDeclaration fieldDeclaration : typeDeclaration.getFieldDeclarations()) {
221239
imports.add(fieldDeclaration.getReturnType());

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

+19-1
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,14 @@
4545
import io.spring.initializr.generator.language.SourceCodeWriter;
4646
import io.spring.initializr.generator.language.SourceStructure;
4747

48+
import org.springframework.util.CollectionUtils;
49+
4850
/**
4951
* A {@link SourceCodeWriter} that writes {@link SourceCode} in Java.
5052
*
5153
* @author Andy Wilkinson
5254
* @author Matt Berteaux
55+
* @author Moritz Halbritter
5356
*/
5457
public class JavaSourceCodeWriter implements SourceCodeWriter<JavaSourceCode> {
5558

@@ -122,6 +125,9 @@ private void writeTo(SourceStructure structure, JavaCompilationUnit compilationU
122125
if (type.getExtends() != null) {
123126
writer.print(" extends " + getUnqualifiedName(type.getExtends()));
124127
}
128+
if (!CollectionUtils.isEmpty(type.getImplements())) {
129+
writeImplements(type, writer);
130+
}
125131
writer.println(" {");
126132
writer.println();
127133
List<JavaFieldDeclaration> fieldDeclarations = type.getFieldDeclarations();
@@ -145,6 +151,18 @@ private void writeTo(SourceStructure structure, JavaCompilationUnit compilationU
145151
}
146152
}
147153

154+
private void writeImplements(JavaTypeDeclaration type, IndentingWriter writer) {
155+
writer.print(" implements ");
156+
Iterator<String> iterator = type.getImplements().iterator();
157+
while (iterator.hasNext()) {
158+
String name = iterator.next();
159+
writer.print(getUnqualifiedName(name));
160+
if (iterator.hasNext()) {
161+
writer.print(", ");
162+
}
163+
}
164+
}
165+
148166
private void writeAnnotations(IndentingWriter writer, Annotatable annotatable, Runnable separator) {
149167
annotatable.annotations().values().forEach((annotation) -> {
150168
annotation.write(writer, CodeBlock.JAVA_FORMATTING_OPTIONS);
@@ -213,7 +231,7 @@ private Set<String> determineImports(JavaCompilationUnit compilationUnit) {
213231
List<String> imports = new ArrayList<>();
214232
for (JavaTypeDeclaration typeDeclaration : compilationUnit.getTypeDeclarations()) {
215233
imports.add(typeDeclaration.getExtends());
216-
234+
imports.addAll(typeDeclaration.getImplements());
217235
imports.addAll(appendImports(typeDeclaration.annotations().values(), Annotation::getImports));
218236
for (JavaFieldDeclaration fieldDeclaration : typeDeclaration.getFieldDeclarations()) {
219237
imports.add(fieldDeclaration.getReturnType());

initializr-generator/src/main/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriter.java

+20-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
import io.spring.initializr.generator.language.SourceCodeWriter;
4747
import io.spring.initializr.generator.language.SourceStructure;
4848

49+
import org.springframework.util.CollectionUtils;
50+
4951
/**
5052
* A {@link SourceCodeWriter} that writes {@link SourceCode} in Kotlin.
5153
*
@@ -95,9 +97,14 @@ private void writeTo(SourceStructure structure, KotlinCompilationUnit compilatio
9597
writeAnnotations(writer, type);
9698
writeModifiers(writer, type.getModifiers());
9799
writer.print("class " + type.getName());
98-
if (type.getExtends() != null) {
100+
boolean hasExtends = type.getExtends() != null;
101+
if (hasExtends) {
99102
writer.print(" : " + getUnqualifiedName(type.getExtends()) + "()");
100103
}
104+
if (!CollectionUtils.isEmpty(type.getImplements())) {
105+
writer.print(hasExtends ? ", " : " : ");
106+
writeImplements(type, writer);
107+
}
101108
List<KotlinPropertyDeclaration> propertyDeclarations = type.getPropertyDeclarations();
102109
List<KotlinFunctionDeclaration> functionDeclarations = type.getFunctionDeclarations();
103110
boolean hasDeclarations = !propertyDeclarations.isEmpty() || !functionDeclarations.isEmpty();
@@ -136,6 +143,17 @@ private void writeTo(SourceStructure structure, KotlinCompilationUnit compilatio
136143
}
137144
}
138145

146+
private void writeImplements(KotlinTypeDeclaration type, IndentingWriter writer) {
147+
Iterator<String> iterator = type.getImplements().iterator();
148+
while (iterator.hasNext()) {
149+
String name = iterator.next();
150+
writer.print(getUnqualifiedName(name));
151+
if (iterator.hasNext()) {
152+
writer.print(", ");
153+
}
154+
}
155+
}
156+
139157
private String escapeKotlinKeywords(String packageName) {
140158
return Arrays.stream(packageName.split("\\."))
141159
.map((segment) -> this.language.isKeyword(segment) ? "`" + segment + "`" : segment)
@@ -240,6 +258,7 @@ private Set<String> determineImports(KotlinCompilationUnit compilationUnit) {
240258
List<String> imports = new ArrayList<>();
241259
for (KotlinTypeDeclaration typeDeclaration : compilationUnit.getTypeDeclarations()) {
242260
imports.add(typeDeclaration.getExtends());
261+
imports.addAll(typeDeclaration.getImplements());
243262
imports.addAll(appendImports(typeDeclaration.annotations().values(), Annotation::getImports));
244263
typeDeclaration.getPropertyDeclarations()
245264
.forEach(((propertyDeclaration) -> imports.addAll(determinePropertyImports(propertyDeclaration))));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.spring.initializr.generator.language;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
23+
/**
24+
* Tests for {@link TypeDeclaration}.
25+
*
26+
* @author Moritz Halbritter
27+
*/
28+
class TypeDeclarationTests {
29+
30+
@Test
31+
void implementWithVarArgs() {
32+
TypeDeclaration declaration = new TypeDeclaration("Test");
33+
declaration.implement("Interface1", "Interface2");
34+
assertThat(declaration.getImplements()).containsExactly("Interface1", "Interface2");
35+
}
36+
37+
}

initializr-generator/src/test/java/io/spring/initializr/generator/language/groovy/GroovySourceCodeWriterTests.java

+25
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
*
4949
* @author Stephane Nicoll
5050
* @author Matt Berteaux
51+
* @author Moritz Halbritter
5152
*/
5253
class GroovySourceCodeWriterTests {
5354

@@ -109,6 +110,30 @@ void emptyTypeDeclarationWithSuperClass() throws IOException {
109110
"class Test extends TestParent {", "", "}");
110111
}
111112

113+
@Test
114+
void shouldAddImplements() throws IOException {
115+
GroovySourceCode sourceCode = new GroovySourceCode();
116+
GroovyCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test");
117+
GroovyTypeDeclaration test = compilationUnit.createTypeDeclaration("Test");
118+
test.implement(List.of("com.example.build.Interface1", "com.example.build.Interface2"));
119+
List<String> lines = writeSingleType(sourceCode, "com/example/Test.groovy");
120+
assertThat(lines).containsExactly("package com.example", "", "import com.example.build.Interface1",
121+
"import com.example.build.Interface2", "", "class Test implements Interface1, Interface2 {", "", "}");
122+
}
123+
124+
@Test
125+
void shouldAddExtendsAndImplements() throws IOException {
126+
GroovySourceCode sourceCode = new GroovySourceCode();
127+
GroovyCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test");
128+
GroovyTypeDeclaration test = compilationUnit.createTypeDeclaration("Test");
129+
test.extend("com.example.build.TestParent");
130+
test.implement(List.of("com.example.build.Interface1", "com.example.build.Interface2"));
131+
List<String> lines = writeSingleType(sourceCode, "com/example/Test.groovy");
132+
assertThat(lines).containsExactly("package com.example", "", "import com.example.build.Interface1",
133+
"import com.example.build.Interface2", "import com.example.build.TestParent", "",
134+
"class Test extends TestParent implements Interface1, Interface2 {", "", "}");
135+
}
136+
112137
@Test
113138
void method() throws IOException {
114139
GroovySourceCode sourceCode = new GroovySourceCode();

initializr-generator/src/test/java/io/spring/initializr/generator/language/java/JavaSourceCodeWriterTests.java

+25
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
*
4949
* @author Andy Wilkinson
5050
* @author Matt Berteaux
51+
* @author Moritz Halbritter
5152
*/
5253
class JavaSourceCodeWriterTests {
5354

@@ -108,6 +109,30 @@ void emptyTypeDeclarationWithSuperClass() throws IOException {
108109
"class Test extends TestParent {", "", "}");
109110
}
110111

112+
@Test
113+
void shouldAddImplements() throws IOException {
114+
JavaSourceCode sourceCode = new JavaSourceCode();
115+
JavaCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test");
116+
JavaTypeDeclaration test = compilationUnit.createTypeDeclaration("Test");
117+
test.implement(List.of("com.example.build.Interface1", "com.example.build.Interface2"));
118+
List<String> lines = writeSingleType(sourceCode, "com/example/Test.java");
119+
assertThat(lines).containsExactly("package com.example;", "", "import com.example.build.Interface1;",
120+
"import com.example.build.Interface2;", "", "class Test implements Interface1, Interface2 {", "", "}");
121+
}
122+
123+
@Test
124+
void shouldAddExtendsAndImplements() throws IOException {
125+
JavaSourceCode sourceCode = new JavaSourceCode();
126+
JavaCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test");
127+
JavaTypeDeclaration test = compilationUnit.createTypeDeclaration("Test");
128+
test.extend("com.example.build.TestParent");
129+
test.implement(List.of("com.example.build.Interface1", "com.example.build.Interface2"));
130+
List<String> lines = writeSingleType(sourceCode, "com/example/Test.java");
131+
assertThat(lines).containsExactly("package com.example;", "", "import com.example.build.Interface1;",
132+
"import com.example.build.Interface2;", "import com.example.build.TestParent;", "",
133+
"class Test extends TestParent implements Interface1, Interface2 {", "", "}");
134+
}
135+
111136
@Test
112137
void method() throws IOException {
113138
JavaSourceCode sourceCode = new JavaSourceCode();

initializr-generator/src/test/java/io/spring/initializr/generator/language/kotlin/KotlinSourceCodeWriterTests.java

+25
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
*
4848
* @author Stephane Nicoll
4949
* @author Matt Berteaux
50+
* @author Moritz Halbritter
5051
*/
5152
class KotlinSourceCodeWriterTests {
5253

@@ -108,6 +109,30 @@ void emptyTypeDeclarationWithSuperClass() throws IOException {
108109
"class Test : TestParent()");
109110
}
110111

112+
@Test
113+
void shouldImplementInterfaces() throws IOException {
114+
KotlinSourceCode sourceCode = new KotlinSourceCode();
115+
KotlinCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test");
116+
KotlinTypeDeclaration test = compilationUnit.createTypeDeclaration("Test");
117+
test.implement(List.of("com.example.build.Interface1", "com.example.build.Interface2"));
118+
List<String> lines = writeSingleType(sourceCode, "com/example/Test.kt");
119+
assertThat(lines).containsExactly("package com.example", "", "import com.example.build.Interface1",
120+
"import com.example.build.Interface2", "", "class Test : Interface1, Interface2");
121+
}
122+
123+
@Test
124+
void shouldExtendAndImplement() throws IOException {
125+
KotlinSourceCode sourceCode = new KotlinSourceCode();
126+
KotlinCompilationUnit compilationUnit = sourceCode.createCompilationUnit("com.example", "Test");
127+
KotlinTypeDeclaration test = compilationUnit.createTypeDeclaration("Test");
128+
test.extend("com.example.build.TestParent");
129+
test.implement(List.of("com.example.build.Interface1", "com.example.build.Interface2"));
130+
List<String> lines = writeSingleType(sourceCode, "com/example/Test.kt");
131+
assertThat(lines).containsExactly("package com.example", "", "import com.example.build.Interface1",
132+
"import com.example.build.Interface2", "import com.example.build.TestParent", "",
133+
"class Test : TestParent(), Interface1, Interface2");
134+
}
135+
111136
@Test
112137
void function() throws IOException {
113138
KotlinSourceCode sourceCode = new KotlinSourceCode();

0 commit comments

Comments
 (0)