Skip to content

Commit b3ceb0f

Browse files
committed
Add core JavaPoet utilities
This commit adds utilities that facilitate code generation patterns used by the AOT engine. Closes gh-28028
1 parent dfae8ef commit b3ceb0f

File tree

7 files changed

+740
-0
lines changed

7 files changed

+740
-0
lines changed
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*
2+
* Copyright 2002-2022 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 org.springframework.javapoet.support;
18+
19+
import java.io.IOException;
20+
import java.io.StringWriter;
21+
import java.util.function.Consumer;
22+
import java.util.stream.Collectors;
23+
24+
import javax.lang.model.element.Modifier;
25+
26+
import org.springframework.javapoet.CodeBlock;
27+
import org.springframework.javapoet.JavaFile;
28+
import org.springframework.javapoet.MethodSpec;
29+
import org.springframework.javapoet.TypeSpec;
30+
31+
/**
32+
* A code snippet using tabs indentation that is fully processed by JavaPoet so
33+
* that imports are resolved.
34+
*
35+
* @author Stephane Nicoll
36+
* @since 6.0
37+
*/
38+
public final class CodeSnippet {
39+
40+
private static final String START_SNIPPET = "// start-snippet\n";
41+
42+
private static final String END_SNIPPET = "// end-snippet";
43+
44+
private final String fileContent;
45+
46+
private final String snippet;
47+
48+
49+
CodeSnippet(String fileContent, String snippet) {
50+
this.fileContent = fileContent;
51+
this.snippet = snippet;
52+
}
53+
54+
55+
String getFileContent() {
56+
return this.fileContent;
57+
}
58+
59+
/**
60+
* Return the rendered code snippet.
61+
* @return a code snippet where imports have been resolved
62+
*/
63+
public String getSnippet() {
64+
return this.snippet;
65+
}
66+
67+
/**
68+
* Specify if an import statement for the specified type is present.
69+
* @param type the type to check
70+
* @return true if this type has an import statement, false otherwise
71+
*/
72+
public boolean hasImport(Class<?> type) {
73+
return hasImport(type.getName());
74+
}
75+
76+
/**
77+
* Specify if an import statement for the specified class name is present.
78+
* @param className the name of the class to check
79+
* @return true if this type has an import statement, false otherwise
80+
*/
81+
public boolean hasImport(String className) {
82+
return getFileContent().lines().anyMatch(candidate ->
83+
candidate.equals(String.format("import %s;", className)));
84+
}
85+
86+
/**
87+
* Return a new {@link CodeSnippet} where the specified number of indentations
88+
* have been removed.
89+
* @param indent the number of indent to remove
90+
* @return a CodeSnippet instance with the number of indentations removed
91+
*/
92+
public CodeSnippet removeIndent(int indent) {
93+
return new CodeSnippet(this.fileContent, this.snippet.lines().map(line ->
94+
removeIndent(line, indent)).collect(Collectors.joining("\n")));
95+
}
96+
97+
/**
98+
* Create a {@link CodeSnippet} using the specified code.
99+
* @param code the code snippet
100+
* @return a {@link CodeSnippet} instance
101+
*/
102+
public static CodeSnippet of(CodeBlock code) {
103+
return new Builder().build(code);
104+
}
105+
106+
/**
107+
* Process the specified code and return a fully-processed code snippet
108+
* as a String.
109+
* @param code a consumer to use to generate the code snippet
110+
* @return a resolved code snippet
111+
*/
112+
public static String process(Consumer<CodeBlock.Builder> code) {
113+
CodeBlock.Builder body = CodeBlock.builder();
114+
code.accept(body);
115+
return process(body.build());
116+
}
117+
118+
/**
119+
* Process the specified {@link CodeBlock code} and return a
120+
* fully-processed code snippet as a String.
121+
* @param code the code snippet
122+
* @return a resolved code snippet
123+
*/
124+
public static String process(CodeBlock code) {
125+
return of(code).getSnippet();
126+
}
127+
128+
private String removeIndent(String line, int indent) {
129+
for (int i = 0; i < indent; i++) {
130+
if (line.startsWith("\t")) {
131+
line = line.substring(1);
132+
}
133+
}
134+
return line;
135+
}
136+
137+
private static final class Builder {
138+
139+
private static final String INDENT = "\t";
140+
141+
private static final String SNIPPET_INDENT = INDENT + INDENT;
142+
143+
public CodeSnippet build(CodeBlock code) {
144+
MethodSpec.Builder method = MethodSpec.methodBuilder("test")
145+
.addModifiers(Modifier.PUBLIC);
146+
CodeBlock.Builder body = CodeBlock.builder();
147+
body.add(START_SNIPPET);
148+
body.add(code);
149+
body.add(END_SNIPPET);
150+
method.addCode(body.build());
151+
String fileContent = write(createTestJavaFile(method.build()));
152+
String snippet = isolateGeneratedContent(fileContent);
153+
return new CodeSnippet(fileContent, snippet);
154+
}
155+
156+
private String isolateGeneratedContent(String javaFile) {
157+
int start = javaFile.indexOf(START_SNIPPET);
158+
String tmp = javaFile.substring(start + START_SNIPPET.length());
159+
int end = tmp.indexOf(END_SNIPPET);
160+
tmp = tmp.substring(0, end);
161+
// Remove indent
162+
return tmp.lines().map(line -> {
163+
if (!line.startsWith(SNIPPET_INDENT)) {
164+
throw new IllegalStateException("Missing indent for " + line);
165+
}
166+
return line.substring(SNIPPET_INDENT.length());
167+
}).collect(Collectors.joining("\n"));
168+
}
169+
170+
private JavaFile createTestJavaFile(MethodSpec method) {
171+
return JavaFile.builder("example", TypeSpec.classBuilder("Test")
172+
.addModifiers(Modifier.PUBLIC)
173+
.addMethod(method).build()).indent(INDENT).build();
174+
}
175+
176+
private String write(JavaFile file) {
177+
try {
178+
StringWriter out = new StringWriter();
179+
file.writeTo(out);
180+
return out.toString();
181+
}
182+
catch (IOException ex) {
183+
throw new IllegalStateException("Failed to write " + file, ex);
184+
}
185+
}
186+
187+
}
188+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2002-2022 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 org.springframework.javapoet.support;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.function.Consumer;
22+
23+
import org.springframework.javapoet.CodeBlock;
24+
import org.springframework.javapoet.CodeBlock.Builder;
25+
26+
27+
/**
28+
* A {@link CodeBlock} wrapper for joining multiple blocks.
29+
*
30+
* @author Stephane Nicoll
31+
* @since 6.0
32+
*/
33+
public class MultiCodeBlock {
34+
35+
private final List<CodeBlock> codeBlocks = new ArrayList<>();
36+
37+
38+
/**
39+
* Add the specified {@link CodeBlock}.
40+
* @param code the code block to add
41+
*/
42+
public void add(CodeBlock code) {
43+
if (code.isEmpty()) {
44+
throw new IllegalArgumentException("Could not add empty CodeBlock");
45+
}
46+
this.codeBlocks.add(code);
47+
}
48+
49+
/**
50+
* Add a {@link CodeBlock} using the specified callback.
51+
* @param code the callback to use
52+
*/
53+
public void add(Consumer<Builder> code) {
54+
Builder builder = CodeBlock.builder();
55+
code.accept(builder);
56+
add(builder.build());
57+
}
58+
59+
/**
60+
* Add a code block using the specified formatted String and the specified
61+
* arguments.
62+
* @param code the code
63+
* @param arguments the arguments
64+
* @see Builder#add(String, Object...)
65+
*/
66+
public void add(String code, Object... arguments) {
67+
add(CodeBlock.of(code, arguments));
68+
}
69+
70+
/**
71+
* Return a {@link CodeBlock} that joins the different blocks registered in
72+
* this instance with the specified delimiter.
73+
* @param delimiter the delimiter to use (not {@literal null})
74+
* @return a {@link CodeBlock} joining the blocks of this instance with the
75+
* specified {@code delimiter}
76+
* @see CodeBlock#join(Iterable, String)
77+
*/
78+
public CodeBlock join(String delimiter) {
79+
return CodeBlock.join(this.codeBlocks, delimiter);
80+
}
81+
82+
}

0 commit comments

Comments
 (0)