Skip to content

Commit

Permalink
Provide more control over registration of GeneratedFiles
Browse files Browse the repository at this point in the history
This commit provides an advanced handling of generated files that
provides more control over files registration. The callback provides
a FileHandler that can determine if the file already exists and its
content. The caller can then chose to override the content or leave it
as it is.

Closes gh-31331
  • Loading branch information
snicoll committed Jun 22, 2024
1 parent e6b77d3 commit 2650da2
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
Expand All @@ -18,24 +18,29 @@

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.CopyOption;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Objects;
import java.util.function.Function;

import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.InputStreamSource;
import org.springframework.util.Assert;
import org.springframework.util.function.ThrowingConsumer;

/**
* {@link GeneratedFiles} implementation that stores generated files using a
* {@link FileSystem}.
*
* @author Phillip Webb
* @author Stephane Nicoll
* @since 6.0
*/
public class FileSystemGeneratedFiles implements GeneratedFiles {
public class FileSystemGeneratedFiles extends GeneratedFiles {

private final Function<Kind, Path> roots;

Expand Down Expand Up @@ -80,21 +85,54 @@ private static Function<Kind, Path> conventionRoots(Path root) {
}

@Override
public void addFile(Kind kind, String path, InputStreamSource content) {
public void handleFile(Kind kind, String path, ThrowingConsumer<FileHandler> handler) {
FileSystemFileHandler fileHandler = new FileSystemFileHandler(toPath(kind, path));
handler.accept(fileHandler);
}

private Path toPath(Kind kind, String path) {
Assert.notNull(kind, "'kind' must not be null");
Assert.hasLength(path, "'path' must not be empty");
Assert.notNull(content, "'content' 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);
return relativePath;
}

static final class FileSystemFileHandler extends FileHandler {

private final Path path;

FileSystemFileHandler(Path path) {
super(Files.exists(path), () -> new FileSystemResource(path));
this.path = path;
}

@Override
protected void copy(InputStreamSource content, boolean override) {
if (override) {
copy(content, StandardCopyOption.REPLACE_EXISTING);
}
else {
copy(content);
}
}
catch (IOException ex) {
throw new IllegalStateException(ex);

private void copy(InputStreamSource content, CopyOption... copyOptions) {
try {
try (InputStream inputStream = content.getInputStream()) {
Files.createDirectories(this.path.getParent());
Files.copy(inputStream, this.path, copyOptions);
}
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}

@Override
public String toString() {
return this.path.toString();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
Expand All @@ -16,8 +16,11 @@

package org.springframework.aot.generate;

import java.util.function.Supplier;

import org.springframework.core.io.InputStreamSource;
import org.springframework.javapoet.JavaFile;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
Expand All @@ -36,14 +39,14 @@
* @see InMemoryGeneratedFiles
* @see FileSystemGeneratedFiles
*/
public interface GeneratedFiles {
public abstract class 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) {
public void addSourceFile(JavaFile javaFile) {
validatePackage(javaFile.packageName, javaFile.typeSpec.name);
String className = javaFile.packageName + "." + javaFile.typeSpec.name;
addSourceFile(className, javaFile::writeTo);
Expand All @@ -56,7 +59,7 @@ default void addSourceFile(JavaFile javaFile) {
* of the file
* @param content the contents of the file
*/
default void addSourceFile(String className, CharSequence content) {
public void addSourceFile(String className, CharSequence content) {
addSourceFile(className, appendable -> appendable.append(content));
}

Expand All @@ -68,7 +71,7 @@ default void addSourceFile(String className, CharSequence content) {
* @param content a {@link ThrowingConsumer} that accepts an
* {@link Appendable} which will receive the file contents
*/
default void addSourceFile(String className, ThrowingConsumer<Appendable> content) {
public void addSourceFile(String className, ThrowingConsumer<Appendable> content) {
addFile(Kind.SOURCE, getClassNamePath(className), content);
}

Expand All @@ -80,7 +83,7 @@ default void addSourceFile(String className, ThrowingConsumer<Appendable> conten
* @param content an {@link InputStreamSource} that will provide an input
* stream containing the file contents
*/
default void addSourceFile(String className, InputStreamSource content) {
public void addSourceFile(String className, InputStreamSource content) {
addFile(Kind.SOURCE, getClassNamePath(className), content);
}

Expand All @@ -90,7 +93,7 @@ default void addSourceFile(String className, InputStreamSource content) {
* @param path the relative path of the file
* @param content the contents of the file
*/
default void addResourceFile(String path, CharSequence content) {
public void addResourceFile(String path, CharSequence content) {
addResourceFile(path, appendable -> appendable.append(content));
}

Expand All @@ -101,7 +104,7 @@ default void addResourceFile(String path, CharSequence content) {
* @param content a {@link ThrowingConsumer} that accepts an
* {@link Appendable} which will receive the file contents
*/
default void addResourceFile(String path, ThrowingConsumer<Appendable> content) {
public void addResourceFile(String path, ThrowingConsumer<Appendable> content) {
addFile(Kind.RESOURCE, path, content);
}

Expand All @@ -112,7 +115,7 @@ default void addResourceFile(String path, ThrowingConsumer<Appendable> content)
* @param content an {@link InputStreamSource} that will provide an input
* stream containing the file contents
*/
default void addResourceFile(String path, InputStreamSource content) {
public void addResourceFile(String path, InputStreamSource content) {
addFile(Kind.RESOURCE, path, content);
}

Expand All @@ -123,7 +126,7 @@ default void addResourceFile(String path, InputStreamSource content) {
* @param content an {@link InputStreamSource} that will provide an input
* stream containing the file contents
*/
default void addClassFile(String path, InputStreamSource content) {
public void addClassFile(String path, InputStreamSource content) {
addFile(Kind.CLASS, path, content);
}

Expand All @@ -134,7 +137,7 @@ default void addClassFile(String path, InputStreamSource content) {
* @param path the relative path of the file
* @param content the contents of the file
*/
default void addFile(Kind kind, String path, CharSequence content) {
public void addFile(Kind kind, String path, CharSequence content) {
addFile(kind, path, appendable -> appendable.append(content));
}

Expand All @@ -146,7 +149,7 @@ default void addFile(Kind kind, String path, CharSequence content) {
* @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) {
public void addFile(Kind kind, String path, ThrowingConsumer<Appendable> content) {
Assert.notNull(content, "'content' must not be null");
addFile(kind, path, new AppendableConsumerInputStreamSource(content));
}
Expand All @@ -159,7 +162,21 @@ default void addFile(Kind kind, String path, ThrowingConsumer<Appendable> conten
* @param content an {@link InputStreamSource} that will provide an input
* stream containing the file contents
*/
void addFile(Kind kind, String path, InputStreamSource content);
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");
handleFile(kind, path, handler -> handler.create(content));
}

/**
* Add a generated file of the specified {@link Kind} with the given
* {@linkplain FileHandler handler}.
* @param kind the kind of file being written
* @param path the relative path of the file
* @param handler a consumer of a {@link FileHandler} for the file
*/
public abstract void handleFile(Kind kind, String path, ThrowingConsumer<FileHandler> handler);

private static String getClassNamePath(String className) {
Assert.hasLength(className, "'className' must not be empty");
Expand Down Expand Up @@ -194,7 +211,7 @@ private static boolean isJavaIdentifier(String className) {
/**
* The various kinds of generated files that are supported.
*/
enum Kind {
public enum Kind {

/**
* A source file containing Java code that should be compiled.
Expand All @@ -215,4 +232,62 @@ enum Kind {

}

/**
* Provide access to a particular file and offer convenient method to save
* or override its content.
*/
public abstract static class FileHandler {

private final boolean exists;

private final Supplier<InputStreamSource> existingContent;

protected FileHandler(boolean exists, Supplier<InputStreamSource> existingContent) {
this.exists = exists;
this.existingContent = existingContent;
}

/**
* Specify whether the file already exists.
* @return {@code true} if the file already exists
*/
public boolean exists() {
return this.exists;
}

/**
* Return an {@link InputStreamSource} for the content of the file or
* {@code null} if the file does not exist.
*/
@Nullable
public InputStreamSource getContent() {
return (exists() ? this.existingContent.get() : null);
}

/**
* Create a file with the given {@linkplain InputStreamSource content}.
* @throws IllegalStateException if the file already exists
*/
public void create(InputStreamSource content) {
Assert.notNull(content, "'content' must not be null");
if (exists()) {
throw new IllegalStateException("%s already exists".formatted(this));
}
copy(content, false);
}

/**
* Override the content of the file handled by this instance using the
* given {@linkplain InputStreamSource content}. If the file does not
* exist, it is created.
*/
public void override(InputStreamSource content) {
Assert.notNull(content, "'content' must not be null");
copy(content, true);
}

protected abstract void copy(InputStreamSource content, boolean override);

}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
Expand All @@ -26,27 +26,25 @@
import org.springframework.core.io.InputStreamSource;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.function.ThrowingConsumer;

/**
* {@link GeneratedFiles} implementation that keeps generated files in-memory.
*
* @author Phillip Webb
* @author Stephane Nicoll
* @since 6.0
*/
public class InMemoryGeneratedFiles implements GeneratedFiles {
public class InMemoryGeneratedFiles extends 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");
public void handleFile(Kind kind, String path, ThrowingConsumer<FileHandler> handler) {
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);
handler.accept(new InMemoryFileHandler(paths, path));
}

/**
Expand Down Expand Up @@ -89,4 +87,27 @@ public InputStreamSource getGeneratedFile(Kind kind, String path) {
return (paths != null ? paths.get(path) : null);
}

private static class InMemoryFileHandler extends FileHandler {

private final Map<String, InputStreamSource> paths;

private final String key;

InMemoryFileHandler(Map<String, InputStreamSource> paths, String key) {
super(paths.containsKey(key), () -> paths.get(key));
this.paths = paths;
this.key = key;
}

@Override
protected void copy(InputStreamSource content, boolean override) {
this.paths.put(this.key, content);
}

@Override
public String toString() {
return this.key;
}
}

}
Loading

0 comments on commit 2650da2

Please sign in to comment.