Initial commit. Woot!
This commit is contained in:
commit
cd63bd60a7
9
.gitattributes
vendored
Normal file
9
.gitattributes
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#
|
||||||
|
# https://help.github.com/articles/dealing-with-line-endings/
|
||||||
|
#
|
||||||
|
# Linux start script should use lf
|
||||||
|
/gradlew text eol=lf
|
||||||
|
|
||||||
|
# These are Windows script files and should use crlf
|
||||||
|
*.bat text eol=crlf
|
||||||
|
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Ignore Gradle project-specific cache directory
|
||||||
|
.gradle
|
||||||
|
|
||||||
|
# Ignore Gradle build output directory
|
||||||
|
build
|
||||||
|
.idea
|
30
buildSrc/build.gradle
Normal file
30
buildSrc/build.gradle
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java-gradle-plugin'
|
||||||
|
id 'groovy-gradle-plugin'
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation libs.junit.jupiter.api
|
||||||
|
}
|
||||||
|
|
||||||
|
gradlePlugin {
|
||||||
|
plugins {
|
||||||
|
create('GroowtAntlrPlugin') {
|
||||||
|
id = 'GroowtAntlrPlugin'
|
||||||
|
implementationClass = 'groowt.gradle.antlr.GroowtAntlrPlugin'
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testing {
|
||||||
|
suites {
|
||||||
|
test {
|
||||||
|
useJUnitJupiter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
buildSrc/settings.gradle
Normal file
7
buildSrc/settings.gradle
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
dependencyResolutionManagement {
|
||||||
|
versionCatalogs {
|
||||||
|
create('libs') {
|
||||||
|
from files('../gradle/libs.versions.toml')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation libs.junit.jupiter.api
|
||||||
|
testRuntimeOnly libs.log4j.slf4jBinding
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: hook into testResources and add a default log4j2.xml config for testing
|
||||||
|
|
||||||
|
testing {
|
||||||
|
suites {
|
||||||
|
test {
|
||||||
|
useJUnitJupiter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package groowt.gradle.antlr;
|
||||||
|
|
||||||
|
import org.gradle.api.plugins.antlr.AntlrTask;
|
||||||
|
|
||||||
|
public class GroowtAntlrAllTask extends AntlrTask {}
|
@ -0,0 +1,34 @@
|
|||||||
|
package groowt.gradle.antlr;
|
||||||
|
|
||||||
|
import org.gradle.api.Action;
|
||||||
|
import org.gradle.api.model.ObjectFactory;
|
||||||
|
import org.gradle.api.provider.Property;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
public abstract class GroowtAntlrExtension {
|
||||||
|
|
||||||
|
private final SourceSpecContainer sourceSpecs;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public GroowtAntlrExtension(ObjectFactory objectFactory) {
|
||||||
|
this.sourceSpecs = objectFactory.newInstance(SourceSpecContainer.class, (Action<SourceSpec>) sourceSpec -> {
|
||||||
|
sourceSpec.getPackageName().convention(this.getPackageName());
|
||||||
|
sourceSpec.getVisitor().convention(this.getVisitor());
|
||||||
|
sourceSpec.getIsCompileDependency().convention(true);
|
||||||
|
sourceSpec.getDebug().convention(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Property<String> getPackageName();
|
||||||
|
public abstract Property<Boolean> getVisitor();
|
||||||
|
|
||||||
|
public SourceSpecContainer getSourceSpecs() {
|
||||||
|
return this.sourceSpecs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sourceSpecs(Action<SourceSpecContainer> configure) {
|
||||||
|
configure.execute(this.getSourceSpecs());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,216 @@
|
|||||||
|
package groowt.gradle.antlr;
|
||||||
|
|
||||||
|
import org.gradle.api.Action;
|
||||||
|
import org.gradle.api.Plugin;
|
||||||
|
import org.gradle.api.Project;
|
||||||
|
import org.gradle.api.file.FileTree;
|
||||||
|
import org.gradle.api.model.ObjectFactory;
|
||||||
|
import org.gradle.api.plugins.JavaPluginExtension;
|
||||||
|
import org.gradle.api.plugins.antlr.AntlrPlugin;
|
||||||
|
import org.gradle.api.plugins.antlr.AntlrSourceDirectorySet;
|
||||||
|
import org.gradle.api.plugins.antlr.AntlrTask;
|
||||||
|
import org.gradle.api.provider.Provider;
|
||||||
|
import org.gradle.api.tasks.SourceSet;
|
||||||
|
import org.gradle.api.tasks.TaskContainer;
|
||||||
|
import org.gradle.api.tasks.compile.JavaCompile;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static groowt.gradle.antlr.GroowtAntlrUtil.*;
|
||||||
|
|
||||||
|
public final class GroowtAntlrPlugin implements Plugin<Project> {
|
||||||
|
|
||||||
|
public static final String taskGroup = "groowtAntlr";
|
||||||
|
|
||||||
|
private static final String packageArg = "-package";
|
||||||
|
private static final String traceArg = "-trace";
|
||||||
|
private static final String visitorArg = "-visitor";
|
||||||
|
|
||||||
|
private final ObjectFactory objectFactory;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public GroowtAntlrPlugin(ObjectFactory objectFactory) {
|
||||||
|
this.objectFactory = objectFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NullableProviderList<String> getArguments(SourceSpec sourceSpec) {
|
||||||
|
final NullableProviderList<String> arguments = new NullableProviderList<>();
|
||||||
|
arguments.addCollectionProvider(sourceSpec.getPackageName().map(packageName -> List.of(packageArg, packageName)));
|
||||||
|
arguments.addProvider(sourceSpec.getVisitor().map(isVisitor -> isVisitor ? visitorArg : null));
|
||||||
|
arguments.addProvider(sourceSpec.getDebug().map(isDebug -> isDebug ? traceArg : null));
|
||||||
|
return arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For afterEvaluate!
|
||||||
|
private static List<GroowtAntlrTask> createAndRegister(Project project, GroowtAntlrExtension extension) {
|
||||||
|
final TaskContainer taskContainer = project.getTasks();
|
||||||
|
return extension.getSourceSpecs().stream().map(sourceSpec -> {
|
||||||
|
return taskContainer.create(
|
||||||
|
getGenerateTaskName(sourceSpec),
|
||||||
|
GroowtAntlrTask.class,
|
||||||
|
sourceSpec,
|
||||||
|
(Action<GroowtAntlrTask>) task -> {
|
||||||
|
task.setGroup(taskGroup);
|
||||||
|
task.setArguments(getArguments(sourceSpec));
|
||||||
|
task.setSource(sourceSpec.getResolvedSource().getSourceFile());
|
||||||
|
task.setOutputDirectory(
|
||||||
|
getOutputDirectory(
|
||||||
|
project,
|
||||||
|
sourceSpec.getResolvedSource().getSourceSet(),
|
||||||
|
sourceSpec.getPackageName()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For afterEvaluate!
|
||||||
|
private static void addCompileDependencies(Project project, List<GroowtAntlrTask> tasks) {
|
||||||
|
tasks.forEach(task -> {
|
||||||
|
// if it is a compile dependency, add it as input to java source set
|
||||||
|
final var isCompileDependency = task.getSourceSpec().getIsCompileDependency().get();
|
||||||
|
if (isCompileDependency) {
|
||||||
|
project.getTasks().withType(JavaCompile.class).configureEach(javaCompile -> {
|
||||||
|
javaCompile.dependsOn(task);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private record GenerateAllSpec(
|
||||||
|
@Nullable FileTree source,
|
||||||
|
@NotNull List<String> args,
|
||||||
|
@NotNull Provider<String> packageName
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private GenerateAllSpec getBlankGenerateAllSpec() {
|
||||||
|
return new GenerateAllSpec(null, List.of(), this.objectFactory.property(String.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerateAllSpec getGenerateAllSpecFromTask(GroowtAntlrTask task) {
|
||||||
|
return new GenerateAllSpec(task.getSource(), task.getArguments(), task.getSourceSpec().getPackageName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @Nullable FileTree combineFileTrees(@Nullable FileTree f0, @Nullable FileTree f1) {
|
||||||
|
if (f0 != null && f1 != null) {
|
||||||
|
return f0.plus(f1);
|
||||||
|
} else if (f0 != null) {
|
||||||
|
return f0;
|
||||||
|
} else {
|
||||||
|
return f1; // null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> combineArguments(List<String> a0, List<String> a1) {
|
||||||
|
final List<String> result = new ArrayList<>(a0);
|
||||||
|
final Iterator<String> a1Iter = a1.iterator();
|
||||||
|
while (a1Iter.hasNext()) {
|
||||||
|
final String arg = a1Iter.next();
|
||||||
|
if (arg.equals(packageArg) && result.contains(packageArg)) {
|
||||||
|
if (!a1Iter.hasNext()) {
|
||||||
|
throw new IllegalStateException("shouldn't get here");
|
||||||
|
}
|
||||||
|
final String a0PackageName = result.get(result.indexOf(arg) + 1);
|
||||||
|
final String a1PackageName = a1Iter.next();
|
||||||
|
if (!a0PackageName.equals(a1PackageName)) {
|
||||||
|
throw new IllegalArgumentException("Cannot have separate package arguments for two files from the same source set.");
|
||||||
|
}
|
||||||
|
} else if (!result.contains(arg)) {
|
||||||
|
result.add(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Provider<String> combinePackageNames(Provider<String> p0, Provider<String> p1) {
|
||||||
|
return p0.zip(p1, (pn0, pn1) -> {
|
||||||
|
if (!pn0.equals(pn1)) {
|
||||||
|
throw new IllegalArgumentException("Cannot have separate package names for two files from the same source set.");
|
||||||
|
}
|
||||||
|
return pn0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GenerateAllSpec combineGenerateAllSpecs(GenerateAllSpec s0, GenerateAllSpec s1) {
|
||||||
|
return new GenerateAllSpec(
|
||||||
|
combineFileTrees(s0.source(), s1.source()),
|
||||||
|
combineArguments(s0.args(), s1.args()),
|
||||||
|
combinePackageNames(s0.packageName(), s1.packageName())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For afterEvaluate!
|
||||||
|
private static void addGenerateAllTasks(Project project, List<GroowtAntlrTask> tasks) {
|
||||||
|
final Map<SourceSet, List<GroowtAntlrTask>> sourceSetToTasks = tasks.stream().collect(Collectors.groupingBy(
|
||||||
|
task -> task.getSourceSpec().getResolvedSource().getSourceSet()
|
||||||
|
));
|
||||||
|
|
||||||
|
final Map<SourceSet, GenerateAllSpec> sourceSetToSpec = new HashMap<>();
|
||||||
|
sourceSetToTasks.forEach((sourceSet, sourceSetTasks) -> {
|
||||||
|
List<GenerateAllSpec> specs = sourceSetTasks.stream().map(task ->
|
||||||
|
new GenerateAllSpec(task.getSource(), task.getArguments(), task.getSourceSpec().getPackageName())
|
||||||
|
).toList();
|
||||||
|
specs.stream().reduce(GroowtAntlrPlugin::combineGenerateAllSpecs).ifPresent(allSpec -> {
|
||||||
|
sourceSetToSpec.put(sourceSet, allSpec);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
sourceSetToSpec.forEach((sourceSet, spec) -> {
|
||||||
|
project.getTasks().register(
|
||||||
|
getGenerateAllTaskName(sourceSet),
|
||||||
|
GroowtAntlrAllTask.class,
|
||||||
|
task -> {
|
||||||
|
task.setGroup(taskGroup);
|
||||||
|
if (spec.source() != null) {
|
||||||
|
task.setSource(spec.source());
|
||||||
|
}
|
||||||
|
task.setArguments(spec.args());
|
||||||
|
task.setOutputDirectory(
|
||||||
|
getOutputDirectory(project, sourceSet, spec.packageName()).get().getAsFile()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void apply(Project project) {
|
||||||
|
project.getPluginManager().apply(AntlrPlugin.class);
|
||||||
|
|
||||||
|
// undo the antlr plugin creating its own tasks
|
||||||
|
project.getTasks().withType(AntlrTask.class, antlrTask -> {
|
||||||
|
if (!(antlrTask instanceof GroowtAntlrTask || antlrTask instanceof GroowtAntlrAllTask)) {
|
||||||
|
antlrTask.setEnabled(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// create extension
|
||||||
|
final GroowtAntlrExtension extension = project.getExtensions().create("groowtAntlr", GroowtAntlrExtension.class);
|
||||||
|
extension.getPackageName().convention("");
|
||||||
|
extension.getVisitor().convention(false);
|
||||||
|
|
||||||
|
// find all antlr files first and add them to extension
|
||||||
|
project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets().forEach(sourceSet -> {
|
||||||
|
final Set<File> antlrFiles = sourceSet.getExtensions().getByType(AntlrSourceDirectorySet.class).getFiles();
|
||||||
|
for (final File antlrFile : antlrFiles) {
|
||||||
|
if (isAntlrSourceFile(antlrFile)) {
|
||||||
|
extension.getSourceSpecs().register(sourceSet, antlrFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// after evaluate, generate tasks for each registered sourceSpec
|
||||||
|
project.afterEvaluate(postEvaluateProject -> {
|
||||||
|
final List<GroowtAntlrTask> tasks = createAndRegister(postEvaluateProject, extension);
|
||||||
|
addCompileDependencies(project, tasks);
|
||||||
|
addGenerateAllTasks(project, tasks);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package groowt.gradle.antlr;
|
||||||
|
|
||||||
|
import org.gradle.api.Action;
|
||||||
|
import org.gradle.api.file.Directory;
|
||||||
|
import org.gradle.api.file.DirectoryProperty;
|
||||||
|
import org.gradle.api.model.ObjectFactory;
|
||||||
|
import org.gradle.api.plugins.antlr.AntlrTask;
|
||||||
|
import org.gradle.api.provider.Provider;
|
||||||
|
import org.gradle.api.tasks.Internal;
|
||||||
|
import org.gradle.api.tasks.Nested;
|
||||||
|
import org.gradle.api.tasks.OutputDirectory;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class GroowtAntlrTask extends AntlrTask {
|
||||||
|
|
||||||
|
private final ObjectFactory objectFactory;
|
||||||
|
private final SourceSpec sourceSpec;
|
||||||
|
|
||||||
|
private Provider<Directory> outputDirectory;
|
||||||
|
private NullableProviderList<String> arguments;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public GroowtAntlrTask(ObjectFactory objectFactory, SourceSpec sourceSpec, Action<? super GroowtAntlrTask> configure) {
|
||||||
|
this.objectFactory = objectFactory;
|
||||||
|
this.sourceSpec = sourceSpec;
|
||||||
|
configure.execute(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
public SourceSpec getSourceSpec() {
|
||||||
|
return this.sourceSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull File getOutputDirectory() {
|
||||||
|
return this.outputDirectory.get().getAsFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOutputDirectory(@NotNull File outputDirectory) {
|
||||||
|
final DirectoryProperty directoryProperty = this.objectFactory.directoryProperty();
|
||||||
|
directoryProperty.set(outputDirectory);
|
||||||
|
this.outputDirectory = directoryProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOutputDirectory(Provider<Directory> outputDirectoryProvider) {
|
||||||
|
this.outputDirectory = outputDirectoryProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Internal
|
||||||
|
public @NotNull List<String> getArguments() {
|
||||||
|
return this.arguments.getElements();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setArguments(@NotNull List<String> arguments) {
|
||||||
|
this.arguments = new NullableProviderList<>();
|
||||||
|
this.arguments.addAllElements(arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setArguments(@NotNull NullableProviderList<String> arguments) {
|
||||||
|
this.arguments = arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
147
buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrUtil.java
Normal file
147
buildSrc/src/main/java/groowt/gradle/antlr/GroowtAntlrUtil.java
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
package groowt.gradle.antlr;
|
||||||
|
|
||||||
|
import org.gradle.api.Project;
|
||||||
|
import org.gradle.api.file.Directory;
|
||||||
|
import org.gradle.api.file.SourceDirectorySet;
|
||||||
|
import org.gradle.api.provider.Provider;
|
||||||
|
import org.gradle.api.tasks.SourceSet;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public final class GroowtAntlrUtil {
|
||||||
|
|
||||||
|
public static final List<String> antlrFileExtensions = List.of("g4", "g");
|
||||||
|
|
||||||
|
private static final Pattern extensionPattern = Pattern.compile("(?<name>.*)\\.(?<ext>.*)$");
|
||||||
|
|
||||||
|
public static File resolve(File from, File to) {
|
||||||
|
return from.toPath().resolve(to.toPath()).toFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File relativize(File from, File to) {
|
||||||
|
return from.toPath().relativize(to.toPath()).toFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAntlrSourceFile(File file) {
|
||||||
|
final var m = extensionPattern.matcher(file.getName());
|
||||||
|
if (m.matches()) {
|
||||||
|
return antlrFileExtensions.contains(m.group("ext"));
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Cannot determine extension of file: " + file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<String> sourceFileToIdentifierParts(File sourceDir, File sourceFile) {
|
||||||
|
final var relative = getRelativePathToSourceFile(sourceDir, sourceFile);
|
||||||
|
final List<String> result = new ArrayList<>();
|
||||||
|
for (int i = 0; i < relative.getNameCount(); i++) {
|
||||||
|
final var name = relative.getName(i);
|
||||||
|
final var m = extensionPattern.matcher(name.toString());
|
||||||
|
if (m.matches()) {
|
||||||
|
result.add(m.group("name"));
|
||||||
|
} else {
|
||||||
|
result.add(name.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path getRelativePathToSourceFile(File sourceDir, File sourceFile) {
|
||||||
|
if (!sourceDir.isAbsolute()) {
|
||||||
|
throw new IllegalArgumentException("sourceDir must be absolute, given: " + sourceDir);
|
||||||
|
}
|
||||||
|
if (sourceFile.isAbsolute()) {
|
||||||
|
final var sourceDirPath = sourceDir.toPath();
|
||||||
|
final var sourceFilePath = sourceFile.toPath();
|
||||||
|
return sourceDirPath.relativize(sourceFilePath);
|
||||||
|
} else {
|
||||||
|
return sourceFile.toPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getSourceIdentifier(ResolvedSource resolvedSource) {
|
||||||
|
final List<String> parts = new ArrayList<>();
|
||||||
|
if (!resolvedSource.getSourceSet().getName().equals("main")) {
|
||||||
|
parts.add(resolvedSource.getSourceSet().getName());
|
||||||
|
}
|
||||||
|
parts.addAll(sourceFileToIdentifierParts(resolvedSource.getSourceDir(), resolvedSource.getSourceFile()));
|
||||||
|
final List<String> capitalizedParts = parts.stream()
|
||||||
|
.map(part -> {
|
||||||
|
final var first = part.substring(0, 1);
|
||||||
|
final var rest = part.substring(1);
|
||||||
|
return first.toUpperCase() + rest;
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
return String.join("", capitalizedParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Provider<Directory> getOutputDirectory(
|
||||||
|
Project project,
|
||||||
|
SourceSet sourceSet,
|
||||||
|
Provider<String> packageNameProvider
|
||||||
|
) {
|
||||||
|
return project.getLayout().getBuildDirectory().flatMap(buildDir -> {
|
||||||
|
return buildDir.dir(packageNameProvider.map(packageName -> {
|
||||||
|
return String.join(File.separator, List.of(
|
||||||
|
"generated-src",
|
||||||
|
"antlr",
|
||||||
|
sourceSet.getName(),
|
||||||
|
packageName.replace(".", File.separator)
|
||||||
|
));
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getGenerateTaskName(SourceSpec sourceSpec) {
|
||||||
|
return sourceSpec.getResolvedSource().getSourceSet().getTaskName("generate", sourceSpec.getIdentifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getGenerateAllTaskName(SourceSet sourceSet) {
|
||||||
|
return sourceSet.getTaskName("generate", "AllAntlr");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResolvedSource resolveSource(
|
||||||
|
Project project,
|
||||||
|
SourceSet sourceSet,
|
||||||
|
SourceDirectorySet sourceDirectorySet,
|
||||||
|
File sourceFile
|
||||||
|
) {
|
||||||
|
if (!isAntlrSourceFile(sourceFile)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"The given source file " + sourceFile + " is not a recognized antlr file (bad extension)."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<File> potentialSrcDirs = new ArrayList<>();
|
||||||
|
|
||||||
|
if (sourceFile.isAbsolute()) {
|
||||||
|
for (final File srcDir : sourceDirectorySet.getSrcDirs()) {
|
||||||
|
if (sourceFile.getPath().startsWith(srcDir.getPath())) {
|
||||||
|
potentialSrcDirs.add(srcDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (final File srcDir : sourceDirectorySet.getSrcDirs()) {
|
||||||
|
if (resolve(srcDir, sourceFile).exists()) {
|
||||||
|
potentialSrcDirs.add(srcDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (potentialSrcDirs.size() > 1) {
|
||||||
|
throw new IllegalArgumentException("Multiple source directories in " + sourceDirectorySet.getName() + " contain a source file " + sourceFile);
|
||||||
|
} else if (potentialSrcDirs.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("No directories in " + sourceDirectorySet.getName() + " contain a source file " + sourceFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
final File srcDir = potentialSrcDirs.getFirst();
|
||||||
|
return new ResolvedSource(project, sourceSet, srcDir, resolve(srcDir, sourceFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroowtAntlrUtil() {}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,160 @@
|
|||||||
|
package groowt.gradle.antlr;
|
||||||
|
|
||||||
|
import org.gradle.api.provider.Provider;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public final class NullableProviderList<T> {
|
||||||
|
|
||||||
|
private sealed interface Element<T> extends Iterable<T> permits BareElement, ElementProvider, CollectionProvider {
|
||||||
|
boolean isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class BareElement<T> implements Element<T> {
|
||||||
|
|
||||||
|
private final T element;
|
||||||
|
|
||||||
|
public BareElement(T element) {
|
||||||
|
this.element = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPresent() {
|
||||||
|
return this.element != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public Iterator<T> iterator() {
|
||||||
|
return new Iterator<>() {
|
||||||
|
private boolean hasNext = true;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return this.hasNext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T next() {
|
||||||
|
if (!this.hasNext) throw new IllegalStateException();
|
||||||
|
this.hasNext = false;
|
||||||
|
return BareElement.this.element;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ElementProvider<T> implements Element<T> {
|
||||||
|
|
||||||
|
private final Provider<T> provider;
|
||||||
|
|
||||||
|
public ElementProvider(Provider<T> provider) {
|
||||||
|
this.provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPresent() {
|
||||||
|
return this.provider.isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public Iterator<T> iterator() {
|
||||||
|
return new Iterator<T>() {
|
||||||
|
private boolean hasNext = true;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return this.hasNext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T next() {
|
||||||
|
if (!this.hasNext) throw new IllegalStateException();
|
||||||
|
this.hasNext = false;
|
||||||
|
return ElementProvider.this.provider.get();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CollectionProvider<T> implements Element<T> {
|
||||||
|
|
||||||
|
private final Provider<Collection<T>> collectionProvider;
|
||||||
|
|
||||||
|
public CollectionProvider(Provider<Collection<T>> collectionProvider) {
|
||||||
|
this.collectionProvider = collectionProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPresent() {
|
||||||
|
return this.collectionProvider.isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public Iterator<T> iterator() {
|
||||||
|
return this.collectionProvider.get().iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private final List<Element<T>> elements = new ArrayList<>();
|
||||||
|
|
||||||
|
public void addElement(@Nullable T element) {
|
||||||
|
this.elements.add(new BareElement<>(element));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addProvider(Provider<T> elementProvider) {
|
||||||
|
this.elements.add(new ElementProvider<>(elementProvider));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCollectionProvider(Provider<Collection<T>> collectionProvider) {
|
||||||
|
this.elements.add(new CollectionProvider<>(collectionProvider));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAllElements(Collection<T> elements) {
|
||||||
|
for (final T element : elements) {
|
||||||
|
this.elements.add(new BareElement<>(element));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAllProviders(Collection<Provider<T>> providers) {
|
||||||
|
for (final Provider<T> provider : providers) {
|
||||||
|
this.elements.add(new ElementProvider<>(provider));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAllCollectionProviders(Collection<Provider<Collection<T>>> collectionProviders) {
|
||||||
|
for (final Provider<Collection<T>> collectionProvider : collectionProviders) {
|
||||||
|
this.elements.add(new CollectionProvider<>(collectionProvider));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> getElements() {
|
||||||
|
return this.getElements(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> getElements(@Nullable Supplier<T> onNullElement) {
|
||||||
|
final List<T> result = new ArrayList<>();
|
||||||
|
for (final Element<T> element : this.elements) {
|
||||||
|
if (element.isPresent()) {
|
||||||
|
for (final T t : element) {
|
||||||
|
result.add(t);
|
||||||
|
}
|
||||||
|
} else if (onNullElement != null) {
|
||||||
|
result.add(onNullElement.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
package groowt.gradle.antlr;
|
||||||
|
|
||||||
|
import org.gradle.api.Project;
|
||||||
|
import org.gradle.api.tasks.InputDirectory;
|
||||||
|
import org.gradle.api.tasks.InputFile;
|
||||||
|
import org.gradle.api.tasks.Internal;
|
||||||
|
import org.gradle.api.tasks.SourceSet;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static groowt.gradle.antlr.GroowtAntlrUtil.relativize;
|
||||||
|
|
||||||
|
public final class ResolvedSource {
|
||||||
|
|
||||||
|
private final Project project;
|
||||||
|
private final SourceSet sourceSet;
|
||||||
|
private final File sourceDir;
|
||||||
|
private final File sourceFile;
|
||||||
|
|
||||||
|
public ResolvedSource(Project project, SourceSet sourceSet, File sourceDir, File sourceFile) {
|
||||||
|
this.project = project;
|
||||||
|
this.sourceSet = sourceSet;
|
||||||
|
this.sourceDir = sourceDir;
|
||||||
|
this.sourceFile = sourceFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Internal
|
||||||
|
public Project getProject() {
|
||||||
|
return this.project;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Internal
|
||||||
|
public SourceSet getSourceSet() {
|
||||||
|
return this.sourceSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
@InputDirectory
|
||||||
|
public File getSourceDir() {
|
||||||
|
return this.sourceDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
@InputFile
|
||||||
|
public File getSourceFile() {
|
||||||
|
return this.sourceFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String relativizeSourceDir() {
|
||||||
|
return relativize(this.project.getProjectDir(), this.sourceDir).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String relativeSourceFile() {
|
||||||
|
return relativize(this.sourceDir, this.sourceFile).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj == this) {
|
||||||
|
return true;
|
||||||
|
} else if (obj instanceof ResolvedSource other) {
|
||||||
|
return this.project.equals(other.project)
|
||||||
|
&& this.sourceSet.equals(other.sourceSet)
|
||||||
|
&& this.sourceDir.equals(other.sourceDir)
|
||||||
|
&& this.sourceFile.equals(other.sourceFile);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = this.project.hashCode();
|
||||||
|
result = 31 * result + this.sourceSet.hashCode();
|
||||||
|
result = 31 * result + this.sourceDir.hashCode();
|
||||||
|
result = 31 * result + this.sourceFile.hashCode();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ResolvedSource("
|
||||||
|
+ String.join(", ", List.of(
|
||||||
|
"sourceSet: " + this.sourceSet.getName(),
|
||||||
|
"sourceDir: " + this.relativizeSourceDir(),
|
||||||
|
"sourceFile: " + this.relativeSourceFile()
|
||||||
|
))
|
||||||
|
+ ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
43
buildSrc/src/main/java/groowt/gradle/antlr/SourceSpec.java
Normal file
43
buildSrc/src/main/java/groowt/gradle/antlr/SourceSpec.java
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package groowt.gradle.antlr;
|
||||||
|
|
||||||
|
import org.gradle.api.provider.Property;
|
||||||
|
import org.gradle.api.tasks.Input;
|
||||||
|
import org.gradle.api.tasks.Internal;
|
||||||
|
import org.gradle.api.tasks.Nested;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
public abstract class SourceSpec {
|
||||||
|
|
||||||
|
private final String identifier;
|
||||||
|
private final ResolvedSource resolvedSource;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public SourceSpec(String identifier, ResolvedSource resolvedSource) {
|
||||||
|
this.identifier = identifier;
|
||||||
|
this.resolvedSource = resolvedSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Internal
|
||||||
|
public String getIdentifier() {
|
||||||
|
return this.identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
public ResolvedSource getResolvedSource() {
|
||||||
|
return this.resolvedSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input
|
||||||
|
public abstract Property<Boolean> getIsCompileDependency();
|
||||||
|
|
||||||
|
@Input
|
||||||
|
public abstract Property<Boolean> getDebug();
|
||||||
|
|
||||||
|
@Input
|
||||||
|
public abstract Property<String> getPackageName();
|
||||||
|
|
||||||
|
@Input
|
||||||
|
public abstract Property<Boolean> getVisitor();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package groowt.gradle.antlr;
|
||||||
|
|
||||||
|
import org.gradle.api.Action;
|
||||||
|
import org.gradle.api.Project;
|
||||||
|
import org.gradle.api.internal.CollectionCallbackActionDecorator;
|
||||||
|
import org.gradle.api.internal.DefaultDomainObjectSet;
|
||||||
|
import org.gradle.api.model.ObjectFactory;
|
||||||
|
import org.gradle.api.plugins.antlr.AntlrSourceDirectorySet;
|
||||||
|
import org.gradle.api.tasks.SourceSet;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import static groowt.gradle.antlr.GroowtAntlrUtil.getSourceIdentifier;
|
||||||
|
import static groowt.gradle.antlr.GroowtAntlrUtil.resolveSource;
|
||||||
|
|
||||||
|
public class SourceSpecContainer extends DefaultDomainObjectSet<SourceSpec> {
|
||||||
|
|
||||||
|
private final Project project;
|
||||||
|
private final ObjectFactory objectFactory;
|
||||||
|
private final Action<SourceSpec> applyConventions;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public SourceSpecContainer(Project project, ObjectFactory objectFactory, Action<SourceSpec> applyConventions) {
|
||||||
|
super(SourceSpec.class, CollectionCallbackActionDecorator.NOOP);
|
||||||
|
this.project = project;
|
||||||
|
this.objectFactory = objectFactory;
|
||||||
|
this.applyConventions = applyConventions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param sourceFilePaths instances of File or String
|
||||||
|
*/
|
||||||
|
public void ignore(SourceSet sourceSet, Object... sourceFilePaths) {
|
||||||
|
for (final Object sourceFilePath : sourceFilePaths) {
|
||||||
|
switch (sourceFilePath) {
|
||||||
|
case File f -> this.ignoreFile(sourceSet, f);
|
||||||
|
case String s -> this.ignoreFile(sourceSet, new File(s));
|
||||||
|
default -> throw new IllegalArgumentException("Can only ignore Files or Strings, given: " + sourceFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ignoreFile(SourceSet sourceSet, File target) {
|
||||||
|
this.removeIf(potentialSourceSpec -> {
|
||||||
|
final SourceSet potentialSourceSet = potentialSourceSpec.getResolvedSource().getSourceSet();
|
||||||
|
if (!sourceSet.equals(potentialSourceSet)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final var antlrSourceDirectorySet = sourceSet.getExtensions().getByType(AntlrSourceDirectorySet.class);
|
||||||
|
final ResolvedSource toIgnore = resolveSource(this.project, sourceSet, antlrSourceDirectorySet, target);
|
||||||
|
final File potentialSourceFile = potentialSourceSpec.getResolvedSource().getSourceFile();
|
||||||
|
return toIgnore.getSourceFile().equals(potentialSourceFile);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void register(SourceSet sourceSet, String sourceFilePath) {
|
||||||
|
this.register(sourceSet, new File(sourceFilePath), sourceSpec -> {}); // no-op action
|
||||||
|
}
|
||||||
|
|
||||||
|
public void register(SourceSet sourceSet, File sourceFile) {
|
||||||
|
this.register(sourceSet, sourceFile, sourceSpec -> {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void register(SourceSet sourceSet, String sourceFilePath, Action<? super SourceSpec> action) {
|
||||||
|
this.register(sourceSet, new File(sourceFilePath), action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void register(SourceSet sourceSet, File sourceFile, Action<? super SourceSpec> action) {
|
||||||
|
final var antlrSourceDirectorySet = sourceSet.getExtensions().getByType(AntlrSourceDirectorySet.class);
|
||||||
|
final var resolvedSource = resolveSource(this.project, sourceSet, antlrSourceDirectorySet, sourceFile);
|
||||||
|
|
||||||
|
final String identifier = getSourceIdentifier(resolvedSource);
|
||||||
|
final var specOptional = this.stream().filter(sourceSpec -> sourceSpec.getIdentifier().equals(identifier)).findFirst();
|
||||||
|
|
||||||
|
if (specOptional.isPresent()) {
|
||||||
|
// we already have one, so find and run the action against it
|
||||||
|
final var spec = specOptional.get();
|
||||||
|
action.execute(spec);
|
||||||
|
} else {
|
||||||
|
// create a new one
|
||||||
|
final var spec = this.objectFactory.newInstance(SourceSpec.class, identifier, resolvedSource);
|
||||||
|
this.applyConventions.execute(spec);
|
||||||
|
action.execute(spec);
|
||||||
|
this.add(spec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
//file:noinspection ConfigurationAvoidance
|
||||||
|
package groowt.gradle.antlr
|
||||||
|
|
||||||
|
import org.gradle.api.plugins.JavaPlugin
|
||||||
|
import org.gradle.api.plugins.JavaPluginExtension
|
||||||
|
import org.gradle.api.plugins.antlr.AntlrPlugin
|
||||||
|
import org.gradle.testfixtures.ProjectBuilder
|
||||||
|
import org.junit.jupiter.api.Disabled
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.function.Executable
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow
|
||||||
|
|
||||||
|
class GroowtAntlrPluginTests {
|
||||||
|
|
||||||
|
@Disabled('TODO: figure out why register(SourceSet, String) is not working.')
|
||||||
|
@Test
|
||||||
|
void smokeScreen() {
|
||||||
|
def projectDir = File.createTempDir()
|
||||||
|
new FileTreeBuilder(projectDir).tap {
|
||||||
|
dir(['src', 'main', 'antlr'].join(File.separator)) {
|
||||||
|
file('MyGrammar.g4') {
|
||||||
|
write("parser grammar MyGrammar;")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def project = ProjectBuilder.builder().with {
|
||||||
|
withProjectDir(projectDir)
|
||||||
|
build()
|
||||||
|
}
|
||||||
|
|
||||||
|
project.pluginManager.with {
|
||||||
|
apply(JavaPlugin)
|
||||||
|
apply(AntlrPlugin)
|
||||||
|
apply(GroowtAntlrPlugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
def mainSourceSet = project.extensions.getByType(JavaPluginExtension).sourceSets.findByName('main')
|
||||||
|
|
||||||
|
project.extensions.getByType(GroowtAntlrExtension).sourceSpecs.with {
|
||||||
|
register(mainSourceSet, 'MyGrammar.g4')
|
||||||
|
}
|
||||||
|
|
||||||
|
def findTask = {
|
||||||
|
project.tasks.named('generateMyGrammar', GroowtAntlrTask)
|
||||||
|
} as Executable
|
||||||
|
|
||||||
|
assertDoesNotThrow(findTask) {
|
||||||
|
"Could not find task 'generateMyGrammar' (all tasks: ${project.tasks*.name.join(', ')})"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,132 @@
|
|||||||
|
//file:noinspection ConfigurationAvoidance
|
||||||
|
package groowt.gradle.antlr
|
||||||
|
|
||||||
|
import groovy.transform.TupleConstructor
|
||||||
|
import org.gradle.api.plugins.JavaPlugin
|
||||||
|
import org.gradle.api.plugins.JavaPluginExtension
|
||||||
|
import org.gradle.api.plugins.antlr.AntlrPlugin
|
||||||
|
import org.gradle.api.provider.Provider
|
||||||
|
import org.gradle.api.tasks.SourceSet
|
||||||
|
import org.gradle.testfixtures.ProjectBuilder
|
||||||
|
import org.junit.jupiter.api.DynamicTest
|
||||||
|
import org.junit.jupiter.api.TestFactory
|
||||||
|
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
import static groowt.gradle.antlr.GroowtAntlrUtil.*
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertIterableEquals
|
||||||
|
import static org.junit.jupiter.api.DynamicTest.dynamicTest
|
||||||
|
|
||||||
|
class GroowtAntlrUtilTests {
|
||||||
|
|
||||||
|
@TupleConstructor
|
||||||
|
private static class SourceFileToIdentifierTestSpec {
|
||||||
|
File srcDir
|
||||||
|
File srcFile
|
||||||
|
List<String> expected
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestFactory
|
||||||
|
Collection<DynamicTest> sourceFileToIdentifierPartsTests() {
|
||||||
|
def srcDir = File.createTempDir()
|
||||||
|
def getSpec = { String path, List<String> expected ->
|
||||||
|
new SourceFileToIdentifierTestSpec(srcDir, new File(path), expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
def specs = [
|
||||||
|
getSpec('MyGrammar.g4', ['MyGrammar']),
|
||||||
|
getSpec('subDir/MyGrammar.g4', ['subDir', 'MyGrammar']),
|
||||||
|
getSpec('subDir/subSubDir/MyGrammar.g4', ['subDir', 'subSubDir', 'MyGrammar']),
|
||||||
|
getSpec('My.grammar.g4', ['My.grammar'])
|
||||||
|
]
|
||||||
|
|
||||||
|
return specs.collect { spec ->
|
||||||
|
dynamicTest(spec.srcFile.toString()) {
|
||||||
|
def actual = sourceFileToIdentifierParts(spec.srcDir, spec.srcFile)
|
||||||
|
assertIterableEquals(spec.expected, actual) {
|
||||||
|
"Unexpected result: ${actual}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestFactory
|
||||||
|
Collection<DynamicTest> getSourceIdentifierTests() {
|
||||||
|
def projectDir = File.createTempDir()
|
||||||
|
File srcDir
|
||||||
|
new FileTreeBuilder(projectDir).with {
|
||||||
|
srcDir = dir(['src', 'main', 'antlr'].join(File.separator))
|
||||||
|
}
|
||||||
|
|
||||||
|
def project = ProjectBuilder.builder()
|
||||||
|
.withProjectDir(projectDir)
|
||||||
|
.build()
|
||||||
|
project.pluginManager.with {
|
||||||
|
apply(JavaPlugin)
|
||||||
|
apply(AntlrPlugin)
|
||||||
|
}
|
||||||
|
def mainSourceSet = project.extensions.getByType(JavaPluginExtension)
|
||||||
|
.sourceSets.getByName('main')
|
||||||
|
|
||||||
|
def getResolvedSource = { String path ->
|
||||||
|
new ResolvedSource(project, mainSourceSet, srcDir, new File(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
Closure<Tuple2<String, ResolvedSource>> getSpec = { String expected, String path ->
|
||||||
|
new Tuple2(expected, getResolvedSource(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
def specs = [
|
||||||
|
getSpec('MyGrammar', 'MyGrammar.g4'),
|
||||||
|
getSpec('SubDirMyGrammar', 'subDir/MyGrammar.g4')
|
||||||
|
]
|
||||||
|
|
||||||
|
return specs.collect { spec ->
|
||||||
|
dynamicTest(spec.v2.sourceFile.toString()) {
|
||||||
|
def actual = getSourceIdentifier(spec.v2)
|
||||||
|
assertEquals(spec.v1, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestFactory
|
||||||
|
Collection<DynamicTest> getOutputDirTests() {
|
||||||
|
def project = ProjectBuilder.builder().build()
|
||||||
|
project.pluginManager.tap {
|
||||||
|
apply(JavaPlugin)
|
||||||
|
apply(AntlrPlugin)
|
||||||
|
}
|
||||||
|
project.layout.buildDirectory.set(new File('build'))
|
||||||
|
|
||||||
|
def mainSourceSet = project.extensions.getByType(JavaPluginExtension)
|
||||||
|
.sourceSets.getByName('main')
|
||||||
|
|
||||||
|
Closure<Tuple3<Path, SourceSet, Provider<String>>> getSpec = { String givenPackageName ->
|
||||||
|
def expectedPackagePath = givenPackageName.replace('.', File.separator)
|
||||||
|
def expected = Path.of('build', ['generated-src', 'antlr', mainSourceSet.name, expectedPackagePath] as String[])
|
||||||
|
def packageProperty = project.objects.property(String).tap { set(givenPackageName) }
|
||||||
|
new Tuple3(expected, mainSourceSet, packageProperty)
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Tuple3<Path, SourceSet, Provider<String>>> specs = [
|
||||||
|
getSpec('antlr.one.two.three'), // build/generated-src/antlr/main/antlr/one/two/three
|
||||||
|
getSpec('test.antlr'), // build/generated-src/antlr/main/test/antlr
|
||||||
|
getSpec('antlr'), // build/generated-src/antlr/main/antlr
|
||||||
|
getSpec('') // build/generated-src/antlr/main
|
||||||
|
]
|
||||||
|
|
||||||
|
def projectPath = project.layout.projectDirectory.asFile.toPath()
|
||||||
|
|
||||||
|
return specs.collect { spec ->
|
||||||
|
def givenPackageName = spec.v3.get()
|
||||||
|
dynamicTest(givenPackageName.empty ? '<empty>' : givenPackageName) {
|
||||||
|
def actualPath = getOutputDirectory(project, spec.v2, spec.v3)
|
||||||
|
.get().asFile.toPath()
|
||||||
|
def result = projectPath.relativize(actualPath)
|
||||||
|
assertEquals(spec.v1, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
5
gradle.properties
Normal file
5
gradle.properties
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# This file was generated by the Gradle 'init' task.
|
||||||
|
# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties
|
||||||
|
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.caching=true
|
32
gradle/libs.versions.toml
Normal file
32
gradle/libs.versions.toml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# This file was generated by the Gradle 'init' task.
|
||||||
|
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
|
||||||
|
|
||||||
|
[versions]
|
||||||
|
antlr = '4.13.1'
|
||||||
|
groovy = '4.0.21'
|
||||||
|
junit = '5.10.2'
|
||||||
|
kotlin = '1.9.23'
|
||||||
|
log4j = '2.23.1'
|
||||||
|
mockito = '5.11.0'
|
||||||
|
slf4j = '2.0.12'
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
antlr = { module = 'org.antlr:antlr4', version.ref = 'antlr' }
|
||||||
|
antlr-runtime = { module = 'org.antlr:antlr4-runtime', version.ref = 'antlr' }
|
||||||
|
asm = 'org.ow2.asm:asm:9.7'
|
||||||
|
groovy = { module = 'org.apache.groovy:groovy', version.ref = 'groovy' }
|
||||||
|
groovy-all = { module = 'org.apache.groovy:groovy-all', version.ref = 'groovy' }
|
||||||
|
groovy-console = { module = 'org.apache.groovy:groovy-console', version.ref = 'groovy' }
|
||||||
|
groovy-templates = { module = 'org.apache.groovy:groovy-templates', version.ref = 'groovy' }
|
||||||
|
jakarta-inject = 'jakarta.inject:jakarta.inject-api:2.0.1'
|
||||||
|
jansi = 'org.fusesource.jansi:jansi:2.4.1'
|
||||||
|
jetbrains-anotations = 'org.jetbrains:annotations:24.1.0'
|
||||||
|
junit-jupiter-api = { module = 'org.junit.jupiter:junit-jupiter-api', version.ref = 'junit' }
|
||||||
|
kotlin-stdlib = { module = 'org.jetbrains.kotlin:kotlin-stdlib', version.ref = 'kotlin' }
|
||||||
|
kotlin-test = { module = 'org.jetbrains.kotlin:kotlin-test', version.ref = 'kotlin' }
|
||||||
|
log4j-core = { module = 'org.apache.logging.log4j:log4j-core', version.ref = 'log4j' }
|
||||||
|
log4j-slf4jBinding = { module = 'org.apache.logging.log4j:log4j-slf4j2-impl', version.ref = 'log4j' }
|
||||||
|
mockito-core = { module = 'org.mockito:mockito-core', version.ref = 'mockito' }
|
||||||
|
mockito-junit = { module = 'org.mockito:mockito-junit-jupiter', version.ref = 'mockito' }
|
||||||
|
picocli = 'info.picocli:picocli:4.7.5'
|
||||||
|
slf4j-api = { module = 'org.slf4j:slf4j-api', version.ref = 'slf4j' }
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
249
gradlew
vendored
Executable file
249
gradlew
vendored
Executable file
@ -0,0 +1,249 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original 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.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
92
gradlew.bat
vendored
Normal file
92
gradlew.bat
vendored
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
12
settings.gradle
Normal file
12
settings.gradle
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
pluginManagement {
|
||||||
|
plugins {
|
||||||
|
id 'org.jetbrains.kotlin.jvm' version '1.9.23'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = 'groowt'
|
||||||
|
include 'views', 'view-components', 'web-views'
|
||||||
|
file('util').eachDir {
|
||||||
|
include it.name
|
||||||
|
project(":$it.name").projectDir = it
|
||||||
|
}
|
10
util/di/build.gradle
Normal file
10
util/di/build.gradle
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
plugins {
|
||||||
|
id 'GroowtConventions'
|
||||||
|
id 'java-library'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api libs.jakarta.inject
|
||||||
|
compileOnlyApi libs.jetbrains.anotations
|
||||||
|
implementation libs.slf4j.api, libs.groovy
|
||||||
|
}
|
@ -0,0 +1,197 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static groowt.util.di.ObjectFactoryUtil.toTypes;
|
||||||
|
|
||||||
|
// TODO: maybe inject fields
|
||||||
|
public abstract class AbstractInjectingObjectFactory implements ObjectFactory {
|
||||||
|
|
||||||
|
protected record CachedInjectConstructor<T>(Class<T> clazz, Constructor<T> constructor) {}
|
||||||
|
|
||||||
|
protected record CachedNonInjectConstructor<T>(
|
||||||
|
Class<T> clazz,
|
||||||
|
Constructor<T> constructor,
|
||||||
|
Class<?>[] paramTypes
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private final Map<Class<?>, Constructor<?>[]> cachedAllConstructors = new HashMap<>();
|
||||||
|
private final Collection<CachedInjectConstructor<?>> cachedInjectConstructors = new ArrayList<>();
|
||||||
|
private final Collection<CachedNonInjectConstructor<?>> cachedNonInjectConstructors = new ArrayList<>();
|
||||||
|
private final Map<Class<?>, Collection<Method>> cachedSetters = new HashMap<>();
|
||||||
|
private final Map<Method, Parameter> cachedSetterParameters = new HashMap<>();
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <T> @Nullable Constructor<T> findCachedInjectConstructor(Class<T> clazz) {
|
||||||
|
for (final CachedInjectConstructor<?> cachedConstructor : this.cachedInjectConstructors) {
|
||||||
|
if (clazz.equals(cachedConstructor.clazz())) {
|
||||||
|
return (Constructor<T>) cachedConstructor.constructor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implNote If overridden, please cache any found inject constructors using {@link #putCachedInjectConstructor}.
|
||||||
|
*
|
||||||
|
* @param clazz the {@link Class} in which to search for an <code>{@literal @}Inject</code> annotated constructor.
|
||||||
|
* @return the inject constructor, or {@code null} if none found.
|
||||||
|
* @param <T> the type of the class
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected <T> @Nullable Constructor<T> findInjectConstructor(Class<T> clazz) {
|
||||||
|
final Constructor<T> cachedInjectConstructor = this.findCachedInjectConstructor(clazz);
|
||||||
|
if (cachedInjectConstructor != null) {
|
||||||
|
return cachedInjectConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Constructor<?>[] constructors = this.cachedAllConstructors.computeIfAbsent(clazz, Class::getConstructors);
|
||||||
|
|
||||||
|
final List<Constructor<?>> injectConstructors = Arrays.stream(constructors)
|
||||||
|
.filter(constructor -> constructor.isAnnotationPresent(Inject.class))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (injectConstructors.size() > 1) {
|
||||||
|
// one day maybe support multiple inject constructors
|
||||||
|
throw new UnsupportedOperationException("Cannot have more than one @Inject constructor in class: " + clazz);
|
||||||
|
} else if (injectConstructors.size() == 1) {
|
||||||
|
final Constructor<T> injectConstructor = (Constructor<T>) injectConstructors.getFirst();
|
||||||
|
this.putCachedInjectConstructor(new CachedInjectConstructor<>(clazz, injectConstructor));
|
||||||
|
return injectConstructor;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final void putCachedInjectConstructor(CachedInjectConstructor<?> cached) {
|
||||||
|
this.cachedInjectConstructors.add(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <T> @Nullable Constructor<T> findCachedNonInjectConstructor(Class<T> clazz, Class<?>[] paramTypes) {
|
||||||
|
for (final CachedNonInjectConstructor<?> cachedConstructor : this.cachedNonInjectConstructors) {
|
||||||
|
if (clazz.equals(cachedConstructor.clazz()) && Arrays.equals(cachedConstructor.paramTypes(), paramTypes)) {
|
||||||
|
return (Constructor<T>) cachedConstructor.constructor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implNote If overridden, please cache any found non-inject constructors using {@link #putCachedNonInjectConstructor}.
|
||||||
|
*
|
||||||
|
* @param clazz the {@link Class} in which to search for a constructor which does not have an <code>{@literal @}Inject</code>
|
||||||
|
* annotation
|
||||||
|
* @param constructorArgs the given constructor args
|
||||||
|
* @return the found non-inject constructor appropriate for the given constructor args, or {@code null} if no
|
||||||
|
* such constructor exists
|
||||||
|
* @param <T> the type
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected <T> @Nullable Constructor<T> findNonInjectConstructor(Class<T> clazz, Object[] constructorArgs) {
|
||||||
|
final Class<?>[] types = toTypes(constructorArgs);
|
||||||
|
final Constructor<T> cachedConstructor = this.findCachedNonInjectConstructor(clazz, types);
|
||||||
|
if (cachedConstructor != null) {
|
||||||
|
return cachedConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Constructor<?>[] constructors = this.cachedAllConstructors.computeIfAbsent(clazz, Class::getConstructors);
|
||||||
|
for (Constructor<?> constructor : constructors) {
|
||||||
|
if (Arrays.equals(constructor.getParameterTypes(), types)) {
|
||||||
|
final Constructor<T> found = (Constructor<T>) constructor;
|
||||||
|
this.putCachedNonInjectConstructor(new CachedNonInjectConstructor<>(clazz, found, types));
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final void putCachedNonInjectConstructor(CachedNonInjectConstructor<?> cached) {
|
||||||
|
this.cachedNonInjectConstructors.add(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implNote Please call {@code super.findConstructor()} first, and then implement custom
|
||||||
|
* constructor finding logic. If the custom logic finds a constructor, please cache it
|
||||||
|
* using either {@link #putCachedNonInjectConstructor} or {@link #putCachedInjectConstructor}.
|
||||||
|
*/
|
||||||
|
protected <T> Constructor<T> findConstructor(Class<T> clazz, Object[] args) {
|
||||||
|
final Constructor<T> injectConstructor = this.findInjectConstructor(clazz);
|
||||||
|
if (injectConstructor != null) {
|
||||||
|
return injectConstructor;
|
||||||
|
}
|
||||||
|
final Constructor<T> nonInjectConstructor = this.findNonInjectConstructor(clazz, args);
|
||||||
|
if (nonInjectConstructor != null) {
|
||||||
|
return nonInjectConstructor;
|
||||||
|
}
|
||||||
|
throw new RuntimeException("Could not find an appropriate constructor for " + clazz.getName()
|
||||||
|
+ " with args " + Arrays.toString(toTypes(args))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Collection<Method> getAllInjectSetters(Class<?> clazz) {
|
||||||
|
final Method[] allMethods = clazz.getMethods();
|
||||||
|
final Collection<Method> injectSetters = new ArrayList<>();
|
||||||
|
for (final var method : allMethods) {
|
||||||
|
if (
|
||||||
|
method.isAnnotationPresent(Inject.class)
|
||||||
|
&& method.getName().startsWith("set")
|
||||||
|
&& !Modifier.isStatic(method.getModifiers())
|
||||||
|
&& method.getParameterCount() == 1
|
||||||
|
) {
|
||||||
|
injectSetters.add(method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return injectSetters;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Collection<Method> getCachedSettersFor(Object target) {
|
||||||
|
return this.cachedSetters.computeIfAbsent(target.getClass(), this::getAllInjectSetters);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Parameter getCachedInjectParameter(Method setter) {
|
||||||
|
return this.cachedSetterParameters.computeIfAbsent(setter, s -> {
|
||||||
|
if (s.getParameterCount() != 1) {
|
||||||
|
throw new IllegalArgumentException("Setter " + s.getName() + " has a parameter count other than one (1)!");
|
||||||
|
}
|
||||||
|
return s.getParameters()[0];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void injectSetter(Object target, Method setter) {
|
||||||
|
try {
|
||||||
|
setter.invoke(target, this.getSetterInjectArg(target.getClass(), setter, this.getCachedInjectParameter(setter)));
|
||||||
|
} catch (InvocationTargetException | IllegalAccessException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void injectSetters(Object target) {
|
||||||
|
this.getCachedSettersFor(target).forEach(setter -> this.injectSetter(target, setter));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public <T> T createInstance(Class<T> clazz, Object... constructorArgs) {
|
||||||
|
final Constructor<T> constructor = this.findConstructor(clazz, constructorArgs);
|
||||||
|
final Object[] allArgs = this.createArgs(constructor, constructorArgs);
|
||||||
|
try {
|
||||||
|
final T instance = constructor.newInstance(allArgs);
|
||||||
|
this.injectSetters(instance);
|
||||||
|
return instance;
|
||||||
|
} catch (InvocationTargetException | IllegalAccessException | InstantiationException e) {
|
||||||
|
throw new RuntimeException(e); // In the future, we might have an option to ignore exceptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Object[] createArgs(Constructor<?> constructor, Object[] constructorArgs);
|
||||||
|
|
||||||
|
protected abstract Object getSetterInjectArg(Class<?> targetType, Method setter, Parameter toInject);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import groowt.util.di.filters.FilterHandler;
|
||||||
|
import groowt.util.di.filters.IterableFilterHandler;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import static groowt.util.di.RegistryObjectFactoryUtil.orElseSupply;
|
||||||
|
|
||||||
|
public abstract class AbstractRegistryObjectFactory extends AbstractInjectingObjectFactory implements RegistryObjectFactory {
|
||||||
|
|
||||||
|
public static abstract class AbstractBuilder<T extends DefaultRegistryObjectFactory> implements Builder<T> {
|
||||||
|
|
||||||
|
private final Collection<FilterHandler<?, ?>> filterHandlers = new ArrayList<>();
|
||||||
|
private final Collection<IterableFilterHandler<?, ?>> iterableFilterHandlers = new ArrayList<>();
|
||||||
|
private final Registry registry;
|
||||||
|
private @Nullable RegistryObjectFactory parent;
|
||||||
|
|
||||||
|
public AbstractBuilder(Registry registry) {
|
||||||
|
this.registry = registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AbstractBuilder() {
|
||||||
|
this.registry = new DefaultRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Registry getRegistry() {
|
||||||
|
return this.registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Collection<FilterHandler<?, ?>> getFilterHandlers() {
|
||||||
|
return this.filterHandlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Collection<IterableFilterHandler<?, ?>> getIterableFilterHandlers() {
|
||||||
|
return this.iterableFilterHandlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected @Nullable RegistryObjectFactory getParent() {
|
||||||
|
return this.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureRegistry(Consumer<? super Registry> configure) {
|
||||||
|
configure.accept(this.registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addFilterHandler(FilterHandler<?, ?> handler) {
|
||||||
|
this.filterHandlers.add(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addIterableFilterHandler(IterableFilterHandler<?, ?> handler) {
|
||||||
|
this.iterableFilterHandlers.add(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setParent(@Nullable RegistryObjectFactory parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final Registry registry;
|
||||||
|
@Nullable private final RegistryObjectFactory parent;
|
||||||
|
|
||||||
|
public AbstractRegistryObjectFactory(Registry registry, @Nullable RegistryObjectFactory parent) {
|
||||||
|
this.registry = registry;
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureRegistry(Consumer<? super Registry> use) {
|
||||||
|
use.accept(this.registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <A extends Annotation> @Nullable ScopeHandler<A> findScopeHandler(Class<A> scopeType) {
|
||||||
|
return this.registry.getScopeHandler(scopeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <A extends Annotation> @Nullable QualifierHandler<A> findQualifierHandler(Class<A> qualifierType) {
|
||||||
|
return this.registry.getQualifierHandler(qualifierType);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final <T> Optional<T> findInParent(Function<? super RegistryObjectFactory, @Nullable T> finder) {
|
||||||
|
return this.parent != null ? Optional.ofNullable(finder.apply(this.parent)) : Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final <T> Optional<T> findInSelfOrParent(Function<? super RegistryObjectFactory, @Nullable T> finder) {
|
||||||
|
return orElseSupply(
|
||||||
|
finder.apply(this),
|
||||||
|
() -> this.parent != null ? finder.apply(this.parent) : null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final <T> T getInSelfOrParent(
|
||||||
|
Function<? super RegistryObjectFactory, @Nullable T> finder,
|
||||||
|
Supplier<? extends RuntimeException> exceptionSupplier
|
||||||
|
) {
|
||||||
|
return orElseSupply(
|
||||||
|
finder.apply(this),
|
||||||
|
() -> this.parent != null ? finder.apply(this.parent) : null
|
||||||
|
).orElseThrow(exceptionSupplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
3
util/di/src/main/java/groowt/util/di/Binding.java
Normal file
3
util/di/src/main/java/groowt/util/di/Binding.java
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
sealed public interface Binding<T> permits ClassBinding, ProviderBinding, SingletonBinding, LazySingletonBinding {}
|
@ -0,0 +1,12 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import jakarta.inject.Provider;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public interface BindingConfigurator<T> {
|
||||||
|
void to(Class<? extends T> target);
|
||||||
|
void toProvider(Provider<? extends T> provider);
|
||||||
|
void toSingleton(T target);
|
||||||
|
void toLazySingleton(Supplier<T> singletonSupplier);
|
||||||
|
}
|
32
util/di/src/main/java/groowt/util/di/BindingUtil.java
Normal file
32
util/di/src/main/java/groowt/util/di/BindingUtil.java
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import jakarta.inject.Provider;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public final class BindingUtil {
|
||||||
|
|
||||||
|
public static <T> Consumer<BindingConfigurator<T>> toClass(Class<? extends T> clazz) {
|
||||||
|
return bc -> bc.to(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Consumer<BindingConfigurator<T>> toProvider(Provider<? extends T> provider) {
|
||||||
|
return bc -> bc.toProvider(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Consumer<BindingConfigurator<T>> toSingleton(T singleton) {
|
||||||
|
return bc -> bc.toSingleton(singleton);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Consumer<BindingConfigurator<T>> toLazySingleton(Supplier<T> singletonSupplier) {
|
||||||
|
return bc -> bc.toLazySingleton(singletonSupplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Consumer<BindingConfigurator<T>> toSelf() {
|
||||||
|
return bc -> {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private BindingUtil() {}
|
||||||
|
|
||||||
|
}
|
3
util/di/src/main/java/groowt/util/di/ClassBinding.java
Normal file
3
util/di/src/main/java/groowt/util/di/ClassBinding.java
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
public record ClassBinding<T>(Class<T> from, Class<? extends T> to) implements Binding<T> {}
|
@ -0,0 +1,80 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import jakarta.inject.Named;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
public class DefaultNamedRegistryExtension implements NamedRegistryExtension {
|
||||||
|
|
||||||
|
protected static class NamedQualifierHandler implements QualifierHandler<Named> {
|
||||||
|
|
||||||
|
private final DefaultNamedRegistryExtension extension;
|
||||||
|
|
||||||
|
public NamedQualifierHandler(DefaultNamedRegistryExtension extension) {
|
||||||
|
this.extension = extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> @Nullable Binding<T> handle(Named named, Class<T> dependencyClass) {
|
||||||
|
return this.extension.getBinding(
|
||||||
|
new SimpleKeyHolder<>(NamedRegistryExtension.class, dependencyClass, named.value())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final Map<String, Binding<?>> bindings = new HashMap<>();
|
||||||
|
protected final QualifierHandler<Named> qualifierHandler = this.getNamedQualifierHandler();
|
||||||
|
|
||||||
|
protected QualifierHandler<Named> getNamedQualifierHandler() {
|
||||||
|
return new NamedQualifierHandler(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public @Nullable <A extends Annotation> QualifierHandler<A> getQualifierHandler(Class<A> qualifierType) {
|
||||||
|
return Named.class.equals(qualifierType) ? (QualifierHandler<A>) this.qualifierHandler : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<String> getKeyClass() {
|
||||||
|
return String.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <B extends KeyBinder<String>, T> void bind(KeyHolder<B, ? extends String, T> keyHolder, Consumer<? super BindingConfigurator<T>> configure) {
|
||||||
|
final var configurator = new SimpleBindingConfigurator<>(keyHolder.type());
|
||||||
|
configure.accept(configurator);
|
||||||
|
this.bindings.put(keyHolder.key(), configurator.getBinding());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public @Nullable <B extends KeyBinder<String>, T> Binding<T> getBinding(KeyHolder<B, ? extends String, T> keyHolder) {
|
||||||
|
return (Binding<T>) this.bindings.getOrDefault(keyHolder.key(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <B extends KeyBinder<String>, T> void removeBinding(KeyHolder<B, ? extends String, T> keyHolder) {
|
||||||
|
this.bindings.remove(keyHolder.key());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <B extends KeyBinder<String>, T> void removeBindingIf(KeyHolder<B, ? extends String, T> keyHolder, Predicate<? super Binding<T>> filter) {
|
||||||
|
final String key = keyHolder.key();
|
||||||
|
if (this.bindings.containsKey(key) && filter.test(this.getBinding(keyHolder))) {
|
||||||
|
this.bindings.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearAllBindings() {
|
||||||
|
this.bindings.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
200
util/di/src/main/java/groowt/util/di/DefaultRegistry.java
Normal file
200
util/di/src/main/java/groowt/util/di/DefaultRegistry.java
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
public class DefaultRegistry implements Registry {
|
||||||
|
|
||||||
|
protected record ClassKeyBinding<T>(Class<T> key, Binding<T> binding) {}
|
||||||
|
|
||||||
|
protected final Collection<ClassKeyBinding<?>> classBindings = new ArrayList<>();
|
||||||
|
protected final Collection<RegistryExtension> extensions = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeBinding(Class<?> key) {
|
||||||
|
this.classBindings.removeIf(classKeyBinding -> classKeyBinding.key().equals(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <T> void removeBindingIf(Class<T> key, Predicate<Binding<T>> filter) {
|
||||||
|
this.classBindings.removeIf(classKeyBinding ->
|
||||||
|
classKeyBinding.key().equals(key) && filter.test((Binding<T>) classKeyBinding.binding())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <E extends RegistryExtension> List<E> getAllRegistryExtensions(Class<E> extensionType) {
|
||||||
|
return this.extensions.stream()
|
||||||
|
.filter(extension -> extensionType.isAssignableFrom(extension.getClass()))
|
||||||
|
.map(extensionType::cast)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private <E extends RegistryExtension> E getOneRegistryExtension(Class<E> extensionType) {
|
||||||
|
final List<E> extensions = this.getAllRegistryExtensions(extensionType);
|
||||||
|
if (extensions.size() == 1) {
|
||||||
|
return extensions.getFirst();
|
||||||
|
} else if (extensions.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("There is no " + extensionType + " registered for this " + this);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("There is more than one " + extensionType + " registered for this " + this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addExtension(RegistryExtension extension) {
|
||||||
|
final List<? extends RegistryExtension> existing = this.getAllRegistryExtensions(extension.getClass());
|
||||||
|
if (existing.isEmpty()) {
|
||||||
|
this.extensions.add(extension);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("There is already at least one " + extension.getClass() + " registered in " + this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends RegistryExtension> E getExtension(Class<E> extensionType) {
|
||||||
|
return this.getOneRegistryExtension(extensionType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends RegistryExtension> Collection<E> getExtensions(Class<E> extensionType) {
|
||||||
|
return this.getAllRegistryExtensions(extensionType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeExtension(RegistryExtension extension) {
|
||||||
|
this.extensions.remove(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable <A extends Annotation> QualifierHandler<A> getQualifierHandler(Class<A> qualifierType) {
|
||||||
|
final List<QualifierHandler<A>> handlers = new ArrayList<>();
|
||||||
|
for (final var extension : this.extensions) {
|
||||||
|
if (extension instanceof QualifierHandlerContainer handlerContainer) {
|
||||||
|
final var handler = handlerContainer.getQualifierHandler(qualifierType);
|
||||||
|
if (handler != null) {
|
||||||
|
handlers.add(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (handlers.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
} else if (handlers.size() > 1) {
|
||||||
|
throw new RuntimeException("There is more than one QualifierHandler for " + qualifierType.getName());
|
||||||
|
} else {
|
||||||
|
return handlers.getFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable <A extends Annotation> ScopeHandler<A> getScopeHandler(Class<A> scopeType) {
|
||||||
|
final List<ScopeHandler<A>> handlers = new ArrayList<>();
|
||||||
|
for (final var extension : this.extensions) {
|
||||||
|
if (extension instanceof ScopeHandlerContainer handlerContainer) {
|
||||||
|
final var handler = handlerContainer.getScopeHandler(scopeType);
|
||||||
|
if (handler != null) {
|
||||||
|
handlers.add(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (handlers.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
} else if (handlers.size() > 1) {
|
||||||
|
throw new RuntimeException("There is more than one ScopeHandler for " + scopeType.getName());
|
||||||
|
} else {
|
||||||
|
return handlers.getFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> void bind(Class<T> key, Consumer<? super BindingConfigurator<T>> configure) {
|
||||||
|
final var configurator = new SimpleBindingConfigurator<>(key);
|
||||||
|
configure.accept(configurator);
|
||||||
|
this.classBindings.add(new ClassKeyBinding<>(key, configurator.getBinding()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public @Nullable <T> Binding<T> getBinding(Class<T> key) {
|
||||||
|
for (final var classKeyBinding : this.classBindings) {
|
||||||
|
if (key.isAssignableFrom(classKeyBinding.key())) {
|
||||||
|
return (Binding<T>) classKeyBinding.binding();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeyBinder<?> findKeyBinder(Class<?> keyClass) {
|
||||||
|
final List<KeyBinder<?>> binders = new ArrayList<>();
|
||||||
|
for (final var extension : this.extensions) {
|
||||||
|
if (extension instanceof KeyBinder<?> keyBinder && keyBinder.getKeyClass().isAssignableFrom(keyClass)) {
|
||||||
|
binders.add(keyBinder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (binders.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("There are no configured RegistryExtensions that can handle keys with type " + keyClass.getName());
|
||||||
|
} else if (binders.size() > 1) {
|
||||||
|
throw new IllegalArgumentException("There is more than one configured RegistryExtension that can handle keys with type " + keyClass.getName());
|
||||||
|
} else {
|
||||||
|
return binders.getFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
protected final void withKeyBinder(KeyHolder<?, ?, ?> keyHolder, Consumer<KeyBinder> action) {
|
||||||
|
action.accept(this.findKeyBinder(keyHolder.key().getClass()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
protected final <R> @Nullable R tapKeyBinder(
|
||||||
|
KeyHolder<?, ?, ?> keyHolder,
|
||||||
|
Function<KeyBinder, @Nullable R> function
|
||||||
|
) {
|
||||||
|
return function.apply(this.findKeyBinder(keyHolder.key().getClass()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <T> void bind(KeyHolder<?, ?, T> keyHolder, Consumer<? super BindingConfigurator<T>> configure) {
|
||||||
|
this.withKeyBinder(keyHolder, b -> b.bind(keyHolder, configure));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public @Nullable <T> Binding<T> getBinding(KeyHolder<?, ?, T> keyHolder) {
|
||||||
|
return this.tapKeyBinder(keyHolder, b -> b.getBinding(keyHolder));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <T> void removeBinding(KeyHolder<?, ?, T> keyHolder) {
|
||||||
|
this.withKeyBinder(keyHolder, b -> b.removeBinding(keyHolder));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <T> void removeBindingIf(
|
||||||
|
KeyHolder<?, ?, T> keyHolder,
|
||||||
|
Predicate<? super Binding<T>> filter
|
||||||
|
) {
|
||||||
|
this.withKeyBinder(keyHolder, b -> b.removeBindingIf(keyHolder, filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearAllBindings() {
|
||||||
|
this.classBindings.clear();
|
||||||
|
for (final var extension : this.extensions) {
|
||||||
|
if (extension instanceof KeyBinder<?> keyBinder) {
|
||||||
|
keyBinder.clearAllBindings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,313 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import groowt.util.di.filters.FilterHandler;
|
||||||
|
import groowt.util.di.filters.IterableFilterHandler;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Parameter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import static groowt.util.di.RegistryObjectFactoryUtil.*;
|
||||||
|
|
||||||
|
public class DefaultRegistryObjectFactory extends AbstractRegistryObjectFactory {
|
||||||
|
|
||||||
|
public static final class Builder extends AbstractRegistryObjectFactory.AbstractBuilder<DefaultRegistryObjectFactory> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@code Builder} initialized with a {@link DefaultRegistry}, which is in-turn configured with a
|
||||||
|
* {@link NamedRegistryExtension} and a {@link SingletonScopeHandler}.
|
||||||
|
*
|
||||||
|
* @return the builder
|
||||||
|
*/
|
||||||
|
public static Builder withDefaults() {
|
||||||
|
final var b = new Builder();
|
||||||
|
|
||||||
|
b.configureRegistry(r -> {
|
||||||
|
r.addExtension(new DefaultNamedRegistryExtension());
|
||||||
|
r.addExtension(new SingletonRegistryExtension(r));
|
||||||
|
});
|
||||||
|
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return a blank builder with a {@link Registry} from the given {@link Supplier}.
|
||||||
|
*/
|
||||||
|
public static Builder withRegistry(Supplier<? extends Registry> registrySupplier) {
|
||||||
|
return new Builder(registrySupplier.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return a blank builder which will use {@link DefaultRegistry}.
|
||||||
|
*/
|
||||||
|
public static Builder blank() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Builder(Registry registry) {
|
||||||
|
super(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Builder() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DefaultRegistryObjectFactory build() {
|
||||||
|
return new DefaultRegistryObjectFactory(
|
||||||
|
this.getRegistry(),
|
||||||
|
this.getParent(),
|
||||||
|
this.getFilterHandlers(),
|
||||||
|
this.getIterableFilterHandlers()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(DefaultRegistryObjectFactory.class); // leave it for the future!
|
||||||
|
|
||||||
|
private final Collection<FilterHandler<?, ?>> filterHandlers;
|
||||||
|
private final Collection<IterableFilterHandler<?, ?>> iterableFilterHandlers;
|
||||||
|
|
||||||
|
protected DefaultRegistryObjectFactory(
|
||||||
|
Registry registry,
|
||||||
|
@Nullable RegistryObjectFactory parent,
|
||||||
|
Collection<? extends FilterHandler<?, ?>> filterHandlers,
|
||||||
|
Collection<? extends IterableFilterHandler<?, ?>> iterableFilterHandlers
|
||||||
|
) {
|
||||||
|
super(registry, parent);
|
||||||
|
this.filterHandlers = new ArrayList<>(filterHandlers);
|
||||||
|
this.filterHandlers.forEach(handler -> checkIsValidFilter(handler.getAnnotationClass()));
|
||||||
|
|
||||||
|
this.iterableFilterHandlers = new ArrayList<>(iterableFilterHandlers);
|
||||||
|
this.iterableFilterHandlers.forEach(handler -> checkIsValidIterableFilter(handler.getAnnotationClass()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given parameter has any qualifier annotations; if it does,
|
||||||
|
* it delegates finding the desired object to the registered {@link QualifierHandler}.
|
||||||
|
*
|
||||||
|
* @param parameter the parameter
|
||||||
|
* @return the object returned from the {@code QualifierHandler}, or {@code null} if no qualifier
|
||||||
|
* is present or the {@code QualifierHandler} itself returns {@code null}.
|
||||||
|
*
|
||||||
|
* @throws RuntimeException if no {@code QualifierHandler} is registered for a qualifier annotation present on the
|
||||||
|
* given parameter, or if the handler itself throws an exception.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected final @Nullable Object tryQualifiers(Parameter parameter) {
|
||||||
|
final Class<?> paramType = parameter.getType();
|
||||||
|
final List<Annotation> qualifiers = RegistryObjectFactoryUtil.getQualifierAnnotations(parameter.getAnnotations());
|
||||||
|
if (qualifiers.size() > 1) {
|
||||||
|
throw new RuntimeException("Parameter " + parameter + " cannot have more than one Qualifier annotation.");
|
||||||
|
} else if (qualifiers.size() == 1) {
|
||||||
|
final Annotation qualifier = qualifiers.getFirst();
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
final QualifierHandler handler = this.getInSelfOrParent(
|
||||||
|
f -> f.findQualifierHandler(qualifier.annotationType()),
|
||||||
|
() -> new RuntimeException("There is no configured QualifierHandler for " + qualifier.annotationType().getName())
|
||||||
|
);
|
||||||
|
final Binding<?> binding = handler.handle(qualifier, paramType);
|
||||||
|
if (binding != null) {
|
||||||
|
return this.handleBinding(binding, EMPTY_OBJECT_ARRAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// no Qualifier or the QualifierHandler didn't return a Binding
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the {@code resolvedArg} against all filters present on the given parameter.
|
||||||
|
*
|
||||||
|
* @param parameter the parameter
|
||||||
|
* @param resolvedArg the resolved argument
|
||||||
|
*
|
||||||
|
* @throws RuntimeException if the {@link FilterHandler} itself throws an exception.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||||
|
protected final void checkFilters(Parameter parameter, Object resolvedArg) {
|
||||||
|
final Annotation[] allAnnotations = parameter.getAnnotations();
|
||||||
|
final Collection<Annotation> filterAnnotations = getFilterAnnotations(allAnnotations);
|
||||||
|
if (!filterAnnotations.isEmpty()) {
|
||||||
|
final Collection<FilterHandler<?, ?>> filtersForParamType = this.filterHandlers.stream()
|
||||||
|
.filter(filterHandler ->
|
||||||
|
filterHandler.getArgumentClass().isAssignableFrom(parameter.getType())
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
for (final Annotation filterAnnotation : filterAnnotations) {
|
||||||
|
for (final FilterHandler<?, ?> filterHandler : filtersForParamType) {
|
||||||
|
if (filterAnnotation.annotationType().equals(filterHandler.getAnnotationClass())) {
|
||||||
|
// hopefully we've checked everything
|
||||||
|
((FilterHandler<Annotation, Object>) filterHandler).check(filterAnnotation, resolvedArg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final Collection<Annotation> iterableFilterAnnotations = getIterableFilterAnnotations(allAnnotations);
|
||||||
|
if (!iterableFilterAnnotations.isEmpty() && resolvedArg instanceof Iterable iterable) {
|
||||||
|
for (final var annotation : iterableFilterAnnotations) {
|
||||||
|
this.iterableFilterHandlers.stream()
|
||||||
|
.filter(handler -> handler.getAnnotationClass().equals(annotation.annotationType()))
|
||||||
|
.forEach(handler -> {
|
||||||
|
((IterableFilterHandler<Annotation, Object>) handler).check(annotation, iterable);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final Object resolveInjectedArg(Parameter parameter) {
|
||||||
|
final Object qualifierProvidedArg = this.tryQualifiers(parameter);
|
||||||
|
if (qualifierProvidedArg != null) {
|
||||||
|
this.checkFilters(parameter, qualifierProvidedArg);
|
||||||
|
return qualifierProvidedArg;
|
||||||
|
} else {
|
||||||
|
final Object created = this.get(parameter.getType());
|
||||||
|
this.checkFilters(parameter, created);
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final void resolveInjectedArgs(Object[] dest, Parameter[] params) {
|
||||||
|
for (int i = 0; i < params.length; i++) {
|
||||||
|
dest[i] = this.resolveInjectedArg(params[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final void resolveGivenArgs(Object[] dest, Parameter[] params, Object[] givenArgs, int startIndex) {
|
||||||
|
for (int i = startIndex; i < dest.length; i++) {
|
||||||
|
final int resolveIndex = i - startIndex;
|
||||||
|
final Object arg = givenArgs[resolveIndex];
|
||||||
|
this.checkFilters(params[resolveIndex], arg);
|
||||||
|
dest[i] = arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: when there is a null arg, we lose the type. Therefore this algorithm breaks. Fix this.
|
||||||
|
@Override
|
||||||
|
protected Object[] createArgs(Constructor<?> constructor, Object[] givenArgs) {
|
||||||
|
final Class<?>[] paramTypes = constructor.getParameterTypes();
|
||||||
|
|
||||||
|
// check no arg
|
||||||
|
if (paramTypes.length == 0 && givenArgs.length == 0) {
|
||||||
|
// no args given, none needed, so return empty array
|
||||||
|
return EMPTY_OBJECT_ARRAY;
|
||||||
|
} else if (paramTypes.length == 0) { // implicit that givenArgs.length != 0
|
||||||
|
// zero expected, but got given args
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Expected zero args for constructor " + constructor + " but received " + Arrays.toString(givenArgs)
|
||||||
|
);
|
||||||
|
} else if (givenArgs.length > paramTypes.length) {
|
||||||
|
// expected is more than zero, but received too many given
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Too many args given for constructor " + constructor + "; received " + Arrays.toString(givenArgs)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Parameter[] allParams = constructor.getParameters();
|
||||||
|
final Object[] resolvedArgs = new Object[allParams.length];
|
||||||
|
|
||||||
|
if (givenArgs.length == 0) {
|
||||||
|
// if no given args, then they are all injected
|
||||||
|
this.resolveInjectedArgs(resolvedArgs, allParams);
|
||||||
|
} else if (givenArgs.length == paramTypes.length) {
|
||||||
|
// all are given
|
||||||
|
this.resolveGivenArgs(resolvedArgs, allParams, givenArgs, 0);
|
||||||
|
} else {
|
||||||
|
// some are injected, some are given
|
||||||
|
// everything before (non-inclusive) is injected
|
||||||
|
// everything after (inclusive) is given
|
||||||
|
// ex: 1 inject, 1 given -> 2 (allParams) - 1 = 1
|
||||||
|
// ex: 0 inject, 1 given -> 1 - 1 = 0
|
||||||
|
final int firstGivenIndex = allParams.length - givenArgs.length;
|
||||||
|
|
||||||
|
final Parameter[] injectedParams = new Parameter[firstGivenIndex];
|
||||||
|
final Parameter[] givenParams = new Parameter[allParams.length - firstGivenIndex];
|
||||||
|
|
||||||
|
System.arraycopy(allParams, 0, injectedParams, 0, injectedParams.length);
|
||||||
|
System.arraycopy(allParams, firstGivenIndex, givenParams, 0, allParams.length - firstGivenIndex);
|
||||||
|
|
||||||
|
this.resolveInjectedArgs(resolvedArgs, injectedParams);
|
||||||
|
this.resolveGivenArgs(resolvedArgs, givenParams, givenArgs, firstGivenIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <T> T handleBinding(Binding<T> binding, Object[] constructorArgs) {
|
||||||
|
return switch (binding) {
|
||||||
|
case ClassBinding<T>(Class<T> ignored, Class<? extends T> to) -> {
|
||||||
|
final Annotation scopeAnnotation = getScopeAnnotation(to);
|
||||||
|
if (scopeAnnotation != null) {
|
||||||
|
final Class<? extends Annotation> scopeClass = scopeAnnotation.annotationType();
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
final ScopeHandler scopeHandler = this.getInSelfOrParent(
|
||||||
|
f -> f.findScopeHandler(scopeClass),
|
||||||
|
() -> new RuntimeException("There is no configured ScopeHandler for " + scopeClass.getName())
|
||||||
|
);
|
||||||
|
final Binding<T> scopedBinding = scopeHandler.onScopedDependencyRequest(scopeAnnotation, to, this);
|
||||||
|
yield this.handleBinding(scopedBinding, constructorArgs);
|
||||||
|
} else {
|
||||||
|
yield this.createInstance(to, constructorArgs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ProviderBinding<T> providerBinding -> providerBinding.provider().get();
|
||||||
|
case SingletonBinding<T> singletonBinding -> singletonBinding.to();
|
||||||
|
case LazySingletonBinding<T> lazySingletonBinding -> lazySingletonBinding.singletonSupplier().get();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final <T> @Nullable Binding<T> searchRegistry(Class<T> from) {
|
||||||
|
return this.registry.getBinding(from);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected @Nullable <T> T tryParent(Class<T> clazz, Object[] constructorArgs) {
|
||||||
|
return this.findInParent(f -> f.getOrNull(clazz, constructorArgs)).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Object getSetterInjectArg(Class<?> targetType, Method setter, Parameter toInject) {
|
||||||
|
return this.resolveInjectedArg(toInject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public <T> T get(Class<T> clazz, Object... constructorArgs) {
|
||||||
|
final Binding<T> binding = this.searchRegistry(clazz);
|
||||||
|
if (binding != null) {
|
||||||
|
return this.handleBinding(binding, constructorArgs);
|
||||||
|
}
|
||||||
|
final T parentResult = this.tryParent(clazz, constructorArgs);
|
||||||
|
if (parentResult != null) {
|
||||||
|
return parentResult;
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("No bindings for " + clazz + " with args " + Arrays.toString(constructorArgs) + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public <T> T getOrDefault(Class<T> clazz, T defaultValue, Object... constructorArgs) {
|
||||||
|
final Binding<T> binding = this.searchRegistry(clazz);
|
||||||
|
if (binding != null) {
|
||||||
|
return this.handleBinding(binding, constructorArgs);
|
||||||
|
}
|
||||||
|
final T parentResult = this.tryParent(clazz, constructorArgs);
|
||||||
|
return parentResult != null ? parentResult : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
10
util/di/src/main/java/groowt/util/di/ExtensionContainer.java
Normal file
10
util/di/src/main/java/groowt/util/di/ExtensionContainer.java
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public interface ExtensionContainer {
|
||||||
|
void addExtension(RegistryExtension extension);
|
||||||
|
<E extends RegistryExtension> E getExtension(Class<E> extensionType);
|
||||||
|
<E extends RegistryExtension> Collection<E> getExtensions(Class<E> extensionType);
|
||||||
|
void removeExtension(RegistryExtension extension);
|
||||||
|
}
|
16
util/di/src/main/java/groowt/util/di/KeyBinder.java
Normal file
16
util/di/src/main/java/groowt/util/di/KeyBinder.java
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.function.BiPredicate;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
public interface KeyBinder<K> {
|
||||||
|
Class<K> getKeyClass();
|
||||||
|
<B extends KeyBinder<K>, T> void bind(KeyHolder<B, ? extends K, T> keyHolder, Consumer<? super BindingConfigurator<T>> configure);
|
||||||
|
<B extends KeyBinder<K>, T> @Nullable Binding<T> getBinding(KeyHolder<B, ? extends K, T> keyHolder);
|
||||||
|
<B extends KeyBinder<K>, T> void removeBinding(KeyHolder<B, ? extends K, T> keyHolder);
|
||||||
|
<B extends KeyBinder<K>, T> void removeBindingIf(KeyHolder<B, ? extends K, T> keyHolder, Predicate<? super Binding<T>> filter);
|
||||||
|
void clearAllBindings();
|
||||||
|
}
|
7
util/di/src/main/java/groowt/util/di/KeyHolder.java
Normal file
7
util/di/src/main/java/groowt/util/di/KeyHolder.java
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
public interface KeyHolder<B extends KeyBinder<K>, K, T> {
|
||||||
|
Class<B> binderType();
|
||||||
|
Class<T> type();
|
||||||
|
K key();
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public record LazySingletonBinding<T>(Supplier<T> singletonSupplier) implements Binding<T> {}
|
@ -0,0 +1,9 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
public interface NamedRegistryExtension extends RegistryExtension, KeyBinder<String>, QualifierHandlerContainer {
|
||||||
|
|
||||||
|
static <T> KeyHolder<NamedRegistryExtension, String, T> named(String name, Class<T> type) {
|
||||||
|
return new SimpleKeyHolder<>(NamedRegistryExtension.class, type, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
61
util/di/src/main/java/groowt/util/di/ObjectFactory.java
Normal file
61
util/di/src/main/java/groowt/util/di/ObjectFactory.java
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Contract;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link ObjectFactory} is an object that can construct objects of given types.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface ObjectFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance of the given {@code instanceType} with the given constructor args.
|
||||||
|
*
|
||||||
|
* @apiNote An implementation may provide a subclass of the given instance type,
|
||||||
|
* or it may directly instantiate the given type, if it is a class
|
||||||
|
* and it can determine the correct constructor from the given arguments.
|
||||||
|
* See individual implementation documentation for exact behavior.
|
||||||
|
*
|
||||||
|
* @implSpec It is up to individual implementations of {@link ObjectFactory} to determine how to
|
||||||
|
* select the appropriate constructor for the given type. The returned
|
||||||
|
* instance must be new and in a valid state.
|
||||||
|
*
|
||||||
|
* @param instanceType the {@link Class} of the desired type
|
||||||
|
* @param constructorArgs any arguments to pass to the constructor(s) of the class.
|
||||||
|
* @return the new instance
|
||||||
|
* @param <T> the desired type
|
||||||
|
*/
|
||||||
|
@Contract("_, _-> new")
|
||||||
|
<T> T createInstance(Class<T> instanceType, Object... constructorArgs);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Very similar to {@link #createInstance(Class, Object...)}, but catches any {@link RuntimeException}
|
||||||
|
* thrown by {@link #createInstance} and subsequently passes it to the given {@link Function}, returning
|
||||||
|
* instead the return value of the {@link Function}.
|
||||||
|
*
|
||||||
|
* @param instanceType the desired type of the created instance
|
||||||
|
* @param onException a {@link Function} to handle when an exception occurs and return a value nonetheless
|
||||||
|
* @param constructorArgs arguments to pass to the constructor
|
||||||
|
* @return the created instance
|
||||||
|
* @param <T> the desired type
|
||||||
|
*
|
||||||
|
* @throws RuntimeException if the given {@link Function} itself throws a RuntimeException
|
||||||
|
*
|
||||||
|
* @see #createInstance(Class, Object...)
|
||||||
|
*/
|
||||||
|
@Contract("_, _, _ -> new")
|
||||||
|
default <T> T createInstanceCatching(
|
||||||
|
Class<T> instanceType,
|
||||||
|
Function<? super RuntimeException, ? extends T> onException,
|
||||||
|
Object... constructorArgs
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return this.createInstance(instanceType, constructorArgs);
|
||||||
|
} catch (RuntimeException runtimeException) {
|
||||||
|
return onException.apply(runtimeException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
23
util/di/src/main/java/groowt/util/di/ObjectFactoryUtil.java
Normal file
23
util/di/src/main/java/groowt/util/di/ObjectFactoryUtil.java
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
|
|
||||||
|
@ApiStatus.Internal
|
||||||
|
public final class ObjectFactoryUtil {
|
||||||
|
|
||||||
|
public static Class<?>[] toTypes(Object... objects) {
|
||||||
|
final Class<?>[] types = new Class<?>[objects.length];
|
||||||
|
for (int i = 0; i < objects.length; i++) {
|
||||||
|
final Object o = objects[i];
|
||||||
|
if (o != null) {
|
||||||
|
types[i] = o.getClass();
|
||||||
|
} else {
|
||||||
|
types[i] = Object.class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectFactoryUtil() {}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import jakarta.inject.Provider;
|
||||||
|
|
||||||
|
public record ProviderBinding<T>(Class<T> to, Provider<? extends T> provider) implements Binding<T> {}
|
10
util/di/src/main/java/groowt/util/di/QualifierHandler.java
Normal file
10
util/di/src/main/java/groowt/util/di/QualifierHandler.java
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface QualifierHandler<A extends Annotation> {
|
||||||
|
<T> @Nullable Binding<T> handle(A annotation, Class<T> dependencyClass);
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import jakarta.inject.Qualifier;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
|
||||||
|
public interface QualifierHandlerContainer {
|
||||||
|
|
||||||
|
static void checkIsValidQualifier(Class<? extends Annotation> annotationClass) {
|
||||||
|
if (!annotationClass.isAnnotationPresent(Qualifier.class)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"The given qualifier annotation + " + annotationClass + " is itself not annotated with @Qualifier"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<A extends Annotation> @Nullable QualifierHandler<A> getQualifierHandler(Class<A> qualifierType);
|
||||||
|
|
||||||
|
}
|
20
util/di/src/main/java/groowt/util/di/Registry.java
Normal file
20
util/di/src/main/java/groowt/util/di/Registry.java
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
public interface Registry extends ExtensionContainer, QualifierHandlerContainer, ScopeHandlerContainer {
|
||||||
|
<T> void bind(Class<T> key, Consumer<? super BindingConfigurator<T>> configure);
|
||||||
|
@Nullable <T> Binding<T> getBinding(Class<T> key);
|
||||||
|
void removeBinding(Class<?> key);
|
||||||
|
<T> void removeBindingIf(Class<T> key, Predicate<Binding<T>> filter);
|
||||||
|
|
||||||
|
<T> void bind(KeyHolder<?, ?, T> keyHolder, Consumer<? super BindingConfigurator<T>> configure);
|
||||||
|
<T> @Nullable Binding<T> getBinding(KeyHolder<?, ?, T> keyHolder);
|
||||||
|
<T> void removeBinding(KeyHolder<?, ?, T> keyHolder);
|
||||||
|
<T> void removeBindingIf(KeyHolder<?, ?, T> keyHolder, Predicate<? super Binding<T>> filter);
|
||||||
|
void clearAllBindings();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
public interface RegistryExtension {}
|
@ -0,0 +1,99 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import groowt.util.di.filters.FilterHandler;
|
||||||
|
import groowt.util.di.filters.IterableFilterHandler;
|
||||||
|
import jakarta.inject.Provider;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link RegistryObjectFactory} is an {@link ObjectFactory} that offers the ability
|
||||||
|
* to provide desired objects based on an instance of
|
||||||
|
* {@link Registry} to determine how to provide those objects.
|
||||||
|
*/
|
||||||
|
public interface RegistryObjectFactory extends ObjectFactory {
|
||||||
|
|
||||||
|
interface Builder<T extends RegistryObjectFactory> {
|
||||||
|
void configureRegistry(Consumer<? super Registry> configure);
|
||||||
|
void addFilterHandler(FilterHandler<?, ?> handler);
|
||||||
|
void addIterableFilterHandler(IterableFilterHandler<?, ?> handler);
|
||||||
|
T build();
|
||||||
|
}
|
||||||
|
|
||||||
|
void configureRegistry(Consumer<? super Registry> use);
|
||||||
|
|
||||||
|
<A extends Annotation> @Nullable ScopeHandler<A> findScopeHandler(Class<A> scopeType);
|
||||||
|
|
||||||
|
<A extends Annotation> @Nullable QualifierHandler<A> findQualifierHandler(Class<A> qualifierType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an object with the desired type. How it is retrieved/created
|
||||||
|
* depends upon the {@link Binding} present in this {@link RegistryObjectFactory}'s held
|
||||||
|
* instances of {@link Registry}. The type of the {@link Binding} determines
|
||||||
|
* how the object is fetched:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link ClassBinding}: A new instance of the object is created using the given {@code constructorArgs}.</li>
|
||||||
|
* <li>{@link ProviderBinding}: An instance of the object is fetched from the bound {@link Provider}.
|
||||||
|
* Whether the instance is new or not depends on the {@link Provider}.</li>
|
||||||
|
* <li>{@link SingletonBinding}: The bound singleton object is returned.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @implNote If {@code constructorArgs} are provided
|
||||||
|
* and the {@link Binding} for the desired type is not a
|
||||||
|
* {@link ClassBinding}, the implementation should
|
||||||
|
* either throw an exception or log a warning at the least.
|
||||||
|
*
|
||||||
|
* @param clazz the {@link Class} of the desired type
|
||||||
|
* @param constructorArgs As in {@link #createInstance(Class, Object...)},
|
||||||
|
* the arguments which will be used to create the desired object
|
||||||
|
* if the {@link Binding} is a {@link ClassBinding}.
|
||||||
|
* @return an object of the desired type
|
||||||
|
* @param <T> the desired type
|
||||||
|
*
|
||||||
|
* @throws RuntimeException if there is no registered {@link Binding} or there is a problem
|
||||||
|
* fetching or constructing the object.
|
||||||
|
*/
|
||||||
|
<T> T get(Class<T> clazz, Object... constructorArgs);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similarly to {@link #get(Class, Object...)}, fetches an object
|
||||||
|
* of the desired type, but does not throw if there is no registered {@link Binding}
|
||||||
|
* in any of the held instances of {@link Registry},
|
||||||
|
* and instead returns the given {@code defaultValue}.
|
||||||
|
*
|
||||||
|
* @param clazz the {@link Class} of the desired type
|
||||||
|
* @param defaultValue the defaultValue to return
|
||||||
|
* @param constructorArgs see {@link #get(Class, Object...)}
|
||||||
|
* @return an object of the desired type
|
||||||
|
* @param <T> the desired type
|
||||||
|
*
|
||||||
|
* @throws RuntimeException if there <em>is</em> a registered {@link Binding} and there is a problem
|
||||||
|
* fetching or constructing the object.
|
||||||
|
*
|
||||||
|
* @see #get(Class, Object...)
|
||||||
|
*/
|
||||||
|
<T> T getOrDefault(Class<T> clazz, T defaultValue, Object... constructorArgs);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to {@link #getOrDefault(Class, Object, Object...)}, except that
|
||||||
|
* it returns null by default if there is no registered {@link Binding}.
|
||||||
|
*
|
||||||
|
* @param clazz the {@link Class} of the desired type
|
||||||
|
* @param constructorArgs see {@link RegistryObjectFactory#get(Class, Object...)}
|
||||||
|
* @return an object of the desired type
|
||||||
|
* @param <T> the desired type
|
||||||
|
*
|
||||||
|
* @see RegistryObjectFactory#get(Class, Object...)
|
||||||
|
* @see RegistryObjectFactory#getOrDefault(Class, Object, Object...)
|
||||||
|
*
|
||||||
|
* @throws RuntimeException if there <em>is</em> a registered {@code Binding} and there
|
||||||
|
* is a problem fetching or constructing the object.
|
||||||
|
*/
|
||||||
|
default <T> @Nullable T getOrNull(Class<T> clazz, Object... constructorArgs) {
|
||||||
|
return this.getOrDefault(clazz, null, constructorArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import groowt.util.di.filters.Filter;
|
||||||
|
import groowt.util.di.filters.IterableFilter;
|
||||||
|
import jakarta.inject.Qualifier;
|
||||||
|
import jakarta.inject.Scope;
|
||||||
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
@ApiStatus.Internal
|
||||||
|
public final class RegistryObjectFactoryUtil {
|
||||||
|
|
||||||
|
private RegistryObjectFactoryUtil() {}
|
||||||
|
|
||||||
|
public static List<Annotation> getQualifierAnnotations(Annotation[] annotations) {
|
||||||
|
return Arrays.stream(annotations)
|
||||||
|
.filter(a -> a.annotationType().isAnnotationPresent(Qualifier.class))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Annotation> getFilterAnnotations(Annotation[] annotations) {
|
||||||
|
return Arrays.stream(annotations)
|
||||||
|
.filter(a -> a.annotationType().isAnnotationPresent(Filter.class))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Annotation> getIterableFilterAnnotations(Annotation[] annotations) {
|
||||||
|
return Arrays.stream(annotations)
|
||||||
|
.filter(a -> a.annotationType().isAnnotationPresent(IterableFilter.class))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Optional<T> orElseSupply(T first, Supplier<T> onNullFirst) {
|
||||||
|
return first != null ? Optional.of(first) : Optional.ofNullable(onNullFirst.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void checkIsValidFilter(Class<? extends Annotation> annotationClass) {
|
||||||
|
if (!annotationClass.isAnnotationPresent(Filter.class)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"The given filter annotation " + annotationClass.getName() + " is itself not annotated with @Filter"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void checkIsValidIterableFilter(Class<? extends Annotation> annotationClass) {
|
||||||
|
if (!annotationClass.isAnnotationPresent(IterableFilter.class)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"The given iterable filter annotation " + annotationClass.getName() + " is itself not annotated with @IterableFilter"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @Nullable Annotation getScopeAnnotation(Class<?> clazz) {
|
||||||
|
final List<Annotation> scopeAnnotations = Arrays.stream(clazz.getAnnotations())
|
||||||
|
.filter(annotation -> annotation.annotationType().isAnnotationPresent(Scope.class))
|
||||||
|
.toList();
|
||||||
|
if (scopeAnnotations.size() > 1) {
|
||||||
|
throw new RuntimeException(clazz.getName() + " has too many annotations that are themselves annotated with @Scope");
|
||||||
|
}
|
||||||
|
return scopeAnnotations.size() == 1 ? scopeAnnotations.getFirst() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
8
util/di/src/main/java/groowt/util/di/ScopeHandler.java
Normal file
8
util/di/src/main/java/groowt/util/di/ScopeHandler.java
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
|
||||||
|
public interface ScopeHandler<A extends Annotation> {
|
||||||
|
<T> Binding<T> onScopedDependencyRequest(A annotation, Class<T> dependencyClass, RegistryObjectFactory objectFactory);
|
||||||
|
void reset();
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import jakarta.inject.Scope;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
|
||||||
|
public interface ScopeHandlerContainer {
|
||||||
|
|
||||||
|
static void checkIsValidScope(Class<? extends Annotation> scope) {
|
||||||
|
if (!scope.isAnnotationPresent(Scope.class)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"The given scope annotation " + scope + " is itself not annotated with @Scope"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<A extends Annotation> @Nullable ScopeHandler<A> getScopeHandler(Class<A> scopeType);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import jakarta.inject.Provider;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public class SimpleBindingConfigurator<T> implements BindingConfigurator<T> {
|
||||||
|
|
||||||
|
private final Class<T> from;
|
||||||
|
private @Nullable Binding<T> binding;
|
||||||
|
|
||||||
|
public SimpleBindingConfigurator(Class<T> from) {
|
||||||
|
this.from = from;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final Binding<T> getBinding() {
|
||||||
|
return this.binding != null
|
||||||
|
? this.binding
|
||||||
|
: new ClassBinding<>(this.from, this.from); // return SelfBinding in case we never called anything
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void to(Class<? extends T> target) {
|
||||||
|
this.binding = new ClassBinding<>(this.from, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toProvider(Provider<? extends T> provider) {
|
||||||
|
this.binding = new ProviderBinding<>(this.from, provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toSingleton(T target) {
|
||||||
|
this.binding = new SingletonBinding<>(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void toLazySingleton(Supplier<T> singletonSupplier) {
|
||||||
|
this.binding = new LazySingletonBinding<>(singletonSupplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
public record SimpleKeyHolder<B extends KeyBinder<K>, K, T>(Class<B> binderType, Class<T> type, K key)
|
||||||
|
implements KeyHolder<B, K, T> {}
|
@ -0,0 +1,3 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
public record SingletonBinding<T>(T to) implements Binding<T> {}
|
@ -0,0 +1,22 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import jakarta.inject.Singleton;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
|
||||||
|
public class SingletonRegistryExtension implements RegistryExtension, ScopeHandlerContainer {
|
||||||
|
|
||||||
|
private final SingletonScopeHandler handler;
|
||||||
|
|
||||||
|
public SingletonRegistryExtension(Registry owner) {
|
||||||
|
this.handler = new SingletonScopeHandler(owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public @Nullable <A extends Annotation> ScopeHandler<A> getScopeHandler(Class<A> scopeType) {
|
||||||
|
return Singleton.class.isAssignableFrom(scopeType) ? (ScopeHandler<A>) this.handler : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import jakarta.inject.Singleton;
|
||||||
|
|
||||||
|
import static groowt.util.di.BindingUtil.toSingleton;
|
||||||
|
|
||||||
|
public final class SingletonScopeHandler implements ScopeHandler<Singleton> {
|
||||||
|
|
||||||
|
private final Registry owner;
|
||||||
|
|
||||||
|
public SingletonScopeHandler(Registry owner) {
|
||||||
|
this.owner = owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> Binding<T> onScopedDependencyRequest(
|
||||||
|
Singleton annotation,
|
||||||
|
Class<T> dependencyClass,
|
||||||
|
RegistryObjectFactory objectFactory
|
||||||
|
) {
|
||||||
|
final Binding<T> potentialBinding = this.owner.getBinding(dependencyClass);
|
||||||
|
if (potentialBinding != null) {
|
||||||
|
return potentialBinding;
|
||||||
|
} else {
|
||||||
|
this.owner.bind(dependencyClass, toSingleton(objectFactory.createInstance(dependencyClass)));
|
||||||
|
return this.owner.getBinding(dependencyClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
throw new UnsupportedOperationException("Cannot reset the Singleton scope!");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
10
util/di/src/main/java/groowt/util/di/annotation/Given.java
Normal file
10
util/di/src/main/java/groowt/util/di/annotation/Given.java
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package groowt.util.di.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
@Target(ElementType.PARAMETER)
|
||||||
|
public @interface Given {}
|
10
util/di/src/main/java/groowt/util/di/filters/Filter.java
Normal file
10
util/di/src/main/java/groowt/util/di/filters/Filter.java
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package groowt.util.di.filters;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target({ ElementType.ANNOTATION_TYPE })
|
||||||
|
public @interface Filter {}
|
@ -0,0 +1,39 @@
|
|||||||
|
package groowt.util.di.filters;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.BiPredicate;
|
||||||
|
|
||||||
|
public interface FilterHandler<A extends Annotation, T> {
|
||||||
|
|
||||||
|
boolean check(A annotation, T arg);
|
||||||
|
Class<A> getAnnotationClass();
|
||||||
|
Class<T> getArgumentClass();
|
||||||
|
|
||||||
|
default FilterHandler<A, T> and(BiPredicate<A, T> and) {
|
||||||
|
Objects.requireNonNull(and);
|
||||||
|
return new SimpleFilterHandler<>(
|
||||||
|
(a, t) -> this.check(a, t) && and.test(a, t),
|
||||||
|
this.getAnnotationClass(),
|
||||||
|
this.getArgumentClass()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
default FilterHandler<A, T> or(BiPredicate<A, T> or) {
|
||||||
|
Objects.requireNonNull(or);
|
||||||
|
return new SimpleFilterHandler<>(
|
||||||
|
(a, t) -> this.check(a, t) || or.test(a, t),
|
||||||
|
this.getAnnotationClass(),
|
||||||
|
this.getArgumentClass()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
default FilterHandler<A, T> negate() {
|
||||||
|
return new SimpleFilterHandler<>(
|
||||||
|
(a, t) -> !this.check(a, t),
|
||||||
|
this.getAnnotationClass(),
|
||||||
|
this.getArgumentClass()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package groowt.util.di.filters;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
import java.util.function.BiPredicate;
|
||||||
|
|
||||||
|
import static groowt.util.di.filters.FilterUtil.isAssignableToAnyOf;
|
||||||
|
|
||||||
|
public final class FilterHandlers {
|
||||||
|
|
||||||
|
private FilterHandlers() {}
|
||||||
|
|
||||||
|
public static <A extends Annotation, T> FilterHandler<A, T> of(
|
||||||
|
Class<A> annotationClass,
|
||||||
|
Class<T> argClass,
|
||||||
|
BiPredicate<A, T> predicate
|
||||||
|
) {
|
||||||
|
return new SimpleFilterHandler<>(predicate, annotationClass, argClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Filter
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.PARAMETER)
|
||||||
|
public @interface AllowTypes {
|
||||||
|
Class<?>[] value();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> FilterHandler<AllowTypes, T> getAllowsTypesFilterHandler(Class<T> targetType) {
|
||||||
|
return of(
|
||||||
|
AllowTypes.class,
|
||||||
|
targetType,
|
||||||
|
(annotation, target) -> isAssignableToAnyOf(target.getClass(), annotation.value())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
16
util/di/src/main/java/groowt/util/di/filters/FilterUtil.java
Normal file
16
util/di/src/main/java/groowt/util/di/filters/FilterUtil.java
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package groowt.util.di.filters;
|
||||||
|
|
||||||
|
public final class FilterUtil {
|
||||||
|
|
||||||
|
private FilterUtil() {}
|
||||||
|
|
||||||
|
public static boolean isAssignableToAnyOf(Class<?> subject, Class<?>[] tests) {
|
||||||
|
for (final var test : tests) {
|
||||||
|
if (test.isAssignableFrom(subject)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package groowt.util.di.filters;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.ANNOTATION_TYPE)
|
||||||
|
public @interface IterableFilter {}
|
@ -0,0 +1,8 @@
|
|||||||
|
package groowt.util.di.filters;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
|
||||||
|
public interface IterableFilterHandler<A extends Annotation, E> {
|
||||||
|
boolean check(A annotation, Iterable<E> iterable);
|
||||||
|
Class<A> getAnnotationClass();
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package groowt.util.di.filters;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
import java.util.function.BiPredicate;
|
||||||
|
|
||||||
|
import static groowt.util.di.filters.FilterUtil.isAssignableToAnyOf;
|
||||||
|
|
||||||
|
public final class IterableFilterHandlers {
|
||||||
|
|
||||||
|
private IterableFilterHandlers() {}
|
||||||
|
|
||||||
|
public static <A extends Annotation, E> IterableFilterHandler<A, E> of(
|
||||||
|
Class<A> filterType,
|
||||||
|
BiPredicate<A, E> elementPredicate
|
||||||
|
) {
|
||||||
|
return new SimpleIterableFilterHandler<>(filterType, elementPredicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@IterableFilter
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.PARAMETER)
|
||||||
|
public @interface IterableElementTypes {
|
||||||
|
Class<?>[] value();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IterableFilterHandler<IterableElementTypes, Object> getIterableElementTypesFilterHandler() {
|
||||||
|
return of(
|
||||||
|
IterableElementTypes.class,
|
||||||
|
(annotation, element) -> isAssignableToAnyOf(element.getClass(), annotation.value())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package groowt.util.di.filters;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.util.function.BiPredicate;
|
||||||
|
|
||||||
|
final class SimpleFilterHandler<A extends Annotation, T> implements FilterHandler<A, T> {
|
||||||
|
|
||||||
|
private final BiPredicate<A, T> predicate;
|
||||||
|
private final Class<A> annotationClass;
|
||||||
|
private final Class<T> argClass;
|
||||||
|
|
||||||
|
public SimpleFilterHandler(BiPredicate<A, T> predicate, Class<A> annotationClass, Class<T> argClass) {
|
||||||
|
this.predicate = predicate;
|
||||||
|
this.annotationClass = annotationClass;
|
||||||
|
this.argClass = argClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean check(A annotation, T arg) {
|
||||||
|
return this.predicate.test(annotation, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<A> getAnnotationClass() {
|
||||||
|
return this.annotationClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<T> getArgumentClass() {
|
||||||
|
return this.argClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package groowt.util.di.filters;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.BiPredicate;
|
||||||
|
|
||||||
|
final class SimpleIterableFilterHandler<A extends Annotation, E> implements IterableFilterHandler<A, E> {
|
||||||
|
|
||||||
|
private final Class<A> annotationClass;
|
||||||
|
private final BiPredicate<A, E> elementPredicate;
|
||||||
|
|
||||||
|
public SimpleIterableFilterHandler(Class<A> annotationClass, BiPredicate<A, E> elementPredicate) {
|
||||||
|
this.annotationClass = annotationClass;
|
||||||
|
this.elementPredicate = elementPredicate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean check(A annotation, Iterable<E> iterable) {
|
||||||
|
for (final var e : Objects.requireNonNull(iterable)) {
|
||||||
|
if (!this.elementPredicate.test(annotation, e)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<A> getAnnotationClass() {
|
||||||
|
return this.annotationClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,170 @@
|
|||||||
|
package groowt.util.di;
|
||||||
|
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.inject.Named;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static groowt.util.di.BindingUtil.*;
|
||||||
|
import static groowt.util.di.NamedRegistryExtension.named;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
public class DefaultRegistryObjectFactoryTests {
|
||||||
|
|
||||||
|
public interface Greeter {
|
||||||
|
String greet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class DefaultGreeter implements Greeter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String greet() {
|
||||||
|
return "Hello, World!";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class GivenArgGreeter implements Greeter {
|
||||||
|
|
||||||
|
private final String greeting;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public GivenArgGreeter(String greeting) {
|
||||||
|
this.greeting = greeting;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String greet() {
|
||||||
|
return this.greeting;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class InjectedArgGreeter implements Greeter {
|
||||||
|
|
||||||
|
private final String greeting;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public InjectedArgGreeter(String greeting) {
|
||||||
|
this.greeting = greeting;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String greet() {
|
||||||
|
return this.greeting;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class InjectedNamedArgGreeter implements Greeter {
|
||||||
|
|
||||||
|
private final String greeting;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public InjectedNamedArgGreeter(@Named("greeting") String greeting) {
|
||||||
|
this.greeting = greeting;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String greet() {
|
||||||
|
return this.greeting;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class InjectedNamedSetterGreeter implements Greeter {
|
||||||
|
|
||||||
|
private String greeting;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public void setGreeting(@Named("greeting") String greeting) {
|
||||||
|
this.greeting = greeting;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String greet() {
|
||||||
|
return this.greeting;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void classSmokeScreen() {
|
||||||
|
final var b = DefaultRegistryObjectFactory.Builder.withDefaults();
|
||||||
|
b.configureRegistry(registry -> {
|
||||||
|
registry.bind(Greeter.class, bc -> bc.to(DefaultGreeter.class));
|
||||||
|
});
|
||||||
|
final RegistryObjectFactory container = b.build();
|
||||||
|
final Greeter greeter = container.get(Greeter.class);
|
||||||
|
assertEquals("Hello, World!", greeter.greet());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void singletonSmokeScreen() {
|
||||||
|
final var b = DefaultRegistryObjectFactory.Builder.withDefaults();
|
||||||
|
b.configureRegistry(registry -> {
|
||||||
|
registry.bind(Greeter.class, toSingleton(new DefaultGreeter()));
|
||||||
|
});
|
||||||
|
final RegistryObjectFactory container = b.build();
|
||||||
|
final Greeter greeter = container.get(Greeter.class);
|
||||||
|
assertEquals("Hello, World!", greeter.greet());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void providerSmokeScreen() {
|
||||||
|
final var b = DefaultRegistryObjectFactory.Builder.withDefaults();
|
||||||
|
b.configureRegistry(registry -> {
|
||||||
|
registry.bind(Greeter.class, toProvider(DefaultGreeter::new));
|
||||||
|
});
|
||||||
|
final RegistryObjectFactory container = b.build();
|
||||||
|
final Greeter greeter = container.get(Greeter.class);
|
||||||
|
assertEquals("Hello, World!", greeter.greet());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenArgSmokeScreen() {
|
||||||
|
final var b = DefaultRegistryObjectFactory.Builder.withDefaults();
|
||||||
|
b.configureRegistry(registry -> {
|
||||||
|
registry.bind(Greeter.class, bc -> bc.to(GivenArgGreeter.class));
|
||||||
|
});
|
||||||
|
final RegistryObjectFactory container = b.build();
|
||||||
|
final Greeter greeter = container.get(Greeter.class, "Hello, World!");
|
||||||
|
assertEquals("Hello, World!", greeter.greet());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void injectedArg() {
|
||||||
|
final var b = DefaultRegistryObjectFactory.Builder.withDefaults();
|
||||||
|
b.configureRegistry(registry -> {
|
||||||
|
registry.bind(Greeter.class, bc -> bc.to(InjectedArgGreeter.class));
|
||||||
|
registry.bind(String.class, toSingleton("Hello, World!"));
|
||||||
|
});
|
||||||
|
final RegistryObjectFactory container = b.build();
|
||||||
|
final Greeter greeter = container.get(Greeter.class);
|
||||||
|
assertEquals("Hello, World!", greeter.greet());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void injectedNamedArg() {
|
||||||
|
final var b = DefaultRegistryObjectFactory.Builder.withDefaults();
|
||||||
|
b.configureRegistry(registry -> {
|
||||||
|
registry.bind(Greeter.class, bc -> bc.to(InjectedNamedArgGreeter.class));
|
||||||
|
registry.bind(named("greeting", String.class), toSingleton("Hello, World!"));
|
||||||
|
});
|
||||||
|
final RegistryObjectFactory container = b.build();
|
||||||
|
final Greeter greeter = container.get(Greeter.class);
|
||||||
|
assertEquals("Hello, World!", greeter.greet());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void injectedSetter() {
|
||||||
|
final var b = DefaultRegistryObjectFactory.Builder.withDefaults();
|
||||||
|
b.configureRegistry(r -> {
|
||||||
|
r.bind(Greeter.class, toClass(InjectedNamedSetterGreeter.class));
|
||||||
|
r.bind(named("greeting", String.class), toSingleton("Hello, World!"));
|
||||||
|
});
|
||||||
|
final RegistryObjectFactory f = b.build();
|
||||||
|
final Greeter greeter = f.get(Greeter.class);
|
||||||
|
assertEquals("Hello, World!", greeter.greet());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
17
util/di/src/test/resources/log4j2.xml
Normal file
17
util/di/src/test/resources/log4j2.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<Configuration xmlns="http://logging.apache.org/log4j/2.0/config">
|
||||||
|
<Appenders>
|
||||||
|
<Console name="root">
|
||||||
|
<PatternLayout>
|
||||||
|
<LevelPatternSelector defaultPattern="[%t] %-5level %logger{1.} %msg%n">
|
||||||
|
<PatternMatch key="DEBUG" pattern="[%t] %-5level %logger{1.}.%M() %msg%n" />
|
||||||
|
</LevelPatternSelector>
|
||||||
|
</PatternLayout>
|
||||||
|
</Console>
|
||||||
|
</Appenders>
|
||||||
|
<Loggers>
|
||||||
|
<Root level="DEBUG">
|
||||||
|
<AppenderRef ref="root" />
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
</Configuration>
|
8
util/extensible/build.gradle
Normal file
8
util/extensible/build.gradle
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
plugins {
|
||||||
|
id 'GroowtConventions'
|
||||||
|
id 'java-library'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnlyApi libs.jetbrains.anotations
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
package groowt.util.extensible;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public abstract class AbstractExtensionContainer<E, F> implements ExtensionContainer<E, F> {
|
||||||
|
|
||||||
|
private final F extensionFactory;
|
||||||
|
private final Collection<E> extensions = new ArrayList<>();
|
||||||
|
|
||||||
|
public AbstractExtensionContainer(F extensionFactory) {
|
||||||
|
this.extensionFactory = extensionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A <strong>copy</strong> of the registered extensions.
|
||||||
|
*/
|
||||||
|
protected Collection<E> getRegisteredExtensions() {
|
||||||
|
return new ArrayList<>(this.extensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void registerExtension(E extension) {
|
||||||
|
this.extensions.add(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends E> @Nullable T findExtension(Class<T> extensionClass) {
|
||||||
|
return this.extensions.stream()
|
||||||
|
.filter(extensionClass::isInstance)
|
||||||
|
.findFirst()
|
||||||
|
.map(extensionClass::cast)
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implNote While this {@link AbstractExtensionContainer} calls
|
||||||
|
* {@link #getExtension}, which will throw if there is no registered
|
||||||
|
* extension, this method may be overridden to not use {@link #getExtension}
|
||||||
|
* and instead implement custom handling logic to avoid throwing, etc.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public <T extends E> void configureExtension(Class<T> extensionClass, Consumer<? super T> configure) {
|
||||||
|
configure.accept(this.getExtension(extensionClass));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends E> T getExtension(Class<T> extensionClass) {
|
||||||
|
return this.extensions.stream()
|
||||||
|
.filter(extensionClass::isInstance)
|
||||||
|
.findFirst()
|
||||||
|
.map(extensionClass::cast)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("There is no registered extension for " + extensionClass.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasExtension(Class<? extends E> extensionClass) {
|
||||||
|
return this.extensions.stream().anyMatch(extensionClass::isInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public F getExtensionFactory() {
|
||||||
|
return this.extensionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package groowt.util.extensible;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
// TODO: groovy methods to handle getting extensions via property accessors
|
||||||
|
public interface Extensible<E, F, C extends ExtensionContainer<E, F>> extends ExtensionAware<E> {
|
||||||
|
<T extends E> T createExtension(Class<T> extensionClass, Object... constructorArgs);
|
||||||
|
C getExtensionContainer();
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package groowt.util.extensible;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public interface ExtensionAware<E> {
|
||||||
|
@Nullable <T extends E> T findExtension(Class<T> extensionClass);
|
||||||
|
<T extends E> void configureExtension(Class<T> extensionClass, Consumer<? super T> configure);
|
||||||
|
<T extends E> T getExtension(Class<T> extensionClass);
|
||||||
|
boolean hasExtension(Class<? extends E> extensionClass);
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package groowt.util.extensible;
|
||||||
|
|
||||||
|
public interface ExtensionContainer<E, F> extends ExtensionAware<E> {
|
||||||
|
F getExtensionFactory();
|
||||||
|
}
|
35
view-components/build.gradle
Normal file
35
view-components/build.gradle
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
id 'java-library'
|
||||||
|
id 'groovy'
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api libs.groovy, libs.groovy.templates, libs.jetbrains.anotations, project(':views')
|
||||||
|
implementation libs.slf4j.api
|
||||||
|
|
||||||
|
testImplementation libs.junit.jupiter.api
|
||||||
|
testRuntimeOnly libs.log4j.slf4jBinding, libs.log4j.core
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
testLogging.showStandardStreams = true
|
||||||
|
}
|
||||||
|
|
||||||
|
testing {
|
||||||
|
suites {
|
||||||
|
test {
|
||||||
|
useJUnitJupiter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
349
view-components/spec.md
Normal file
349
view-components/spec.md
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
# View Components Spec
|
||||||
|
|
||||||
|
The View Components work roughly as follows.
|
||||||
|
|
||||||
|
First, one defines a component by at a minimum implementing the `ViewComponent` interface. Additionally, for ease of use, one can extend one of the built-in helpers, such as `AbstractViewComponent` or `GStringTemplateViewComponent`. For example, here might be a web-style component:
|
||||||
|
```groovy
|
||||||
|
class MyComponent extends GStringTemplateViewComponent {
|
||||||
|
String greeting
|
||||||
|
|
||||||
|
MyComponent(Map<String, Object> attr) {
|
||||||
|
super(new File('someTemplate.gst')) // TODO: figure out what args this actually takes
|
||||||
|
greeting = atr.greeting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Its associated template could be as simple as:
|
||||||
|
```text
|
||||||
|
Hello. Here is a friendly greeting: ${greeting}.
|
||||||
|
```
|
||||||
|
|
||||||
|
However, it could be something more complex such as a JSON document:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"greeting": "<%= greeting %>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Extending this last example, say our `MyComponent` class had this method:
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
def getGreetingInQuotes() {
|
||||||
|
'"' + greeting + '"'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then we could simply do in our JSON document:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"greeting": $greetingInQuotes // or <%= greetingInQuotes %>, whichever you prefer
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And so on.
|
||||||
|
|
||||||
|
Now we have to find a way to invoke our component. We could of course do so programmatically, like:
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
def myComponent = new MyComponent(greeting: 'hello from everywhere!')
|
||||||
|
def rendered = myComponent.render()
|
||||||
|
assert rendered == 'Hello. Here is a friendly greeting: hello from everywhere!'
|
||||||
|
```
|
||||||
|
|
||||||
|
However, this is basically the same as using `GStringTemplateView` (from the `Views` module). For the real power of the View Components, let's see how we can call them from other templates and components.
|
||||||
|
|
||||||
|
## Using Web Components
|
||||||
|
|
||||||
|
Continuing with the `MyComponent` from above, let's create our basic template which will include a rendering of the component. It's as simple as this, say in a file called `myComponentPage.gst`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
<MyComponent greeting="Hello, World!" />
|
||||||
|
```
|
||||||
|
|
||||||
|
To render this template, we need to do the following:
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
import MyComponent
|
||||||
|
|
||||||
|
def pageViewComponent = new PageViewComponent(new File('myComponentPage.gst')).configure {
|
||||||
|
context {
|
||||||
|
registry {
|
||||||
|
rootScope {
|
||||||
|
addWithMapArg(MyComponent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def rendered = pageViewComponent.render()
|
||||||
|
assert rendered == 'Hello. Here is a friendly greeting: Hello, World!'
|
||||||
|
```
|
||||||
|
|
||||||
|
Internally, the `PageComponent` will compile its template to something like the following Groovy script:
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
import static com.jessebrault.groowt.view.component.runtime.Helpers.*
|
||||||
|
|
||||||
|
def getScript() {
|
||||||
|
return { Writer __writer0 ->
|
||||||
|
resolveOrThrow(context, 'MyComponent')([greeting: 'Hello, World!']).renderTo(__writer0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The script above has the single method `getScript()` which returns a single closure. The `PageComponent` class will call this method, receive the closure, set itself as the closure's delegate, and call it with a `Writer`.
|
||||||
|
|
||||||
|
## More Advanced Web Component Example
|
||||||
|
|
||||||
|
Here is quite a broad sketch of how a set of HTML form components might work.
|
||||||
|
|
||||||
|
ViewComponent Groovy classes:
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
// This trait should be included in the HTML lib
|
||||||
|
trait HTMLComponent {
|
||||||
|
String orElseEmpty(boolean cond, Closure lazyOnTrue, Closure<String> format) {
|
||||||
|
cond ? format(lazyOnTrue()) : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
String orElseEmpty(boolean cond, String onTrue) {
|
||||||
|
cond ? onTrue : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
String orElseEmpty(boolean cond, Closure<String> onTrue) {
|
||||||
|
cond ? onTrue() : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
String inQuotes(Object value) {
|
||||||
|
/"$value"/
|
||||||
|
}
|
||||||
|
|
||||||
|
String attr(String name, Object value) {
|
||||||
|
/$name="$value"/
|
||||||
|
}
|
||||||
|
|
||||||
|
String attr(Closure cl) {
|
||||||
|
new RenderAttrClosure(this, cl)() // RenderAttrClosure would be in the lib
|
||||||
|
}
|
||||||
|
|
||||||
|
String joinAttr(Map<String, Object> attr) {
|
||||||
|
orElseEmpty(!attr.isEmpty) {
|
||||||
|
attr.collect(this.&attr).join(' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String tag(String tagName, Map<String, Object> attr, Object inner) {
|
||||||
|
"<$tagName ${joinAttr(attr)}>$inner</$tagName>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our enhanced form class
|
||||||
|
class FormWithModel extends GStringTemplateViewComponent implements HTMLComponent {
|
||||||
|
final String id
|
||||||
|
final Object model
|
||||||
|
final String action
|
||||||
|
final boolean wrap
|
||||||
|
final Map<String, Object> customAttr
|
||||||
|
|
||||||
|
private final Closure children
|
||||||
|
|
||||||
|
FormWithModel(Map<String, Object> attr, Closure children) {
|
||||||
|
super(new File('formWithModel.gst'))
|
||||||
|
this.id = id
|
||||||
|
this.model = attr.model
|
||||||
|
this.action = attr.action
|
||||||
|
this.wrap = attr.wrap != null ? attr.wrap : false
|
||||||
|
this.children = children
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Closure getChildren() {
|
||||||
|
this.children
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ComponentRegistry.Scope getChildrenScope() {
|
||||||
|
new SimpleScope().tap {
|
||||||
|
addWithContextAndMapArgs(Input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasModelProperty(String name) {
|
||||||
|
this.model.metaClass.properties.find { it.name == name } != null
|
||||||
|
}
|
||||||
|
|
||||||
|
Object getModelProperty(String name) {
|
||||||
|
this.model.getProperty(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Input extends GStringTemplateViewComponent implements HTMLComponent {
|
||||||
|
|
||||||
|
private static String getDefaultType(Object model, String name) {
|
||||||
|
def value = model.getProperty(name)
|
||||||
|
return switch (value) {
|
||||||
|
case String -> 'text'
|
||||||
|
default -> throw new UnsupportedOperationException('String model properties not supported yet')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String name
|
||||||
|
final String type
|
||||||
|
final boolean wrap
|
||||||
|
final boolean putLabel
|
||||||
|
final String label
|
||||||
|
|
||||||
|
private final Map<String, Object> customAttr
|
||||||
|
|
||||||
|
Input(ComponentContext context, Map<String, Object> attr) {
|
||||||
|
super(new File('input.gst'))
|
||||||
|
this.context = context
|
||||||
|
this.name = requireNonNull(attr.name)
|
||||||
|
this.type = attr.type ? attr.type : getDefaultType(this.getForm().model, this.name)
|
||||||
|
this.wrap = attr.wrap != null ? attr.wrap : this.getForm().wrap
|
||||||
|
this.putLabel = attr.putLabel != null ? attr.putLabel : true
|
||||||
|
this.label = attr.label != null ? attr.label : ''
|
||||||
|
this.customAttr = attr.findAll { keyHolder, value -> value != null && !(keyHolder in ['name', 'wrap']) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private FormWithModel getForm() {
|
||||||
|
def form = this.context.findNearestAncestorByClass(FormWithModel)
|
||||||
|
if (form == null) {
|
||||||
|
throw new ComponentException("An Input can only be used inside a FormWithModel")
|
||||||
|
} else {
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasValue() {
|
||||||
|
this.form.hasModelProperty(this.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
Object getValue() {
|
||||||
|
this.form.getModelProperty(this.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should be in the HTML lib
|
||||||
|
class SurroundIf extends GStringTemplateViewComponent implements HTMLComponent {
|
||||||
|
|
||||||
|
static Closure<Tuple> tag(String name, Map<String, Object> attr) {
|
||||||
|
return { SurroundIf self ->
|
||||||
|
Tuple.of("<$name ${self.joinAttr(attr)}>", "</$name>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String template = '''
|
||||||
|
<On cond={condition}>
|
||||||
|
<%= tag[0] %>
|
||||||
|
<%= renderChildren() %>
|
||||||
|
<%= tag[1] %>
|
||||||
|
</On>
|
||||||
|
'''
|
||||||
|
|
||||||
|
final boolean condition
|
||||||
|
final Tuple tag
|
||||||
|
|
||||||
|
private final Closure children
|
||||||
|
|
||||||
|
SurroundIf(Map<String, Object> attr, Closure children) {
|
||||||
|
super(template.trim())
|
||||||
|
this.condition = attr.condition
|
||||||
|
switch (attr.tag) {
|
||||||
|
case null -> { this.tag = Tuple.of('', '') }
|
||||||
|
case Closure -> { this.tag = attr.tag.call(this) }
|
||||||
|
case String -> { this.tag = Tuple.of("<$attr.tag>", "</$attr.tag>") }
|
||||||
|
default -> { throw new IllegalArgumentException() }
|
||||||
|
}
|
||||||
|
this.children = children
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Closure getChildren() {
|
||||||
|
this.children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And our template files:
|
||||||
|
|
||||||
|
```text
|
||||||
|
// formWithModel.gst
|
||||||
|
<form (id, name)>
|
||||||
|
${ renderChildren() }
|
||||||
|
</form>
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
// input.gst
|
||||||
|
|
||||||
|
<SurroundOn cond={wrap} tag={<div class="form-control">${ renderChildren() }</div>}>
|
||||||
|
<On cond={putLabel} out={<label for={ name.capitalize() }>$label</label>} />
|
||||||
|
<Switch expr={type}>
|
||||||
|
<When is="text">
|
||||||
|
<input (name, type, *customAttr) value={ hasValue() ? value : null } />
|
||||||
|
</When>
|
||||||
|
<When is="textarea">
|
||||||
|
<textarea (name, *customAttr)>
|
||||||
|
<On cond={ hasValue() } out={value} />
|
||||||
|
</textarea>
|
||||||
|
</When>
|
||||||
|
<Default do={ throw new UnsupportedOperationException() } />
|
||||||
|
</Switch>
|
||||||
|
</SurroundOn>
|
||||||
|
```
|
||||||
|
|
||||||
|
Our basic model:
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
class MessageModel {
|
||||||
|
String from
|
||||||
|
String to
|
||||||
|
String message
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now here is our target page:
|
||||||
|
|
||||||
|
```text
|
||||||
|
// target.gst
|
||||||
|
<FormWithModel id="message_form" model={message} action="/sendMessage" wrap={true}>
|
||||||
|
<Input name="from" />
|
||||||
|
<Input name="to" putLabel={false} />
|
||||||
|
<Input name="message" type="textarea" wrap={false} label="Write your message here: " />
|
||||||
|
</FormWithModel>
|
||||||
|
```
|
||||||
|
|
||||||
|
Now let's render it:
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
def message = new MessageModel(from: 'Jesse', to: 'Jeanna', message: 'Hello, World!')
|
||||||
|
def root = new HTMLRootViewComponent(new File('target.gst')).configure {
|
||||||
|
context {
|
||||||
|
registry {
|
||||||
|
rootScope {
|
||||||
|
addWithMapAndChildrenArgs(SurroundIf, FormWithModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root.message = message
|
||||||
|
def rendered = root.render()
|
||||||
|
assert rendered == '''
|
||||||
|
<form id="message_form" action="/sendMessage">
|
||||||
|
<div class="form-control">
|
||||||
|
<label for="from">From</label>
|
||||||
|
<input name="from" type="text" value="Jesse" />
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<input name="to" type="text" value="Jeanna" />
|
||||||
|
</div>
|
||||||
|
<label for"name">Write your message here:</label>
|
||||||
|
<textarea name="message">Hello, world!</textarea>
|
||||||
|
</form>
|
||||||
|
'''.trim() // may not be slightly correct with indentation, but close enough
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
import groovy.lang.GroovyObjectSupport;
|
||||||
|
import groovy.lang.MetaMethod;
|
||||||
|
import groovy.lang.MissingMethodException;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public abstract class AbstractComponentFactory<T extends ViewComponent> extends GroovyObjectSupport
|
||||||
|
implements ComponentFactory<T> {
|
||||||
|
|
||||||
|
private static final String DO_CREATE = "doCreate";
|
||||||
|
private static final Class<?>[] EMPTY_CLASSES = new Class[0];
|
||||||
|
|
||||||
|
private static Object[] flatten(Object... args) {
|
||||||
|
if (args.length == 0) {
|
||||||
|
return args;
|
||||||
|
} else {
|
||||||
|
final List<Object> result = new ArrayList<>(args.length);
|
||||||
|
for (final var arg : args) {
|
||||||
|
if (arg instanceof Object[] arr) {
|
||||||
|
result.addAll(Arrays.asList(arr));
|
||||||
|
} else {
|
||||||
|
result.add(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toArray(Object[]::new);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Class<?>[] asTypes(Object[] args) {
|
||||||
|
if (args.length == 0) {
|
||||||
|
return EMPTY_CLASSES;
|
||||||
|
}
|
||||||
|
final Class<?>[] result = new Class[args.length];
|
||||||
|
for (int i = 0; i < args.length; i++) {
|
||||||
|
result[i] = args[i].getClass();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Map<Class<?>[], MetaMethod> cache = new HashMap<>();
|
||||||
|
|
||||||
|
private MetaMethod findDoCreateMethod(Object[] allArgs) {
|
||||||
|
return this.cache.computeIfAbsent(asTypes(allArgs), types ->
|
||||||
|
this.getMetaClass().getMetaMethod(DO_CREATE, types)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private T findAndDoCreate(ComponentContext componentContext, Object[] args) {
|
||||||
|
final Object[] contextsAndArgs = flatten(componentContext, args);
|
||||||
|
final MetaMethod contextsAndArgsMethod = this.findDoCreateMethod(contextsAndArgs);
|
||||||
|
if (contextsAndArgsMethod != null) {
|
||||||
|
return (T) contextsAndArgsMethod.invoke(this, contextsAndArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Object[] contextOnly = new Object[] { componentContext };
|
||||||
|
final MetaMethod contextOnlyMethod = this.findDoCreateMethod(contextOnly);
|
||||||
|
if (contextOnlyMethod != null) {
|
||||||
|
return (T) contextOnlyMethod.invoke(this, contextOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
final MetaMethod argsOnlyMethod = this.findDoCreateMethod(args);
|
||||||
|
if (argsOnlyMethod != null) {
|
||||||
|
return (T) argsOnlyMethod.invoke(this, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new MissingMethodException(
|
||||||
|
DO_CREATE,
|
||||||
|
this.getClass(),
|
||||||
|
args
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T create(ComponentContext componentContext, Object... args) {
|
||||||
|
return this.findAndDoCreate(componentContext, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
import groovy.lang.Closure;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public abstract class AbstractViewComponent implements ViewComponent {
|
||||||
|
|
||||||
|
private ComponentTemplate template;
|
||||||
|
|
||||||
|
public AbstractViewComponent() {}
|
||||||
|
|
||||||
|
public AbstractViewComponent(ComponentTemplate template) {
|
||||||
|
this.template = Objects.requireNonNull(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AbstractViewComponent(Class<? extends ComponentTemplate> templateClass) {
|
||||||
|
try {
|
||||||
|
this.template = templateClass.getConstructor().newInstance();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ComponentTemplate getTemplate() {
|
||||||
|
return Objects.requireNonNull(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setTemplate(ComponentTemplate template) {
|
||||||
|
this.template = Objects.requireNonNull(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void beforeRender() {}
|
||||||
|
|
||||||
|
protected void afterRender() {
|
||||||
|
this.getContext().afterComponent(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void renderTo(Writer out) throws IOException {
|
||||||
|
final Closure<?> closure = this.template.getRenderer();
|
||||||
|
closure.setDelegate(this);
|
||||||
|
closure.setResolveStrategy(Closure.DELEGATE_FIRST);
|
||||||
|
this.beforeRender();
|
||||||
|
closure.call(this.getContext(), out);
|
||||||
|
this.afterRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public abstract class CachingComponentTemplateCompiler implements ComponentTemplateCompiler {
|
||||||
|
|
||||||
|
private final Map<Class<? extends ViewComponent>, ComponentTemplate> cache = new HashMap<>();
|
||||||
|
|
||||||
|
protected final void putInCache(Class<? extends ViewComponent> forClass, ComponentTemplate template) {
|
||||||
|
this.cache.put(forClass, template);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final ComponentTemplate getFromCache(Class<? extends ViewComponent> forClass) {
|
||||||
|
return Objects.requireNonNull(this.cache.get(forClass));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final ComponentTemplate getFromCacheOrElse(
|
||||||
|
Class<? extends ViewComponent> forClass,
|
||||||
|
Supplier<? extends ComponentTemplate> onEmpty
|
||||||
|
) {
|
||||||
|
return this.cache.computeIfAbsent(forClass, ignored -> onEmpty.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
public interface ComponentContext {
|
||||||
|
|
||||||
|
@ApiStatus.Internal
|
||||||
|
ComponentFactory<?> resolve(String component);
|
||||||
|
|
||||||
|
@ApiStatus.Internal
|
||||||
|
ViewComponent create(ComponentFactory<?> factory, Object... args);
|
||||||
|
|
||||||
|
@ApiStatus.Internal
|
||||||
|
void afterComponent(ViewComponent component);
|
||||||
|
|
||||||
|
Deque<ComponentScope> getScopeStack();
|
||||||
|
|
||||||
|
void pushScope(ComponentScope scope);
|
||||||
|
void pushDefaultScope();
|
||||||
|
void popScope();
|
||||||
|
|
||||||
|
default ComponentScope getCurrentScope() {
|
||||||
|
return Objects.requireNonNull(this.getScopeStack().peek(), "There is no current scope.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Deque<ViewComponent> getComponentStack();
|
||||||
|
|
||||||
|
@Nullable ViewComponent getParent();
|
||||||
|
@Nullable <T extends ViewComponent> T getParent(Class<T> parentClass);
|
||||||
|
|
||||||
|
@Nullable ViewComponent findNearestAncestor(Predicate<? super ViewComponent> matching);
|
||||||
|
|
||||||
|
default <T extends ViewComponent> @Nullable T findNearestAncestor(
|
||||||
|
Class<T> ancestorClass,
|
||||||
|
Predicate<? super ViewComponent> matching
|
||||||
|
) {
|
||||||
|
return ancestorClass.cast(matching.and(ancestorClass::isInstance));
|
||||||
|
}
|
||||||
|
|
||||||
|
default @Nullable ViewComponent findNearestAncestorByTypeName(String typeName) {
|
||||||
|
return this.findNearestAncestor(component -> component.getTypeName().equals(typeName));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ViewComponent> getAllAncestors();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
public class ComponentCreateException extends RuntimeException {
|
||||||
|
|
||||||
|
private final ComponentTemplate template;
|
||||||
|
private final int line;
|
||||||
|
private final int column;
|
||||||
|
|
||||||
|
public ComponentCreateException(ComponentTemplate template, int line, int column, Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
this.template = template;
|
||||||
|
this.line = line;
|
||||||
|
this.column = column;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
return "Exception while rendering " + this.template.getClass()
|
||||||
|
+ " at line " + this.line + ", column " + this.column + ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
import groovy.lang.Closure;
|
||||||
|
import groovy.lang.GroovyObject;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public interface ComponentFactory<T extends ViewComponent> extends GroovyObject {
|
||||||
|
|
||||||
|
static <T extends ViewComponent> ComponentFactory<T> of(Closure<T> closure) {
|
||||||
|
return new DelegatingComponentFactory<>((context, args) -> closure.call(context, args));
|
||||||
|
}
|
||||||
|
|
||||||
|
static <T extends ViewComponent> ComponentFactory<T> of(Supplier<T> supplier) {
|
||||||
|
return new DelegatingComponentFactory<>((ignored0, ignored1) -> supplier.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
T create(ComponentContext componentContext, Object... args);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
public class ComponentRenderException extends RuntimeException {
|
||||||
|
|
||||||
|
private static String formatMessage(int line, int column) {
|
||||||
|
return "Exception while rendering at line " + line + ", column " + column + ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatMessage(ViewComponent viewComponent) {
|
||||||
|
return "Exception while rendering " + viewComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatMessage(ViewComponent viewComponent, int line, int column) {
|
||||||
|
return "Exception while rendering " + viewComponent + " at line " + line + ", column " + column + ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComponentRenderException() {}
|
||||||
|
|
||||||
|
public ComponentRenderException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComponentRenderException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComponentRenderException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComponentRenderException(int line, int column, Throwable cause) {
|
||||||
|
super(formatMessage(line, column), cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComponentRenderException(ViewComponent component, Throwable cause) {
|
||||||
|
super(formatMessage(component), cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComponentRenderException(ViewComponent component, int line, int column, Throwable cause) {
|
||||||
|
super(formatMessage(component, line, column), cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
public interface ComponentScope {
|
||||||
|
|
||||||
|
void add(String name, ComponentFactory<?> factory);
|
||||||
|
boolean contains(String name);
|
||||||
|
void remove(String name);
|
||||||
|
ComponentFactory<?> get(String name);
|
||||||
|
|
||||||
|
default ComponentFactory<?> factoryMissing(String typeName) {
|
||||||
|
throw new NoFactoryMissingException(this.getClass().getName() + " does not support factoryMissing()");
|
||||||
|
}
|
||||||
|
|
||||||
|
default <T extends ViewComponent> void add(Class<T> clazz, ComponentFactory<T> factory) {
|
||||||
|
this.add(clazz.getName(), factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean contains(Class<? extends ViewComponent> clazz) {
|
||||||
|
return this.contains(clazz.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
default <T extends ViewComponent> ComponentFactory<T> get(Class<T> clazz) {
|
||||||
|
return (ComponentFactory<T>) this.get(clazz.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
default <T extends ViewComponent> ComponentFactory<T> getAs(String name, Class<T> viewComponentType) {
|
||||||
|
return (ComponentFactory<T>) this.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
default void remove(Class<? extends ViewComponent> clazz) {
|
||||||
|
this.remove(clazz.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
default <T extends ViewComponent> ComponentFactory<T> factoryMissing(Class<T> clazz) {
|
||||||
|
return (ComponentFactory<T>) this.factoryMissing(clazz.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
import groovy.lang.Closure;
|
||||||
|
|
||||||
|
public interface ComponentTemplate {
|
||||||
|
Closure<?> getRenderer();
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
public interface ComponentTemplateCompiler {
|
||||||
|
ComponentTemplate compile(Class<? extends ViewComponent> forClass, File templateFile);
|
||||||
|
ComponentTemplate compile(Class<? extends ViewComponent> forClass, String template);
|
||||||
|
ComponentTemplate compile(Class<? extends ViewComponent> forClass, URI templateURI);
|
||||||
|
ComponentTemplate compile(Class<? extends ViewComponent> forClass, URL templateURL);
|
||||||
|
ComponentTemplate compile(Class<? extends ViewComponent> forClass, InputStream inputStream);
|
||||||
|
ComponentTemplate compile(Class<? extends ViewComponent> forClass, Reader reader);
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
import java.io.Reader;
|
||||||
|
|
||||||
|
public class ComponentTemplateCreateException extends RuntimeException {
|
||||||
|
|
||||||
|
private final Class<? extends ViewComponent> forClass;
|
||||||
|
private final Object templateSource;
|
||||||
|
|
||||||
|
public ComponentTemplateCreateException(
|
||||||
|
String message,
|
||||||
|
Class<? extends ViewComponent> forClass,
|
||||||
|
Object templateSource
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.forClass = forClass;
|
||||||
|
this.templateSource = templateSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComponentTemplateCreateException(
|
||||||
|
String message,
|
||||||
|
Throwable cause,
|
||||||
|
Class<? extends ViewComponent> forClass,
|
||||||
|
Object templateSource
|
||||||
|
) {
|
||||||
|
super(message, cause);
|
||||||
|
this.forClass = forClass;
|
||||||
|
this.templateSource = templateSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComponentTemplateCreateException(
|
||||||
|
Throwable cause,
|
||||||
|
Class<? extends ViewComponent> forClass,
|
||||||
|
Object templateSource
|
||||||
|
) {
|
||||||
|
super(cause);
|
||||||
|
this.forClass = forClass;
|
||||||
|
this.templateSource = templateSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Class<? extends ViewComponent> getForClass() {
|
||||||
|
return this.forClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getTemplateSource() {
|
||||||
|
return this.templateSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,144 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
public class DefaultComponentContext implements ComponentContext {
|
||||||
|
|
||||||
|
private final Deque<ComponentScope> scopeStack = new LinkedList<>();
|
||||||
|
private final Deque<ViewComponent> componentStack = new LinkedList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ComponentFactory<?> resolve(String component) {
|
||||||
|
if (scopeStack.isEmpty()) {
|
||||||
|
throw new IllegalStateException("There are no scopes on the scopeStack.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final var getStack = new LinkedList<>(this.scopeStack);
|
||||||
|
while (!getStack.isEmpty()) {
|
||||||
|
final ComponentScope scope = getStack.pop();
|
||||||
|
if (scope.contains(component)) {
|
||||||
|
return scope.get(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final var missingStack = new LinkedList<>(this.scopeStack);
|
||||||
|
NoFactoryMissingException first = null;
|
||||||
|
while (!missingStack.isEmpty()) {
|
||||||
|
final ComponentScope scope = getStack.pop();
|
||||||
|
try {
|
||||||
|
return scope.factoryMissing(component);
|
||||||
|
} catch (NoFactoryMissingException e) {
|
||||||
|
if (first == null) {
|
||||||
|
first = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first == null) {
|
||||||
|
throw new IllegalStateException("First FactoryMissingException is still null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw first;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ViewComponent create(ComponentFactory<?> factory, Object... args) {
|
||||||
|
final ViewComponent component = factory.create(this, args);
|
||||||
|
this.componentStack.push(component);
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterComponent(ViewComponent component) {
|
||||||
|
final var popped = this.componentStack.pop();
|
||||||
|
if (!popped.equals(component)) {
|
||||||
|
throw new IllegalStateException("Popped component does not equal arg to afterComponent()");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Deque<ComponentScope> getScopeStack() {
|
||||||
|
return new LinkedList<>(this.scopeStack);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void pushScope(ComponentScope scope) {
|
||||||
|
this.scopeStack.push(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ComponentScope getNewDefaultScope() {
|
||||||
|
return new DefaultComponentScope();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void pushDefaultScope() {
|
||||||
|
this.pushScope(this.getNewDefaultScope());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void popScope() {
|
||||||
|
this.scopeStack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Deque<ViewComponent> getComponentStack() {
|
||||||
|
return new LinkedList<>(this.componentStack);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable ViewComponent getParent() {
|
||||||
|
if (this.componentStack.size() > 1) {
|
||||||
|
final var child = this.componentStack.pop();
|
||||||
|
final var parent = this.componentStack.pop();
|
||||||
|
this.componentStack.push(parent);
|
||||||
|
this.componentStack.push(child);
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends ViewComponent> @Nullable T getParent(Class<T> parentClass) {
|
||||||
|
return parentClass.cast(this.getParent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable ViewComponent findNearestAncestor(Predicate<? super ViewComponent> matching) {
|
||||||
|
if (this.componentStack.size() > 1) {
|
||||||
|
final Deque<ViewComponent> tmp = new LinkedList<>();
|
||||||
|
tmp.push(this.componentStack.pop()); // child
|
||||||
|
ViewComponent result = null;
|
||||||
|
while (result == null && !this.componentStack.isEmpty()) {
|
||||||
|
final var ancestor = this.componentStack.pop();
|
||||||
|
tmp.push(ancestor);
|
||||||
|
if (matching.test(ancestor)) {
|
||||||
|
result = ancestor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (!tmp.isEmpty()) {
|
||||||
|
this.componentStack.push(tmp.pop());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ViewComponent> getAllAncestors() {
|
||||||
|
if (this.componentStack.size() > 1) {
|
||||||
|
final var child = this.componentStack.pop();
|
||||||
|
final List<ViewComponent> result = new ArrayList<>(this.componentStack);
|
||||||
|
this.componentStack.push(child);
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class DefaultComponentScope implements ComponentScope {
|
||||||
|
|
||||||
|
private final Map<String, ComponentFactory<?>> factories = new HashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void add(String name, ComponentFactory<?> factory) {
|
||||||
|
this.factories.put(name, factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean contains(String name) {
|
||||||
|
return this.factories.containsKey(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(String name) {
|
||||||
|
this.factories.remove(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ComponentFactory<?> get(String name) {
|
||||||
|
return this.factories.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
final class DelegatingComponentFactory<T extends ViewComponent> extends AbstractComponentFactory<T> {
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface ComponentFactoryDelegate<T extends ViewComponent> {
|
||||||
|
T doCreate(ComponentContext context, Object... args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ComponentFactoryDelegate<T> function;
|
||||||
|
|
||||||
|
public DelegatingComponentFactory(ComponentFactoryDelegate<T> function) {
|
||||||
|
this.function = function;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T doCreate(ComponentContext componentContext, Object... args) {
|
||||||
|
return this.function.doCreate(componentContext, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
public class MissingClassTypeException extends MissingComponentException {
|
||||||
|
|
||||||
|
private final String typeName;
|
||||||
|
|
||||||
|
public MissingClassTypeException(ComponentTemplate template, String typeName, int line, int col, Throwable cause) {
|
||||||
|
super(template, cause, line, col);
|
||||||
|
this.typeName = typeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getMissingKeyName() {
|
||||||
|
return "class component " + this.typeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
public abstract class MissingComponentException extends RuntimeException {
|
||||||
|
|
||||||
|
private final ComponentTemplate template;
|
||||||
|
private final int line;
|
||||||
|
private final int col;
|
||||||
|
|
||||||
|
public MissingComponentException(ComponentTemplate template, Throwable cause, int line, int col) {
|
||||||
|
super(cause);
|
||||||
|
this.template = template;
|
||||||
|
this.line = line;
|
||||||
|
this.col = col;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract String getMissingKeyName();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
return "Missing " + this.getMissingKeyName() + " on line " + this.line + ", column " + this.col + ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
public class MissingFragmentTypeException extends MissingComponentException {
|
||||||
|
|
||||||
|
public MissingFragmentTypeException(ComponentTemplate template, int line, int col, Throwable cause) {
|
||||||
|
super(template, cause, line, col);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getMissingKeyName() {
|
||||||
|
return "fragment type";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
public abstract class MissingStringTypeException extends MissingComponentException {
|
||||||
|
|
||||||
|
private final String keyName;
|
||||||
|
|
||||||
|
public MissingStringTypeException(ComponentTemplate template, String keyName, int line, int col, Throwable cause) {
|
||||||
|
super(template, cause, line, col);
|
||||||
|
this.keyName = keyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getMissingKeyName() {
|
||||||
|
return "string-typed component " + this.keyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
public class NoFactoryMissingException extends UnsupportedOperationException {
|
||||||
|
|
||||||
|
public NoFactoryMissingException() {}
|
||||||
|
|
||||||
|
public NoFactoryMissingException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NoFactoryMissingException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NoFactoryMissingException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package groowt.view.component;
|
||||||
|
|
||||||
|
import groowt.view.View;
|
||||||
|
|
||||||
|
public interface ViewComponent extends View {
|
||||||
|
|
||||||
|
default String getTypeName() {
|
||||||
|
return this.getClass().getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setContext(ComponentContext context);
|
||||||
|
ComponentContext getContext();
|
||||||
|
|
||||||
|
}
|
35
views/build.gradle
Normal file
35
views/build.gradle
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
id 'java-library'
|
||||||
|
id 'groovy'
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api libs.groovy, libs.groovy.templates
|
||||||
|
implementation libs.slf4j.api
|
||||||
|
|
||||||
|
testImplementation libs.junit.jupiter.api
|
||||||
|
testRuntimeOnly libs.log4j.slf4jBinding, libs.log4j.core
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
testLogging.showStandardStreams = true
|
||||||
|
}
|
||||||
|
|
||||||
|
testing {
|
||||||
|
suites {
|
||||||
|
test {
|
||||||
|
useJUnitJupiter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
views/src/main/groovy/groowt/view/AbstractView.java
Normal file
34
views/src/main/groovy/groowt/view/AbstractView.java
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package groowt.view;
|
||||||
|
|
||||||
|
import groovy.lang.Closure;
|
||||||
|
import groovy.lang.GroovyObjectSupport;
|
||||||
|
import groovy.lang.MetaProperty;
|
||||||
|
import groovy.lang.Writable;
|
||||||
|
import org.codehaus.groovy.runtime.typehandling.GroovyCastException;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: create a Map implementation that can access the View for keys/values, or do a metaclass thing
|
||||||
|
* TODO: get rid of this and just move the asType stuff to GStringTemplateView
|
||||||
|
*/
|
||||||
|
public abstract class AbstractView extends GroovyObjectSupport implements View {
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T asType(Class<T> clazz) {
|
||||||
|
if (clazz.isAssignableFrom(this.getClass())) {
|
||||||
|
return (T) this;
|
||||||
|
} else if (clazz.equals(Writable.class)) {
|
||||||
|
return (T) this.asWritable();
|
||||||
|
} else if (clazz.equals(Closure.class)) {
|
||||||
|
return (T) this.asClosure();
|
||||||
|
} else if (clazz.equals(Map.class)) {
|
||||||
|
return (T) this.getMetaClass().getProperties().stream()
|
||||||
|
.collect(Collectors.toMap(MetaProperty::getName, metaProperty -> metaProperty.getProperty(this)));
|
||||||
|
} else {
|
||||||
|
throw new GroovyCastException(this, clazz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
196
views/src/main/groovy/groowt/view/GStringTemplateView.java
Normal file
196
views/src/main/groovy/groowt/view/GStringTemplateView.java
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
package groowt.view;
|
||||||
|
|
||||||
|
import groovy.lang.Closure;
|
||||||
|
import groovy.lang.Writable;
|
||||||
|
import groovy.text.GStringTemplateEngine;
|
||||||
|
import groovy.text.Template;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegates to self.
|
||||||
|
*/
|
||||||
|
public class GStringTemplateView extends AbstractView {
|
||||||
|
|
||||||
|
private final GStringTemplateEngine engine;
|
||||||
|
private final Object src;
|
||||||
|
private final Charset charset;
|
||||||
|
private final Template template;
|
||||||
|
|
||||||
|
private final ThreadLocal<Writer> currentWriter = new ThreadLocal<>();
|
||||||
|
private final ThreadLocal<Closure<CharSequence>> yieldClosure = new ThreadLocal<>();
|
||||||
|
|
||||||
|
public GStringTemplateView(Map<String, Object> args) {
|
||||||
|
this.engine = (GStringTemplateEngine) args.getOrDefault("engine", new GStringTemplateEngine());
|
||||||
|
if (!args.containsKey("src")) {
|
||||||
|
throw new IllegalArgumentException("args.src must not be null");
|
||||||
|
}
|
||||||
|
this.src = args.get("src");
|
||||||
|
this.charset = switch (args.get("charset")) {
|
||||||
|
case null -> null;
|
||||||
|
case Charset c -> c;
|
||||||
|
default -> throw new IllegalArgumentException("args.charset, if not null, must be an instance of Charset");
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
switch (this.src) {
|
||||||
|
case String s -> this.template = this.engine.createTemplate(s);
|
||||||
|
case File f -> {
|
||||||
|
if (this.charset != null) {
|
||||||
|
this.template = this.engine.createTemplate(f, this.charset);
|
||||||
|
} else {
|
||||||
|
this.template = this.engine.createTemplate(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Reader r -> this.template = this.engine.createTemplate(r);
|
||||||
|
case URL url -> {
|
||||||
|
if (this.charset != null) {
|
||||||
|
this.template = this.engine.createTemplate(url, this.charset);
|
||||||
|
} else {
|
||||||
|
this.template = this.engine.createTemplate(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> throw new IllegalArgumentException("args.src must be a String, File, Reader, or URL.");
|
||||||
|
}
|
||||||
|
} catch (IOException | ClassNotFoundException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void renderTo(Writer writer) throws IOException {
|
||||||
|
this.currentWriter.set(writer);
|
||||||
|
final Closure<?> closure = (Closure<?>) this.template.make();
|
||||||
|
closure.setDelegate(this);
|
||||||
|
closure.setResolveStrategy(Closure.DELEGATE_FIRST);
|
||||||
|
final Writable writable = (Writable) closure;
|
||||||
|
writable.writeTo(writer);
|
||||||
|
this.currentWriter.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String render(Closure<CharSequence> yieldClosure) {
|
||||||
|
this.yieldClosure.set(yieldClosure);
|
||||||
|
final String result = this.render();
|
||||||
|
this.yieldClosure.remove();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void renderTo(Writer writer, Closure<CharSequence> yieldClosure) throws IOException {
|
||||||
|
this.yieldClosure.set(yieldClosure);
|
||||||
|
this.renderTo(writer);
|
||||||
|
this.yieldClosure.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CharSequence yield(Object... args) {
|
||||||
|
final Closure<?> yieldClosure = this.yieldClosure.get();
|
||||||
|
if (yieldClosure == null) {
|
||||||
|
throw new IllegalStateException("Cannot yield in a GStringTemplateView without passing a Closure to render() or renderTo().");
|
||||||
|
}
|
||||||
|
final Class<?>[] paramTypes = yieldClosure.getParameterTypes();
|
||||||
|
|
||||||
|
// if Writer is first param, pass that
|
||||||
|
if (paramTypes.length > 0) {
|
||||||
|
final Class<?> firstParamType = paramTypes[0];
|
||||||
|
if (Writer.class.isAssignableFrom(firstParamType)) {
|
||||||
|
yieldClosure.call(this.currentWriter.get(), args);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// else just give whatever was passed to yield
|
||||||
|
final Object returned = yieldClosure.call(args);
|
||||||
|
if (returned instanceof CharSequence cs) {
|
||||||
|
return cs;
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("The yield Closure must return an instance of CharSequence or a subtype thereof; given: " + returned + " of type " + returned.getClass());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
public String partial(Object... args) {
|
||||||
|
if (args.length == 0) {
|
||||||
|
throw new IllegalArgumentException("Must provide at least 1 argument. Signature: partial(String | File | Reader | URL | Map, Map | Closure | [Map, Closure])");
|
||||||
|
}
|
||||||
|
|
||||||
|
final Object arg0 = args[0];
|
||||||
|
final Object arg1 = args.length >= 2 ? args[1] : null;
|
||||||
|
final Object arg2 = args.length >= 3 ? args[2] : null;
|
||||||
|
|
||||||
|
final Map<String, Object> createArgs = new HashMap<>();
|
||||||
|
|
||||||
|
switch (arg0) {
|
||||||
|
case String s -> createArgs.put("src", new File(s));
|
||||||
|
case File f -> createArgs.put("src", f);
|
||||||
|
case Reader r -> createArgs.put("src", r);
|
||||||
|
case URL url -> createArgs.put("src", url);
|
||||||
|
case Map m -> createArgs.putAll(m);
|
||||||
|
default -> throw new IllegalArgumentException("First argument must be any of String, File, Reader, URL, or Map.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createArgs.containsKey("engine")) {
|
||||||
|
createArgs.put("engine", this.engine);
|
||||||
|
}
|
||||||
|
if (!createArgs.containsKey("charset") && this.charset != null) {
|
||||||
|
createArgs.put("charset", this.charset);
|
||||||
|
}
|
||||||
|
if (!createArgs.containsKey("parent")) {
|
||||||
|
createArgs.put("parent", this);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (arg1) {
|
||||||
|
case null -> {
|
||||||
|
final View partial = new StandardGStringTemplateView(createArgs);
|
||||||
|
try {
|
||||||
|
partial.renderTo(this.currentWriter.get());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Map m -> {
|
||||||
|
createArgs.put("locals", m);
|
||||||
|
final GStringTemplateView view = new StandardGStringTemplateView(createArgs);
|
||||||
|
final Closure<CharSequence> cl = switch (arg2) {
|
||||||
|
case null -> null;
|
||||||
|
case Closure closure -> closure;
|
||||||
|
default -> throw new IllegalArgumentException("Third argument, if not null, must be a Closure.");
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
if (cl != null) {
|
||||||
|
view.renderTo(this.currentWriter.get(), cl);
|
||||||
|
} else {
|
||||||
|
view.renderTo(this.currentWriter.get());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Closure cl -> {
|
||||||
|
final GStringTemplateView view = new StandardGStringTemplateView(createArgs);
|
||||||
|
try {
|
||||||
|
view.renderTo(this.currentWriter.get(), cl);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> throw new IllegalArgumentException("Second argument, if not null, must be a Map or a Closure.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public URL templateResource(String name) {
|
||||||
|
return this.getClass().getResource(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("GStringTemplateView(src: %s)", this.src);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package groowt.view;
|
||||||
|
|
||||||
|
import groovy.lang.GroovySystem;
|
||||||
|
import groovy.lang.MetaClass;
|
||||||
|
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class StandardGStringTemplateView extends GStringTemplateView {
|
||||||
|
|
||||||
|
static {
|
||||||
|
final MetaClass mc = new StandardGStringTemplateViewMetaClass();
|
||||||
|
mc.initialize();
|
||||||
|
GroovySystem.getMetaClassRegistry().setMetaClass(StandardGStringTemplateView.class, mc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Map<String, Object> locals;
|
||||||
|
private final View parent;
|
||||||
|
|
||||||
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
|
public StandardGStringTemplateView(Map<String, Object> args) {
|
||||||
|
super(args);
|
||||||
|
final Object localsArg = args.get("locals");
|
||||||
|
this.locals = localsArg instanceof Map map
|
||||||
|
? Collections.unmodifiableMap(map)
|
||||||
|
: localsArg != null
|
||||||
|
? Collections.unmodifiableMap(DefaultGroovyMethods.asType(localsArg, Map.class))
|
||||||
|
: Collections.emptyMap();
|
||||||
|
this.parent = args.containsKey("parent") ? DefaultGroovyMethods.asType(args.get("parent"), View.class) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getLocals() {
|
||||||
|
return locals;
|
||||||
|
}
|
||||||
|
|
||||||
|
public View getParent() {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "StandardGStringTemplateView(super: " + super.toString() + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,178 @@
|
|||||||
|
package groowt.view;
|
||||||
|
|
||||||
|
import groovy.lang.*;
|
||||||
|
import org.codehaus.groovy.runtime.metaclass.MethodSelectionException;
|
||||||
|
import org.codehaus.groovy.util.FastArray;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class StandardGStringTemplateViewMetaClass extends ExpandoMetaClass {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(StandardGStringTemplateViewMetaClass.class);
|
||||||
|
|
||||||
|
private static StandardGStringTemplateView asView(Object object) {
|
||||||
|
return object instanceof StandardGStringTemplateView view ? view : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object[] asArgsArray(Object object) {
|
||||||
|
return object instanceof Object[] objects ? objects : new Object[] { object };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MetaMethod findMetaMethod(MetaClass metaClass, String methodName, Object[] argsArray) {
|
||||||
|
final List<MetaMethod> metaMethods = metaClass.getMetaMethods().stream().filter(metaMethod ->
|
||||||
|
metaMethod.getName().equals(methodName) && metaMethod.isValidMethod(argsArray)
|
||||||
|
).toList();
|
||||||
|
if (metaMethods.size() > 1) {
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
final Class[] argTypes = Arrays.stream(argsArray).map(Object::getClass).toArray(Class[]::new);
|
||||||
|
throw new MethodSelectionException(methodName, new FastArray(metaMethods), argTypes);
|
||||||
|
} else if (metaMethods.size() == 1) {
|
||||||
|
return metaMethods.getFirst();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void warnWrongType(Object object) {
|
||||||
|
logger.warn(
|
||||||
|
"StandardGStringTemplateViewMetaClass should only be used as a MetaClass of StandardGStringTemplateViewMetaClass or a subclass thereof; given "
|
||||||
|
+ object + " of type " + object.getClass()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MetaClass findMetaClass(Object object) {
|
||||||
|
return switch (object) {
|
||||||
|
case GroovyObject groovyObject -> groovyObject.getMetaClass();
|
||||||
|
default -> GroovySystem.getMetaClassRegistry().getMetaClass(object.getClass());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public StandardGStringTemplateViewMetaClass() {
|
||||||
|
super(StandardGStringTemplateView.class, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object invokeMethod(Object object, String methodName, Object arguments) {
|
||||||
|
final StandardGStringTemplateView view = asView(object);
|
||||||
|
if (view == null) {
|
||||||
|
logger.warn("StandardGStringTemplateViewMetaClass should only be used as a MetaClass of StandardGStringTemplateViewMetaClass.");
|
||||||
|
return super.invokeMethod(object, methodName, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
// local closure
|
||||||
|
final Map<String, Object> locals = view.getLocals();
|
||||||
|
if (locals.containsKey(methodName)) {
|
||||||
|
final Object local = locals.get(methodName);
|
||||||
|
if (local instanceof @SuppressWarnings("rawtypes") Closure closure) {
|
||||||
|
return closure.call(arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for both self and parent
|
||||||
|
final Object[] argsArray = asArgsArray(arguments);
|
||||||
|
|
||||||
|
// self
|
||||||
|
final MetaMethod selfMethod = findMetaMethod(this, methodName, argsArray);
|
||||||
|
if (selfMethod != null) {
|
||||||
|
return selfMethod.invoke(view, argsArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
// parent hierarchy
|
||||||
|
View parent = view.getParent();
|
||||||
|
while (parent != null) {
|
||||||
|
final var parentMetaMethod = findMetaMethod(findMetaClass(parent), methodName, argsArray);
|
||||||
|
if (parentMetaMethod != null) {
|
||||||
|
return parentMetaMethod.invoke(parent, argsArray);
|
||||||
|
} else if (parent instanceof StandardGStringTemplateView) {
|
||||||
|
parent = ((StandardGStringTemplateView) parent).getParent();
|
||||||
|
} else {
|
||||||
|
parent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.invokeMethod(object, methodName, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getProperty(Object object, String name) {
|
||||||
|
final StandardGStringTemplateView view = asView(object);
|
||||||
|
if (view == null) {
|
||||||
|
warnWrongType(object);
|
||||||
|
return super.getProperty(object, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// local
|
||||||
|
final Map<String, Object> locals = view.getLocals();
|
||||||
|
if (locals != null && locals.containsKey(name)) {
|
||||||
|
return view.getLocals().get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// self
|
||||||
|
final var metaProperty = this.getMetaProperty(name);
|
||||||
|
if (metaProperty != null) {
|
||||||
|
return metaProperty.getProperty(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
// parent hierarchy
|
||||||
|
View parent = view.getParent();
|
||||||
|
while (parent != null) {
|
||||||
|
final var parentMetaProperty = findMetaClass(parent).getMetaProperty(name);
|
||||||
|
if (parentMetaProperty != null) {
|
||||||
|
return parentMetaProperty.getProperty(parent);
|
||||||
|
} else if (parent instanceof StandardGStringTemplateView) {
|
||||||
|
parent = ((StandardGStringTemplateView) parent).getParent();
|
||||||
|
} else {
|
||||||
|
parent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// all else fails, try super metaClass
|
||||||
|
return super.getProperty(object, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setProperty(Object object, String name, Object value) {
|
||||||
|
final StandardGStringTemplateView view = asView(object);
|
||||||
|
if (view == null) {
|
||||||
|
warnWrongType(object);
|
||||||
|
super.setProperty(object, name, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// local
|
||||||
|
final Map<String, Object> locals = view.getLocals();
|
||||||
|
if (locals != null && locals.containsKey(name)) {
|
||||||
|
locals.put(name, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// self
|
||||||
|
final var selfMetaProperty = this.getMetaProperty(name);
|
||||||
|
if (selfMetaProperty != null) {
|
||||||
|
selfMetaProperty.setProperty(view, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parent hierarchy
|
||||||
|
View parent = view.getParent();
|
||||||
|
while (parent != null) {
|
||||||
|
final var parentMetaProperty = findMetaClass(parent).getMetaProperty(name);
|
||||||
|
if (parentMetaProperty != null) {
|
||||||
|
parentMetaProperty.setProperty(parent, value);
|
||||||
|
return;
|
||||||
|
} else if (parent instanceof StandardGStringTemplateView) {
|
||||||
|
parent = ((StandardGStringTemplateView) parent).getParent();
|
||||||
|
} else {
|
||||||
|
parent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// all else fails, try super metaClass
|
||||||
|
super.setProperty(object, name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
27
views/src/main/groovy/groowt/view/TemplateView.java
Normal file
27
views/src/main/groovy/groowt/view/TemplateView.java
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package groowt.view;
|
||||||
|
|
||||||
|
import groovy.lang.Writable;
|
||||||
|
import groovy.text.Template;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class TemplateView extends AbstractView {
|
||||||
|
|
||||||
|
private final Template template;
|
||||||
|
|
||||||
|
public TemplateView(Template template) {
|
||||||
|
this.template = template;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void renderTo(Writer writer) throws IOException {
|
||||||
|
this.getWritableFrom(this.template).writeTo(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Writable getWritableFrom(Template template) {
|
||||||
|
return template.make(this.asType(Map.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
57
views/src/main/groovy/groowt/view/View.java
Normal file
57
views/src/main/groovy/groowt/view/View.java
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package groowt.view;
|
||||||
|
|
||||||
|
import groovy.lang.Closure;
|
||||||
|
import groovy.lang.Writable;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.io.Writer;
|
||||||
|
|
||||||
|
public interface View {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: consider making this a (package private?) separate class, perhaps with support for GStringTemplateViews, etc.?
|
||||||
|
*/
|
||||||
|
final class ClosureView extends Closure<Object> {
|
||||||
|
|
||||||
|
private final View view;
|
||||||
|
|
||||||
|
public ClosureView(View view) {
|
||||||
|
super(view, view);
|
||||||
|
this.view = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void doCall(Writer writer) throws IOException {
|
||||||
|
this.view.renderTo(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String doCall() {
|
||||||
|
return this.view.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void renderTo(Writer writer) throws IOException;
|
||||||
|
|
||||||
|
default String render() {
|
||||||
|
final Writer w = new StringWriter();
|
||||||
|
try {
|
||||||
|
this.renderTo(w);
|
||||||
|
return w.toString();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default Writable asWritable() {
|
||||||
|
return writer -> {
|
||||||
|
this.renderTo(writer);
|
||||||
|
return writer;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default Closure<Object> asClosure() {
|
||||||
|
return new ClosureView(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user