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:
parent
14b147ce70
commit
fd191d165b
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue