Add support for generating classes
Add `ClassGenerator` and `GeneratedClass` which can be used to generated classes that will ultimately be written to a `JavaFile`. See gh-28414
This commit is contained in:
parent
55d7f7a014
commit
d5374550e5
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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.generate;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.javapoet.ClassName;
|
||||
import org.springframework.javapoet.JavaFile;
|
||||
|
||||
/**
|
||||
* Generates new {@link GeneratedClass} instances.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 6.0
|
||||
* @see GeneratedMethods
|
||||
*/
|
||||
public interface ClassGenerator {
|
||||
|
||||
/**
|
||||
* Get or generate a new {@link GeneratedClass} for a given java file
|
||||
* generator, target and feature name.
|
||||
* @param javaFileGenerator the java file generator
|
||||
* @param target the target of the newly generated class
|
||||
* @param featureName the name of the feature that the generated class
|
||||
* supports
|
||||
* @return a {@link GeneratedClass} instance
|
||||
*/
|
||||
GeneratedClass getOrGenerateClass(JavaFileGenerator javaFileGenerator,
|
||||
Class<?> target, String featureName);
|
||||
|
||||
/**
|
||||
* Get or generate a new {@link GeneratedClass} for a given java file
|
||||
* generator, target and feature name.
|
||||
* @param javaFileGenerator the java file generator
|
||||
* @param target the target of the newly generated class
|
||||
* @param featureName the name of the feature that the generated class
|
||||
* supports
|
||||
* @return a {@link GeneratedClass} instance
|
||||
*/
|
||||
GeneratedClass getOrGenerateClass(JavaFileGenerator javaFileGenerator, String target,
|
||||
String featureName);
|
||||
|
||||
|
||||
/**
|
||||
* Strategy used to generate the java file for the generated class.
|
||||
* Implementations of this interface are included as part of the key used to
|
||||
* identify classes that have already been created and as such should be
|
||||
* static final instances or implement a valid
|
||||
* {@code equals}/{@code hashCode}.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface JavaFileGenerator {
|
||||
|
||||
/**
|
||||
* Generate the file {@link JavaFile} to be written.
|
||||
* @param className the class name of the file
|
||||
* @param methods the generated methods that must be included
|
||||
* @return the generated files
|
||||
*/
|
||||
JavaFile generateJavaFile(ClassName className, GeneratedMethods methods);
|
||||
|
||||
/**
|
||||
* Return method names that must not be generated.
|
||||
* @return the reserved method names
|
||||
*/
|
||||
default Collection<String> getReservedMethodNames() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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.generate;
|
||||
|
||||
import org.springframework.aot.generate.ClassGenerator.JavaFileGenerator;
|
||||
import org.springframework.javapoet.ClassName;
|
||||
import org.springframework.javapoet.JavaFile;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A generated class.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 6.0
|
||||
* @see GeneratedClasses
|
||||
* @see ClassGenerator
|
||||
*/
|
||||
public final class GeneratedClass {
|
||||
|
||||
private final JavaFileGenerator JavaFileGenerator;
|
||||
|
||||
private final ClassName name;
|
||||
|
||||
private final GeneratedMethods methods;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new {@link GeneratedClass} instance with the given name. This
|
||||
* constructor is package-private since names should only be generated via a
|
||||
* {@link GeneratedClasses}.
|
||||
* @param name the generated name
|
||||
*/
|
||||
GeneratedClass(JavaFileGenerator javaFileGenerator, ClassName name) {
|
||||
MethodNameGenerator methodNameGenerator = new MethodNameGenerator(
|
||||
javaFileGenerator.getReservedMethodNames());
|
||||
this.JavaFileGenerator = javaFileGenerator;
|
||||
this.name = name;
|
||||
this.methods = new GeneratedMethods(methodNameGenerator);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the name of the generated class.
|
||||
* @return the name of the generated class
|
||||
*/
|
||||
public ClassName getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the method generator that can be used for this generated class.
|
||||
* @return the method generator
|
||||
*/
|
||||
public MethodGenerator getMethodGenerator() {
|
||||
return this.methods;
|
||||
}
|
||||
|
||||
JavaFile generateJavaFile() {
|
||||
JavaFile javaFile = this.JavaFileGenerator.generateJavaFile(this.name,
|
||||
this.methods);
|
||||
Assert.state(this.name.packageName().equals(javaFile.packageName),
|
||||
() -> "Generated JavaFile should be in package '"
|
||||
+ this.name.packageName() + "'");
|
||||
Assert.state(this.name.simpleName().equals(javaFile.typeSpec.name),
|
||||
() -> "Generated JavaFile should be named '" + this.name.simpleName()
|
||||
+ "'");
|
||||
return javaFile;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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.generate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A managed collection of generated classes.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 6.0
|
||||
* @see GeneratedClass
|
||||
*/
|
||||
public class GeneratedClasses implements ClassGenerator {
|
||||
|
||||
private final ClassNameGenerator classNameGenerator;
|
||||
|
||||
private final Map<Owner, GeneratedClass> classes = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
public GeneratedClasses(ClassNameGenerator classNameGenerator) {
|
||||
Assert.notNull(classNameGenerator, "'classNameGenerator' must not be null");
|
||||
this.classNameGenerator = classNameGenerator;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public GeneratedClass getOrGenerateClass(JavaFileGenerator javaFileGenerator,
|
||||
Class<?> target, String featureName) {
|
||||
|
||||
Assert.notNull(javaFileGenerator, "'javaFileGenerator' must not be null");
|
||||
Assert.notNull(target, "'target' must not be null");
|
||||
Assert.hasLength(featureName, "'featureName' must not be empty");
|
||||
Owner owner = new Owner(javaFileGenerator, target.getName(), featureName);
|
||||
return this.classes.computeIfAbsent(owner,
|
||||
key -> new GeneratedClass(javaFileGenerator,
|
||||
this.classNameGenerator.generateClassName(target, featureName)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public GeneratedClass getOrGenerateClass(JavaFileGenerator javaFileGenerator,
|
||||
String target, String featureName) {
|
||||
|
||||
Assert.notNull(javaFileGenerator, "'javaFileGenerator' must not be null");
|
||||
Assert.hasLength(target, "'target' must not be empty");
|
||||
Assert.hasLength(featureName, "'featureName' must not be empty");
|
||||
Owner owner = new Owner(javaFileGenerator, target, featureName);
|
||||
return this.classes.computeIfAbsent(owner,
|
||||
key -> new GeneratedClass(javaFileGenerator,
|
||||
this.classNameGenerator.generateClassName(target, featureName)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Write generated Spring {@code .factories} files to the given
|
||||
* {@link GeneratedFiles} instance.
|
||||
* @param generatedFiles where to write the generated files
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public void writeTo(GeneratedFiles generatedFiles) throws IOException {
|
||||
Assert.notNull(generatedFiles, "'generatedFiles' must not be null");
|
||||
List<GeneratedClass> generatedClasses = new ArrayList<>(this.classes.values());
|
||||
generatedClasses.sort(Comparator.comparing(GeneratedClass::getName));
|
||||
for (GeneratedClass generatedClass : generatedClasses) {
|
||||
generatedFiles.addSourceFile(generatedClass.generateJavaFile());
|
||||
}
|
||||
}
|
||||
|
||||
private record Owner(JavaFileGenerator javaFileGenerator, String target,
|
||||
String featureName) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.generate;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.javapoet.ClassName;
|
||||
import org.springframework.javapoet.JavaFile;
|
||||
import org.springframework.javapoet.TypeSpec;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for {@link GeneratedClass}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class GeneratedClassTests {
|
||||
|
||||
@Test
|
||||
void getNameReturnsName() {
|
||||
ClassName name = ClassName.bestGuess("com.example.Test");
|
||||
GeneratedClass generatedClass = new GeneratedClass(this::generateJavaFile, name);
|
||||
assertThat(generatedClass.getName()).isSameAs(name);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateJavaFileSuppliesGeneratedMethods() {
|
||||
ClassName name = ClassName.bestGuess("com.example.Test");
|
||||
GeneratedClass generatedClass = new GeneratedClass(this::generateJavaFile, name);
|
||||
MethodGenerator methodGenerator = generatedClass.getMethodGenerator();
|
||||
methodGenerator.generateMethod("test")
|
||||
.using(builder -> builder.addJavadoc("Test Method"));
|
||||
assertThat(generatedClass.generateJavaFile().toString()).contains("Test Method");
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateJavaFileWhenHasBadPackageThrowsException() {
|
||||
ClassName name = ClassName.bestGuess("com.example.Test");
|
||||
GeneratedClass generatedClass = new GeneratedClass(
|
||||
this::generateBadPackageJavaFile, name);
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(
|
||||
() -> assertThat(generatedClass.generateJavaFile().toString()))
|
||||
.withMessageContaining("should be in package");
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateJavaFileWhenHasBadNameThrowsException() {
|
||||
ClassName name = ClassName.bestGuess("com.example.Test");
|
||||
GeneratedClass generatedClass = new GeneratedClass(this::generateBadNameJavaFile,
|
||||
name);
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(
|
||||
() -> assertThat(generatedClass.generateJavaFile().toString()))
|
||||
.withMessageContaining("should be named");
|
||||
}
|
||||
|
||||
private JavaFile generateJavaFile(ClassName className, GeneratedMethods methods) {
|
||||
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className);
|
||||
methods.doWithMethodSpecs(classBuilder::addMethod);
|
||||
return JavaFile.builder(className.packageName(), classBuilder.build()).build();
|
||||
}
|
||||
|
||||
private JavaFile generateBadPackageJavaFile(ClassName className,
|
||||
GeneratedMethods methods) {
|
||||
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className);
|
||||
return JavaFile.builder("naughty", classBuilder.build()).build();
|
||||
}
|
||||
|
||||
private JavaFile generateBadNameJavaFile(ClassName className,
|
||||
GeneratedMethods methods) {
|
||||
TypeSpec.Builder classBuilder = TypeSpec.classBuilder("Naughty");
|
||||
return JavaFile.builder(className.packageName(), classBuilder.build()).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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.generate;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aot.generate.ClassGenerator.JavaFileGenerator;
|
||||
import org.springframework.javapoet.ClassName;
|
||||
import org.springframework.javapoet.JavaFile;
|
||||
import org.springframework.javapoet.TypeSpec;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link GeneratedClasses}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class GeneratedClassesTests {
|
||||
|
||||
private GeneratedClasses generatedClasses = new GeneratedClasses(
|
||||
new ClassNameGenerator());
|
||||
|
||||
private static final JavaFileGenerator JAVA_FILE_GENERATOR = GeneratedClassesTests::generateJavaFile;
|
||||
|
||||
@Test
|
||||
void createWhenClassNameGeneratorIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new GeneratedClasses(null))
|
||||
.withMessage("'classNameGenerator' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrGenerateWithClassTargetWhenJavaFileGeneratorIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.generatedClasses.getOrGenerateClass(null,
|
||||
TestTarget.class, "test"))
|
||||
.withMessage("'javaFileGenerator' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrGenerateWithClassTargetWhenTargetIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.generatedClasses
|
||||
.getOrGenerateClass(JAVA_FILE_GENERATOR, (Class<?>) null, "test"))
|
||||
.withMessage("'target' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrGenerateWithClassTargetWhenFeatureIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.generatedClasses
|
||||
.getOrGenerateClass(JAVA_FILE_GENERATOR, TestTarget.class, null))
|
||||
.withMessage("'featureName' must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrGenerateWithStringTargetWhenJavaFileGeneratorIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.generatedClasses.getOrGenerateClass(null,
|
||||
TestTarget.class.getName(), "test"))
|
||||
.withMessage("'javaFileGenerator' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrGenerateWithStringTargetWhenTargetIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.generatedClasses
|
||||
.getOrGenerateClass(JAVA_FILE_GENERATOR, (String) null, "test"))
|
||||
.withMessage("'target' must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrGenerateWithStringTargetWhenFeatureIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.generatedClasses.getOrGenerateClass(
|
||||
JAVA_FILE_GENERATOR, TestTarget.class.getName(), null))
|
||||
.withMessage("'featureName' must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrGenerateWhenNewReturnsGeneratedMethod() {
|
||||
GeneratedClass generatedClass1 = this.generatedClasses
|
||||
.getOrGenerateClass(JAVA_FILE_GENERATOR, TestTarget.class, "one");
|
||||
GeneratedClass generatedClass2 = this.generatedClasses.getOrGenerateClass(
|
||||
JAVA_FILE_GENERATOR, TestTarget.class.getName(), "two");
|
||||
assertThat(generatedClass1).isNotNull().isNotEqualTo(generatedClass2);
|
||||
assertThat(generatedClass2).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOrGenerateWhenRepeatReturnsSameGeneratedMethod() {
|
||||
GeneratedClasses generated = this.generatedClasses;
|
||||
GeneratedClass generatedClass1 = generated.getOrGenerateClass(JAVA_FILE_GENERATOR,
|
||||
TestTarget.class, "one");
|
||||
GeneratedClass generatedClass2 = generated.getOrGenerateClass(JAVA_FILE_GENERATOR,
|
||||
TestTarget.class, "one");
|
||||
GeneratedClass generatedClass3 = generated.getOrGenerateClass(JAVA_FILE_GENERATOR,
|
||||
TestTarget.class.getName(), "one");
|
||||
GeneratedClass generatedClass4 = generated.getOrGenerateClass(JAVA_FILE_GENERATOR,
|
||||
TestTarget.class, "two");
|
||||
assertThat(generatedClass1).isNotNull().isSameAs(generatedClass2)
|
||||
.isSameAs(generatedClass3).isNotSameAs(generatedClass4);
|
||||
}
|
||||
|
||||
static JavaFile generateJavaFile(ClassName className,
|
||||
GeneratedMethods generatedMethods) {
|
||||
TypeSpec typeSpec = TypeSpec.classBuilder(className).addJavadoc("Test").build();
|
||||
return JavaFile.builder(className.packageName(), typeSpec).build();
|
||||
}
|
||||
|
||||
private static class TestTarget {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue