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:
Phillip Webb
2022-04-26 20:51:31 -07:00
parent 55d7f7a014
commit d5374550e5
5 changed files with 487 additions and 0 deletions

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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) {
}
}