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
This commit is contained in:
Phillip Webb
2022-04-13 17:37:53 -07:00
parent f2cf78c525
commit 99173fbd4f
8 changed files with 839 additions and 0 deletions

View File

@@ -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();
}
}

View File

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

View File

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

View File

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

View File

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