Add core JavaPoet utilities

This commit adds utilities that facilitate code generation patterns
used by the AOT engine.

Closes gh-28028
This commit is contained in:
Stephane Nicoll 2022-02-10 14:57:37 +01:00
parent dfae8effa8
commit b3ceb0f625
7 changed files with 740 additions and 0 deletions

View File

@ -0,0 +1,188 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.javapoet.support;
import java.io.IOException;
import java.io.StringWriter;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.lang.model.element.Modifier;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.JavaFile;
import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.TypeSpec;
/**
* A code snippet using tabs indentation that is fully processed by JavaPoet so
* that imports are resolved.
*
* @author Stephane Nicoll
* @since 6.0
*/
public final class CodeSnippet {
private static final String START_SNIPPET = "// start-snippet\n";
private static final String END_SNIPPET = "// end-snippet";
private final String fileContent;
private final String snippet;
CodeSnippet(String fileContent, String snippet) {
this.fileContent = fileContent;
this.snippet = snippet;
}
String getFileContent() {
return this.fileContent;
}
/**
* Return the rendered code snippet.
* @return a code snippet where imports have been resolved
*/
public String getSnippet() {
return this.snippet;
}
/**
* Specify if an import statement for the specified type is present.
* @param type the type to check
* @return true if this type has an import statement, false otherwise
*/
public boolean hasImport(Class<?> type) {
return hasImport(type.getName());
}
/**
* Specify if an import statement for the specified class name is present.
* @param className the name of the class to check
* @return true if this type has an import statement, false otherwise
*/
public boolean hasImport(String className) {
return getFileContent().lines().anyMatch(candidate ->
candidate.equals(String.format("import %s;", className)));
}
/**
* Return a new {@link CodeSnippet} where the specified number of indentations
* have been removed.
* @param indent the number of indent to remove
* @return a CodeSnippet instance with the number of indentations removed
*/
public CodeSnippet removeIndent(int indent) {
return new CodeSnippet(this.fileContent, this.snippet.lines().map(line ->
removeIndent(line, indent)).collect(Collectors.joining("\n")));
}
/**
* Create a {@link CodeSnippet} using the specified code.
* @param code the code snippet
* @return a {@link CodeSnippet} instance
*/
public static CodeSnippet of(CodeBlock code) {
return new Builder().build(code);
}
/**
* Process the specified code and return a fully-processed code snippet
* as a String.
* @param code a consumer to use to generate the code snippet
* @return a resolved code snippet
*/
public static String process(Consumer<CodeBlock.Builder> code) {
CodeBlock.Builder body = CodeBlock.builder();
code.accept(body);
return process(body.build());
}
/**
* Process the specified {@link CodeBlock code} and return a
* fully-processed code snippet as a String.
* @param code the code snippet
* @return a resolved code snippet
*/
public static String process(CodeBlock code) {
return of(code).getSnippet();
}
private String removeIndent(String line, int indent) {
for (int i = 0; i < indent; i++) {
if (line.startsWith("\t")) {
line = line.substring(1);
}
}
return line;
}
private static final class Builder {
private static final String INDENT = "\t";
private static final String SNIPPET_INDENT = INDENT + INDENT;
public CodeSnippet build(CodeBlock code) {
MethodSpec.Builder method = MethodSpec.methodBuilder("test")
.addModifiers(Modifier.PUBLIC);
CodeBlock.Builder body = CodeBlock.builder();
body.add(START_SNIPPET);
body.add(code);
body.add(END_SNIPPET);
method.addCode(body.build());
String fileContent = write(createTestJavaFile(method.build()));
String snippet = isolateGeneratedContent(fileContent);
return new CodeSnippet(fileContent, snippet);
}
private String isolateGeneratedContent(String javaFile) {
int start = javaFile.indexOf(START_SNIPPET);
String tmp = javaFile.substring(start + START_SNIPPET.length());
int end = tmp.indexOf(END_SNIPPET);
tmp = tmp.substring(0, end);
// Remove indent
return tmp.lines().map(line -> {
if (!line.startsWith(SNIPPET_INDENT)) {
throw new IllegalStateException("Missing indent for " + line);
}
return line.substring(SNIPPET_INDENT.length());
}).collect(Collectors.joining("\n"));
}
private JavaFile createTestJavaFile(MethodSpec method) {
return JavaFile.builder("example", TypeSpec.classBuilder("Test")
.addModifiers(Modifier.PUBLIC)
.addMethod(method).build()).indent(INDENT).build();
}
private String write(JavaFile file) {
try {
StringWriter out = new StringWriter();
file.writeTo(out);
return out.toString();
}
catch (IOException ex) {
throw new IllegalStateException("Failed to write " + file, ex);
}
}
}
}

View File

@ -0,0 +1,82 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.javapoet.support;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
/**
* A {@link CodeBlock} wrapper for joining multiple blocks.
*
* @author Stephane Nicoll
* @since 6.0
*/
public class MultiCodeBlock {
private final List<CodeBlock> codeBlocks = new ArrayList<>();
/**
* Add the specified {@link CodeBlock}.
* @param code the code block to add
*/
public void add(CodeBlock code) {
if (code.isEmpty()) {
throw new IllegalArgumentException("Could not add empty CodeBlock");
}
this.codeBlocks.add(code);
}
/**
* Add a {@link CodeBlock} using the specified callback.
* @param code the callback to use
*/
public void add(Consumer<Builder> code) {
Builder builder = CodeBlock.builder();
code.accept(builder);
add(builder.build());
}
/**
* Add a code block using the specified formatted String and the specified
* arguments.
* @param code the code
* @param arguments the arguments
* @see Builder#add(String, Object...)
*/
public void add(String code, Object... arguments) {
add(CodeBlock.of(code, arguments));
}
/**
* Return a {@link CodeBlock} that joins the different blocks registered in
* this instance with the specified delimiter.
* @param delimiter the delimiter to use (not {@literal null})
* @return a {@link CodeBlock} joining the blocks of this instance with the
* specified {@code delimiter}
* @see CodeBlock#join(Iterable, String)
*/
public CodeBlock join(String delimiter) {
return CodeBlock.join(this.codeBlocks, delimiter);
}
}

View File

@ -0,0 +1,194 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.javapoet.support;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
/**
* A {@link CodeBlock} wrapper for multiple statements.
*
* @author Stephane Nicoll
* @since 6.0
*/
public final class MultiStatement {
private final List<Statement> statements = new ArrayList<>();
/**
* Specify if this instance is empty.
* @return {@code true} if no statement is registered, {@code false} otherwise
*/
public boolean isEmpty() {
return this.statements.isEmpty();
}
/**
* Add the specified {@link CodeBlock codeblock} rendered as-is.
* @param codeBlock the code block to add
* @see #addStatement(CodeBlock) to add a code block that represents
* a statement
*/
public void add(CodeBlock codeBlock) {
this.statements.add(Statement.of(codeBlock));
}
/**
* Add a {@link CodeBlock} rendered as-is using the specified callback.
* @param code the callback to use
* @see #addStatement(CodeBlock) to add a code block that represents
* a statement
*/
public void add(Consumer<Builder> code) {
CodeBlock.Builder builder = CodeBlock.builder();
code.accept(builder);
add(builder.build());
}
/**
* Add a statement.
* @param statement the statement to add
*/
public void addStatement(CodeBlock statement) {
this.statements.add(Statement.ofStatement(statement));
}
/**
* Add a statement using the specified callback.
* @param code the callback to use
*/
public void addStatement(Consumer<Builder> code) {
CodeBlock.Builder builder = CodeBlock.builder();
code.accept(builder);
addStatement(builder.build());
}
/**
* Add a statement using the specified formatted String and the specified
* arguments.
* @param code the code of the statement
* @param args the arguments for placeholders
* @see CodeBlock#of(String, Object...)
*/
public void addStatement(String code, Object... args) {
addStatement(CodeBlock.of(code, args));
}
/**
* Add the statements produced from the {@code itemGenerator} applied on the specified
* items.
* @param items the items to handle, each item is represented as a statement
* @param itemGenerator the item generator
* @param <T> the type of the item
*/
public <T> void addAll(Iterable<T> items, Function<T, CodeBlock> itemGenerator) {
items.forEach(element -> addStatement(itemGenerator.apply(element)));
}
/**
* Return a {@link CodeBlock} that applies all the {@code statements} of this
* instance. If only one statement is available, it is not completed using the
* {@code ;} termination so that it can be used in the context of a lambda.
* @return the statement(s)
*/
public CodeBlock toCodeBlock() {
Builder code = CodeBlock.builder();
for (int i = 0; i < this.statements.size(); i++) {
Statement statement = this.statements.get(i);
statement.contribute(code, this.isMulti(), i == this.statements.size() - 1);
}
return code.build();
}
/**
* Return a {@link CodeBlock} that applies all the {@code statements} of this
* instance in the context of a lambda.
* @param lambda the context of the lambda, must end with {@code ->}
* @return the lambda body
*/
public CodeBlock toCodeBlock(CodeBlock lambda) {
Builder code = CodeBlock.builder();
code.add(lambda);
if (isMulti()) {
code.beginControlFlow("");
}
else {
code.add(" ");
}
code.add(toCodeBlock());
if (isMulti()) {
code.add("\n").unindent().add("}");
}
return code.build();
}
/**
* Return a {@link CodeBlock} that applies all the {@code statements} of this
* instance in the context of a lambda.
* @param lambda the context of the lambda, must end with {@code ->}
* @return the lambda body
*/
public CodeBlock toCodeBlock(String lambda) {
return toCodeBlock(CodeBlock.of(lambda));
}
private boolean isMulti() {
return this.statements.size() > 1;
}
private static class Statement {
private final CodeBlock codeBlock;
private final boolean addStatementTermination;
Statement(CodeBlock codeBlock, boolean addStatementTermination) {
this.codeBlock = codeBlock;
this.addStatementTermination = addStatementTermination;
}
void contribute(CodeBlock.Builder code, boolean multi, boolean isLastStatement) {
code.add(this.codeBlock);
if (this.addStatementTermination) {
if (!isLastStatement) {
code.add(";\n");
}
else if (multi) {
code.add(";");
}
}
}
static Statement ofStatement(CodeBlock codeBlock) {
return new Statement(codeBlock, true);
}
static Statement of(CodeBlock codeBlock) {
return new Statement(codeBlock, false);
}
}
}

View File

@ -0,0 +1,9 @@
/**
* Support classes for JavaPoet usage.
*/
@NonNullApi
@NonNullFields
package org.springframework.javapoet.support;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@ -0,0 +1,75 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.javapoet.support;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.javapoet.CodeBlock;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CodeSnippet}.
*
* @author Stephane Nicoll
*/
class CodeSnippetTests {
@Test
void snippetUsesTabs() {
CodeBlock.Builder code = CodeBlock.builder();
code.beginControlFlow("if (condition)");
code.addStatement("bean.doThis()");
code.endControlFlow();
CodeSnippet codeSnippet = CodeSnippet.of(code.build());
assertThat(codeSnippet.getSnippet()).isEqualTo("""
if (condition) {
bean.doThis();
}
""");
}
@Test
void snippetResolvesImports() {
CodeSnippet codeSnippet = CodeSnippet.of(
CodeBlock.of("$T list = new $T<>()", List.class, ArrayList.class));
assertThat(codeSnippet.getSnippet()).isEqualTo("List list = new ArrayList<>()");
assertThat(codeSnippet.hasImport(List.class)).isTrue();
assertThat(codeSnippet.hasImport(ArrayList.class)).isTrue();
}
@Test
void removeIndent() {
CodeBlock.Builder code = CodeBlock.builder();
code.beginControlFlow("if (condition)");
code.addStatement("doStuff()");
code.endControlFlow();
CodeSnippet snippet = CodeSnippet.of(code.build());
assertThat(snippet.getSnippet().lines()).contains("\tdoStuff();");
assertThat(snippet.removeIndent(1).getSnippet().lines()).contains("doStuff();");
}
@Test
void processProvidesSnippet() {
assertThat(CodeSnippet.process(code -> code.add("$T list;", List.class)))
.isEqualTo("List list;");
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.javapoet.support;
import org.junit.jupiter.api.Test;
import org.springframework.javapoet.CodeBlock;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link MultiCodeBlock}.
*
* @author Stephane Nicoll
*/
class MultiCodeBlockTests {
@Test
void joinWithNoElement() {
MultiCodeBlock multi = new MultiCodeBlock();
assertThat(multi.join(", ").isEmpty()).isTrue();
}
@Test
void joinWithEmptyElement() {
MultiCodeBlock multi = new MultiCodeBlock();
assertThatIllegalArgumentException().isThrownBy(() -> multi.add(CodeBlock.builder().build()));
}
@Test
void joinWithSingleElement() {
MultiCodeBlock multi = new MultiCodeBlock();
multi.add(CodeBlock.of("$S", "Hello"));
assertThat(multi.join(", ")).hasToString("\"Hello\"");
}
@Test
void joinWithSeveralElement() {
MultiCodeBlock multi = new MultiCodeBlock();
multi.add(CodeBlock.of("$S", "Hello"));
multi.add(code -> code.add("42"));
multi.add("null");
assertThat(multi.join(", ")).hasToString("\"Hello\", 42, null");
}
}

View File

@ -0,0 +1,131 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.javapoet.support;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.javapoet.CodeBlock;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link MultiStatement}.
*
* @author Stephane Nicoll
*/
class MultiStatementTests {
@Test
void isEmptyWithNoStatement() {
assertThat(new MultiStatement().isEmpty()).isTrue();
}
@Test
void isEmptyWithStatement() {
MultiStatement statements = new MultiStatement();
statements.addStatement(CodeBlock.of("int i = 0"));
assertThat(statements.isEmpty()).isFalse();
}
@Test
void singleStatement() {
MultiStatement statements = new MultiStatement();
statements.addStatement("field.method($S)", "hello");
CodeBlock codeBlock = statements.toCodeBlock();
assertThat(codeBlock.toString()).isEqualTo("field.method(\"hello\")");
}
@Test
void singleStatementWithCallback() {
MultiStatement statements = new MultiStatement();
statements.addStatement(code -> code.add("field.method($S)", "hello"));
CodeBlock codeBlock = statements.toCodeBlock();
assertThat(codeBlock.toString()).isEqualTo("field.method(\"hello\")");
}
@Test
void singleStatementWithCodeBlock() {
MultiStatement statements = new MultiStatement();
statements.addStatement(CodeBlock.of("field.method($S)", "hello"));
CodeBlock codeBlock = statements.toCodeBlock();
assertThat(codeBlock.toString()).isEqualTo("field.method(\"hello\")");
}
@Test
void multiStatements() {
MultiStatement statements = new MultiStatement();
statements.addStatement("field.method($S)", "hello");
statements.addStatement("field.anotherMethod($S)", "hello");
CodeBlock codeBlock = statements.toCodeBlock();
assertThat(codeBlock.toString()).isEqualTo("""
field.method("hello");
field.anotherMethod("hello");""");
}
@Test
void multiStatementsWithCodeBlockRenderedAsIs() {
MultiStatement statements = new MultiStatement();
statements.addStatement("field.method($S)", "hello");
statements.add(CodeBlock.of(("// Hello\n")));
statements.add(code -> code.add("// World\n"));
statements.addStatement("field.anotherMethod($S)", "hello");
CodeBlock codeBlock = statements.toCodeBlock();
assertThat(codeBlock.toString()).isEqualTo("""
field.method("hello");
// Hello
// World
field.anotherMethod("hello");""");
}
@Test
void singleStatementWithLambda() {
MultiStatement statements = new MultiStatement();
statements.addStatement("field.method($S)", "hello");
CodeBlock codeBlock = statements.toCodeBlock(CodeBlock.of("() ->"));
assertThat(codeBlock.toString()).isEqualTo("() -> field.method(\"hello\")");
}
@Test
void multiStatementsWithLambda() {
MultiStatement statements = new MultiStatement();
statements.addStatement("field.method($S)", "hello");
statements.addStatement("field.anotherMethod($S)", "hello");
CodeBlock codeBlock = statements.toCodeBlock(CodeBlock.of("() ->"));
assertThat(codeBlock.toString().lines()).containsExactly(
"() -> {",
" field.method(\"hello\");",
" field.anotherMethod(\"hello\");",
"}");
}
@Test
void multiStatementsWithAddAll() {
MultiStatement statements = new MultiStatement();
statements.addAll(List.of(0, 1, 2),
index -> CodeBlock.of("field[$L] = $S", index, "hello"));
CodeBlock codeBlock = statements.toCodeBlock("() ->");
assertThat(codeBlock.toString().lines()).containsExactly(
"() -> {",
" field[0] = \"hello\";",
" field[1] = \"hello\";",
" field[2] = \"hello\";",
"}");
}
}