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