From 496ed729a0ee5a47eaebf3b5e481220160342f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 3 Dec 2025 14:13:18 +0100 Subject: [PATCH] Add support for AOT generated resources This commit updates the AOT infrastructure to handle generated resources in a similar fashion than generated classes: naming conventions, feature prefixes, and uniqueness are applied. The new abstraction also provides a more explicit contract that guides users to either create the resource or create it if it does not exist and validate its content if it does. As part of this change ClassNameGenerator has been renamed to NameGenerator as it is responsible to generate names for both classes and resources. Closes gh-35862 --- ...BeanRegistrationsAotContributionTests.java | 4 +- .../context/aot/ContextAotProcessor.java | 15 +- .../test/generate/TestGenerationContext.java | 12 +- .../generate/DefaultGenerationContext.java | 34 ++- .../aot/generate/GeneratedClasses.java | 22 +- .../aot/generate/GeneratedResource.java | 255 ++++++++++++++++++ .../aot/generate/GeneratedResources.java | 163 +++++++++++ .../aot/generate/GenerationContext.java | 14 +- ...sNameGenerator.java => NameGenerator.java} | 71 +++-- .../aot/generate/ClassNameGeneratorTests.java | 100 ------- .../DefaultGenerationContextTests.java | 74 ++++- .../aot/generate/GeneratedClassesTests.java | 6 +- .../aot/generate/GeneratedResourceTests.java | 159 +++++++++++ .../aot/generate/GeneratedResourcesTests.java | 184 +++++++++++++ .../aot/generate/NameGeneratorTests.java | 168 ++++++++++++ .../context/aot/TestContextAotGenerator.java | 14 +- .../aot/TestContextGenerationContext.java | 11 +- 17 files changed, 1122 insertions(+), 184 deletions(-) create mode 100644 spring-core/src/main/java/org/springframework/aot/generate/GeneratedResource.java create mode 100644 spring-core/src/main/java/org/springframework/aot/generate/GeneratedResources.java rename spring-core/src/main/java/org/springframework/aot/generate/{ClassNameGenerator.java => NameGenerator.java} (59%) delete mode 100644 spring-core/src/test/java/org/springframework/aot/generate/ClassNameGeneratorTests.java create mode 100644 spring-core/src/test/java/org/springframework/aot/generate/GeneratedResourceTests.java create mode 100644 spring-core/src/test/java/org/springframework/aot/generate/GeneratedResourcesTests.java create mode 100644 spring-core/src/test/java/org/springframework/aot/generate/NameGeneratorTests.java diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContributionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContributionTests.java index 3af50179ff9..e924595866d 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContributionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContributionTests.java @@ -27,10 +27,10 @@ import javax.lang.model.element.Modifier; import org.junit.jupiter.api.Test; -import org.springframework.aot.generate.ClassNameGenerator; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.MethodReference; import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; +import org.springframework.aot.generate.NameGenerator; import org.springframework.aot.generate.ValueCodeGenerationException; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.factory.aot.BeanRegistrationsAotContribution.Registration; @@ -109,7 +109,7 @@ class BeanRegistrationsAotContributionTests { @Test void applyToWhenHasNameGeneratesPrefixedFeatureName() { this.generationContext = new TestGenerationContext( - new ClassNameGenerator(TestGenerationContext.TEST_TARGET, "Management")); + new NameGenerator(TestGenerationContext.TEST_TARGET, "Management")); this.beanFactoryInitializationCode = new MockBeanFactoryInitializationCode(this.generationContext); RegisteredBean registeredBean = registerBean(new RootBeanDefinition(TestBean.class)); BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(this.methodGeneratorFactory, diff --git a/spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.java index ddfc13b5f38..e32ee1a5c47 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.java @@ -23,9 +23,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import org.springframework.aot.generate.ClassNameGenerator; import org.springframework.aot.generate.DefaultGenerationContext; import org.springframework.aot.generate.FileSystemGeneratedFiles; +import org.springframework.aot.generate.NameGenerator; import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.TypeReference; @@ -102,7 +102,7 @@ public abstract class ContextAotProcessor extends AbstractAotProcessorBy default, a standard {@link ClassNameGenerator} using the configured + * Callback to customize the {@link NameGenerator}. + *

By default, a standard {@link NameGenerator} using the configured * {@linkplain #getApplicationClass() application entry point} as the default * target is used. - * @return the class name generator + * @return the name generator + * @since 7.1 */ - protected ClassNameGenerator createClassNameGenerator() { - return new ClassNameGenerator(ClassName.get(getApplicationClass())); + protected NameGenerator createNameGenerator() { + return new NameGenerator(ClassName.get(getApplicationClass())); } /** diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generate/TestGenerationContext.java b/spring-core-test/src/main/java/org/springframework/aot/test/generate/TestGenerationContext.java index 8469f433e89..ce4617877c3 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/test/generate/TestGenerationContext.java +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generate/TestGenerationContext.java @@ -18,10 +18,10 @@ package org.springframework.aot.test.generate; import java.util.function.UnaryOperator; -import org.springframework.aot.generate.ClassNameGenerator; import org.springframework.aot.generate.DefaultGenerationContext; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.InMemoryGeneratedFiles; +import org.springframework.aot.generate.NameGenerator; import org.springframework.core.test.tools.TestCompiler; import org.springframework.javapoet.ClassName; @@ -42,11 +42,11 @@ public class TestGenerationContext extends DefaultGenerationContext implements U public static final ClassName TEST_TARGET = ClassName.get("com.example", "TestTarget"); /** - * Create an instance using the specified {@link ClassNameGenerator}. - * @param classNameGenerator the class name generator to use + * Create an instance using the specified {@link NameGenerator}. + * @param nameGenerator the name generator to use for classes and resources */ - public TestGenerationContext(ClassNameGenerator classNameGenerator) { - super(classNameGenerator, new InMemoryGeneratedFiles()); + public TestGenerationContext(NameGenerator nameGenerator) { + super(nameGenerator, new InMemoryGeneratedFiles()); } /** @@ -54,7 +54,7 @@ public class TestGenerationContext extends DefaultGenerationContext implements U * @param target the default target class name to use */ public TestGenerationContext(ClassName target) { - this(new ClassNameGenerator(target)); + this(new NameGenerator(target)); } /** diff --git a/spring-core/src/main/java/org/springframework/aot/generate/DefaultGenerationContext.java b/spring-core/src/main/java/org/springframework/aot/generate/DefaultGenerationContext.java index 1ae9a1e0c1e..644993c1538 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/DefaultGenerationContext.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/DefaultGenerationContext.java @@ -41,6 +41,8 @@ public class DefaultGenerationContext implements GenerationContext { private final GeneratedClasses generatedClasses; + private final GeneratedResources generatedResources; + private final GeneratedFiles generatedFiles; private final RuntimeHints runtimeHints; @@ -48,27 +50,28 @@ public class DefaultGenerationContext implements GenerationContext { /** * Create a new {@link DefaultGenerationContext} instance backed by the - * specified {@link ClassNameGenerator} and {@link GeneratedFiles}. - * @param classNameGenerator the naming convention to use for generated - * class names + * specified {@link NameGenerator} and {@link GeneratedFiles}. + * @param nameGenerator the naming convention to use for generated + * classes and resources * @param generatedFiles the generated files */ - public DefaultGenerationContext(ClassNameGenerator classNameGenerator, GeneratedFiles generatedFiles) { - this(classNameGenerator, generatedFiles, new RuntimeHints()); + public DefaultGenerationContext(NameGenerator nameGenerator, GeneratedFiles generatedFiles) { + this(nameGenerator, generatedFiles, new RuntimeHints()); } /** * Create a new {@link DefaultGenerationContext} instance backed by the - * specified {@link ClassNameGenerator}, {@link GeneratedFiles}, and + * specified {@link NameGenerator}, {@link GeneratedFiles}, and * {@link RuntimeHints}. - * @param classNameGenerator the naming convention to use for generated - * class names + * @param nameGenerator the naming convention to use for generated + * classes and resources * @param generatedFiles the generated files * @param runtimeHints the runtime hints */ - public DefaultGenerationContext(ClassNameGenerator classNameGenerator, GeneratedFiles generatedFiles, + public DefaultGenerationContext(NameGenerator nameGenerator, GeneratedFiles generatedFiles, RuntimeHints runtimeHints) { - this(new GeneratedClasses(classNameGenerator), generatedFiles, runtimeHints); + this(new GeneratedClasses(nameGenerator), new GeneratedResources(nameGenerator), + generatedFiles, runtimeHints); } /** @@ -78,14 +81,16 @@ public class DefaultGenerationContext implements GenerationContext { * @param generatedFiles the generated files * @param runtimeHints the runtime hints */ - DefaultGenerationContext(GeneratedClasses generatedClasses, + DefaultGenerationContext(GeneratedClasses generatedClasses, GeneratedResources generatedResources, GeneratedFiles generatedFiles, RuntimeHints runtimeHints) { Assert.notNull(generatedClasses, "'generatedClasses' must not be null"); + Assert.notNull(generatedResources, "'generatedResources' must not be null"); Assert.notNull(generatedFiles, "'generatedFiles' must not be null"); Assert.notNull(runtimeHints, "'runtimeHints' must not be null"); this.sequenceGenerator = new ConcurrentHashMap<>(); this.generatedClasses = generatedClasses; + this.generatedResources = generatedResources; this.generatedFiles = generatedFiles; this.runtimeHints = runtimeHints; } @@ -104,6 +109,7 @@ public class DefaultGenerationContext implements GenerationContext { } this.sequenceGenerator = existing.sequenceGenerator; this.generatedClasses = existing.generatedClasses.withFeatureNamePrefix(featureName); + this.generatedResources = existing.generatedResources.withFeatureNamePrefix(featureName); this.generatedFiles = existing.generatedFiles; this.runtimeHints = existing.runtimeHints; } @@ -114,6 +120,11 @@ public class DefaultGenerationContext implements GenerationContext { return this.generatedClasses; } + @Override + public GeneratedResources getGeneratedResources() { + return this.generatedResources; + } + @Override public GeneratedFiles getGeneratedFiles() { return this.generatedFiles; @@ -134,6 +145,7 @@ public class DefaultGenerationContext implements GenerationContext { */ public void writeGeneratedContent() { this.generatedClasses.writeTo(this.generatedFiles); + this.generatedResources.writeTo(this.generatedFiles); } } diff --git a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedClasses.java b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedClasses.java index 0b526d2d26f..b63313d1a6a 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedClasses.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedClasses.java @@ -42,7 +42,7 @@ import org.springframework.util.Assert; */ public class GeneratedClasses { - private final ClassNameGenerator classNameGenerator; + private final NameGenerator nameGenerator; private final List classes; @@ -51,16 +51,16 @@ public class GeneratedClasses { /** * Create a new instance using the specified naming conventions. - * @param classNameGenerator the class name generator to use + * @param nameGenerator the name generator to use */ - GeneratedClasses(ClassNameGenerator classNameGenerator) { - this(classNameGenerator, new ArrayList<>(), new ConcurrentHashMap<>()); + GeneratedClasses(NameGenerator nameGenerator) { + this(nameGenerator, new ArrayList<>(), new ConcurrentHashMap<>()); } - private GeneratedClasses(ClassNameGenerator classNameGenerator, + private GeneratedClasses(NameGenerator nameGenerator, List classes, Map classesByOwner) { - Assert.notNull(classNameGenerator, "'classNameGenerator' must not be null"); - this.classNameGenerator = classNameGenerator; + Assert.notNull(nameGenerator, "'nameGenerator' must not be null"); + this.nameGenerator = nameGenerator; this.classes = classes; this.classesByOwner = classesByOwner; } @@ -81,7 +81,7 @@ public class GeneratedClasses { Assert.hasLength(featureName, "'featureName' must not be empty"); Assert.notNull(type, "'type' must not be null"); - Owner owner = new Owner(this.classNameGenerator.getFeatureNamePrefix(), featureName, null); + Owner owner = new Owner(this.nameGenerator.getFeatureNamePrefix(), featureName, null); GeneratedClass generatedClass = this.classesByOwner.computeIfAbsent(owner, key -> createAndAddGeneratedClass(featureName, null, type)); generatedClass.assertSameType(type); return generatedClass; @@ -105,7 +105,7 @@ public class GeneratedClasses { Assert.hasLength(featureName, "'featureName' must not be empty"); 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); + Owner owner = new Owner(this.nameGenerator.getFeatureNamePrefix(), featureName, targetComponent); GeneratedClass generatedClass = this.classesByOwner.computeIfAbsent(owner, key -> createAndAddGeneratedClass(featureName, targetComponent, type)); generatedClass.assertSameType(type); @@ -180,7 +180,7 @@ public class GeneratedClasses { private GeneratedClass createAndAddGeneratedClass(String featureName, @Nullable ClassName targetComponent, Consumer type) { - ClassName className = this.classNameGenerator.generateClassName(featureName, targetComponent); + ClassName className = this.nameGenerator.generateClassName(featureName, targetComponent); GeneratedClass generatedClass = new GeneratedClass(className, type); this.classes.add(generatedClass); return generatedClass; @@ -208,7 +208,7 @@ public class GeneratedClasses { * @return a new instance for the specified feature name prefix */ GeneratedClasses withFeatureNamePrefix(String featureNamePrefix) { - return new GeneratedClasses(this.classNameGenerator.withFeatureNamePrefix(featureNamePrefix), + return new GeneratedClasses(this.nameGenerator.withFeatureNamePrefix(featureNamePrefix), this.classes, this.classesByOwner); } diff --git a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedResource.java b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedResource.java new file mode 100644 index 00000000000..d7df629e990 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedResource.java @@ -0,0 +1,255 @@ +/* + * Copyright 2002-present 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.io.InputStream; +import java.util.Arrays; +import java.util.StringJoiner; +import java.util.function.Consumer; + +import org.jspecify.annotations.Nullable; + +import org.springframework.aot.generate.GeneratedFiles.Kind; +import org.springframework.core.io.InputStreamSource; +import org.springframework.util.Assert; +import org.springframework.util.function.ThrowingConsumer; + +/** + * A single generated resource. + * + * @author Stephane Nicoll + * @since 7.1 + * @see GeneratedResources + */ +public class GeneratedResource { + + private final String path; + + private final Content content; + + /** + * Create a new, empty instance with the given {@code path}. + * @param path the absolute path of this resource on the classpath + */ + GeneratedResource(String path) { + this.path = path; + this.content = new DefaultContent(); + } + + /** + * Return the absolute path of this resource on the classpath. + *

The path returned by this method does not have a leading slash and is + * suitable for generated code that use {@link ClassLoader#getResource(String)}. + * @return the path of this resource on the classpath + * @see #hasContent() + */ + public String getPath() { + return this.path; + } + + /** + * Whether this instance has content associated to it. Instances that do + * not have content are not contributed to {@link GeneratedFiles}. + * @return {@code true} if this instance has content + * @see #handle(Consumer) + */ + public boolean hasContent() { + return this.content.exists(); + } + + /** + * Handle this instance, using the provided {@link Content}. + *

Generated resources are usually written only once, but there are cases + * where they need to be stored in a unique path and several rounds of + * AOT processing may touch the same file. For cases like this use + * {@code createOrValidate} as only one attempt to create the content is + * allowed. + * @param content the content to use + */ + public void handle(Consumer content) { + content.accept(this.content); + } + + void writeTo(GeneratedFiles generatedFiles) { + if (hasContent()) { + generatedFiles.addFile(Kind.RESOURCE, this.path, this.content); + } + } + + @Override + public String toString() { + return new StringJoiner(", ", GeneratedResource.class.getSimpleName() + "[", "]") + .add("path='" + this.path + "'") + .toString(); + } + + /** + * Abstraction of the content of a generated resource. + */ + public interface Content extends InputStreamSource { + + /** + * Specify if this content exists. Use this as a check before calling + * {@link #getInputStream()}. + * @return {@code true} if this instance has been created + */ + boolean exists(); + + /** + * Create this instance with the content from the given {@link CharSequence}. + * @param content the content + * @throws IllegalStateException if this instance has already been created + */ + void create(CharSequence content); + + /** + * Create this instance with the content from the given {@link CharSequence} + * if it does not exist or validate that the existing content matches. + * @param content the content + * @throws IllegalArgumentException if this instance has already been created, + * but its content does not match the given {@code content} + */ + void createOrValidate(CharSequence content); + + /** + * Create this instance with the content written to an {@link Appendable} + * passed to the given {@link ThrowingConsumer}. + * @param content a {@link ThrowingConsumer} that accepts an + * {@link Appendable} which will receive the content + * @throws IllegalStateException if this instance has already been created + */ + void create(ThrowingConsumer content); + + /** + * Create this instance with the content written to an {@link Appendable} + * passed to the given {@link ThrowingConsumer} if it does not exist or + * validate that the existing content matches. + * @param content a {@link ThrowingConsumer} that accepts an + * {@link Appendable} which will receive the content + * @throws IllegalArgumentException if this instance has already been created, + * but its content does not match the given {@code content} + */ + void createOrValidate(ThrowingConsumer content); + + /** + * Create this instance with the content from the given {@link InputStreamSource}. + * @param content an {@link InputStreamSource} that will provide an input + * stream containing the content + * @throws IllegalStateException if this instance has already been created + */ + void create(InputStreamSource content); + + /** + * Create this instance with the content from the given {@link InputStreamSource} + * if it does not exist or validate that the existing content matches. + * @param content an {@link InputStreamSource} that will provide an input + * stream containing the content + * @throws IllegalArgumentException if this instance has already been created, + * but its content does not match the given {@code content} + */ + void createOrValidate(InputStreamSource content); + + } + + private final class DefaultContent implements Content { + + private @Nullable InputStreamSource source; + + @Override + public InputStream getInputStream() throws IOException { + Assert.state(this.source != null, "No content is set for " + GeneratedResource.this); + return this.source.getInputStream(); + } + + @Override + public boolean exists() { + return (this.source != null); + } + + @Override + public void create(CharSequence content) { + create(appendable -> appendable.append(content)); + } + + @Override + public void createOrValidate(CharSequence content) { + createOrValidate(appendable -> appendable.append(content)); + } + + @Override + public void create(ThrowingConsumer content) { + create(new AppendableConsumerInputStreamSource(content)); + } + + @Override + public void createOrValidate(ThrowingConsumer content) { + createOrValidate(new AppendableConsumerInputStreamSource(content)); + } + + @Override + public void create(InputStreamSource content) { + Assert.notNull(content, "'content' must not be null"); + if (exists()) { + throw new IllegalStateException( + "Content for generated resource at '%s' has already been set".formatted(getPath())); + } + this.source = content; + } + + @Override + public void createOrValidate(InputStreamSource content) { + Assert.notNull(content, "'content' must not be null"); + if (this.source == null) { + this.source = content; + } + if (!hasSomeContentAs(content)) { + throw new IllegalArgumentException( + "Content for generated resource at '%s' differs from the content that has already been written" + .formatted(getPath())); + } + } + + private boolean hasSomeContentAs(InputStreamSource content) { + try (InputStream input1 = getInputStream(); InputStream input2 = content.getInputStream()) { + if (input1 == input2) { + return true; + } + byte[] buffer1 = new byte[8192]; + byte[] buffer2 = new byte[8192]; + int count1; + int count2; + while ((count1 = input1.read(buffer1)) != -1) { + count2 = input2.read(buffer2); + if (count1 != count2) { + return false; + } + if (!Arrays.equals(buffer1, 0, count1, buffer2, 0, count2)) { + return false; + } + } + return input2.read() == -1; + } + catch (IOException ex) { + throw new RuntimeException("Failed to validate content for generated resource at '%s'" + .formatted(getPath()), ex); + } + + } + + } +} diff --git a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedResources.java b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedResources.java new file mode 100644 index 00000000000..0353fd4a573 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedResources.java @@ -0,0 +1,163 @@ +/* + * Copyright 2002-present 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.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.jspecify.annotations.Nullable; + +import org.springframework.javapoet.ClassName; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A managed collection of generated resources. + * + *

This class is stateful, so the same instance should be used for all resource + * generation. + * + * @author Stephane Nicoll + * @since 7.1 + * @see GeneratedResource + */ +public class GeneratedResources { + + private final NameGenerator nameGenerator; + + private final List resources; + + private final Map resourcesByPath; + + private final Map resourcesByOwner; + + + GeneratedResources(NameGenerator nameGenerator) { + this(nameGenerator, new ArrayList<>(), new ConcurrentHashMap<>(), new ConcurrentHashMap<>()); + } + + private GeneratedResources(NameGenerator nameGenerator, List resources, + Map resourcesByPath, Map resourcesByOwner) { + + this.nameGenerator = nameGenerator; + this.resources = resources; + this.resourcesByPath = resourcesByPath; + this.resourcesByOwner = resourcesByOwner; + } + + + /** + * Return the {@link GeneratedResource} at the given {@code path}. + *

A valid {@code path} is an absolute location on the classpath with + * directories separated with {@code /}, and without a leading slash. + * For instance {@code com/example/my-resource.txt}. + * @param path the absolute path of the resource on the classpath + * @return a generated resource + */ + public GeneratedResource getOrAdd(String path) { + Assert.hasLength(path, "'path' must not be empty"); + if (!path.equals(cleanedPath(path))) { + throw new IllegalArgumentException("Invalid classpath location '%s'".formatted(path)); + } + return this.resourcesByPath.computeIfAbsent(path, key -> { + GeneratedResource resource = new GeneratedResource(path); + this.resources.add(resource); + return resource; + }); + } + + private static String cleanedPath(String path) { + String cleanedPath = StringUtils.cleanPath(path); + if (cleanedPath.startsWith("/")) { + cleanedPath = cleanedPath.substring(1); + } + return cleanedPath; + } + + public GeneratedResource getOrAddForFeature(String extension, String featureName) { + Assert.hasLength(extension, "'extension' must not be empty"); + Assert.hasLength(featureName, "'featureName' must not be empty"); + Owner owner = new Owner(extension, this.nameGenerator.getFeatureNamePrefix(), featureName, null); + return this.resourcesByOwner.computeIfAbsent(owner, + key -> createAndAddGeneratedResource(extension, featureName, null)); + } + + public GeneratedResource getOrAddForFeatureComponent(String extension, String featureName, + ClassName targetComponent) { + Assert.hasLength(extension, "'extension' must not be empty"); + Assert.hasLength(featureName, "'featureName' must not be empty"); + Assert.notNull(targetComponent, "'targetComponent' must not be null"); + Owner owner = new Owner(extension, this.nameGenerator.getFeatureNamePrefix(), featureName, targetComponent); + return this.resourcesByOwner.computeIfAbsent(owner, + key -> createAndAddGeneratedResource(extension, featureName, targetComponent)); + } + + public GeneratedResource addForFeature(String extension, String featureName) { + Assert.hasLength(extension, "'extension' must not be empty"); + Assert.hasLength(featureName, "'featureName' must not be empty"); + return createAndAddGeneratedResource(extension, featureName, null); + } + + public GeneratedResource addForFeatureComponent(String extension, String featureName, + ClassName targetComponent) { + Assert.hasLength(extension, "'extension' must not be empty"); + Assert.hasLength(featureName, "'featureName' must not be empty"); + Assert.notNull(targetComponent, "'targetComponent' must not be null"); + return createAndAddGeneratedResource(extension, featureName, targetComponent); + } + + private GeneratedResource createAndAddGeneratedResource(String extension, String featureName, + @Nullable ClassName targetComponent) { + + String path = this.nameGenerator.generateResourcePath(extension, featureName, targetComponent); + GeneratedResource generatedResource = new GeneratedResource(path); + this.resources.add(generatedResource); + return generatedResource; + } + + /** + * Write the {@link GeneratedResource generated resources} using the given + * {@link GeneratedFiles} instance. + * @param generatedFiles where to write the generated resources + */ + void writeTo(GeneratedFiles generatedFiles) { + Assert.notNull(generatedFiles, "'generatedFiles' must not be null"); + List generatedResources = new ArrayList<>(this.resources); + generatedResources.sort(Comparator.comparing(GeneratedResource::getPath)); + for (GeneratedResource generatedResource : generatedResources) { + generatedResource.writeTo(generatedFiles); + } + } + + /** + * Create a new instance using the specified feature name prefix to qualify + * generated paths for a dedicated round of AOT processing. + * @param featureNamePrefix the feature name prefix to use + * @return a new instance for the specified feature name prefix + */ + GeneratedResources withFeatureNamePrefix(String featureNamePrefix) { + return new GeneratedResources(this.nameGenerator.withFeatureNamePrefix(featureNamePrefix), + this.resources, this.resourcesByPath, this.resourcesByOwner); + } + + private record Owner(String extension, String featureNamePrefix, String featureName, @Nullable ClassName target) { + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generate/GenerationContext.java b/spring-core/src/main/java/org/springframework/aot/generate/GenerationContext.java index 040f30370a3..63001c4ad48 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/GenerationContext.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/GenerationContext.java @@ -49,10 +49,22 @@ public interface GenerationContext { */ GeneratedClasses getGeneratedClasses(); + /** + * Get the {@link GeneratedResources} used by the context. + *

All generated resources are written at the end of AOT processing. + * @return the generated resources + * @since 7.1 + */ + GeneratedResources getGeneratedResources(); + /** * Get the {@link GeneratedFiles} used by the context. - *

Used to write resource, java source, or class bytecode files. + *

Used to write java source, resource, or class bytecode files. + * For java source and resource, use {@link #getGeneratedResources()} + * and {@link #getGeneratedClasses()}, respectively. * @return the generated files + * @see #getGeneratedClasses() + * @see #getGeneratedResources() */ GeneratedFiles getGeneratedFiles(); diff --git a/spring-core/src/main/java/org/springframework/aot/generate/ClassNameGenerator.java b/spring-core/src/main/java/org/springframework/aot/generate/NameGenerator.java similarity index 59% rename from spring-core/src/main/java/org/springframework/aot/generate/ClassNameGenerator.java rename to spring-core/src/main/java/org/springframework/aot/generate/NameGenerator.java index 7dcf615a092..52cabf5c88a 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/ClassNameGenerator.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/NameGenerator.java @@ -19,6 +19,7 @@ package org.springframework.aot.generate; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import org.jspecify.annotations.Nullable; @@ -28,19 +29,21 @@ import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** - * Generate unique class names based on a target {@link ClassName} and a - * feature name. + * Generate unique names based on a target {@link ClassName} and a + * feature name. Support generating class names and resource paths. * *

This class is stateful, so the same instance should be used for all name * generation. * * @author Phillip Webb * @author Stephane Nicoll - * @since 6.0 + * @since 7.1 */ -public final class ClassNameGenerator { +public class NameGenerator { - private static final String SEPARATOR = "__"; + private static final String CLASS_NAME_SEPARATOR = "__"; + + private static final String RESOURCE_NAME_SEPARATOR = "-"; private static final String AOT_FEATURE = "Aot"; @@ -56,7 +59,7 @@ public final class ClassNameGenerator { * feature name prefix. * @param defaultTarget the default target class to use */ - public ClassNameGenerator(ClassName defaultTarget) { + public NameGenerator(ClassName defaultTarget) { this(defaultTarget, ""); } @@ -66,11 +69,11 @@ public final class ClassNameGenerator { * @param defaultTarget the default target class to use * @param featureNamePrefix the prefix to use to qualify feature names */ - public ClassNameGenerator(ClassName defaultTarget, String featureNamePrefix) { + public NameGenerator(ClassName defaultTarget, String featureNamePrefix) { this(defaultTarget, featureNamePrefix, new ConcurrentHashMap<>()); } - private ClassNameGenerator(ClassName defaultTarget, String featureNamePrefix, + private NameGenerator(ClassName defaultTarget, String featureNamePrefix, Map sequenceGenerator) { Assert.notNull(defaultTarget, "'defaultTarget' must not be null"); this.defaultTarget = defaultTarget; @@ -100,15 +103,50 @@ public final class ClassNameGenerator { * @return a unique generated class name */ public ClassName generateClassName(String featureName, @Nullable ClassName target) { - return generateSequencedClassName(getRootName(featureName, target)); + return generateUniqueName(getRootClassName(featureName, target), uniqueName -> + ClassName.get(ClassUtils.getPackageName(uniqueName), + ClassUtils.getShortName(uniqueName))); } - private String getRootName(String featureName, @Nullable ClassName target) { + private String getRootClassName(String featureName, @Nullable ClassName target) { Assert.hasLength(featureName, "'featureName' must not be empty"); featureName = clean(featureName); ClassName targetToUse = (target != null ? target : this.defaultTarget); String featureNameToUse = this.featureNamePrefix + featureName; - return toName(targetToUse).replace("$", "_") + SEPARATOR + StringUtils.capitalize(featureNameToUse); + return toName(targetToUse).replace("$", "_") + CLASS_NAME_SEPARATOR + StringUtils.capitalize(featureNameToUse); + } + + /** + * Generate a unique resource path based on the specified {@code extension}, + * {@code featureName} and {@code target}. If the {@code target} is + * {@code null}, the configured main target of this instance is used. + *

The resource path is a suffixed version of the target. For instance, a + * {@code com.example.Demo} target with an {@code metadata} feature name + * and a {@code json} extension leads to a + * {@code com/example/Demo--metadata.json} generated resource path. + * The feature name is qualified by the configured feature name prefix, + * if any. + *

Generated resource paths are unique. If such a feature was already + * requested for this target, a counter is used to ensure uniqueness. + * @param extension the file extension + * @param featureName the name of the feature that the generated path + * supports + * @param target the class the generated resource relates to, or + * {@code null} to use the main target + * @return a unique absolute path for the resource + */ + public String generateResourcePath(String extension, String featureName, @Nullable ClassName target) { + Assert.hasLength(extension, "'extension' must not be empty"); + return generateUniqueName(getRootResourceName(featureName, target), + uniqueName -> uniqueName + "." + extension); + } + + private String getRootResourceName(String featureName, @Nullable ClassName target) { + Assert.hasLength(featureName, "'featureName' must not be empty"); + ClassName targetToUse = (target != null ? target : this.defaultTarget); + String featureNameToUse = this.featureNamePrefix + + (this.featureNamePrefix.isEmpty() ? "" : RESOURCE_NAME_SEPARATOR) + featureName; + return toName(targetToUse).replace("$", "_").replace(".", "/") + RESOURCE_NAME_SEPARATOR + featureNameToUse; } private String clean(String name) { @@ -125,26 +163,25 @@ public final class ClassNameGenerator { return (!clean.isEmpty()) ? clean.toString() : AOT_FEATURE; } - private ClassName generateSequencedClassName(String name) { + private T generateUniqueName(String name, Function factory) { 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)); + return factory.apply(name); } /** - * Create a new {@link ClassNameGenerator} instance for the specified + * Create a new {@link NameGenerator} 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, + NameGenerator withFeatureNamePrefix(String featureNamePrefix) { + return new NameGenerator(this.defaultTarget, featureNamePrefix, this.sequenceGenerator); } diff --git a/spring-core/src/test/java/org/springframework/aot/generate/ClassNameGeneratorTests.java b/spring-core/src/test/java/org/springframework/aot/generate/ClassNameGeneratorTests.java deleted file mode 100644 index 9463ddffd06..00000000000 --- a/spring-core/src/test/java/org/springframework/aot/generate/ClassNameGeneratorTests.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2002-present 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.InputStream; - -import org.junit.jupiter.api.Test; - -import org.springframework.javapoet.ClassName; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - -/** - * Tests for {@link ClassNameGenerator}. - * - * @author Phillip Webb - */ -class ClassNameGeneratorTests { - - private static final ClassName TEST_TARGET = ClassName.get("com.example", "Test"); - - private final ClassNameGenerator generator = new ClassNameGenerator(TEST_TARGET); - - @Test - void generateClassNameWhenTargetClassIsNullUsesMainTarget() { - ClassName generated = this.generator.generateClassName("test", null); - assertThat(generated).hasToString("com.example.Test__Test"); - } - - @Test - void generateClassNameUseFeatureNamePrefix() { - ClassName generated = new ClassNameGenerator(TEST_TARGET, "One") - .generateClassName("test", ClassName.get(InputStream.class)); - assertThat(generated).hasToString("java.io.InputStream__OneTest"); - } - - @Test - void generateClassNameWithNoTextFeatureNamePrefix() { - ClassName generated = new ClassNameGenerator(TEST_TARGET, " ") - .generateClassName("test", ClassName.get(InputStream.class)); - assertThat(generated).hasToString("java.io.InputStream__Test"); - } - - @Test - void generatedClassNameWhenFeatureIsEmptyThrowsException() { - assertThatIllegalArgumentException() - .isThrownBy(() -> this.generator.generateClassName("", ClassName.get(InputStream.class))) - .withMessage("'featureName' must not be empty"); - } - - @Test - void generatedClassNameWhenFeatureIsNotAllLettersThrowsException() { - assertThat(this.generator.generateClassName("name!", ClassName.get(InputStream.class))) - .hasToString("java.io.InputStream__Name"); - assertThat(this.generator.generateClassName("1NameHere", ClassName.get(InputStream.class))) - .hasToString("java.io.InputStream__NameHere"); - assertThat(this.generator.generateClassName("Y0pe", ClassName.get(InputStream.class))) - .hasToString("java.io.InputStream__YPe"); - } - - @Test - void generateClassNameWithClassWhenLowercaseFeatureNameGeneratesName() { - ClassName generated = this.generator.generateClassName("bytes", ClassName.get(InputStream.class)); - assertThat(generated).hasToString("java.io.InputStream__Bytes"); - } - - @Test - void generateClassNameWithClassWhenInnerClassGeneratesName() { - ClassName innerBean = ClassName.get("com.example", "Test", "InnerBean"); - ClassName generated = this.generator.generateClassName("EventListener", innerBean); - assertThat(generated) - .hasToString("com.example.Test_InnerBean__EventListener"); - } - - @Test - void generateClassWithClassWhenMultipleCallsGeneratesSequencedName() { - ClassName generated1 = this.generator.generateClassName("bytes",ClassName.get(InputStream.class)); - ClassName generated2 = this.generator.generateClassName("bytes", ClassName.get(InputStream.class)); - ClassName generated3 = this.generator.generateClassName("bytes", ClassName.get(InputStream.class)); - assertThat(generated1).hasToString("java.io.InputStream__Bytes"); - assertThat(generated2).hasToString("java.io.InputStream__Bytes1"); - assertThat(generated3).hasToString("java.io.InputStream__Bytes2"); - } - -} diff --git a/spring-core/src/test/java/org/springframework/aot/generate/DefaultGenerationContextTests.java b/spring-core/src/test/java/org/springframework/aot/generate/DefaultGenerationContextTests.java index 9fda605bedb..fe0cce846c4 100644 --- a/spring-core/src/test/java/org/springframework/aot/generate/DefaultGenerationContextTests.java +++ b/spring-core/src/test/java/org/springframework/aot/generate/DefaultGenerationContextTests.java @@ -41,7 +41,10 @@ class DefaultGenerationContextTests { private static final Consumer typeSpecCustomizer = type -> {}; private final GeneratedClasses generatedClasses = new GeneratedClasses( - new ClassNameGenerator(SAMPLE_TARGET)); + new NameGenerator(SAMPLE_TARGET)); + + private final GeneratedResources generatedResources = new GeneratedResources( + new NameGenerator(SAMPLE_TARGET)); private final InMemoryGeneratedFiles generatedFiles = new InMemoryGeneratedFiles(); @@ -51,7 +54,7 @@ class DefaultGenerationContextTests { @Test void createWithOnlyGeneratedFilesCreatesContext() { DefaultGenerationContext context = new DefaultGenerationContext( - new ClassNameGenerator(SAMPLE_TARGET), this.generatedFiles); + new NameGenerator(SAMPLE_TARGET), this.generatedFiles); assertThat(context.getGeneratedFiles()).isSameAs(this.generatedFiles); assertThat(context.getRuntimeHints()).isInstanceOf(RuntimeHints.class); } @@ -59,7 +62,7 @@ class DefaultGenerationContextTests { @Test void createCreatesContext() { DefaultGenerationContext context = new DefaultGenerationContext( - this.generatedClasses, this.generatedFiles, this.runtimeHints); + this.generatedClasses, this.generatedResources, this.generatedFiles, this.runtimeHints); assertThat(context.getGeneratedFiles()).isNotNull(); assertThat(context.getRuntimeHints()).isNotNull(); } @@ -67,15 +70,23 @@ class DefaultGenerationContextTests { @Test void createWhenGeneratedClassesIsNullThrowsException() { assertThatIllegalArgumentException() - .isThrownBy(() -> new DefaultGenerationContext((GeneratedClasses) null, + .isThrownBy(() -> new DefaultGenerationContext(null, this.generatedResources, this.generatedFiles, this.runtimeHints)) .withMessage("'generatedClasses' must not be null"); } + @Test + void createWhenGeneratedResourcesIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new DefaultGenerationContext(this.generatedClasses, null, + this.generatedFiles, this.runtimeHints)) + .withMessage("'generatedResources' must not be null"); + } + @Test void createWhenGeneratedFilesIsNullThrowsException() { assertThatIllegalArgumentException() - .isThrownBy(() -> new DefaultGenerationContext(this.generatedClasses, + .isThrownBy(() -> new DefaultGenerationContext(this.generatedClasses, this.generatedResources, null, this.runtimeHints)) .withMessage("'generatedFiles' must not be null"); } @@ -83,60 +94,74 @@ class DefaultGenerationContextTests { @Test void createWhenRuntimeHintsIsNullThrowsException() { assertThatIllegalArgumentException() - .isThrownBy(() -> new DefaultGenerationContext(this.generatedClasses, + .isThrownBy(() -> new DefaultGenerationContext(this.generatedClasses, this.generatedResources, this.generatedFiles, null)) .withMessage("'runtimeHints' must not be null"); } @Test - void getGeneratedClassesReturnsClassNameGenerator() { + void getGeneratedClassesReturnsGeneratedClasses() { DefaultGenerationContext context = new DefaultGenerationContext( - this.generatedClasses, this.generatedFiles, this.runtimeHints); + this.generatedClasses, this.generatedResources, this.generatedFiles, this.runtimeHints); assertThat(context.getGeneratedClasses()).isSameAs(this.generatedClasses); } + @Test + void getGeneratedResourcesReturnsGeneratedResources() { + DefaultGenerationContext context = new DefaultGenerationContext( + this.generatedClasses, this.generatedResources, this.generatedFiles, this.runtimeHints); + assertThat(context.getGeneratedResources()).isSameAs(this.generatedResources); + } + @Test void getGeneratedFilesReturnsGeneratedFiles() { DefaultGenerationContext context = new DefaultGenerationContext( - this.generatedClasses, this.generatedFiles, this.runtimeHints); + this.generatedClasses, this.generatedResources, this.generatedFiles, this.runtimeHints); assertThat(context.getGeneratedFiles()).isSameAs(this.generatedFiles); } @Test void getRuntimeHintsReturnsRuntimeHints() { DefaultGenerationContext context = new DefaultGenerationContext( - this.generatedClasses, this.generatedFiles, this.runtimeHints); + this.generatedClasses, this.generatedResources, this.generatedFiles, this.runtimeHints); assertThat(context.getRuntimeHints()).isSameAs(this.runtimeHints); } @Test void withNameUpdateNamingConvention() { DefaultGenerationContext context = new DefaultGenerationContext( - new ClassNameGenerator(SAMPLE_TARGET), this.generatedFiles); + new NameGenerator(SAMPLE_TARGET), this.generatedFiles); GenerationContext anotherContext = context.withName("Another"); GeneratedClass generatedClass = anotherContext.getGeneratedClasses() .addForFeature("Test", typeSpecCustomizer); assertThat(generatedClass.getName().simpleName()).endsWith("__AnotherTest"); + GeneratedResource generatedResource = anotherContext.getGeneratedResources() + .addForFeature("txt", "test"); + assertThat(generatedResource.getPath()).endsWith("-Another-test.txt"); } @Test void withNameKeepsTrackOfAllGeneratedFiles() { DefaultGenerationContext context = new DefaultGenerationContext( - new ClassNameGenerator(SAMPLE_TARGET), this.generatedFiles); + new NameGenerator(SAMPLE_TARGET), this.generatedFiles); context.getGeneratedClasses().addForFeature("Test", typeSpecCustomizer); + context.getGeneratedResources().addForFeature("txt", "test").handle(this::createTestContent); GenerationContext anotherContext = context.withName("Another"); assertThat(anotherContext.getGeneratedClasses()).isNotSameAs(context.getGeneratedClasses()); + assertThat(anotherContext.getGeneratedResources()).isNotSameAs(context.getGeneratedResources()); assertThat(anotherContext.getGeneratedFiles()).isSameAs(context.getGeneratedFiles()); assertThat(anotherContext.getRuntimeHints()).isSameAs(context.getRuntimeHints()); anotherContext.getGeneratedClasses().addForFeature("Test", typeSpecCustomizer); + anotherContext.getGeneratedResources().addForFeature("txt", "test").handle(this::createTestContent); context.writeGeneratedContent(); assertThat(this.generatedFiles.getGeneratedFiles(Kind.SOURCE)).hasSize(2); + assertThat(this.generatedFiles.getGeneratedFiles(Kind.RESOURCE)).hasSize(2); } @Test - void withNameGeneratesUniqueName() { + void withNameGeneratesUniqueClassNames() { DefaultGenerationContext context = new DefaultGenerationContext( - new ClassNameGenerator(SAMPLE_TARGET), this.generatedFiles); + new NameGenerator(SAMPLE_TARGET), this.generatedFiles); context.withName("Test").getGeneratedClasses() .addForFeature("Feature", typeSpecCustomizer); context.withName("Test").getGeneratedClasses() @@ -150,4 +175,25 @@ class DefaultGenerationContextTests { "com/example/SampleTarget__Test2Feature.java"); } + @Test + void withNameGeneratesUniqueResourcePaths() { + DefaultGenerationContext context = new DefaultGenerationContext( + new NameGenerator(SAMPLE_TARGET), this.generatedFiles); + context.withName("Test").getGeneratedResources() + .addForFeature("txt", "feature").handle(this::createTestContent); + context.withName("Test").getGeneratedResources() + .addForFeature("txt", "feature").handle(this::createTestContent); + context.withName("Test").getGeneratedResources() + .addForFeature("txt", "feature").handle(this::createTestContent); + context.writeGeneratedContent(); + assertThat(this.generatedFiles.getGeneratedFiles(Kind.RESOURCE)).containsOnlyKeys( + "com/example/SampleTarget-Test-feature.txt", + "com/example/SampleTarget-Test1-feature.txt", + "com/example/SampleTarget-Test2-feature.txt"); + } + + private void createTestContent(GeneratedResource.Content content) { + content.create("test"); + } + } diff --git a/spring-core/src/test/java/org/springframework/aot/generate/GeneratedClassesTests.java b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedClassesTests.java index f8c08506d24..766297b0ed7 100644 --- a/spring-core/src/test/java/org/springframework/aot/generate/GeneratedClassesTests.java +++ b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedClassesTests.java @@ -42,12 +42,12 @@ class GeneratedClassesTests { private static final Consumer emptyTypeCustomizer = type -> {}; private final GeneratedClasses generatedClasses = new GeneratedClasses( - new ClassNameGenerator(ClassName.get("com.example", "Test"))); + new NameGenerator(ClassName.get("com.example", "Test"))); @Test - void createWhenClassNameGeneratorIsNullThrowsException() { + void createWhenNameGeneratorIsNullThrowsException() { assertThatIllegalArgumentException().isThrownBy(() -> new GeneratedClasses(null)) - .withMessage("'classNameGenerator' must not be null"); + .withMessage("'nameGenerator' must not be null"); } @Test diff --git a/spring-core/src/test/java/org/springframework/aot/generate/GeneratedResourceTests.java b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedResourceTests.java new file mode 100644 index 00000000000..a8a43e621a4 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedResourceTests.java @@ -0,0 +1,159 @@ +/* + * Copyright 2002-present 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.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import org.springframework.aot.generate.GeneratedFiles.Kind; +import org.springframework.core.io.InputStreamSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link GeneratedResource}. + * + * @author Stephane Nicoll + */ +class GeneratedResourceTests { + + private static final String TEST_RESOURCE_PATH = "com/example/one.properties"; + + @Test + void emptyResourceDoesNotExist() { + GeneratedResource resource = new GeneratedResource(TEST_RESOURCE_PATH); + assertThat(resource.getPath()).isEqualTo(TEST_RESOURCE_PATH); + assertThat(resource.hasContent()).isFalse(); + } + + @Test + void handleResourceHasContent() { + GeneratedResource resource = new GeneratedResource(TEST_RESOURCE_PATH); + resource.handle(writer -> writer.create("test=1")); + assertThat(resource.hasContent()).isTrue(); + } + + @Test + void createResourceWithCharSequence() throws IOException { + GeneratedResource resource = new GeneratedResource(TEST_RESOURCE_PATH); + resource.handle(writer -> writer.create("test=1")); + InMemoryGeneratedFiles generatedFiles = applyToGeneratedFiles(resource); + assertThat(generatedFiles.getGeneratedFileContent(Kind.RESOURCE, TEST_RESOURCE_PATH)).isEqualTo("test=1"); + } + + @Test + void createResourceWithAppendableCallback() throws IOException { + GeneratedResource resource = new GeneratedResource(TEST_RESOURCE_PATH); + resource.handle(writer -> writer.create(appendable -> appendable.append("test").append("=").append("1"))); + InMemoryGeneratedFiles generatedFiles = applyToGeneratedFiles(resource); + assertThat(generatedFiles.getGeneratedFileContent(Kind.RESOURCE, TEST_RESOURCE_PATH)).isEqualTo("test=1"); + } + + @Test + void createResourceWithNullInputStreamSource() throws IOException { + GeneratedResource resource = new GeneratedResource(TEST_RESOURCE_PATH); + byte[] content = "test=1".getBytes(StandardCharsets.UTF_8); + InputStreamSource source = () -> new ByteArrayInputStream(content); + resource.handle(writer -> writer.create(source)); + InMemoryGeneratedFiles generatedFiles = applyToGeneratedFiles(resource); + assertThat(generatedFiles.getGeneratedFileContent(Kind.RESOURCE, TEST_RESOURCE_PATH)).isEqualTo("test=1"); + } + + @Test + void createResourceInvokedTwiceWithSameContent() { + GeneratedResource resource = new GeneratedResource(TEST_RESOURCE_PATH); + resource.handle(writer -> writer.create("test=1")); + assertThatIllegalStateException() + .isThrownBy(() -> resource.handle(writer -> writer.create(appendable -> appendable.append("test=1")))) + .withMessage("Content for generated resource at 'com/example/one.properties' has already been set"); + } + + @Test + void createOrValidateWithNoContent() throws IOException { + GeneratedResource resource = new GeneratedResource(TEST_RESOURCE_PATH); + resource.handle(writer -> writer.createOrValidate(appendable -> appendable.append("test").append("=").append("1"))); + InMemoryGeneratedFiles generatedFiles = applyToGeneratedFiles(resource); + assertThat(generatedFiles.getGeneratedFileContent(Kind.RESOURCE, TEST_RESOURCE_PATH)).isEqualTo("test=1"); + } + + @Test + void createOrValidateWithStringInvokedTwiceWithSameContent() throws IOException { + GeneratedResource resource = new GeneratedResource(TEST_RESOURCE_PATH); + resource.handle(writer -> writer.createOrValidate(appendable -> appendable.append("test").append("=").append("1"))); + resource.handle(writer -> writer.createOrValidate("test=1")); + InMemoryGeneratedFiles generatedFiles = applyToGeneratedFiles(resource); + assertThat(generatedFiles.getGeneratedFileContent(Kind.RESOURCE, TEST_RESOURCE_PATH)).isEqualTo("test=1"); + } + + @Test + void createOrValidateWithAppendableInvokedTwiceWithSameContent() throws IOException { + GeneratedResource resource = new GeneratedResource(TEST_RESOURCE_PATH); + resource.handle(writer -> writer.createOrValidate("test=1")); + resource.handle(writer -> writer.createOrValidate(appendable -> appendable.append("test").append("=").append("1"))); + InMemoryGeneratedFiles generatedFiles = applyToGeneratedFiles(resource); + assertThat(generatedFiles.getGeneratedFileContent(Kind.RESOURCE, TEST_RESOURCE_PATH)).isEqualTo("test=1"); + } + + @Test + void createOrValidateWithInputStreamSourceInvokedTwiceWithSameContent() throws IOException { + InputStreamSource source = new AppendableConsumerInputStreamSource(appendable -> appendable.append("test").append("=").append("1")); + GeneratedResource resource = new GeneratedResource(TEST_RESOURCE_PATH); + resource.handle(writer -> writer.createOrValidate(source)); + resource.handle(writer -> writer.createOrValidate(source)); + InMemoryGeneratedFiles generatedFiles = applyToGeneratedFiles(resource); + assertThat(generatedFiles.getGeneratedFileContent(Kind.RESOURCE, TEST_RESOURCE_PATH)).isEqualTo("test=1"); + } + + @ParameterizedTest + @ValueSource(strings = { "test=2", "", "test=11" }) + void createOrValidateInvokedTwiceWithDifferentContent(String newContent) { + GeneratedResource resource = new GeneratedResource(TEST_RESOURCE_PATH); + resource.handle(writer -> writer.createOrValidate(appendable -> appendable.append("test").append("=").append("1"))); + resource.handle(writer -> assertThatIllegalArgumentException() + .isThrownBy(() -> writer.createOrValidate(newContent)) + .withMessage("Content for generated resource at 'com/example/one.properties' differs from the content that has already been written")); + } + + @Test + void writeToWithNewResources() { + GeneratedResource resource = new GeneratedResource(TEST_RESOURCE_PATH); + InMemoryGeneratedFiles generatedFiles = applyToGeneratedFiles(resource); + assertThat(generatedFiles.getGeneratedFiles(Kind.RESOURCE)).isEmpty(); + } + + @Test + void getInputStreamWhenContentDoesNotExist() { + GeneratedResource resource = new GeneratedResource(TEST_RESOURCE_PATH); + resource.handle(content -> assertThatIllegalStateException() + .isThrownBy(content::getInputStream) + .withMessage("No content is set for GeneratedResource[path='com/example/one.properties']")); + } + + InMemoryGeneratedFiles applyToGeneratedFiles(GeneratedResource resource) { + InMemoryGeneratedFiles files = new InMemoryGeneratedFiles(); + resource.writeTo(files); + return files; + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/generate/GeneratedResourcesTests.java b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedResourcesTests.java new file mode 100644 index 00000000000..8e0518343b3 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedResourcesTests.java @@ -0,0 +1,184 @@ +/* + * Copyright 2002-present 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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link GeneratedResource}. + * + * @author Stephane Nicoll + */ +class GeneratedResourcesTests { + + private static final String TEST_RESOURCE_PATH = "com/example/one.properties"; + + private static final ClassName TEST_COMPONENT = ClassName.get("org.springframework", "Example"); + + private final GeneratedResources generatedResources = new GeneratedResources( + new NameGenerator(ClassName.get("com.example", "Test"))); + + + @Test + void getOrAddWhenPathIsEmptyThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.generatedResources.getOrAdd("")) + .withMessage("'path' must not be empty"); + } + + @Test + void getOrAddWhenPathWithLeadingSlashThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.generatedResources.getOrAdd("/resource.txt")) + .withMessage("Invalid classpath location '/resource.txt'"); + } + + @Test + void getOrAddForFeatureWithEmptyExtensionThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.generatedResources.getOrAddForFeature("", "test")) + .withMessage("'extension' must not be empty"); + } + + @Test + void getOrAddForFeatureWithEmptyFeatureThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.generatedResources.getOrAddForFeature("txt", "")) + .withMessage("'featureName' must not be empty"); + } + + @Test + void getOrAddForUsesProvidedPath() { + GeneratedResource generatedClass = this.generatedResources.getOrAdd("META-INF/test.properties"); + assertThat(generatedClass.getPath()).isEqualTo("META-INF/test.properties"); + } + + @Test + void getOrAddWWhenNewReturnsEmptyGeneratedResource() { + GeneratedResource generatedResource = this.generatedResources.getOrAdd(TEST_RESOURCE_PATH); + assertThat(generatedResource.getPath()).isEqualTo(TEST_RESOURCE_PATH); + assertThat(generatedResource.hasContent()).isFalse(); + } + + @Test + void getOrAddWhenRepeatReturnsSameGeneratedResource() { + GeneratedResource generatedResource1 = this.generatedResources.getOrAdd(TEST_RESOURCE_PATH); + GeneratedResource generatedResource2 = this.generatedResources.getOrAdd(TEST_RESOURCE_PATH); + GeneratedResource generatedResource3 = this.generatedResources.getOrAdd(TEST_RESOURCE_PATH); + assertThat(generatedResource1).isNotNull().isSameAs(generatedResource2).isSameAs(generatedResource3); + } + + @Test + void getOrAddForFeatureUsesDefaultTarget() { + GeneratedResource generatedClass = this.generatedResources.getOrAddForFeature("txt", "one"); + assertThat(generatedClass.getPath()).isEqualTo("com/example/Test-one.txt"); + } + + @Test + void getOrAddForFeatureWhenNewReturnsGeneratedResource() { + GeneratedResource generatedClass1 = this.generatedResources.getOrAddForFeature("txt", "one"); + GeneratedResource generatedClass2 = this.generatedResources.getOrAddForFeature("zip", "one"); + assertThat(generatedClass1).isNotNull().isNotEqualTo(generatedClass2); + assertThat(generatedClass2).isNotNull(); + } + + @Test + void getOrAddForFeatureWhenRepeatReturnsSameGeneratedResource() { + GeneratedResource generatedClass1 = this.generatedResources.getOrAddForFeature("txt", "one"); + GeneratedResource generatedClass2 = this.generatedResources.getOrAddForFeature("txt", "one"); + GeneratedResource generatedClass3 = this.generatedResources.getOrAddForFeature("txt", "one"); + assertThat(generatedClass1).isNotNull().isSameAs(generatedClass2).isSameAs(generatedClass3); + } + + @Test + void getOrAddForFeatureComponentUsesTarget() { + GeneratedResource generatedClass = this.generatedResources.getOrAddForFeatureComponent("txt", "one", + TEST_COMPONENT); + assertThat(generatedClass.getPath()).isEqualTo("org/springframework/Example-one.txt"); + } + + @Test + void getOrAddForFeatureComponentWhenNewReturnsGeneratedResource() { + GeneratedResource generatedClass1 = this.generatedResources.getOrAddForFeatureComponent("txt", "one", TEST_COMPONENT); + GeneratedResource generatedClass2 = this.generatedResources.getOrAddForFeatureComponent("zip", "one", TEST_COMPONENT); + assertThat(generatedClass1).isNotNull().isNotEqualTo(generatedClass2); + assertThat(generatedClass2).isNotNull(); + } + + @Test + void getOrAddForFeatureComponentWhenRepeatReturnsSameGeneratedResource() { + GeneratedResource generatedClass1 = this.generatedResources.getOrAddForFeatureComponent("txt", "one", TEST_COMPONENT); + GeneratedResource generatedClass2 = this.generatedResources.getOrAddForFeatureComponent("txt", "one", TEST_COMPONENT); + GeneratedResource generatedClass3 = this.generatedResources.getOrAddForFeatureComponent("txt", "one", TEST_COMPONENT); + assertThat(generatedClass1).isNotNull().isSameAs(generatedClass2).isSameAs(generatedClass3); + } + + @Test + void addForFeatureWithSameNameReturnsDifferentInstances() { + GeneratedResource generatedResource1 = this.generatedResources + .addForFeature("txt", "one"); + GeneratedResource generatedResource2 = this.generatedResources + .addForFeature("txt", "one"); + assertThat(generatedResource1).isNotSameAs(generatedResource2); + assertThat(generatedResource1.getPath()).endsWith("-one.txt"); + assertThat(generatedResource2.getPath()).endsWith("-one1.txt"); + } + + @Test + void addForFeatureComponentWithSameNameReturnsDifferentInstances() { + GeneratedResource generatedResource1 = this.generatedResources + .addForFeatureComponent("txt", "one", TEST_COMPONENT); + GeneratedResource generatedResource2 = this.generatedResources + .addForFeatureComponent("txt", "one", TEST_COMPONENT); + assertThat(generatedResource1).isNotSameAs(generatedResource2); + assertThat(generatedResource1.getPath()).endsWith("-one.txt"); + assertThat(generatedResource2.getPath()).endsWith("-one1.txt"); + } + + @Test + void withFeatureNameUpdatesNamingConventions() { + GeneratedResource generatedResources1 = this.generatedResources + .addForFeatureComponent("txt", "one", TEST_COMPONENT); + GeneratedResource generatedResources2 = this.generatedResources.withFeatureNamePrefix("another") + .addForFeatureComponent("txt", "one", TEST_COMPONENT); + assertThat(generatedResources1.getPath()).endsWith("Example-one.txt"); + assertThat(generatedResources2.getPath()).endsWith("Example-another-one.txt"); + } + + @Test + void writeToAddResources() { + this.generatedResources.addForFeatureComponent("txt", "one", TEST_COMPONENT) + .handle(this::createTestContent); + this.generatedResources.addForFeatureComponent("json", "two", TEST_COMPONENT) + .handle(this::createTestContent); + InMemoryGeneratedFiles generatedFiles = new InMemoryGeneratedFiles(); + this.generatedResources.writeTo(generatedFiles); + assertThat(generatedFiles.getGeneratedFiles(GeneratedFiles.Kind.RESOURCE)).containsOnlyKeys( + "org/springframework/Example-one.txt", "org/springframework/Example-two.json"); + } + + private void createTestContent(GeneratedResource.Content content) { + content.create("test"); + } + + +} diff --git a/spring-core/src/test/java/org/springframework/aot/generate/NameGeneratorTests.java b/spring-core/src/test/java/org/springframework/aot/generate/NameGeneratorTests.java new file mode 100644 index 00000000000..7db6afae879 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generate/NameGeneratorTests.java @@ -0,0 +1,168 @@ +/* + * Copyright 2002-present 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.InputStream; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.javapoet.ClassName; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link NameGenerator}. + * + * @author Phillip Webb + */ +class NameGeneratorTests { + + private static final ClassName TEST_TARGET = ClassName.get("com.example", "Test"); + + private final NameGenerator generator = new NameGenerator(TEST_TARGET); + + @Nested + class ClassNameTests { + + @Test + void generateClassNameWhenTargetClassIsNullUsesMainTarget() { + ClassName generated = generator.generateClassName("test", null); + assertThat(generated).hasToString("com.example.Test__Test"); + } + + @Test + void generateClassNameUseFeatureNamePrefix() { + ClassName generated = new NameGenerator(TEST_TARGET, "One") + .generateClassName("test", ClassName.get(InputStream.class)); + assertThat(generated).hasToString("java.io.InputStream__OneTest"); + } + + @Test + void generateClassNameWithNoTextFeatureNamePrefix() { + ClassName generated = new NameGenerator(TEST_TARGET, " ") + .generateClassName("test", ClassName.get(InputStream.class)); + assertThat(generated).hasToString("java.io.InputStream__Test"); + } + + @Test + void generatedClassNameWhenFeatureIsEmptyThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> generator.generateClassName("", ClassName.get(InputStream.class))) + .withMessage("'featureName' must not be empty"); + } + + @Test + void generatedClassNameWhenFeatureIsNotAllLettersThrowsException() { + assertThat(generator.generateClassName("name!", ClassName.get(InputStream.class))) + .hasToString("java.io.InputStream__Name"); + assertThat(generator.generateClassName("1NameHere", ClassName.get(InputStream.class))) + .hasToString("java.io.InputStream__NameHere"); + assertThat(generator.generateClassName("Y0pe", ClassName.get(InputStream.class))) + .hasToString("java.io.InputStream__YPe"); + } + + @Test + void generateClassNameWithClassWhenLowercaseFeatureNameGeneratesName() { + ClassName generated = generator.generateClassName("bytes", ClassName.get(InputStream.class)); + assertThat(generated).hasToString("java.io.InputStream__Bytes"); + } + + @Test + void generateClassNameWithClassWhenInnerClassGeneratesName() { + ClassName innerBean = ClassName.get("com.example", "Test", "InnerBean"); + ClassName generated = generator.generateClassName("EventListener", innerBean); + assertThat(generated) + .hasToString("com.example.Test_InnerBean__EventListener"); + } + + @Test + void generateClassWithClassWhenMultipleCallsGeneratesSequencedName() { + ClassName generated1 = generator.generateClassName("bytes", ClassName.get(InputStream.class)); + ClassName generated2 = generator.generateClassName("bytes", ClassName.get(InputStream.class)); + ClassName generated3 = generator.generateClassName("bytes", ClassName.get(InputStream.class)); + assertThat(generated1).hasToString("java.io.InputStream__Bytes"); + assertThat(generated2).hasToString("java.io.InputStream__Bytes1"); + assertThat(generated3).hasToString("java.io.InputStream__Bytes2"); + } + + } + + @Nested + class ResourcePathTests { + + @Test + void generateResourcePathWhenTargetClassIsNullUsesMainTarget() { + String generated = generator.generateResourcePath("txt", "test", null); + assertThat(generated).isEqualTo("com/example/Test-test.txt"); + } + + @Test + void generateResourcePathUseFeatureNamePrefix() { + String generated = new NameGenerator(TEST_TARGET, "one") + .generateResourcePath("txt", "test", ClassName.get(InputStream.class)); + assertThat(generated).hasToString("java/io/InputStream-one-test.txt"); + } + + @Test + void generateResourcePathWithEmptyFeatureNamePrefix() { + String generated = new NameGenerator(TEST_TARGET, "") + .generateResourcePath("txt", "test", ClassName.get(InputStream.class)); + assertThat(generated).hasToString("java/io/InputStream-test.txt"); + } + + @Test + void generateResourcePathWithNoTextFeatureNamePrefix() { + String generated = new NameGenerator(TEST_TARGET, " ") + .generateResourcePath("txt", "test", ClassName.get(InputStream.class)); + assertThat(generated).hasToString("java/io/InputStream-test.txt"); + } + + @Test + void generatedResourcePathWhenExtensionIsEmptyThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> generator.generateResourcePath("", "test", ClassName.get(InputStream.class))) + .withMessage("'extension' must not be empty"); + } + + @Test + void generatedResourcePathWhenFeatureIsEmptyThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> generator.generateResourcePath("txt", "", ClassName.get(InputStream.class))) + .withMessage("'featureName' must not be empty"); + } + + @Test + void generateResourcePathWhenCaseFeatureNameGeneratesName() { + String generated = generator.generateResourcePath("txt", "Bytes", ClassName.get(InputStream.class)); + assertThat(generated).hasToString("java/io/InputStream-Bytes.txt"); + } + + @Test + void generateResourcePathWhenMultipleCallsGeneratesSequencedName() { + String generated1 = generator.generateResourcePath("txt","bytes", ClassName.get(InputStream.class)); + String generated2 = generator.generateResourcePath("txt","bytes", ClassName.get(InputStream.class)); + String generated3 = generator.generateResourcePath("txt", "bytes", ClassName.get(InputStream.class)); + assertThat(generated1).hasToString("java/io/InputStream-bytes.txt"); + assertThat(generated2).hasToString("java/io/InputStream-bytes1.txt"); + assertThat(generated3).hasToString("java/io/InputStream-bytes2.txt"); + } + + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java index fae442e6c30..9b0cf3349ab 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextAotGenerator.java @@ -29,11 +29,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.aot.AotDetector; -import org.springframework.aot.generate.ClassNameGenerator; import org.springframework.aot.generate.DefaultGenerationContext; import org.springframework.aot.generate.GeneratedClasses; import org.springframework.aot.generate.GeneratedFiles; import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.generate.NameGenerator; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; @@ -378,9 +378,9 @@ public class TestContextAotGenerator { } DefaultGenerationContext createGenerationContext(Class testClass) { - ClassNameGenerator classNameGenerator = new ClassNameGenerator(ClassName.get(testClass)); + NameGenerator nameGenerator = new NameGenerator(ClassName.get(testClass)); TestContextGenerationContext generationContext = - new TestContextGenerationContext(classNameGenerator, this.generatedFiles, this.runtimeHints); + new TestContextGenerationContext(nameGenerator, this.generatedFiles, this.runtimeHints); return generationContext.withName(nextTestContextId()); } @@ -389,9 +389,9 @@ public class TestContextAotGenerator { } private void generateAotTestContextInitializerMappings(MultiValueMap> initializerClassMappings) { - ClassNameGenerator classNameGenerator = new ClassNameGenerator(ClassName.get(AotTestContextInitializers.class)); + NameGenerator nameGenerator = new NameGenerator(ClassName.get(AotTestContextInitializers.class)); DefaultGenerationContext generationContext = - new DefaultGenerationContext(classNameGenerator, this.generatedFiles, this.runtimeHints); + new DefaultGenerationContext(nameGenerator, this.generatedFiles, this.runtimeHints); GeneratedClasses generatedClasses = generationContext.getGeneratedClasses(); AotTestContextInitializersCodeGenerator codeGenerator = @@ -402,9 +402,9 @@ public class TestContextAotGenerator { } private void generateAotTestAttributeMappings() { - ClassNameGenerator classNameGenerator = new ClassNameGenerator(ClassName.get(AotTestAttributes.class)); + NameGenerator nameGenerator = new NameGenerator(ClassName.get(AotTestAttributes.class)); DefaultGenerationContext generationContext = - new DefaultGenerationContext(classNameGenerator, this.generatedFiles, this.runtimeHints); + new DefaultGenerationContext(nameGenerator, this.generatedFiles, this.runtimeHints); GeneratedClasses generatedClasses = generationContext.getGeneratedClasses(); Map attributes = AotTestAttributesFactory.getAttributes(); diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextGenerationContext.java b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextGenerationContext.java index 6a858893270..51faa4525c6 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextGenerationContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextGenerationContext.java @@ -18,9 +18,9 @@ package org.springframework.test.context.aot; import org.jspecify.annotations.Nullable; -import org.springframework.aot.generate.ClassNameGenerator; import org.springframework.aot.generate.DefaultGenerationContext; import org.springframework.aot.generate.GeneratedFiles; +import org.springframework.aot.generate.NameGenerator; import org.springframework.aot.hint.RuntimeHints; /** @@ -37,16 +37,17 @@ class TestContextGenerationContext extends DefaultGenerationContext { /** * Create a new {@link TestContextGenerationContext} instance backed by the - * specified {@link ClassNameGenerator}, {@link GeneratedFiles}, and + * specified {@link NameGenerator}, {@link GeneratedFiles}, and * {@link RuntimeHints}. - * @param classNameGenerator the naming convention to use for generated class names + * @param nameGenerator the naming convention to use for generated classes + * and resources * @param generatedFiles the generated files * @param runtimeHints the runtime hints */ TestContextGenerationContext( - ClassNameGenerator classNameGenerator, GeneratedFiles generatedFiles, RuntimeHints runtimeHints) { + NameGenerator nameGenerator, GeneratedFiles generatedFiles, RuntimeHints runtimeHints) { - super(classNameGenerator, generatedFiles, runtimeHints); + super(nameGenerator, generatedFiles, runtimeHints); this.featureName = null; }