Migrate AOT tests to use GeneratedClasses and refine/polish AOT APIs

Migrate all AOT tests to make use of `GeneratedClasses` rather than
directly generating Java files. This commit also refines and polishes
AOT APIs to being greater consistency.

Specifically:

	- The `MethodGenerator` interface has been removed in favor of
	  working directly with `GeneratedMethods`.
	- The visibility of several constructors and methods has been
	  reduced to package-private.
	- The `using(...)` and `builder` methods have been removed in
	  favor of setting the `Consumer` callbacks directly as
	  constructor arguments.
	- Variable names for builders are now named `type` or `method`
	  depending on what they're building.

Closes gh-28831
This commit is contained in:
Phillip Webb
2022-06-23 13:48:38 -07:00
parent 4f8516e2c3
commit f2d31b7a20
51 changed files with 1255 additions and 1399 deletions

View File

@@ -47,6 +47,7 @@ public final class ClassNameGenerator {
private final Map<String, AtomicInteger> sequenceGenerator;
/**
* Create a new instance using the specified {@code defaultTarget} and no
* feature name prefix.
@@ -68,11 +69,15 @@ public final class ClassNameGenerator {
private ClassNameGenerator(Class<?> defaultTarget, String featureNamePrefix,
Map<String, AtomicInteger> sequenceGenerator) {
Assert.notNull(defaultTarget, "'defaultTarget' must not be null");
this.defaultTarget = defaultTarget;
this.featureNamePrefix = (!StringUtils.hasText(featureNamePrefix) ? "" : featureNamePrefix);
this.sequenceGenerator = sequenceGenerator;
}
String getFeatureNamePrefix() {
return this.featureNamePrefix;
}
/**
* Generate a unique {@link ClassName} based on the specified
@@ -85,46 +90,22 @@ public final class ClassNameGenerator {
* if any.
* <p>Generated class names are unique. If such a feature was already
* requested for this target, a counter is used to ensure uniqueness.
* @param target the class the newly generated class relates to, or
* {@code null} to use the main target
* @param featureName the name of the feature that the generated class
* supports
* @param target the class the newly generated class relates to, or
* {@code null} to use the main target
* @return a unique generated class name
*/
public ClassName generateClassName(@Nullable Class<?> target, String featureName) {
return generateSequencedClassName(getClassName(target, featureName));
public ClassName generateClassName(String featureName, @Nullable Class<?> target) {
return generateSequencedClassName(getRootName(featureName, target));
}
/**
* Return a class name based on the specified {@code target} and
* {@code featureName}. This uses the same algorithm as
* {@link #generateClassName(Class, String)} but does not register
* the class name, nor add a unique suffix to it if necessary.
* @param target the class the newly generated class relates to, or
* {@code null} to use the main target
* @param featureName the name of the feature that the generated class
* supports
* @return the class name
*/
String getClassName(@Nullable Class<?> target, String featureName) {
private String getRootName(String featureName, @Nullable Class<?> target) {
Assert.hasLength(featureName, "'featureName' must not be empty");
featureName = clean(featureName);
Class<?> targetToUse = (target != null ? target : this.defaultTarget);
String featureNameToUse = this.featureNamePrefix + featureName;
return targetToUse.getName().replace("$", "_")
+ SEPARATOR + StringUtils.capitalize(featureNameToUse);
}
/**
* Return a new {@link ClassNameGenerator} instance for the specified
* feature name prefix, keeping track of all the class names generated
* by this instance.
* @param featureNamePrefix the feature name prefix to use
* @return a new instance for the specified feature name prefix
*/
ClassNameGenerator usingFeatureNamePrefix(String featureNamePrefix) {
return new ClassNameGenerator(this.defaultTarget, featureNamePrefix,
this.sequenceGenerator);
return targetToUse.getName().replace("$", "_") + SEPARATOR + StringUtils.capitalize(featureNameToUse);
}
private String clean(String name) {
@@ -142,15 +123,26 @@ public final class ClassNameGenerator {
}
private ClassName generateSequencedClassName(String name) {
name = addSequence(name);
int sequence = this.sequenceGenerator.computeIfAbsent(name, key ->
new AtomicInteger()).getAndIncrement();
if (sequence > 0) {
name = name + sequence;
}
return ClassName.get(ClassUtils.getPackageName(name),
ClassUtils.getShortName(name));
}
private String addSequence(String name) {
int sequence = this.sequenceGenerator
.computeIfAbsent(name, key -> new AtomicInteger()).getAndIncrement();
return (sequence > 0) ? name + sequence : name;
/**
* Return a new {@link ClassNameGenerator} instance for the specified
* feature name prefix, keeping track of all the class names generated
* by this instance.
* @param featureNamePrefix the feature name prefix to use
* @return a new instance for the specified feature name prefix
*/
ClassNameGenerator withFeatureNamePrefix(String featureNamePrefix) {
return new ClassNameGenerator(this.defaultTarget, featureNamePrefix,
this.sequenceGenerator);
}
}

View File

@@ -74,9 +74,9 @@ public class DefaultGenerationContext implements GenerationContext {
private DefaultGenerationContext(DefaultGenerationContext existing, String name) {
int sequence = existing.sequenceGenerator
.computeIfAbsent(name, key -> new AtomicInteger()).getAndIncrement();
String nameToUse = (sequence > 0 ? name + sequence : name);
String featureName = (sequence > 0 ? name + sequence : name);
this.sequenceGenerator = existing.sequenceGenerator;
this.generatedClasses = existing.generatedClasses.withName(nameToUse);
this.generatedClasses = existing.generatedClasses.withFeatureNamePrefix(featureName);
this.generatedFiles = existing.generatedFiles;
this.runtimeHints = existing.runtimeHints;
}

View File

@@ -16,15 +16,18 @@
package org.springframework.aot.generate;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import org.springframework.javapoet.ClassName;
import org.springframework.javapoet.JavaFile;
import org.springframework.javapoet.TypeSpec;
import org.springframework.javapoet.TypeSpec.Builder;
import org.springframework.util.Assert;
/**
* A generated class is a container for generated methods.
* A single generated class.
*
* @author Phillip Webb
* @author Stephane Nicoll
@@ -33,26 +36,49 @@ import org.springframework.javapoet.TypeSpec.Builder;
*/
public final class GeneratedClass {
private final Consumer<Builder> typeSpecCustomizer;
private final ClassName name;
private final GeneratedMethods methods;
private final Consumer<TypeSpec.Builder> type;
private final Map<MethodName, AtomicInteger> methodNameSequenceGenerator = new ConcurrentHashMap<>();
/**
* 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
* @param type a {@link Consumer} used to build the type
*/
GeneratedClass(Consumer<Builder> typeSpecCustomizer, ClassName name) {
this.typeSpecCustomizer = typeSpecCustomizer;
GeneratedClass(ClassName name, Consumer<TypeSpec.Builder> type) {
this.name = name;
this.methods = new GeneratedMethods(new MethodNameGenerator());
this.type = type;
this.methods = new GeneratedMethods(this::generateSequencedMethodName);
}
/**
* Update this instance with a set of reserved method names that should not
* be used for generated methods. Reserved names are often needed when a
* generated class implements a specific interface.
* @param reservedMethodNames the reserved method names
*/
public void reserveMethodNames(String... reservedMethodNames) {
for (String reservedMethodName : reservedMethodNames) {
String generatedName = generateSequencedMethodName(MethodName.of(reservedMethodNames));
Assert.state(generatedName.equals(reservedMethodName),
() -> String.format("Unable to reserve method name '%s'", reservedMethodName));
}
}
private String generateSequencedMethodName(MethodName name) {
int sequence = this.methodNameSequenceGenerator
.computeIfAbsent(name, key -> new AtomicInteger()).getAndIncrement();
return (sequence > 0) ? name.toString() + sequence : name.toString();
}
/**
* Return the name of the generated class.
* @return the name of the generated class
@@ -62,18 +88,28 @@ public final class GeneratedClass {
}
/**
* Return the method generator that can be used for this generated class.
* @return the method generator
* Return generated methods for this instance.
* @return the generated methods
*/
public MethodGenerator getMethodGenerator() {
public GeneratedMethods getMethods() {
return this.methods;
}
JavaFile generateJavaFile() {
TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder(this.name);
this.typeSpecCustomizer.accept(typeSpecBuilder);
this.methods.doWithMethodSpecs(typeSpecBuilder::addMethod);
return JavaFile.builder(this.name.packageName(), typeSpecBuilder.build()).build();
TypeSpec.Builder type = getBuilder(this.type);
this.methods.doWithMethodSpecs(type::addMethod);
return JavaFile.builder(this.name.packageName(), type.build()).build();
}
private TypeSpec.Builder getBuilder(Consumer<TypeSpec.Builder> type) {
TypeSpec.Builder builder = TypeSpec.classBuilder(this.name);
type.accept(builder);
return builder;
}
void assertSameType(Consumer<TypeSpec.Builder> type) {
Assert.state(type == this.type || getBuilder(this.type).build().equals(getBuilder(type).build()),
"'type' consumer generated different result");
}
}

View File

@@ -46,11 +46,12 @@ public class GeneratedClasses {
private final Map<Owner, GeneratedClass> classesByOwner;
/**
* Create a new instance using the specified naming conventions.
* @param classNameGenerator the class name generator to use
*/
public GeneratedClasses(ClassNameGenerator classNameGenerator) {
GeneratedClasses(ClassNameGenerator classNameGenerator) {
this(classNameGenerator, new ArrayList<>(), new ConcurrentHashMap<>());
}
@@ -62,29 +63,92 @@ public class GeneratedClasses {
this.classesByOwner = classesByOwner;
}
/**
* Prepare a {@link GeneratedClass} for the specified {@code featureName}
* targeting the specified {@code component}.
* @param featureName the name of the feature to associate with the generated class
* @param component the target component
* @return a {@link Builder} for further configuration
* Get or add a generated class for the specified {@code featureName} and no
* particular component. If this method has previously been called with the
* given {@code featureName} the existing class will be returned, otherwise
* a new class will be generated.
* @param featureName the name of the feature to associate with the
* generated class
* @param type a {@link Consumer} used to build the type
* @return an existing or newly generated class
*/
public Builder forFeatureComponent(String featureName, Class<?> component) {
public GeneratedClass getOrAddForFeature(String featureName,
Consumer<TypeSpec.Builder> type) {
Assert.hasLength(featureName, "'featureName' must not be empty");
Assert.notNull(component, "'component' must not be null");
return new Builder(featureName, component);
Assert.notNull(type, "'type' must not be null");
Owner owner = new Owner(this.classNameGenerator.getFeatureNamePrefix(), featureName, null);
GeneratedClass generatedClass = this.classesByOwner.computeIfAbsent(owner, key -> createAndAddGeneratedClass(featureName, null, type));
generatedClass.assertSameType(type);
return generatedClass;
}
/**
* Prepare a {@link GeneratedClass} for the specified {@code featureName}
* and no particular component. This should be used for high-level code
* generation that are widely applicable and for entry points.
* @param featureName the name of the feature to associate with the generated class
* @return a {@link Builder} for further configuration
* Get or add a generated class for the specified {@code featureName}
* targeting the specified {@code component}. If this method has previously
* been called with the given {@code featureName}/{@code target} the
* existing class will be returned, otherwise a new class will be generated,
* otherwise a new class will be generated.
* @param featureName the name of the feature to associate with the
* generated class
* @param targetComponent the target component
* @param type a {@link Consumer} used to build the type
* @return an existing or newly generated class
*/
public Builder forFeature(String featureName) {
public GeneratedClass getOrAddForFeatureComponent(String featureName,
Class<?> targetComponent, Consumer<TypeSpec.Builder> type) {
Assert.hasLength(featureName, "'featureName' must not be empty");
return new Builder(featureName, null);
Assert.notNull(targetComponent, "'targetComponent' must not be null");
Assert.notNull(type, "'type' must not be null");
Owner owner = new Owner(this.classNameGenerator.getFeatureNamePrefix(), featureName, targetComponent);
GeneratedClass generatedClass = this.classesByOwner.computeIfAbsent(owner, key ->
createAndAddGeneratedClass(featureName, targetComponent, type));
generatedClass.assertSameType(type);
return generatedClass;
}
/**
* Add a new generated class for the specified {@code featureName} and no
* particular component.
* @param featureName the name of the feature to associate with the
* generated class
* @param type a {@link Consumer} used to build the type
* @return the newly generated class
*/
public GeneratedClass addForFeature(String featureName, Consumer<TypeSpec.Builder> type) {
Assert.hasLength(featureName, "'featureName' must not be empty");
Assert.notNull(type, "'type' must not be null");
return createAndAddGeneratedClass(featureName, null, type);
}
/**
* Add a new generated class for the specified {@code featureName} targeting
* the specified {@code component}.
* @param featureName the name of the feature to associate with the
* generated class
* @param targetComponent the target component
* @param type a {@link Consumer} used to build the type
* @return the newly generated class
*/
public GeneratedClass addForFeatureComponent(String featureName,
Class<?> targetComponent, Consumer<TypeSpec.Builder> type) {
Assert.hasLength(featureName, "'featureName' must not be empty");
Assert.notNull(targetComponent, "'targetComponent' must not be null");
Assert.notNull(type, "'type' must not be null");
return createAndAddGeneratedClass(featureName, targetComponent, type);
}
private GeneratedClass createAndAddGeneratedClass(String featureName,
@Nullable Class<?> targetComponent, Consumer<TypeSpec.Builder> type) {
ClassName className = this.classNameGenerator.generateClassName(featureName, targetComponent);
GeneratedClass generatedClass = new GeneratedClass(className, type);
this.classes.add(generatedClass);
return generatedClass;
}
/**
@@ -93,7 +157,7 @@ public class GeneratedClasses {
* @param generatedFiles where to write the generated classes
* @throws IOException on IO error
*/
public void writeTo(GeneratedFiles generatedFiles) throws IOException {
void writeTo(GeneratedFiles generatedFiles) throws IOException {
Assert.notNull(generatedFiles, "'generatedFiles' must not be null");
List<GeneratedClass> generatedClasses = new ArrayList<>(this.classes);
generatedClasses.sort(Comparator.comparing(GeneratedClass::getName));
@@ -102,62 +166,12 @@ public class GeneratedClasses {
}
}
GeneratedClasses withName(String name) {
return new GeneratedClasses(this.classNameGenerator.usingFeatureNamePrefix(name),
GeneratedClasses withFeatureNamePrefix(String name) {
return new GeneratedClasses(this.classNameGenerator.withFeatureNamePrefix(name),
this.classes, this.classesByOwner);
}
private record Owner(String id, String className) {
}
public class Builder {
private final String featureName;
@Nullable
private final Class<?> target;
Builder(String featureName, @Nullable Class<?> target) {
this.target = target;
this.featureName = featureName;
}
/**
* Generate a new {@link GeneratedClass} using the specified type
* customizer.
* @param typeSpecCustomizer a customizer for the {@link TypeSpec.Builder}
* @return a new {@link GeneratedClass}
*/
public GeneratedClass generate(Consumer<TypeSpec.Builder> typeSpecCustomizer) {
Assert.notNull(typeSpecCustomizer, "'typeSpecCustomizer' must not be null");
return createGeneratedClass(typeSpecCustomizer);
}
/**
* Get or generate a new {@link GeneratedClass} for the specified {@code id}.
* @param id a unique identifier
* @param typeSpecCustomizer a customizer for the {@link TypeSpec.Builder}
* @return a {@link GeneratedClass} instance
*/
public GeneratedClass getOrGenerate(String id, Consumer<TypeSpec.Builder> typeSpecCustomizer) {
Assert.hasLength(id, "'id' must not be empty");
Assert.notNull(typeSpecCustomizer, "'typeSpecCustomizer' must not be null");
Owner owner = new Owner(id, GeneratedClasses.this.classNameGenerator
.getClassName(this.target, this.featureName));
return GeneratedClasses.this.classesByOwner.computeIfAbsent(owner,
key -> createGeneratedClass(typeSpecCustomizer));
}
private GeneratedClass createGeneratedClass(Consumer<TypeSpec.Builder> typeSpecCustomizer) {
ClassName className = GeneratedClasses.this.classNameGenerator
.generateClassName(this.target, this.featureName);
GeneratedClass generatedClass = new GeneratedClass(typeSpecCustomizer, className);
GeneratedClasses.this.classes.add(generatedClass);
return generatedClass;
}
private record Owner(String featureNamePrefix, String featureName, @Nullable Class<?> target) {
}

View File

@@ -19,8 +19,6 @@ package org.springframework.aot.generate;
import java.util.function.Consumer;
import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.MethodSpec.Builder;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@@ -29,24 +27,28 @@ import org.springframework.util.Assert;
* @author Phillip Webb
* @since 6.0
* @see GeneratedMethods
* @see MethodGenerator
*/
public final class GeneratedMethod {
private final String name;
@Nullable
private MethodSpec spec;
private final MethodSpec methodSpec;
/**
* Create a new {@link GeneratedMethod} instance with the given name. This
* constructor is package-private since names should only be generated via
* {@link GeneratedMethods}.
* @param name the generated name
* @param name the generated method name
* @param method consumer to generate the method
*/
GeneratedMethod(String name) {
GeneratedMethod(String name, Consumer<MethodSpec.Builder> method) {
this.name = name;
MethodSpec.Builder builder = MethodSpec.methodBuilder(getName());
method.accept(builder);
this.methodSpec = builder.build();
Assert.state(this.name.equals(this.methodSpec.name),
"'method' consumer must not change the generated method name");
}
@@ -64,35 +66,13 @@ public final class GeneratedMethod {
* @throws IllegalStateException if one of the {@code generateBy(...)}
* methods has not been called
*/
public MethodSpec getSpec() {
Assert.state(this.spec != null,
() -> "Method '%s' has no method spec defined".formatted(this.name));
return this.spec;
}
/**
* Generate the method using the given consumer.
* @param builder a consumer that will accept a method spec builder and
* configure it as necessary
* @return this instance
*/
public GeneratedMethod using(Consumer<MethodSpec.Builder> builder) {
Builder builderToUse = MethodSpec.methodBuilder(this.name);
builder.accept(builderToUse);
MethodSpec spec = builderToUse.build();
assertNameHasNotBeenChanged(spec);
this.spec = spec;
return this;
}
private void assertNameHasNotBeenChanged(MethodSpec spec) {
Assert.isTrue(this.name.equals(spec.name),
() -> "'spec' must use the generated name '%s'".formatted(this.name));
MethodSpec getMethodSpec() {
return this.methodSpec;
}
@Override
public String toString() {
return (this.spec != null) ? this.spec.toString() : this.name.toString();
return this.name.toString();
}
}

View File

@@ -17,12 +17,13 @@
package org.springframework.aot.generate;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.MethodSpec.Builder;
import org.springframework.util.Assert;
/**
@@ -32,49 +33,63 @@ import org.springframework.util.Assert;
* @since 6.0
* @see GeneratedMethod
*/
public class GeneratedMethods implements Iterable<GeneratedMethod>, MethodGenerator {
public class GeneratedMethods {
private final MethodNameGenerator methodNameGenerator;
private final Function<MethodName, String> methodNameGenerator;
private final List<GeneratedMethod> generatedMethods = new ArrayList<>();
private final MethodName prefix;
/**
* Create a new {@link GeneratedMethods} instance backed by a new
* {@link MethodNameGenerator}.
*/
public GeneratedMethods() {
this(new MethodNameGenerator());
}
private final List<GeneratedMethod> generatedMethods;
/**
* Create a new {@link GeneratedMethods} instance backed by the given
* {@link MethodNameGenerator}.
* @param methodNameGenerator the method name generator
*/
public GeneratedMethods(MethodNameGenerator methodNameGenerator) {
GeneratedMethods(Function<MethodName, String> methodNameGenerator) {
Assert.notNull(methodNameGenerator, "'methodNameGenerator' must not be null");
this.methodNameGenerator = methodNameGenerator;
this.prefix = MethodName.NONE;
this.generatedMethods = new ArrayList<>();
}
private GeneratedMethods(Function<MethodName, String> methodNameGenerator,
MethodName prefix, List<GeneratedMethod> generatedMethods) {
@Override
public GeneratedMethod generateMethod(Object... methodNameParts) {
return add(methodNameParts);
this.methodNameGenerator = methodNameGenerator;
this.prefix = prefix;
this.generatedMethods = generatedMethods;
}
/**
* Add a new {@link GeneratedMethod}. The returned instance must define the
* method spec by calling {@code using(builder -> ...)}.
* @param methodNameParts the method name parts that should be used to
* generate a unique method name
* Add a new {@link GeneratedMethod}.
* @param suggestedName the suggested name for the method
* @param method a {@link Consumer} used to build method
* @return the newly added {@link GeneratedMethod}
*/
public GeneratedMethod add(Object... methodNameParts) {
GeneratedMethod method = new GeneratedMethod(
this.methodNameGenerator.generateMethodName(methodNameParts));
this.generatedMethods.add(method);
return method;
public GeneratedMethod add(String suggestedName, Consumer<Builder> method) {
Assert.notNull(suggestedName, "'suggestedName' must not be null");
return add(MethodName.of(suggestedName), method);
}
/**
* Add a new {@link GeneratedMethod}.
* @param suggestedName the suggested name for the method
* @param method a {@link Consumer} used to build the method
* @return the newly added {@link GeneratedMethod}
*/
public GeneratedMethod add(MethodName suggestedName, Consumer<Builder> method) {
Assert.notNull(suggestedName, "'suggestedName' must not be null");
Assert.notNull(method, "'method' must not be null");
String generatedName = this.methodNameGenerator.apply(this.prefix.and(suggestedName));
GeneratedMethod generatedMethod = new GeneratedMethod(generatedName, method);
this.generatedMethods.add(generatedMethod);
return generatedMethod;
}
public GeneratedMethods withPrefix(String prefix) {
Assert.notNull(prefix, "'prefix' must not be null");
return new GeneratedMethods(this.methodNameGenerator, this.prefix.and(prefix), this.generatedMethods);
}
/**
@@ -82,20 +97,11 @@ public class GeneratedMethods implements Iterable<GeneratedMethod>, MethodGenera
* that have been added to this collection.
* @param action the action to perform
*/
public void doWithMethodSpecs(Consumer<MethodSpec> action) {
stream().map(GeneratedMethod::getSpec).forEach(action);
void doWithMethodSpecs(Consumer<MethodSpec> action) {
stream().map(GeneratedMethod::getMethodSpec).forEach(action);
}
@Override
public Iterator<GeneratedMethod> iterator() {
return this.generatedMethods.iterator();
}
/**
* Return a {@link Stream} of all the methods in this collection.
* @return a stream of {@link GeneratedMethod} instances
*/
public Stream<GeneratedMethod> stream() {
Stream<GeneratedMethod> stream() {
return this.generatedMethods.stream();
}

View File

@@ -43,8 +43,7 @@ import org.springframework.aot.hint.SerializationHints;
public interface GenerationContext {
/**
* Return the {@link GeneratedClasses} being used by the context. Allows a
* single generated class to be shared across multiple AOT processors. All
* Return the {@link GeneratedClasses} being used by the context. All
* generated classes are written at the end of AOT processing.
* @return the generated classes
*/

View File

@@ -1,72 +0,0 @@
/*
* 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;
/**
* Generates new {@link GeneratedMethod} instances.
*
* @author Phillip Webb
* @since 6.0
* @see GeneratedMethods
*/
@FunctionalInterface
public interface MethodGenerator {
/**
* Generate a new {@link GeneratedMethod}. The returned instance must define
* the method spec by calling {@code using(builder -> ...)}.
* @param methodNameParts the method name parts that should be used to
* generate a unique method name
* @return the newly added {@link GeneratedMethod}
*/
GeneratedMethod generateMethod(Object... methodNameParts);
/**
* Return a new {@link MethodGenerator} instance that generates method with
* additional implicit method name parts. The final generated name will be
* of the following form:
* <p>
* <table border="1">
* <tr>
* <th>Original</th>
* <th>Updated</th>
* </tr>
* <tr>
* <td>run</td>
* <td>&lt;name&gt;Run</td>
* </tr>
* <tr>
* <td>getValue</td>
* <td>get&lt;Name&gt;Value</td>
* </tr>
* <tr>
* <td>setValue</td>
* <td>set&lt;Name&gt;Value</td>
* </tr>
* <tr>
* <td>isEnabled</td>
* <td>is&lt;Name&gt;Enabled</td>
* </tr>
* </table>
* @param nameParts the implicit name parts
* @return a new {@link MethodGenerator} instance
*/
default MethodGenerator withName(Object... nameParts) {
return new MethodGeneratorWithName(this, nameParts);
}
}

View File

@@ -1,71 +0,0 @@
/*
* 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.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Internal class used to support {@link MethodGenerator#withName(Object...)}.
*
* @author Phillip Webb
* @since 6.0
*/
class MethodGeneratorWithName implements MethodGenerator {
private static final String[] PREFIXES = { "get", "set", "is" };
private final MethodGenerator methodGenerator;
private final Object[] nameParts;
MethodGeneratorWithName(MethodGenerator methodGenerator, Object[] nameParts) {
this.methodGenerator = methodGenerator;
this.nameParts = nameParts;
}
@Override
public GeneratedMethod generateMethod(Object... methodNameParts) {
return this.methodGenerator.generateMethod(generateName(methodNameParts));
}
private Object[] generateName(Object... methodNameParts) {
String joined = MethodNameGenerator.join(methodNameParts);
String prefix = getPrefix(joined);
String suffix = joined.substring(prefix.length());
Object[] result = this.nameParts;
if (StringUtils.hasLength(prefix)) {
result = ObjectUtils.addObjectToArray(result, prefix, 0);
}
if (StringUtils.hasLength(suffix)) {
result = ObjectUtils.addObjectToArray(result, suffix);
}
return result;
}
private String getPrefix(String name) {
for (String candidate : PREFIXES) {
if (name.startsWith(candidate)) {
return candidate;
}
}
return "";
}
}

View File

@@ -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 java.util.Arrays;
import java.util.stream.Collectors;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A camel-case method name that can be built from distinct parts.
*
* @author Phillip Webb
* @since 6.0
*/
public final class MethodName {
private static final String[] PREFIXES = { "get", "set", "is" };
/**
* An empty method name.
*/
public static final MethodName NONE = of();
private final String value;
private MethodName(String value) {
this.value = value;
}
/**
* Create a new method name from the specific parts. The returned name will
* be in camel-case and will only contain valid characters from the parts.
* @param parts the parts the form the name
* @return a method name instance
*/
public static MethodName of(String... parts) {
Assert.notNull(parts, "'parts' must not be null");
return new MethodName(join(parts));
}
/**
* Create a new method name by concatenating the specified name to this name.
* @param name the name to concatenate
* @return a new method name instance
*/
public MethodName and(MethodName name) {
Assert.notNull(name, "'name' must not be null");
return and(name.value);
}
/**
* Create a new method name by concatenating the specified parts to this name.
* @param parts the parts to concatenate
* @return a new method name instance
*/
public MethodName and(String... parts) {
Assert.notNull(parts, "'parts' must not be null");
String joined = join(parts);
String prefix = getPrefix(joined);
String suffix = joined.substring(prefix.length());
return of(prefix, this.value, suffix);
}
private String getPrefix(String name) {
for (String candidate : PREFIXES) {
if (name.startsWith(candidate)) {
return candidate;
}
}
return "";
}
@Override
public int hashCode() {
return this.value.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if ((obj == null) || (getClass() != obj.getClass())) {
return false;
}
return this.value.equals(((MethodName) obj).value);
}
@Override
public String toString() {
return (!StringUtils.hasLength(this.value)) ? "$$aot" : this.value ;
}
private static String join(String[] parts) {
return StringUtils.uncapitalize(Arrays.stream(parts).map(MethodName::clean)
.map(StringUtils::capitalize).collect(Collectors.joining()));
}
private static String clean(String part) {
char[] chars = (part != null) ? part.toCharArray() : new char[0];
StringBuilder name = new StringBuilder(chars.length);
boolean uppercase = false;
for (char ch : chars) {
char outputChar = (!uppercase) ? ch : Character.toUpperCase(ch);
name.append((!Character.isLetter(ch)) ? "" : outputChar);
uppercase = (ch == '.');
}
return name.toString();
}
}

View File

@@ -1,123 +0,0 @@
/*
* 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.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Generates unique method names that can be used in ahead-of-time generated
* source code. This class is stateful so one instance should be used per
* generated type.
*
* @author Phillip Webb
* @since 6.0
*/
public class MethodNameGenerator {
private final Map<String, AtomicInteger> sequenceGenerator = new ConcurrentHashMap<>();
/**
* Create a new {@link MethodNameGenerator} instance without any reserved
* names.
*/
public MethodNameGenerator() {
}
/**
* Create a new {@link MethodNameGenerator} instance with the specified
* reserved names.
* @param reservedNames the method names to reserve
*/
public MethodNameGenerator(String... reservedNames) {
this(List.of(reservedNames));
}
/**
* Create a new {@link MethodNameGenerator} instance with the specified
* reserved names.
* @param reservedNames the method names to reserve
*/
public MethodNameGenerator(Iterable<String> reservedNames) {
Assert.notNull(reservedNames, "'reservedNames' must not be null");
for (String reservedName : reservedNames) {
addSequence(StringUtils.uncapitalize(reservedName));
}
}
/**
* Generate a new method name from the given parts.
* @param parts the parts used to build the name.
* @return the generated method name
*/
public String generateMethodName(Object... parts) {
String generatedName = join(parts);
return addSequence(generatedName.isEmpty() ? "$$aot" : generatedName);
}
private String addSequence(String name) {
int sequence = this.sequenceGenerator
.computeIfAbsent(name, key -> new AtomicInteger()).getAndIncrement();
return (sequence > 0) ? name + sequence : name;
}
/**
* Join the specified parts to create a valid camel case method name.
* @param parts the parts to join
* @return a method name from the joined parts.
*/
public static String join(Object... parts) {
Stream<String> capitalizedPartNames = Arrays.stream(parts)
.map(MethodNameGenerator::getPartName).map(StringUtils::capitalize);
return StringUtils.uncapitalize(capitalizedPartNames.collect(Collectors.joining()));
}
private static String getPartName(@Nullable Object part) {
if (part == null) {
return "";
}
if (part instanceof Class<?> clazz) {
return clean(ClassUtils.getShortName(clazz));
}
return clean(part.toString());
}
private static String clean(String string) {
char[] chars = string.toCharArray();
StringBuilder name = new StringBuilder(chars.length);
boolean uppercase = false;
for (char ch : chars) {
char outputChar = (!uppercase) ? ch : Character.toUpperCase(ch);
name.append((!Character.isLetter(ch)) ? "" : outputChar);
uppercase = ch == '.';
}
return name.toString();
}
}