From 99173fbd4f082172d8d9cbe92d0cd1f2b313e19c Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 13 Apr 2022 17:37:53 -0700 Subject: [PATCH] Add GeneratedFiles interface and support classes Add a `GeneratedFiles` interface that can be used to add generated source, class and resource files. An in-memory implementation is provided for testing and a filesystem implementation is provided to actually save the files to disk. See gh-28414 --- .../AppendableConsumerInputStreamSource.java | 56 +++++ .../generate/FileSystemGeneratedFiles.java | 101 +++++++++ .../aot/generate/GeneratedFiles.java | 207 ++++++++++++++++++ .../aot/generate/InMemoryGeneratedFiles.java | 95 ++++++++ .../aot/generate/package-info.java | 10 + .../FileSystemGeneratedFilesTests.java | 112 ++++++++++ .../aot/generate/GeneratedFilesTests.java | 171 +++++++++++++++ .../generate/InMemoryGeneratedFilesTests.java | 87 ++++++++ 8 files changed, 839 insertions(+) create mode 100644 spring-core/src/main/java/org/springframework/aot/generate/AppendableConsumerInputStreamSource.java create mode 100644 spring-core/src/main/java/org/springframework/aot/generate/FileSystemGeneratedFiles.java create mode 100644 spring-core/src/main/java/org/springframework/aot/generate/GeneratedFiles.java create mode 100644 spring-core/src/main/java/org/springframework/aot/generate/InMemoryGeneratedFiles.java create mode 100644 spring-core/src/main/java/org/springframework/aot/generate/package-info.java create mode 100644 spring-core/src/test/java/org/springframework/aot/generate/FileSystemGeneratedFilesTests.java create mode 100644 spring-core/src/test/java/org/springframework/aot/generate/GeneratedFilesTests.java create mode 100644 spring-core/src/test/java/org/springframework/aot/generate/InMemoryGeneratedFilesTests.java diff --git a/spring-core/src/main/java/org/springframework/aot/generate/AppendableConsumerInputStreamSource.java b/spring-core/src/main/java/org/springframework/aot/generate/AppendableConsumerInputStreamSource.java new file mode 100644 index 0000000000..faf34eb53d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/AppendableConsumerInputStreamSource.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.generate; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.springframework.core.io.InputStreamSource; +import org.springframework.util.function.ThrowingConsumer; + +/** + * Adapter class to convert a {@link ThrowingConsumer} of {@link Appendable} to + * an {@link InputStreamSource}. + * + * @author Phillip Webb + * @since 6.0 + */ +class AppendableConsumerInputStreamSource implements InputStreamSource { + + private final ThrowingConsumer content; + + + AppendableConsumerInputStreamSource(ThrowingConsumer content) { + this.content = content; + } + + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(toString().getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String toString() { + StringBuilder buffer = new StringBuilder(); + this.content.accept(buffer); + return buffer.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generate/FileSystemGeneratedFiles.java b/spring-core/src/main/java/org/springframework/aot/generate/FileSystemGeneratedFiles.java new file mode 100644 index 0000000000..2839096707 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/FileSystemGeneratedFiles.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.generate; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Function; + +import org.springframework.core.io.InputStreamSource; +import org.springframework.util.Assert; + +/** + * {@link GeneratedFiles} implementation that stores generated files using a + * {@link FileSystem}. + * + * @author Phillip Webb + * @since 6.0 + */ +public class FileSystemGeneratedFiles implements GeneratedFiles { + + private final Function roots; + + + /** + * Create a new {@link FileSystemGeneratedFiles} instance with all files + * stored under the specific {@code root}. The following subdirectories are + * created for the different file {@link Kind kinds}: + *
    + *
  • {@code sources}
  • + *
  • {@code resources}
  • + *
  • {@code classes}
  • + *
+ * @param root the root path + * @see #FileSystemGeneratedFiles(Function) + */ + public FileSystemGeneratedFiles(Path root) { + this(conventionRoots(root)); + } + + /** + * Create a new {@link FileSystemGeneratedFiles} instance with all files + * stored under the root provided by the given {@link Function}. + * @param roots a function that returns the root to use for the given + * {@link Kind} + */ + public FileSystemGeneratedFiles(Function roots) { + Assert.notNull(roots, "'roots' must not be null"); + Assert.isTrue(Arrays.stream(Kind.values()).map(roots).noneMatch(Objects::isNull), + "'roots' must return a value for all file kinds"); + this.roots = roots; + } + + + private static Function conventionRoots(Path root) { + Assert.notNull(root, "'root' must not be null"); + return kind -> switch (kind) { + case SOURCE -> root.resolve("sources"); + case RESOURCE -> root.resolve("resources"); + case CLASS -> root.resolve("classes"); + }; + } + + @Override + public void addFile(Kind kind, String path, InputStreamSource content) { + Assert.notNull(kind, "'kind' must not be null"); + Assert.hasLength(path, "'path' must not be empty"); + Assert.notNull(content, "'kind' must not be null"); + Path root = this.roots.apply(kind).toAbsolutePath().normalize(); + Path relativePath = root.resolve(path).toAbsolutePath().normalize(); + Assert.isTrue(relativePath.startsWith(root), () -> "'path' must be relative"); + try { + try (InputStream inputStream = content.getInputStream()) { + Files.createDirectories(relativePath.getParent()); + Files.copy(inputStream, relativePath); + } + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedFiles.java b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedFiles.java new file mode 100644 index 0000000000..6065d5b991 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedFiles.java @@ -0,0 +1,207 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.generate; + +import org.springframework.core.io.InputStreamSource; +import org.springframework.javapoet.JavaFile; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.function.ThrowingConsumer; + +/** + * Interface that can be used to add {@link Kind#SOURCE source}, + * {@link Kind#RESOURCE resource} or {@link Kind#CLASS class} files generated + * during ahead-of-time processing. Source and resource files are written using + * UTF-8 encoding. + * + * @author Phillip Webb + * @author Brian Clozel + * @author Stephane Nicoll + * @since 6.0 + * @see InMemoryGeneratedFiles + * @see FileSystemGeneratedFiles + */ +public interface GeneratedFiles { + + /** + * Add a generated {@link Kind#SOURCE source file} with content from the + * given {@link JavaFile}. + * @param javaFile the java file to add + */ + default void addSourceFile(JavaFile javaFile) { + String className = javaFile.packageName + "." + javaFile.typeSpec.name; + addSourceFile(className, javaFile::writeTo); + } + + /** + * Add a generated {@link Kind#SOURCE source file} with content from the + * given {@link CharSequence}. + * @param className the class name that should be used to determine the path + * of the file + * @param content the contents of the file + */ + default void addSourceFile(String className, CharSequence content) { + addSourceFile(className, appendable -> appendable.append(content)); + } + + /** + * Add a generated {@link Kind#SOURCE source file} with content written to + * an {@link Appendable} passed to the given {@link ThrowingConsumer}. + * @param className the class name that should be used to determine the path + * of the file + * @param content a {@link ThrowingConsumer} that accepts an + * {@link Appendable} which will receive the file contents + */ + default void addSourceFile(String className, ThrowingConsumer content) { + addFile(Kind.SOURCE, getClassNamePath(className), content); + } + + /** + * Add a generated {@link Kind#SOURCE source file} with content from the + * given {@link InputStreamSource}. + * @param className the class name that should be used to determine the path + * of the file + * @param content an {@link InputStreamSource} that will provide an input + * stream containing the file contents + */ + default void addSourceFile(String className, InputStreamSource content) { + addFile(Kind.SOURCE, getClassNamePath(className), content); + } + + /** + * Add a generated {@link Kind#RESOURCE resource file} with content from the + * given {@link CharSequence}. + * @param path the relative path of the file + * @param content the contents of the file + */ + default void addResourceFile(String path, CharSequence content) { + addResourceFile(path, appendable -> appendable.append(content)); + } + + /** + * Add a generated {@link Kind#RESOURCE resource file} with content written + * to an {@link Appendable} passed to the given {@link ThrowingConsumer}. + * @param path the relative path of the file + * @param content a {@link ThrowingConsumer} that accepts an + * {@link Appendable} which will receive the file contents + */ + default void addResourceFile(String path, ThrowingConsumer content) { + addFile(Kind.RESOURCE, path, content); + } + + /** + * Add a generated {@link Kind#RESOURCE resource file} with content from the + * given {@link InputStreamSource}. + * @param path the relative path of the file + * @param content an {@link InputStreamSource} that will provide an input + * stream containing the file contents + */ + default void addResourceFile(String path, InputStreamSource content) { + addFile(Kind.RESOURCE, path, content); + } + + /** + * Add a generated {@link Kind#CLASS class file} with content from the given + * {@link InputStreamSource}. + * @param path the relative path of the file + * @param content an {@link InputStreamSource} that will provide an input + * stream containing the file contents + */ + default void addClassFile(String path, InputStreamSource content) { + addFile(Kind.CLASS, path, content); + } + + /** + * Add a generated file of the specified {@link Kind} with content from the + * given {@link CharSequence}. + * @param kind the kind of file being written + * @param path the relative path of the file + * @param content the contents of the file + */ + default void addFile(Kind kind, String path, CharSequence content) { + addFile(kind, path, appendable -> appendable.append(content)); + } + + /** + * Add a generated file of the specified {@link Kind} with content written + * to an {@link Appendable} passed to the given {@link ThrowingConsumer}. + * @param kind the kind of file being written + * @param path the relative path of the file + * @param content a {@link ThrowingConsumer} that accepts an + * {@link Appendable} which will receive the file contents + */ + default void addFile(Kind kind, String path, ThrowingConsumer content) { + Assert.notNull(content, "'content' must not be null"); + addFile(kind, path, new AppendableConsumerInputStreamSource(content)); + } + + /** + * Add a generated file of the specified {@link Kind} with content from the + * given {@link InputStreamSource}. + * @param kind the kind of file being written + * @param path the relative path of the file + * @param content an {@link InputStreamSource} that will provide an input + * stream containing the file contents + */ + void addFile(Kind kind, String path, InputStreamSource content); + + private static String getClassNamePath(String className) { + Assert.hasLength(className, "'className' must not be empty"); + Assert.isTrue(isJavaIdentifier(className), + "'className' must be a valid identifier"); + return ClassUtils.convertClassNameToResourcePath(className) + ".java"; + } + + private static boolean isJavaIdentifier(String className) { + char[] chars = className.toCharArray(); + for (int i = 0; i < chars.length; i++) { + if (i == 0 && !Character.isJavaIdentifierStart(chars[i])) { + return false; + } + if (i > 0 && chars[i] != '.' && !Character.isJavaIdentifierPart(chars[i])) { + return false; + } + } + return true; + } + + + /** + * The various kinds of generated files that are supported. + */ + enum Kind { + + /** + * A source file containing Java code that should be compiled. + */ + SOURCE, + + /** + * A resource file that should be directly added to final application. + * For example, a {@code .properties} file. + */ + RESOURCE, + + /** + * A class file containing bytecode. For example, the result of a proxy + * generated using cglib. + */ + CLASS + + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generate/InMemoryGeneratedFiles.java b/spring-core/src/main/java/org/springframework/aot/generate/InMemoryGeneratedFiles.java new file mode 100644 index 0000000000..8bca51ca3f --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/InMemoryGeneratedFiles.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.generate; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.core.io.InputStreamSource; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link GeneratedFiles} implementation that keeps generated files in-memory. + * + * @author Phillip Webb + * @since 6.0 + */ +public class InMemoryGeneratedFiles implements GeneratedFiles { + + private final Map> files = new HashMap<>(); + + + @Override + public void addFile(Kind kind, String path, InputStreamSource content) { + Assert.notNull(kind, "'kind' must not be null"); + Assert.hasLength(path, "'path' must not be empty"); + Assert.notNull(content, "'content' must not be null"); + Map paths = this.files.computeIfAbsent(kind, + key -> new LinkedHashMap<>()); + Assert.state(!paths.containsKey(path), + () -> "Path '" + path + "' already in use"); + paths.put(path, content); + } + + /** + * Return a {@link Map} of the generated files of a specific {@link Kind}. + * @param kind the kind of generated file + * @return a {@link Map} of paths to {@link InputStreamSource} instances + */ + public Map getGeneratedFiles(Kind kind) { + Assert.notNull(kind, "'kind' must not be null"); + return Collections + .unmodifiableMap(this.files.getOrDefault(kind, Collections.emptyMap())); + } + + /** + * Return the content of the specified file. + * @param kind the kind of generated file + * @param path the path of the file + * @return the file content or {@code null} if no file could be found + * @throws IOException on read error + */ + @Nullable + public String getGeneratedFileContent(Kind kind, String path) throws IOException { + InputStreamSource source = getGeneratedFile(kind, path); + if (source != null) { + return new String(source.getInputStream().readAllBytes(), + StandardCharsets.UTF_8); + } + return null; + } + + /** + * Return the {@link InputStreamSource} of specified file. + * @param kind the kind of generated file + * @param path the path of the file + * @return the file source or {@code null} if no file could be found + */ + @Nullable + public InputStreamSource getGeneratedFile(Kind kind, String path) { + Assert.notNull(kind, "'kind' must not be null"); + Assert.hasLength(path, "'path' must not be empty"); + Map paths = this.files.get(kind); + return (paths != null) ? paths.get(path) : null; + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/generate/package-info.java b/spring-core/src/main/java/org/springframework/aot/generate/package-info.java new file mode 100644 index 0000000000..c6c380a8e2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/generate/package-info.java @@ -0,0 +1,10 @@ +/** + * Support classes for components that contribute generated code equivalent to a + * runtime behavior. + */ +@NonNullApi +@NonNullFields +package org.springframework.aot.generate; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/test/java/org/springframework/aot/generate/FileSystemGeneratedFilesTests.java b/spring-core/src/test/java/org/springframework/aot/generate/FileSystemGeneratedFilesTests.java new file mode 100644 index 0000000000..c3cc52a864 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generate/FileSystemGeneratedFilesTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.generate; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.aot.generate.GeneratedFiles.Kind; +import org.springframework.core.io.ByteArrayResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link FileSystemGeneratedFiles}. + * + * @author Phillip Webb + */ +class FileSystemGeneratedFilesTests { + + @TempDir + Path root; + + @Test + void addFilesCopiesToFileSystem() { + FileSystemGeneratedFiles generatedFiles = new FileSystemGeneratedFiles(this.root); + generatedFiles.addSourceFile("com.example.Test", "{}"); + generatedFiles.addResourceFile("META-INF/test", "test"); + generatedFiles.addClassFile("com/example/TestProxy.class", + new ByteArrayResource("!".getBytes(StandardCharsets.UTF_8))); + assertThat(this.root.resolve("sources/com/example/Test.java")).content() + .isEqualTo("{}"); + assertThat(this.root.resolve("resources/META-INF/test")).content() + .isEqualTo("test"); + assertThat(this.root.resolve("classes/com/example/TestProxy.class")).content() + .isEqualTo("!"); + } + + @Test + void addFilesWithCustomRootsCopiesToFileSystem() { + FileSystemGeneratedFiles generatedFiles = new FileSystemGeneratedFiles( + kind -> this.root.resolve("the-" + kind)); + generatedFiles.addSourceFile("com.example.Test", "{}"); + generatedFiles.addResourceFile("META-INF/test", "test"); + generatedFiles.addClassFile("com/example/TestProxy.class", + new ByteArrayResource("!".getBytes(StandardCharsets.UTF_8))); + assertThat(this.root.resolve("the-SOURCE/com/example/Test.java")).content() + .isEqualTo("{}"); + assertThat(this.root.resolve("the-RESOURCE/META-INF/test")).content() + .isEqualTo("test"); + assertThat(this.root.resolve("the-CLASS/com/example/TestProxy.class")).content() + .isEqualTo("!"); + } + + @Test + void createWhenRootIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new FileSystemGeneratedFiles((Path) null)) + .withMessage("'root' must not be null"); + } + + @Test + void createWhenRootsIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> new FileSystemGeneratedFiles((Function) null)) + .withMessage("'roots' must not be null"); + } + + @Test + void createWhenRootsResultsInNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> new FileSystemGeneratedFiles(kind -> (kind != Kind.CLASS) + ? this.root.resolve(kind.toString()) : null)) + .withMessage("'roots' must return a value for all file kinds"); + } + + @Test + void addFileWhenPathIsOutsideOfRootThrowsException() { + FileSystemGeneratedFiles generatedFiles = new FileSystemGeneratedFiles(this.root); + assertPathMustBeRelative(generatedFiles, "/test"); + assertPathMustBeRelative(generatedFiles, "../test"); + assertPathMustBeRelative(generatedFiles, "test/../../test"); + } + + private void assertPathMustBeRelative(FileSystemGeneratedFiles generatedFiles, + String path) { + assertThatIllegalArgumentException() + .isThrownBy(() -> generatedFiles.addResourceFile(path, "test")) + .withMessage("'path' must be relative"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/generate/GeneratedFilesTests.java b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedFilesTests.java new file mode 100644 index 0000000000..9f1f68ef30 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedFilesTests.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.generate; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import javax.lang.model.element.Modifier; + +import org.assertj.core.api.AbstractStringAssert; +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.GeneratedFiles.Kind; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.InputStreamSource; +import org.springframework.core.io.Resource; +import org.springframework.javapoet.JavaFile; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.TypeSpec; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link GeneratedFiles}. + * + * @author Phillip Webb + */ +class GeneratedFilesTests { + + private final TestGeneratedFiles generatedFiles = new TestGeneratedFiles(); + + @Test + void addSourceFileWithJavaFileAddsFile() throws Exception { + MethodSpec main = MethodSpec.methodBuilder("main") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC).returns(void.class) + .addParameter(String[].class, "args") + .addStatement("$T.out.println($S)", System.class, "Hello, World!") + .build(); + TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") + .addModifiers(Modifier.PUBLIC, Modifier.FINAL).addMethod(main).build(); + JavaFile javaFile = JavaFile.builder("com.example", helloWorld).build(); + this.generatedFiles.addSourceFile(javaFile); + assertThatFileAdded(Kind.SOURCE, "com/example/HelloWorld.java") + .contains("Hello, World!"); + } + + @Test + void addSourceFileWithCharSequenceAddsFile() throws Exception { + this.generatedFiles.addSourceFile("com.example.HelloWorld", "{}"); + assertThatFileAdded(Kind.SOURCE, "com/example/HelloWorld.java").isEqualTo("{}"); + } + + @Test + void addSourceFileWithCharSequenceWhenClassNameIsEmptyThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.generatedFiles.addSourceFile("", "{}")) + .withMessage("'className' must not be empty"); + } + + @Test + void addSourceFileWithCharSequenceWhenClassNameIsInvalidThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> this.generatedFiles + .addSourceFile("com/example/HelloWorld.java", "{}")) + .withMessage("'className' must be a valid identifier"); + } + + @Test + void addSourceFileWithConsumedAppendableAddsFile() throws Exception { + this.generatedFiles.addSourceFile("com.example.HelloWorld", + appendable -> appendable.append("{}")); + assertThatFileAdded(Kind.SOURCE, "com/example/HelloWorld.java").isEqualTo("{}"); + } + + @Test + void addSourceFileWithInputStreamSourceAddsFile() throws Exception { + Resource resource = new ByteArrayResource("{}".getBytes(StandardCharsets.UTF_8)); + this.generatedFiles.addSourceFile("com.example.HelloWorld", resource); + assertThatFileAdded(Kind.SOURCE, "com/example/HelloWorld.java").isEqualTo("{}"); + } + + @Test + void addResourceFileWithCharSequenceAddsFile() throws Exception { + this.generatedFiles.addResourceFile("META-INF/file", "test"); + assertThatFileAdded(Kind.RESOURCE, "META-INF/file").isEqualTo("test"); + } + + @Test + void addResourceFileWithConsumedAppendableAddsFile() throws Exception { + this.generatedFiles.addResourceFile("META-INF/file", + appendable -> appendable.append("test")); + assertThatFileAdded(Kind.RESOURCE, "META-INF/file").isEqualTo("test"); + } + + @Test + void addResourceFileWithInputStreamSourceAddsFile() throws IOException { + Resource resource = new ByteArrayResource( + "test".getBytes(StandardCharsets.UTF_8)); + this.generatedFiles.addResourceFile("META-INF/file", resource); + assertThatFileAdded(Kind.RESOURCE, "META-INF/file").isEqualTo("test"); + } + + @Test + void addClassFileWithInputStreamSourceAddsFile() throws IOException { + Resource resource = new ByteArrayResource( + "test".getBytes(StandardCharsets.UTF_8)); + this.generatedFiles.addClassFile("com/example/HelloWorld.class", resource); + assertThatFileAdded(Kind.CLASS, "com/example/HelloWorld.class").isEqualTo("test"); + } + + @Test + void addFileWithCharSequenceAddsFile() throws Exception { + this.generatedFiles.addFile(Kind.RESOURCE, "META-INF/file", "test"); + assertThatFileAdded(Kind.RESOURCE, "META-INF/file").isEqualTo("test"); + } + + @Test + void addFileWithConsumedAppendableAddsFile() throws IOException { + this.generatedFiles.addFile(Kind.SOURCE, "com/example/HelloWorld.java", + appendable -> appendable.append("{}")); + assertThatFileAdded(Kind.SOURCE, "com/example/HelloWorld.java").isEqualTo("{}"); + } + + private AbstractStringAssert assertThatFileAdded(Kind kind, String path) + throws IOException { + return this.generatedFiles.assertThatFileAdded(kind, path); + } + + static class TestGeneratedFiles implements GeneratedFiles { + + private Kind kind; + + private String path; + + private InputStreamSource content; + + @Override + public void addFile(Kind kind, String path, InputStreamSource content) { + this.kind = kind; + this.path = path; + this.content = content; + } + + AbstractStringAssert assertThatFileAdded(Kind kind, String path) + throws IOException { + assertThat(this.kind).as("kind").isEqualTo(kind); + assertThat(this.path).as("path").isEqualTo(path); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + this.content.getInputStream().transferTo(out); + return assertThat(out.toString(StandardCharsets.UTF_8)); + } + + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/generate/InMemoryGeneratedFilesTests.java b/spring-core/src/test/java/org/springframework/aot/generate/InMemoryGeneratedFilesTests.java new file mode 100644 index 0000000000..c8fc1ccfd4 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/generate/InMemoryGeneratedFilesTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.generate; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.GeneratedFiles.Kind; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link InMemoryGeneratedFiles}. + * + * @author Phillip Webb + */ +class InMemoryGeneratedFilesTests { + + private final InMemoryGeneratedFiles generatedFiles = new InMemoryGeneratedFiles(); + + @Test + void addFileAddsInMemoryFile() throws Exception { + this.generatedFiles.addResourceFile("META-INF/test", "test"); + assertThat(this.generatedFiles.getGeneratedFileContent(Kind.RESOURCE, + "META-INF/test")).isEqualTo("test"); + } + + @Test + void addFileWhenFileAlreadyAddedThrowsException() { + this.generatedFiles.addResourceFile("META-INF/test", "test"); + assertThatIllegalStateException().isThrownBy( + () -> this.generatedFiles.addResourceFile("META-INF/test", "test")) + .withMessage("Path 'META-INF/test' already in use"); + } + + @Test + void getGeneratedFilesReturnsFiles() throws Exception { + this.generatedFiles.addResourceFile("META-INF/test1", "test1"); + this.generatedFiles.addResourceFile("META-INF/test2", "test2"); + assertThat(this.generatedFiles.getGeneratedFiles(Kind.RESOURCE)) + .containsKeys("META-INF/test1", "META-INF/test2"); + } + + @Test + void getGeneratedFileContentWhenFileExistsReturnsContent() throws Exception { + this.generatedFiles.addResourceFile("META-INF/test", "test"); + assertThat(this.generatedFiles.getGeneratedFileContent(Kind.RESOURCE, + "META-INF/test")).isEqualTo("test"); + } + + @Test + void getGeneratedFileContentWhenFileIsMissingReturnsNull() throws Exception { + this.generatedFiles.addResourceFile("META-INF/test", "test"); + assertThat(this.generatedFiles.getGeneratedFileContent(Kind.RESOURCE, + "META-INF/missing")).isNull(); + } + + @Test + void getGeneratedFileWhenFileExistsReturnsInputStreamSource() { + this.generatedFiles.addResourceFile("META-INF/test", "test"); + assertThat(this.generatedFiles.getGeneratedFile(Kind.RESOURCE, "META-INF/test")) + .isNotNull(); + } + + @Test + void getGeneratedFileWhenFileIsMissingReturnsNull() { + this.generatedFiles.addResourceFile("META-INF/test", "test"); + assertThat( + this.generatedFiles.getGeneratedFile(Kind.RESOURCE, "META-INF/missing")) + .isNull(); + } + +}