2
0
Fork 0
mirror of https://github.com/ethauvin/rife2-hello.git synced 2025-04-29 00:48:13 -07:00

Compare commits

...

20 commits

Author SHA1 Message Date
938372addc Made GraalVM plugin work with Jetty and JDK 19 2023-03-05 10:56:37 -05:00
a3a5c7c380 Made "src/main/resources/templates" the only default template dir.
Made the template excludes adapt to the template types that are pre-compiled and use a more restrictive pattern.
2023-03-05 10:52:34 -05:00
b51e36ee73
Merge pull request #8 from melix/cc/configurable-templates
Make the template directories configurable
2023-03-05 10:39:48 -05:00
83e52c0e06
Merge pull request #7 from melix/cc/use-official-graalvm-plugin
Use the official GraalVM plugin
2023-03-05 10:39:39 -05:00
Cedric Champeau
d06124c26d
Make the template directories configurable
Adding a directory can be done via the extension:

```gradle
rife2 {
   templateDirectories.from(file("my-template-dir"))
}
```

By default the template directories include both `src/main/templates`
and `src/main/resources/templates`. If you want to ignore those,
then you need to clear the default:

```gradle
rife2 {
    templateDirectories.from.clear()
    templateDirectories.from(file("my-template-dir"))
}
```
2023-03-05 16:36:31 +01:00
Cedric Champeau
a79e616d79
Use the official GraalVM plugin 2023-03-05 16:23:48 +01:00
de5c974f15
Merge pull request #6 from melix/cc/additional-template-dirs
Add support for extra template directories
2023-03-05 09:59:57 -05:00
0a2b061679 Reverted dependencies back to the explicit version 2023-03-05 09:12:58 -05:00
Cedric Champeau
21c85ea93b
Add support for extra template directories
By default, the template was using `src/main/templates` as the source directory
for templates. This has the benefit of properly isolating the templates from
the application classpath: the framework is then responsible for injecting them
to the app runtime in whatever suitable form: either untouched as sources (in
development mode), or precompiled (for production).

With this change, it is also possible to use `src/main/resources/templates` as
a source directory. It _may_ feel more natural to users (although I would disagree),
but it has the drawback that the jars have to be configured to _exclude_ those
resources, because by default whatever is in `src/main/resources` MUST be included
in a jar (independently of the run mode).

It is also possible for the user to configure additional source directories should
they want to.
2023-03-05 15:10:46 +01:00
cf4870745c Made precompiledTemplateTypes plugin option not use HTML by default
Cleanup minimal test project.
2023-03-05 09:07:29 -05:00
5ca1fa305b Gradle plugin test fix 2023-03-05 08:51:14 -05:00
d8c41b45d9
Merge pull request #5 from melix/cc/fix-template-reloading
Fix template reloading
2023-03-05 08:15:10 -05:00
Cedric Champeau
c9f286132c
Fix reloading of templates
This commit fixes how templates were reloaded. There was a bug in
the plugin which used the output of the precompiled templates for
development only dependencies, instead of the templates directory.
This caused the templates to be always compiled and added as a
resource on runtime classpath, when we only wanted the raw templates.

This commit also adds functional tests to the build logic, which
can be executed by running `./gradlew build-logic:test`.
2023-03-05 13:44:10 +01:00
Cedric Champeau
65e579966c
Revert RunTask 2023-03-05 11:22:28 +01:00
86b7ec21d9 Added serialVersionUID to TemplateType 2023-03-04 12:09:59 -05:00
aeaadfb1cc Added support for configurable precompiled template types 2023-03-04 11:53:59 -05:00
ed8046e83d More minor cleanups 2023-03-04 10:39:37 -05:00
c8f47a935e Minor cleanups 2023-03-04 10:22:04 -05:00
d3dedfe189 Updated to RIFE2 1.4.0 2023-03-04 10:21:35 -05:00
a54ab9b23e
Merge pull request #4 from ethauvin/develop
Develop
2023-03-03 23:05:53 -05:00
26 changed files with 701 additions and 155 deletions

View file

@ -92,18 +92,23 @@ GraalVM supports creating a single Ahead-Of-Time
[native executable](https://www.graalvm.org/native-image/) from your java [native executable](https://www.graalvm.org/native-image/) from your java
bytecode. bytecode.
Once you have at least GraalVM 22.3.1 Java 19 installed, you can generate the Once you have at least GraalVM 22.3.1 Java 17 installed, you can generate the native binary with:
UberJar as above, then create your native binary as such:
```bash ```bash
native-image --no-fallback --enable-preview -jar app/build/libs/hello-uber-1.0.jar ./gradlew nativeCompile
``` ```
You'll end up with a `hello-uber-1.0` file that can be executed directly without You'll end up with a `hello-1.0` file that can be executed directly without
the need of a JVM: the need of a JVM:
```bash ```bash
./hello-uber-1.0 ./app/build/native/nativeCompile/hello-1.0
```
Alternatively, you can run the native executable directly with:
```bash
./gradlew nativeRun
``` ```
> **NOTE:** RIFE2 support for GraalVM native-image is still in preliminary > **NOTE:** RIFE2 support for GraalVM native-image is still in preliminary

View file

@ -1,16 +1,22 @@
import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent import org.gradle.api.tasks.testing.logging.TestLogEvent
import com.uwyn.rife2.gradle.TemplateType.*
plugins { plugins {
application
id("com.uwyn.rife2") id("com.uwyn.rife2")
`maven-publish` `maven-publish`
id("org.graalvm.buildtools.native") version "0.9.20"
} }
version = 1.0
group = "com.example"
base { base {
archivesName.set("hello") archivesName.set("hello")
version = 1.0
group = "com.example"
}
application {
mainClass.set("hello.App")
} }
java { java {
@ -26,9 +32,9 @@ repositories {
} }
rife2 { rife2 {
mainClass.set("hello.App") version.set("1.4.0")
version.set("1.3.0")
useAgent.set(true) useAgent.set(true)
precompiledTemplateTypes.add(HTML)
} }
dependencies { dependencies {
@ -63,3 +69,8 @@ publishing {
} }
} }
} }
graalvmNative.binaries.all {
buildArgs.add("--enable-preview") // support for Jetty virtual threads with JDK 19
imageName.set("hello-$version")
}

View file

@ -1,6 +1,6 @@
[ [
{ {
"name":"rife.template.html.hello", "name":"rife.template.html.hello",
"methods":[{"name":"<init>","parameterTypes":[] }] "methods":[{"name":"<init>","parameterTypes":[] }]
} }
] ]

View file

@ -1,5 +1,6 @@
plugins { plugins {
`java-gradle-plugin` `java-gradle-plugin`
groovy
} }
repositories { repositories {
@ -8,13 +9,8 @@ repositories {
dependencies { dependencies {
gradleApi() gradleApi()
} testImplementation(libs.spock.core)
testImplementation(gradleTestKit())
tasks {
withType<JavaCompile> {
options.isDeprecation = true
options.compilerArgs.add("-Xlint:unchecked")
}
} }
gradlePlugin { gradlePlugin {
@ -25,3 +21,11 @@ gradlePlugin {
} }
} }
} }
tasks.withType<Test>().configureEach {
useJUnitPlatform()
testLogging {
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
events = setOf(org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED, org.gradle.api.tasks.testing.logging.TestLogEvent.SKIPPED, org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED)
}
}

View file

@ -1 +1,9 @@
rootProject.name = "build-logic" rootProject.name = "build-logic"
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}

View file

@ -18,11 +18,12 @@ package com.uwyn.rife2.gradle;
import org.gradle.api.DefaultTask; import org.gradle.api.DefaultTask;
import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property; import org.gradle.api.provider.Property;
import org.gradle.api.tasks.CacheableTask; import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Classpath; import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputDirectory; import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputDirectory; import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.PathSensitive; import org.gradle.api.tasks.PathSensitive;
@ -31,6 +32,7 @@ import org.gradle.api.tasks.TaskAction;
import org.gradle.process.ExecOperations; import org.gradle.process.ExecOperations;
import javax.inject.Inject; import javax.inject.Inject;
import java.nio.file.Files;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -40,12 +42,12 @@ public abstract class PrecompileTemplates extends DefaultTask {
@Classpath @Classpath
public abstract ConfigurableFileCollection getClasspath(); public abstract ConfigurableFileCollection getClasspath();
@InputDirectory @InputFiles
@PathSensitive(PathSensitivity.RELATIVE) @PathSensitive(PathSensitivity.RELATIVE)
public abstract DirectoryProperty getTemplatesDirectory(); public abstract ConfigurableFileCollection getTemplatesDirectories();
@Input @Input
public abstract Property<String> getType(); public abstract ListProperty<TemplateType> getTypes();
@Input @Input
@Optional @Optional
@ -63,6 +65,9 @@ public abstract class PrecompileTemplates extends DefaultTask {
@TaskAction @TaskAction
public void precompileTemplates() { public void precompileTemplates() {
for (var type : getTypes().get()) {
getTemplatesDirectories().getFiles().forEach(dir -> {
if (Files.exists(dir.toPath())) {
getExecOperations().javaexec(javaexec -> { getExecOperations().javaexec(javaexec -> {
javaexec.setClasspath(getClasspath()); javaexec.setClasspath(getClasspath());
javaexec.getMainClass().set("rife.template.TemplateDeployer"); javaexec.getMainClass().set("rife.template.TemplateDeployer");
@ -71,13 +76,17 @@ public abstract class PrecompileTemplates extends DefaultTask {
args.add("-verbose"); args.add("-verbose");
} }
args.add("-t"); args.add("-t");
args.add(getType().get()); args.add(type.identifier());
args.add("-d"); args.add("-d");
args.add(getOutputDirectory().get().getAsFile().getPath()); args.add(getOutputDirectory().get().getAsFile().getPath());
args.add("-encoding"); args.add("-encoding");
args.add(getEncoding().orElse("UTF-8").get()); args.add(getEncoding().orElse("UTF-8").get());
args.add(getTemplatesDirectory().get().getAsFile().getPath()); args.add(dir.getPath());
javaexec.args(args); javaexec.args(args);
}); });
} }
});
}
}
} }

View file

