Browse Source
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-28414pull/28422/head
8 changed files with 839 additions and 0 deletions
@ -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<Appendable> content; |
||||||
|
|
||||||
|
|
||||||
|
AppendableConsumerInputStreamSource(ThrowingConsumer<Appendable> 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(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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<Kind, Path> 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}: |
||||||
|
* <ul> |
||||||
|
* <li>{@code sources}</li> |
||||||
|
* <li>{@code resources}</li> |
||||||
|
* <li>{@code classes}</li> |
||||||
|
* </ul> |
||||||
|
* @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<Kind, Path> 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<Kind, Path> 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); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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<Appendable> 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<Appendable> 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<Appendable> 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 |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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<Kind, Map<String, InputStreamSource>> 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<String, InputStreamSource> 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<String, InputStreamSource> 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<String, InputStreamSource> paths = this.files.get(kind); |
||||||
|
return (paths != null) ? paths.get(path) : null; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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; |
||||||
@ -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<Kind, Path>) 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"); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue