Add GeneratedType infrastructure

This commit adds an infrastructure for code that generate types with the
need to write to another package if privileged access is required. An
abstraction around types where methods can be easily added is also
available as part of this commit.

Closes gh-28149
This commit is contained in:
Stephane Nicoll 2022-03-04 10:17:43 +01:00
parent 14b147ce70
commit fd191d165b
5 changed files with 498 additions and 0 deletions

View File

@ -0,0 +1,92 @@
/*
* 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.aot.generator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.javapoet.JavaFile;
/**
* Default {@link GeneratedTypeContext} implementation.
*
* @author Stephane Nicoll
* @since 6.0
*/
public class DefaultGeneratedTypeContext implements GeneratedTypeContext {
private final String packageName;
private final RuntimeHints runtimeHints;
private final Function<String, GeneratedType> generatedTypeFactory;
private final Map<String, GeneratedType> generatedTypes;
/**
* Create a context targeting the specified package name and using the specified
* factory to create a {@link GeneratedType} per requested package name.
* @param packageName the main package name
* @param generatedTypeFactory the factory to use to create a {@link GeneratedType}
* based on a package name.
*/
public DefaultGeneratedTypeContext(String packageName, Function<String, GeneratedType> generatedTypeFactory) {
this.packageName = packageName;
this.runtimeHints = new RuntimeHints();
this.generatedTypeFactory = generatedTypeFactory;
this.generatedTypes = new LinkedHashMap<>();
}
@Override
public RuntimeHints runtimeHints() {
return this.runtimeHints;
}
@Override
public GeneratedType getGeneratedType(String packageName) {
return this.generatedTypes.computeIfAbsent(packageName, this.generatedTypeFactory);
}
@Override
public GeneratedType getMainGeneratedType() {
return getGeneratedType(this.packageName);
}
/**
* Specify if a {@link GeneratedType} for the specified package name is registered.
* @param packageName the package name to use
* @return {@code true} if a type is registered for that package
*/
public boolean hasGeneratedType(String packageName) {
return this.generatedTypes.containsKey(packageName);
}
/**
* Return the list of {@link JavaFile} of known generated type.
* @return the java files of bootstrap classes in this instance
*/
public List<JavaFile> toJavaFiles() {
return this.generatedTypes.values().stream()
.map(GeneratedType::toJavaFile)
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,125 @@
/*
* 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.aot.generator;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
import javax.lang.model.element.Modifier;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.JavaFile;
import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.TypeSpec;
/**
* Wrapper for a generated {@linkplain TypeSpec type}.
*
* @author Stephane Nicoll
* @since 6.0
*/
public class GeneratedType {
private final ClassName className;
private final TypeSpec.Builder type;
private final List<MethodSpec> methods;
GeneratedType(ClassName className, Consumer<TypeSpec.Builder> type) {
this.className = className;
this.type = TypeSpec.classBuilder(className);
type.accept(this.type);
this.methods = new ArrayList<>();
}
/**
* Create an instance for the specified {@link ClassName}, customizing the type with
* the specified {@link Consumer consumer callback}.
* @param className the class name
* @param type a callback to customize the type, i.e. to change default modifiers
* @return a new {@link GeneratedType}
*/
public static GeneratedType of(ClassName className, Consumer<TypeSpec.Builder> type) {
return new GeneratedType(className, type);
}
/**
* Create an instance for the specified {@link ClassName}, as a {@code public} type.
* @param className the class name
* @return a new {@link GeneratedType}
*/
public static GeneratedType of(ClassName className) {
return of(className, type -> type.addModifiers(Modifier.PUBLIC));
}
/**
* Return the {@link ClassName} of this instance.
* @return the class name
*/
public ClassName getClassName() {
return this.className;
}
/**
* Customize the type of this instance.
* @param type the consumer of the type builder
* @return this for method chaining
*/
public GeneratedType customizeType(Consumer<TypeSpec.Builder> type) {
type.accept(this.type);
return this;
}
/**
* Add a method using the state of the specified {@link MethodSpec.Builder},
* updating the name of the method if a similar method already exists.
* @param method a method builder representing the method to add
* @return the added method
*/
public MethodSpec addMethod(MethodSpec.Builder method) {
MethodSpec methodToAdd = createUniqueNameIfNecessary(method.build());
this.methods.add(methodToAdd);
return methodToAdd;
}
/**
* Return a {@link JavaFile} with the state of this instance.
* @return a java file
*/
public JavaFile toJavaFile() {
return JavaFile.builder(this.className.packageName(),
this.type.addMethods(this.methods).build()).indent("\t").build();
}
private MethodSpec createUniqueNameIfNecessary(MethodSpec method) {
List<MethodSpec> candidates = this.methods.stream().filter(isSimilar(method)).toList();
if (candidates.isEmpty()) {
return method;
}
MethodSpec updatedMethod = method.toBuilder().setName(method.name + "_").build();
return createUniqueNameIfNecessary(updatedMethod);
}
private Predicate<MethodSpec> isSimilar(MethodSpec method) {
return candidate -> method.name.equals(candidate.name)
&& method.parameters.size() == candidate.parameters.size();
}
}

View File

@ -0,0 +1,52 @@
/*
* 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.aot.generator;
import org.springframework.aot.hint.RuntimeHints;
/**
* Context passed to object that can generate code, giving access to a main
* {@link GeneratedType} as well as to a {@link GeneratedType} in a given
* package if privileged access is required.
*
* @author Stephane Nicoll
* @since 6.0
*/
public interface GeneratedTypeContext {
/**
* Return the {@link RuntimeHints} instance to use to contribute hints for
* generated types.
* @return the runtime hints
*/
RuntimeHints runtimeHints();
/**
* Return a {@link GeneratedType} for the specified package. If it does not
* exist, it is created.
* @param packageName the package name to use
* @return a generated type
*/
GeneratedType getGeneratedType(String packageName);
/**
* Return the main {@link GeneratedType}.
* @return the generated type for the target package
*/
GeneratedType getMainGeneratedType();
}

View File

@ -0,0 +1,100 @@
/*
* 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.aot.generator;
import javax.lang.model.element.Modifier;
import org.junit.jupiter.api.Test;
import org.springframework.javapoet.ClassName;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DefaultGeneratedTypeContext}.
*
* @author Stephane Nicoll
*/
class DefaultGeneratedTypeContextTests {
@Test
void runtimeHints() {
DefaultGeneratedTypeContext context = createComAcmeContext();
assertThat(context.runtimeHints()).isNotNull();
}
@Test
void getGeneratedTypeMatchesGetMainGeneratedTypeForMainPackage() {
DefaultGeneratedTypeContext context = createComAcmeContext();
assertThat(context.getMainGeneratedType().getClassName()).isEqualTo(ClassName.get("com.acme", "Main"));
assertThat(context.getGeneratedType("com.acme")).isSameAs(context.getMainGeneratedType());
}
@Test
void getMainGeneratedTypeIsLazilyCreated() {
DefaultGeneratedTypeContext context = createComAcmeContext();
assertThat(context.hasGeneratedType("com.acme")).isFalse();
context.getMainGeneratedType();
assertThat(context.hasGeneratedType("com.acme")).isTrue();
}
@Test
void getGeneratedTypeRegisterInstance() {
DefaultGeneratedTypeContext context = createComAcmeContext();
assertThat(context.hasGeneratedType("com.example")).isFalse();
GeneratedType generatedType = context.getGeneratedType("com.example");
assertThat(generatedType).isNotNull();
assertThat(generatedType.getClassName().simpleName()).isEqualTo("Main");
assertThat(context.hasGeneratedType("com.example")).isTrue();
}
@Test
void getGeneratedTypeReuseInstance() {
DefaultGeneratedTypeContext context = createComAcmeContext();
GeneratedType generatedType = context.getGeneratedType("com.example");
assertThat(generatedType.getClassName().packageName()).isEqualTo("com.example");
assertThat(context.getGeneratedType("com.example")).isSameAs(generatedType);
}
@Test
void toJavaFilesWithNoTypeIsEmpty() {
DefaultGeneratedTypeContext writerContext = createComAcmeContext();
assertThat(writerContext.toJavaFiles()).hasSize(0);
}
@Test
void toJavaFilesWithDefaultTypeIsAddedLazily() {
DefaultGeneratedTypeContext writerContext = createComAcmeContext();
writerContext.getMainGeneratedType();
assertThat(writerContext.toJavaFiles()).hasSize(1);
}
@Test
void toJavaFilesWithDefaultTypeAndAdditionaTypes() {
DefaultGeneratedTypeContext writerContext = createComAcmeContext();
writerContext.getGeneratedType("com.example");
writerContext.getGeneratedType("com.another");
writerContext.getGeneratedType("com.another.another");
assertThat(writerContext.toJavaFiles()).hasSize(3);
}
private DefaultGeneratedTypeContext createComAcmeContext() {
return new DefaultGeneratedTypeContext("com.acme", packageName ->
GeneratedType.of(ClassName.get(packageName, "Main"), type -> type.addModifiers(Modifier.PUBLIC)));
}
}

View File

@ -0,0 +1,129 @@
/*
* 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.aot.generator;
import java.io.IOException;
import java.io.StringWriter;
import javax.lang.model.element.Modifier;
import org.junit.jupiter.api.Test;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.FieldSpec;
import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.TypeName;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GeneratedType}.
*
* @author Stephane Nicoll
*/
class GeneratedTypeTests {
private static final ClassName TEST_CLASS_NAME = ClassName.get("com.acme", "Test");
@Test
void className() {
GeneratedType generatedType = new GeneratedType(TEST_CLASS_NAME,
type -> type.addModifiers(Modifier.STATIC));
assertThat(generatedType.getClassName()).isEqualTo(TEST_CLASS_NAME);
assertThat(generateCode(generatedType)).contains("static class Test {");
}
@Test
void createWithCustomField() {
GeneratedType generatedType = new GeneratedType(TEST_CLASS_NAME,
type -> type.addField(FieldSpec.builder(TypeName.BOOLEAN, "enabled").build()));
assertThat(generateCode(generatedType)).contains("boolean enabled;");
}
@Test
void customizeType() {
GeneratedType generatedType = createTestGeneratedType();
generatedType.customizeType(type -> type.addJavadoc("Test javadoc."))
.customizeType(type -> type.addJavadoc(" Another test javadoc"));
assertThat(generateCode(generatedType)).containsSequence(
"/**\n",
" * Test javadoc. Another test javadoc\n",
" */");
}
@Test
void addMethod() {
GeneratedType generatedType = createTestGeneratedType();
generatedType.addMethod(MethodSpec.methodBuilder("test").returns(Integer.class)
.addCode(CodeBlock.of("return 42;")));
assertThat(generateCode(generatedType)).containsSequence(
"\tInteger test() {\n",
"\t\treturn 42;\n",
"\t}");
}
@Test
void addMultipleMethods() {
GeneratedType generatedType = createTestGeneratedType();
generatedType.addMethod(MethodSpec.methodBuilder("first"));
generatedType.addMethod(MethodSpec.methodBuilder("second"));
assertThat(generateCode(generatedType))
.containsSequence("\tvoid first() {\n", "\t}")
.containsSequence("\tvoid second() {\n", "\t}");
}
@Test
void addSimilarMethodGenerateUniqueNames() {
GeneratedType generatedType = createTestGeneratedType();
MethodSpec firstMethod = generatedType.addMethod(MethodSpec.methodBuilder("test"));
MethodSpec secondMethod = generatedType.addMethod(MethodSpec.methodBuilder("test"));
MethodSpec thirdMethod = generatedType.addMethod(MethodSpec.methodBuilder("test"));
assertThat(firstMethod.name).isEqualTo("test");
assertThat(secondMethod.name).isEqualTo("test_");
assertThat(thirdMethod.name).isEqualTo("test__");
assertThat(generateCode(generatedType))
.containsSequence("\tvoid test() {\n", "\t}")
.containsSequence("\tvoid test_() {\n", "\t}")
.containsSequence("\tvoid test__() {\n", "\t}");
}
@Test
void addMethodWithSameNameAndDifferentArgumentsDoesNotChangeName() {
GeneratedType generatedType = createTestGeneratedType();
generatedType.addMethod(MethodSpec.methodBuilder("test"));
MethodSpec secondMethod = generatedType.addMethod(MethodSpec.methodBuilder("test")
.addParameter(String.class, "param"));
assertThat(secondMethod.name).isEqualTo("test");
}
private GeneratedType createTestGeneratedType() {
return GeneratedType.of(TEST_CLASS_NAME);
}
private String generateCode(GeneratedType generatedType) {
try {
StringWriter out = new StringWriter();
generatedType.toJavaFile().writeTo(out);
return out.toString();
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
}