@ -15,15 +15,18 @@
*/ */
package com.uwyn.rife2.gradle; package com.uwyn.rife2.gradle;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property; import org.gradle.api.provider.Property;
@SuppressWarnings("unused")
public abstract class Rife2Extension { public abstract class Rife2Extension {
public abstract Property<String> getMainClass();
public abstract Property<String> getVersion(); public abstract Property<String> getVersion();
public abstract Property<Boolean> getUseAgent(); public abstract Property<Boolean> getUseAgent();
public abstract Property<String> getUberMainClass(); public abstract Property<String> getUberMainClass();
public abstract ListProperty<TemplateType> getPrecompiledTemplateTypes();
public abstract ConfigurableFileCollection getTemplateDirectories();
} }

View file

@ -21,29 +21,35 @@ import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.artifacts.dsl.DependencyHandler; import org.gradle.api.artifacts.dsl.DependencyHandler;
import org.gradle.api.attributes.Attribute; import org.gradle.api.attributes.Attribute;
import org.gradle.api.attributes.AttributeContainer;
import org.gradle.api.attributes.Bundling; import org.gradle.api.attributes.Bundling;
import org.gradle.api.component.AdhocComponentWithVariants; import org.gradle.api.component.AdhocComponentWithVariants;
import org.gradle.api.component.ConfigurationVariantDetails; import org.gradle.api.component.ConfigurationVariantDetails;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DuplicatesStrategy; import org.gradle.api.file.DuplicatesStrategy;
import org.gradle.api.plugins.BasePluginExtension; import org.gradle.api.plugins.BasePluginExtension;
import org.gradle.api.plugins.JavaApplication;
import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.plugins.PluginContainer; import org.gradle.api.plugins.PluginContainer;
import org.gradle.api.tasks.*; import org.gradle.api.tasks.JavaExec;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.bundling.Jar; import org.gradle.api.tasks.bundling.Jar;
import org.gradle.api.tasks.testing.Test; import org.gradle.api.tasks.testing.Test;
import org.gradle.process.CommandLineArgumentProvider; import org.gradle.process.CommandLineArgumentProvider;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.stream.Collectors;
@SuppressWarnings({"ALL", "unused"})
public class Rife2Plugin implements Plugin<Project> { public class Rife2Plugin implements Plugin<Project> {
public static final String DEFAULT_TEMPLATES_DIR = "src/main/templates"; public static final List<String> DEFAULT_TEMPLATES_DIRS = List.of("src/main/resources/templates");
public static final String DEFAULT_GENERATED_RIFE2_CLASSES_DIR = "generated/classes/rife2"; public static final String DEFAULT_GENERATED_RIFE2_CLASSES_DIR = "generated/classes/rife2";
public static final String RIFE2_GROUP = "rife2"; public static final String RIFE2_GROUP = "rife2";
public static final String WEBAPP_SRCDIR = "src/main/webapp"; public static final String WEBAPP_SRCDIR = "src/main/webapp";
public static final String PRECOMPILE_TEMPLATES_TASK_NAME = "precompileTemplates";
@Override @Override
public void apply(Project project) { public void apply(Project project) {
@ -54,67 +60,88 @@ public class Rife2Plugin implements Plugin<Project> {
var configurations = project.getConfigurations(); var configurations = project.getConfigurations();
var dependencyHandler = project.getDependencies(); var dependencyHandler = project.getDependencies();
var tasks = project.getTasks(); var tasks = project.getTasks();
var rife2Configuration = createRife2Configuration(configurations, dependencyHandler, rife2Extension); var rife2Configuration = createRife2Configuration(configurations, dependencyHandler, rife2Extension);
var rife2CompilerClasspath = createRife2CompilerClasspathConfiguration(configurations, rife2Configuration); var rife2CompilerClasspath = createRife2CompilerClasspathConfiguration(configurations, rife2Configuration);
var rife2AgentClasspath = createRife2AgentConfiguration(configurations, dependencyHandler, rife2Extension); var rife2AgentClasspath = createRife2AgentConfiguration(configurations, dependencyHandler, rife2Extension);
configurations.getByName(JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME).extendsFrom(rife2Configuration); configurations.getByName(JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME).extendsFrom(rife2Configuration);
var precompileTemplates = registerPrecompileTemplateTask(project, rife2CompilerClasspath);
createRife2DevelopmentOnlyConfiguration(project, configurations, dependencyHandler, precompileTemplates); var precompileTemplates = registerPrecompileTemplateTask(project, rife2CompilerClasspath, rife2Extension);
createRife2DevelopmentOnlyConfiguration(project, configurations, dependencyHandler, rife2Extension.getTemplateDirectories());
exposePrecompiledTemplatesToTestTask(project, configurations, dependencyHandler, precompileTemplates); exposePrecompiledTemplatesToTestTask(project, configurations, dependencyHandler, precompileTemplates);
configureAgent(project, plugins, rife2Extension, rife2AgentClasspath); configureAgent(project, plugins, rife2Extension, rife2AgentClasspath);
registerRunTask(project, rife2Extension, rife2AgentClasspath);
TaskProvider<Jar> uberJarTask = registerUberJarTask(project, plugins, javaPluginExtension, rife2Extension, tasks, precompileTemplates); TaskProvider<Jar> uberJarTask = registerUberJarTask(project, plugins, javaPluginExtension, rife2Extension, tasks, precompileTemplates);
bundlePrecompiledTemplatesIntoJarFile(tasks, precompileTemplates); bundlePrecompiledTemplatesIntoJarFile(tasks, precompileTemplates, rife2Extension);
configureMavenPublishing(project, plugins, configurations, uberJarTask); configureMavenPublishing(project, plugins, configurations, uberJarTask);
} }
private static void configureMavenPublishing(Project project, PluginContainer plugins, ConfigurationContainer configurations, TaskProvider<Jar> uberJarTask) { @SuppressWarnings("unchecked")
private static void configureMavenPublishing(Project project,
PluginContainer plugins,
ConfigurationContainer configurations,
TaskProvider<Jar> uberJarTask) {
plugins.withId("maven-publish", unused -> { plugins.withId("maven-publish", unused -> {
Configuration rife2UberJarElements = configurations.create("rife2UberJarElements", conf -> { var rife2UberJarElements = configurations.create("rife2UberJarElements", conf -> {
conf.setDescription("Exposes the uber jar archive of the RIFE2 web application."); conf.setDescription("Exposes the uber jar archive of the RIFE2 web application.");
conf.setCanBeResolved(false); conf.setCanBeResolved(false);
conf.setCanBeConsumed(true); conf.setCanBeConsumed(true);
conf.getOutgoing().artifact(uberJarTask, artifact -> artifact.setClassifier("uber")); conf.getOutgoing().artifact(uberJarTask, artifact -> artifact.setClassifier("uber"));
AttributeContainer runtimeAttributes = configurations.getByName(JavaPlugin.RUNTIME_ELEMENTS_CONFIGURATION_NAME).getAttributes();
var runtimeAttributes = configurations.getByName(JavaPlugin.RUNTIME_ELEMENTS_CONFIGURATION_NAME).getAttributes();
conf.attributes(attrs -> { conf.attributes(attrs -> {
for (Attribute<?> attribute : runtimeAttributes.keySet()) { for (Attribute<?> attribute : runtimeAttributes.keySet()) {
Object value = runtimeAttributes.getAttribute(attribute); Object value = runtimeAttributes.getAttribute(attribute);
if (value != null) { //noinspection unchecked
if (Bundling.class.equals(attribute.getType())) { if (Bundling.class.equals(attribute.getType())) {
attrs.attribute(Bundling.BUNDLING_ATTRIBUTE, project.getObjects().named(Bundling.class, Bundling.SHADOWED)); attrs.attribute(Bundling.BUNDLING_ATTRIBUTE, project.getObjects().named(Bundling.class, Bundling.SHADOWED));
} else { } else {
//noinspection unchecked
attrs.attribute((Attribute<Object>) attribute, value); attrs.attribute((Attribute<Object>) attribute, value);
} }
} }
}
}); });
}); });
AdhocComponentWithVariants component = (AdhocComponentWithVariants) project.getComponents().getByName("java");
var component = (AdhocComponentWithVariants) project.getComponents().getByName("java");
component.addVariantsFromConfiguration(rife2UberJarElements, ConfigurationVariantDetails::mapToOptional); component.addVariantsFromConfiguration(rife2UberJarElements, ConfigurationVariantDetails::mapToOptional);
}); });
} }
private static void exposePrecompiledTemplatesToTestTask(Project project, ConfigurationContainer configurations, DependencyHandler dependencyHandler, TaskProvider<PrecompileTemplates> precompileTemplates) { private static void exposePrecompiledTemplatesToTestTask(Project project,
ConfigurationContainer configurations,
DependencyHandler dependencyHandler,
TaskProvider<PrecompileTemplates> precompileTemplatesTask) {
configurations.getByName(JavaPlugin.TEST_RUNTIME_ONLY_CONFIGURATION_NAME) configurations.getByName(JavaPlugin.TEST_RUNTIME_ONLY_CONFIGURATION_NAME)
.getDependencies() .getDependencies()
.add(dependencyHandler.create(project.files(precompileTemplates))); .add(dependencyHandler.create(project.files(precompileTemplatesTask)));
} }
private static void bundlePrecompiledTemplatesIntoJarFile(TaskContainer tasks, TaskProvider<PrecompileTemplates> precompileTemplates) { private static void bundlePrecompiledTemplatesIntoJarFile(TaskContainer tasks,
tasks.named("jar", Jar.class, jar -> jar.from(precompileTemplates)); TaskProvider<PrecompileTemplates> precompileTemplatesTask,
Rife2Extension rife2Extension) {
tasks.named("jar", Jar.class, jar -> {
jar.from(precompileTemplatesTask);
// This isn't great because it needs to be hardcoded, in order to avoid the templates
// declared in `src/main/resources/templates` to be included in the jar file.
// which means that if for whatever reason the user also uses the same directory for
// something else, it will be excluded from the jar file.
rife2Extension.getPrecompiledTemplateTypes().get().forEach(templateType -> jar.exclude("/templates/**." + templateType.identifier().toLowerCase())
);
});
} }
private void createRife2DevelopmentOnlyConfiguration(Project project, private void createRife2DevelopmentOnlyConfiguration(Project project,
ConfigurationContainer configurations, ConfigurationContainer configurations,
DependencyHandler dependencies, DependencyHandler dependencies,
TaskProvider<PrecompileTemplates> precompileTemplatesTask) { ConfigurableFileCollection templateDirectories) {
Configuration rife2DevelopmentOnly = configurations.create("rife2DevelopmentOnly", conf -> { var rife2DevelopmentOnly = configurations.create("rife2DevelopmentOnly", conf -> {
conf.setDescription("Dependencies which should only be visible when running the application in development mode (and not in tests)."); conf.setDescription("Dependencies which should only be visible when running the application in development mode (and not in tests).");
conf.setCanBeConsumed(false); conf.setCanBeConsumed(false);
conf.setCanBeResolved(false); conf.setCanBeResolved(false);
}); });
rife2DevelopmentOnly.getDependencies().add(dependencies.create(project.files(precompileTemplatesTask))); rife2DevelopmentOnly.getDependencies().addAllLater(templateDirectories.getElements().map(locations ->
locations.stream().map(fs -> dependencies.create(project.files(fs))).collect(Collectors.toList()))
);
configurations.getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME).extendsFrom(rife2DevelopmentOnly); configurations.getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME).extendsFrom(rife2DevelopmentOnly);
} }
@ -130,21 +157,29 @@ public class Rife2Plugin implements Plugin<Project> {
var base = project.getExtensions().getByType(BasePluginExtension.class); var base = project.getExtensions().getByType(BasePluginExtension.class);
jar.getArchiveBaseName().convention(project.provider(() -> base.getArchivesName().get() + "-uber")); jar.getArchiveBaseName().convention(project.provider(() -> base.getArchivesName().get() + "-uber"));
jar.setDuplicatesStrategy(DuplicatesStrategy.EXCLUDE); jar.setDuplicatesStrategy(DuplicatesStrategy.EXCLUDE);
var runtimeClasspath = project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME);
jar.from(javaPluginExtension.getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput()); jar.from(javaPluginExtension.getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput());
jar.from(precompileTemplatesTask); jar.from(precompileTemplatesTask);
jar.into("webapp", spec -> spec.from(WEBAPP_SRCDIR)); jar.into("webapp", spec -> spec.from(WEBAPP_SRCDIR));
var runtimeClasspath = project.getConfigurations().getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME);
jar.from(runtimeClasspath.getElements().map(e -> e.stream() jar.from(runtimeClasspath.getElements().map(e -> e.stream()
.filter(f -> f.getAsFile().getName().toLowerCase(Locale.ENGLISH).endsWith(".jar")) .filter(f -> f.getAsFile().getName().toLowerCase(Locale.ENGLISH).endsWith(".jar"))
.map(project::zipTree) .map(project::zipTree)
.toList())); .toList()));
// This isn't great because it needs to be hardcoded, in order to avoid the templates
// declared in `src/main/resources/templates` to be included in the jar file.
// which means that if for whatever reason the user also uses the same directory for
// something else, it will be excluded from the jar file.
jar.exclude("templates");
plugins.withId("application", unused -> jar.manifest(manifest -> plugins.withId("application", unused -> jar.manifest(manifest ->
manifest.getAttributes().put("Main-Class", rife2Extension.getUberMainClass().get())) manifest.getAttributes().put("Main-Class", rife2Extension.getUberMainClass().get()))
); );
}); });
} }
private static void configureAgent(Project project, PluginContainer plugins, Rife2Extension rife2Extension, Configuration rife2AgentClasspath) { private static void configureAgent(Project project,
PluginContainer plugins,
Rife2Extension rife2Extension,
Configuration rife2AgentClasspath) {
CommandLineArgumentProvider agentProvider = () -> { CommandLineArgumentProvider agentProvider = () -> {
if (Boolean.TRUE.equals(rife2Extension.getUseAgent().get())) { if (Boolean.TRUE.equals(rife2Extension.getUseAgent().get())) {
return Collections.singleton("-javaagent:" + rife2AgentClasspath.getAsPath()); return Collections.singleton("-javaagent:" + rife2AgentClasspath.getAsPath());
@ -158,11 +193,14 @@ public class Rife2Plugin implements Plugin<Project> {
private static Rife2Extension createRife2Extension(Project project) { private static Rife2Extension createRife2Extension(Project project) {
var rife2 = project.getExtensions().create("rife2", Rife2Extension.class); var rife2 = project.getExtensions().create("rife2", Rife2Extension.class);
rife2.getUseAgent().convention(false); rife2.getUseAgent().convention(false);
rife2.getUberMainClass().set(rife2.getMainClass() + "Uber"); rife2.getUberMainClass().convention(project.getExtensions().getByType(JavaApplication.class).getMainClass()
.map(mainClass -> mainClass + "Uber"));
DEFAULT_TEMPLATES_DIRS.stream().forEachOrdered(dir -> rife2.getTemplateDirectories().from(project.files(dir)));
return rife2; return rife2;
} }
private static Configuration createRife2CompilerClasspathConfiguration(ConfigurationContainer configurations, Configuration rife2Configuration) { private static Configuration createRife2CompilerClasspathConfiguration(ConfigurationContainer configurations,
Configuration rife2Configuration) {
return configurations.create("rife2CompilerClasspath", conf -> { return configurations.create("rife2CompilerClasspath", conf -> {
conf.setDescription("The RIFE2 compiler classpath"); conf.setDescription("The RIFE2 compiler classpath");
conf.setCanBeConsumed(false); conf.setCanBeConsumed(false);
@ -198,28 +236,16 @@ public class Rife2Plugin implements Plugin<Project> {
} }
private static TaskProvider<PrecompileTemplates> registerPrecompileTemplateTask(Project project, private static TaskProvider<PrecompileTemplates> registerPrecompileTemplateTask(Project project,
Configuration rife2CompilerClasspath) { Configuration rife2CompilerClasspath,
return project.getTasks().register("precompileTemplates", PrecompileTemplates.class, task -> { Rife2Extension rife2Extension) {
return project.getTasks().register(PRECOMPILE_TEMPLATES_TASK_NAME, PrecompileTemplates.class, task -> {
task.setGroup(RIFE2_GROUP); task.setGroup(RIFE2_GROUP);
task.setDescription("Pre-compiles the templates."); task.setDescription("Pre-compiles the templates.");
task.getVerbose().convention(true); task.getVerbose().convention(true);
task.getClasspath().from(rife2CompilerClasspath); task.getClasspath().from(rife2CompilerClasspath);
task.getType().convention("html"); task.getTypes().convention(rife2Extension.getPrecompiledTemplateTypes());
task.getTemplatesDirectory().set(project.getLayout().getProjectDirectory().dir(DEFAULT_TEMPLATES_DIR)); task.getTemplatesDirectories().from(rife2Extension.getTemplateDirectories());
task.getOutputDirectory().set(project.getLayout().getBuildDirectory().dir(DEFAULT_GENERATED_RIFE2_CLASSES_DIR)); task.getOutputDirectory().set(project.getLayout().getBuildDirectory().dir(DEFAULT_GENERATED_RIFE2_CLASSES_DIR));
}); });
} }
private static void registerRunTask(Project project, Rife2Extension rife2Extension,
Configuration rife2CompilerClasspath) {
project.getTasks().register("run", RunTask.class, task -> {
task.setGroup(RIFE2_GROUP);
task.setDescription("Runs this project as a web application.");
task.getAgentClassPath().set(rife2CompilerClasspath.getAsPath());
task.getClasspath().from(project.getExtensions().getByType(SourceSetContainer.class)
.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getRuntimeClasspath());
task.getMainClass().set(rife2Extension.getMainClass());
task.getTemplatesDirectory().set(project.getLayout().getProjectDirectory().dir(DEFAULT_TEMPLATES_DIR));
});
}
} }

View file

@ -1,54 +0,0 @@
/*
* Copyright 2003-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.uwyn.rife2.gradle;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.*;
import org.gradle.process.ExecOperations;
import javax.inject.Inject;
import java.util.List;
@CacheableTask
public abstract class RunTask extends DefaultTask {
@Input
public abstract Property<String> getAgentClassPath();
@Classpath
public abstract ConfigurableFileCollection getClasspath();
@Inject
protected abstract ExecOperations getExecOperations();
@Input
public abstract Property<String> getMainClass();
@InputDirectory
@PathSensitive(PathSensitivity.RELATIVE)
public abstract DirectoryProperty getTemplatesDirectory();
@TaskAction
public void run() {
getExecOperations().javaexec(run -> {
run.setClasspath(getProject().getObjects().fileCollection().from(getTemplatesDirectory()).plus(getClasspath()));
run.getMainClass().set(getMainClass());
run.args(List.of("-javaagent:" + getAgentClassPath().get()));
});
}
}

View file

@ -0,0 +1,25 @@
package com.uwyn.rife2.gradle;
import java.io.Serial;
import java.io.Serializable;
public class TemplateType implements Serializable {
@Serial private static final long serialVersionUID = -2736320275307140837L;
public static TemplateType HTML = new TemplateType("html");
public static TemplateType JSON = new TemplateType("json");
public static TemplateType SVG = new TemplateType("svg");
public static TemplateType XML = new TemplateType("xml");
public static TemplateType TXT = new TemplateType("txt");
public static TemplateType SQL = new TemplateType("sql");
private final String identifier_;
public TemplateType(String identifier) {
identifier_ = identifier;
}
public String identifier() {
return identifier_;
}
}

View file

@ -0,0 +1,42 @@
import com.uwyn.rife2.gradle.TemplateType
plugins {
id("application")
id("com.uwyn.rife2")
}
base {
archivesName = "hello"
version = 1.0
group = "com.example"
}
application {
mainClass = "hello.App"
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
rife2 {
version = "1.4.0"
useAgent = true
precompiledTemplateTypes.add(TemplateType.HTML)
templateDirectories.from(file("src/main/templates"))
}
dependencies {
runtimeOnly("org.eclipse.jetty:jetty-server:11.0.13")
runtimeOnly("org.eclipse.jetty:jetty-servlet:11.0.13")
runtimeOnly("org.slf4j:slf4j-simple:2.0.5")
testImplementation("org.jsoup:jsoup:1.15.3")
testImplementation("org.junit.jupiter:junit-jupiter:5.9.1")
}

View file

@ -0,0 +1 @@
rootProject.name = 'minimal'

View file

@ -0,0 +1,16 @@
package hello;
import rife.engine.*;
public class App extends Site {
public void setup() {
var hello = get("/hello", c -> c.print(c.template("hello")));
get("/", c -> c.redirect(hello));
}
public static void main(String[] args) {
new Server()
.staticResourceBase("src/main/webapp")
.start(new App());
}
}

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><!--v title-->Hello<!--/v--></title>
<link rel="stylesheet" href="{{v webapp:rootUrl/}}css/style.css?{{v context:paramRandom/}}">
</head>
<body>
<p>Hello World</p>
</body>
</html>

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><!--v title-->Hello<!--/v--></title>
<link rel="stylesheet" href="{{v webapp:rootUrl/}}css/style.css?{{v context:paramRandom/}}">
</head>
<body>
<p>Hello World</p>
</body>
</html>

View file

@ -0,0 +1,21 @@
:root {
/* fonts */
--main-font: sans-serif;
/* font sizes */
--main-font-size: 18px;
/* colors */
--main-background-color: #0d0d0d;
--main-text-color: #d0d0d0;
/* margins and padding */
--content-padding: 2em;
}
body {
background: var(--main-background-color);
font-family: var(--main-font);
font-style: var(--main-font-size);
color: var(--main-text-color);
padding: var(--content-padding);
}

View file

@ -0,0 +1,24 @@
/*
* This Java source file was generated by the Gradle 'init' task.
*/
package hello;
import org.junit.jupiter.api.Test;
import rife.test.MockConversation;
import static org.junit.jupiter.api.Assertions.*;
public class AppTest {
@Test
void verifyRoot() {
var m = new MockConversation(new App());
assertEquals(m.doRequest("/").getStatus(), 302);
}
@Test
void verifyHello() {
var m = new MockConversation(new App());
assertEquals("Hello", m.doRequest("/hello")
.getTemplate().getValue("title"));
}
}

View file

@ -0,0 +1,184 @@
package com.uwyn.rife2.gradle
import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome
import org.gradle.util.GFileUtils
import org.gradle.util.GradleVersion
import spock.lang.Specification
import spock.lang.TempDir
import java.nio.file.Path
abstract class AbstractFunctionalTest extends Specification {
private final String gradleVersion = System.getProperty("gradleVersion", GradleVersion.current().version)
@TempDir
Path testDirectory
boolean debug
private StringWriter outputWriter
private StringWriter errorOutputWriter
private String output
private String errorOutput
BuildResult result
Path path(String... pathElements) {
Path cur = testDirectory
pathElements.each {
cur = cur.resolve(it)
}
cur
}
File file(String... pathElements) {
path(pathElements).toFile()
}
File getGroovyBuildFile() {
file("build.gradle")
}
File getBuildFile() {
groovyBuildFile
}
File getKotlinBuildFile() {
file("build.gradle.kts")
}
File getGroovySettingsFile() {
file("settings.gradle")
}
File getKotlinSettingsFile() {
file("settings.gradle.kts")
}
File getSettingsFile() {
groovySettingsFile
}
void run(String... args) {
try {
result = newRunner(args)
.build()
} finally {
recordOutputs()
}
}
void outputContains(String text) {
assert output.normalize().contains(text.normalize())
}
void outputDoesNotContain(String text) {
assert !output.normalize().contains(text.normalize())
}
void errorOutputContains(String text) {
assert errorOutput.normalize().contains(text.normalize())
}
void tasks(@DelegatesTo(value = TaskExecutionGraph, strategy = Closure.DELEGATE_FIRST) Closure spec) {
def graph = new TaskExecutionGraph()
spec.delegate = graph
spec.resolveStrategy = Closure.DELEGATE_FIRST
spec()
}
private void recordOutputs() {
output = outputWriter.toString()
errorOutput = errorOutputWriter.toString()
}
private GradleRunner newRunner(String... args) {
outputWriter = new StringWriter()
errorOutputWriter = new StringWriter()
ArrayList<String> autoArgs = computeAutoArgs()
def runner = GradleRunner.create()
.forwardStdOutput(tee(new OutputStreamWriter(System.out), outputWriter))
.forwardStdError(tee(new OutputStreamWriter(System.err), errorOutputWriter))
.withPluginClasspath()
.withProjectDir(testDirectory.toFile())
.withArguments([*autoArgs, *args])
if (gradleVersion) {
runner.withGradleVersion(gradleVersion)
}
if (debug) {
runner.withDebug(true)
}
runner
}
private ArrayList<String> computeAutoArgs() {
List<String> autoArgs = [
"-s",
"--console=verbose"
]
if (Boolean.getBoolean("config.cache")) {
autoArgs << '--configuration-cache'
}
autoArgs
}
private static Writer tee(Writer one, Writer two) {
return TeeWriter.of(one, two)
}
void fails(String... args) {
try {
result = newRunner(args)
.buildAndFail()
} finally {
recordOutputs()
}
}
private class TaskExecutionGraph {
void succeeded(String... tasks) {
tasks.each { task ->
contains(task)
assert result.task(task).outcome == TaskOutcome.SUCCESS
}
}
void failed(String... tasks) {
tasks.each { task ->
contains(task)
assert result.task(task).outcome == TaskOutcome.FAILED
}
}
void skipped(String... tasks) {
tasks.each { task ->
contains(task)
assert result.task(task).outcome == TaskOutcome.SKIPPED
}
}
void contains(String... tasks) {
tasks.each { task ->
assert result.task(task) != null: "Expected to find task $task in the graph but it was missing"
}
}
void doesNotContain(String... tasks) {
tasks.each { task ->
assert result.task(task) == null: "Task $task should be missing from the task graph but it was found with an outcome of ${result.task(task).outcome}"
}
}
}
void usesProject(String name) {
File sampleDir = new File("src/test-projects/$name")
GFileUtils.copyDirectory(sampleDir, testDirectory.toFile())
}
File file(String path) {
new File(testDirectory.toFile(), path)
}
}

View file

@ -0,0 +1,38 @@
package com.uwyn.rife2.gradle
import java.nio.file.FileSystems
import java.nio.file.Files
class PackagingTest extends AbstractFunctionalTest {
def setup() {
usesProject("minimal")
}
def "#archive contains compiled resources"() {
def jarFile = file(archive).toPath()
when:
run task
then: "compiles templates are found in the archive"
tasks {
succeeded ":${Rife2Plugin.PRECOMPILE_TEMPLATES_TASK_NAME}"
}
Files.exists(jarFile)
try (def fs = FileSystems.newFileSystem(jarFile, [:])) {
fs.getRootDirectories().each {
Files.walk(it).forEach { path ->
println path
}
}
assert Files.exists(fs.getPath("/rife/template/html/hello.class"))
assert Files.exists(fs.getPath("/rife/template/html/world.class"))
assert !Files.exists(fs.getPath("/templates/hello.html"))
assert !Files.exists(fs.getPath("/templates/world.html"))
}
where:
task | archive
'jar' | 'build/libs/hello-1.0.jar'
'uberJar' | 'build/libs/hello-uber-1.0.jar'
}
}

View file

@ -0,0 +1,81 @@
package com.uwyn.rife2.gradle
import groovy.transform.CompileStatic
@CompileStatic
class TeeWriter extends Writer {
private final Writer one
private final Writer two
static TeeWriter of(Writer one, Writer two) {
new TeeWriter(one, two)
}
private TeeWriter(Writer one, Writer two) {
this.one = one
this.two = two
}
@Override
void write(int c) throws IOException {
try {
one.write(c)
} finally {
two.write(c)
}
}
@Override
void write(char[] cbuf) throws IOException {
try {
one.write(cbuf)
} finally {
two.write(cbuf)
}
}
@Override
void write(char[] cbuf, int off, int len) throws IOException {
try {
one.write(cbuf, off, len)
} finally {
two.write(cbuf, off, len)
}
}
@Override
void write(String str) throws IOException {
try {
one.write(str)
} finally {
two.write(str)
}
}
@Override
void write(String str, int off, int len) throws IOException {
try {
one.write(str, off, len)
} finally {
two.write(str, off, len)
}
}
@Override
void flush() throws IOException {
try {
one.flush()
} finally {
two.flush()
}
}
@Override
void close() throws IOException {
try {
one.close()
} finally {
two.close()
}
}
}

View file

@ -0,0 +1,60 @@
package com.uwyn.rife2.gradle
class TemplateCompilationTest extends AbstractFunctionalTest {
def setup() {
usesProject("minimal")
}
def "doesn't precompile templates when calling `run`"() {
given:
buildFile << """
tasks.named("run") {
doFirst {
throw new RuntimeException("force stop")
}
}
"""
when:
fails 'run'
then: "precompile templates task must not be present in task graph"
errorOutputContains("force stop")
tasks {
doesNotContain ":${Rife2Plugin.PRECOMPILE_TEMPLATES_TASK_NAME}"
}
}
def "`run` task classpath includes template sources"() {
given:
buildFile << """
tasks.register("dumpRunClasspath") {
doLast {
def rootPath = rootProject.projectDir.toPath()
tasks.named("run").get().classpath.files.each {
println "Classpath entry: \${rootPath.relativize(it.toPath())}"
}
}
}
"""
when:
run("dumpRunClasspath")
then: "template sources must be present in the classpath"
outputContains("Classpath entry: src/main/templates")
outputContains("Classpath entry: src/main/resources/templates")
}
def "compiles templates when running #task"() {
when:
run task
then: "precompile templates task must be present in task graph"
tasks {
succeeded ":${Rife2Plugin.PRECOMPILE_TEMPLATES_TASK_NAME}"
}
where:
task << ['jar', 'test', 'uberJar']
}
}

17
gradle/libs.versions.toml Normal file
View file

@ -0,0 +1,17 @@
[versions]
jetty = "11.0.13"
jsoup = "1.15.3"
junit-jupiter = "5.9.1"
slf4j = "2.0.5"
spock = "2.3-groovy-3.0"
[libraries]
jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "jetty" }
jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "jetty" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
spock-core = { module = "org.spockframework:spock-core", version.ref = "spock" }
[bundles]
jetty = [ "jetty-server", "jetty-servlet" ]

View file

@ -1,4 +1,8 @@
pluginManagement { pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
includeBuild("build-logic") includeBuild("build-logic")
} }

View file

@ -2,10 +2,9 @@ plugins {
war war
} }
version = 1.0
base { base {
archivesName.set("hello") archivesName.set("hello")
version = 1.0
} }
repositories { repositories {