From dad5cf73dead938035449861d8a5f0053e0eadd5 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Sat, 23 Aug 2025 12:16:17 -0700 Subject: [PATCH 1/7] Add `JtePlugin` to webtools. --- build.gradle | 9 + gradle.properties | 7 +- .../com/diffplug/webtools/jte/JtePlugin.java | 201 ++++++++++++++++++ 3 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/diffplug/webtools/jte/JtePlugin.java diff --git a/build.gradle b/build.gradle index 5ef4b5e..cf9c98a 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,10 @@ apply from: 干.file('spotless/java.gradle') apply from: 干.file('base/maven.gradle') apply from: 干.file('base/sonatype.gradle') +repositories { + mavenCentral() + gradlePluginPortal() +} dependencies { // node.js api 'com.github.eirslett:frontend-maven-plugin:1.15.1' @@ -20,4 +24,9 @@ dependencies { String VER_JETTY = '11.0.25' api "org.eclipse.jetty:jetty-server:$VER_JETTY" api "org.eclipse.jetty:jetty-servlet:$VER_JETTY" + // jte codegen + String VER_JTE = '3.2.1' + api "gg.jte:jte-gradle-plugin:${VER_JTE}" + implementation "gg.jte:jte-runtime:${VER_JTE}" + implementation "gg.jte:jte:${VER_JTE}" } diff --git a/gradle.properties b/gradle.properties index 753b738..7693bdf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ org=diffplug license=apache git_url=github.com/diffplug/webtools plugin_tags=node -plugin_list=node +plugin_list=node jte ver_java=17 @@ -15,3 +15,8 @@ plugin_node_id=com.diffplug.webtools.node plugin_node_impl=com.diffplug.webtools.node.NodePlugin plugin_node_name=DiffPlug NodeJS plugin_node_desc=Runs `npm install` and `npm run xx` in an efficient way + +plugin_jte_id=com.diffplug.webtools.jte +plugin_jte_impl=com.diffplug.webtools.jte.JtePlugin +plugin_jte_name=DiffPlug JTE +plugin_jte_desc=Runs the JTE plugin and adds typesafe model classes diff --git a/src/main/java/com/diffplug/webtools/jte/JtePlugin.java b/src/main/java/com/diffplug/webtools/jte/JtePlugin.java new file mode 100644 index 0000000..e56a9ab --- /dev/null +++ b/src/main/java/com/diffplug/webtools/jte/JtePlugin.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2025 DiffPlug + * + * 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.diffplug.webtools.jte; + +import gg.jte.ContentType; +import gg.jte.TemplateConfig; +import gg.jte.compiler.TemplateParser; +import gg.jte.compiler.TemplateParserVisitorAdapter; +import gg.jte.compiler.TemplateType; +import gg.jte.gradle.JteExtension; +import gg.jte.gradle.JteGradle; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import org.gradle.api.DefaultTask; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.FileType; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputDirectory; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskAction; +import org.gradle.work.ChangeType; +import org.gradle.work.Incremental; +import org.gradle.work.InputChanges; + +public class JtePlugin implements Plugin { + @Override + public void apply(Project project) { + project.getPlugins().apply(JteGradle.class); + project.getPlugins().apply("org.jetbrains.kotlin.jvm"); + var extension = project.getExtensions().getByType(JteExtension.class); + extension.getBinaryStaticContent().set(true); + extension.precompile(); + + JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class); + SourceSet main = javaPluginExtension.getSourceSets().findByName("main"); + + project.getTasks().named("classes").configure(task -> { + task.getInputs().dir(extension.getSourceDirectory()); + }); + var jteModelsTask = project.getTasks().register("jteModels", RenderModelClasses.class, task -> { + var jteModels = new File(project.getLayout().getBuildDirectory().getAsFile().get(), "jte-models"); + main.getJava().srcDir(jteModels); + task.getOutputDir().set(jteModels); + task.getInputDir().set(extension.getSourceDirectory().get().toFile()); + task.getPackageName().set(extension.getPackageName()); + task.getContentType().set(extension.getContentType()); + }); + project.getTasks().named("compileKotlin").configure(task -> task.dependsOn(jteModelsTask)); + } + + public static abstract class RenderModelClasses extends DefaultTask { + @Incremental + @PathSensitive(PathSensitivity.RELATIVE) + @InputDirectory + abstract DirectoryProperty getInputDir(); + + @OutputDirectory + abstract DirectoryProperty getOutputDir(); + + @Input + abstract Property getPackageName(); + + @Input + abstract Property getContentType(); + + @TaskAction + public void render(InputChanges changes) throws IOException { + var templateConfig = new TemplateConfig(getContentType().get(), getPackageName().get()); + var renderer = new JteRenderer(getInputDir().getAsFile().get(), templateConfig); + for (var change : changes.getFileChanges(getInputDir())) { + if (change.getFileType() == FileType.DIRECTORY) { + return; + } + String name = change.getFile().getName(); + if (!name.endsWith(".jte") && !name.endsWith(".kte")) { + continue; + } + var targetFileJte = getOutputDir().file(change.getNormalizedPath()).get().getAsFile().getAbsolutePath(); + var targetFile = new File(targetFileJte.substring(0, targetFileJte.length() - 4) + ".kt"); + if (change.getChangeType() == ChangeType.REMOVED) { + targetFile.delete(); + } else { + targetFile.getParentFile().mkdirs(); + Files.write(targetFile.toPath(), renderer.render(change.getFile()).getBytes(StandardCharsets.UTF_8)); + } + } + } + } + + static String convertJavaToKotlin(String javaType) { + if (javaType.equals("boolean")) { + return "Boolean"; + } else if (javaType.equals("int")) { + return "Int"; + } else { + // e.g. `@param Result records` -> `val records: Result<*>` + return javaType + .replace("", "<*>") + .replace("java.util.Collection", "Collection") + .replace("java.util.List", "List") + .replace("java.util.Map", "Map") + .replace("java.util.Set", "Set"); + } + } + + static class JteRenderer { + final File rootDir; + final TemplateConfig config; + + JteRenderer(File rootDir, TemplateConfig config) { + this.rootDir = rootDir; + this.config = config; + } + + String render(File file) throws IOException { + var pkg = file.getParentFile().getAbsolutePath().substring(rootDir.getAbsolutePath().length() + 1).replace(File.separatorChar, '.'); + var name = file.getName(); + var lastDot = name.lastIndexOf('.'); + name = name.substring(0, lastDot); + String ext = file.getName().substring(lastDot); + + var imports = new LinkedHashSet(); + imports.add("gg.jte.TemplateEngine"); + imports.add("gg.jte.TemplateOutput"); + var params = new LinkedHashMap(); + + new TemplateParser(Files.readString(file.toPath()), TemplateType.Template, new TemplateParserVisitorAdapter() { + @Override + public void onImport(String importClass) { + imports.add(importClass.replace("static ", "")); + } + + @Override + public void onParam(String parameter) { + var idxOfColon = parameter.indexOf(':'); + if (idxOfColon == -1) { // .jte + // lastIndexOf accounts for valid multiple spaces, e.g `Map featureMap` + var spaceIdx = parameter.lastIndexOf(' '); + var type = parameter.substring(0, spaceIdx).trim(); + var name = parameter.substring(spaceIdx + 1).trim(); + if (name.endsWith("Nullable")) { + type += "?"; + } + params.put(name, convertJavaToKotlin(type)); + } else { // .kte + var name = parameter.substring(0, idxOfColon).trim(); + var type = parameter.substring(idxOfColon + 1).trim(); + params.put(name, type); + } + } + }, config).parse(); + + var builder = new StringBuilder(); + builder.append("package " + pkg + "\n"); + builder.append("\n"); + for (var imp : imports) { + builder.append("import " + imp + "\n"); + } + builder.append("\n"); + builder.append("class " + name + "(\n"); + params.forEach((paramName, type) -> { + builder.append("\tval " + paramName + ": " + type + ",\n"); + }); + builder.append("\t) : common.JteModel {\n"); + builder.append("\n"); + builder.append("\toverride fun render(engine: TemplateEngine, output: TemplateOutput) {\n"); + builder.append("\t\tengine.render(\"" + pkg.replace('.', '/') + "/" + name + ext + "\", mapOf(\n"); + params.forEach((paramName, type) -> { + builder.append("\t\t\t\"" + paramName + "\" to " + paramName + ",\n"); + }); + builder.append("\t\t), output)\n"); + builder.append("\t}\n"); + builder.append("}"); + return builder.toString(); + } + } +} From 317ae615fd7e617134253e2e45410956330dcc18 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Sat, 23 Aug 2025 12:32:40 -0700 Subject: [PATCH 2/7] Start to use reflection to decouple our version. --- build.gradle | 2 ++ src/main/java/com/diffplug/webtools/jte/JtePlugin.java | 10 ++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index cf9c98a..a5428a8 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,8 @@ repositories { gradlePluginPortal() } dependencies { + // reflection for version decoupling + implementation 'org.jooq:joor:0.9.15' // node.js api 'com.github.eirslett:frontend-maven-plugin:1.15.1' implementation 'com.diffplug.durian:durian-swt.os:5.0.1' diff --git a/src/main/java/com/diffplug/webtools/jte/JtePlugin.java b/src/main/java/com/diffplug/webtools/jte/JtePlugin.java index e56a9ab..a70db0f 100644 --- a/src/main/java/com/diffplug/webtools/jte/JtePlugin.java +++ b/src/main/java/com/diffplug/webtools/jte/JtePlugin.java @@ -15,13 +15,13 @@ */ package com.diffplug.webtools.jte; +import static org.joor.Reflect.onClass; + import gg.jte.ContentType; import gg.jte.TemplateConfig; import gg.jte.compiler.TemplateParser; import gg.jte.compiler.TemplateParserVisitorAdapter; import gg.jte.compiler.TemplateType; -import gg.jte.gradle.JteExtension; -import gg.jte.gradle.JteGradle; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -49,11 +49,9 @@ public class JtePlugin implements Plugin { @Override public void apply(Project project) { - project.getPlugins().apply(JteGradle.class); + project.getPlugins().apply("gg.jte.gradle"); project.getPlugins().apply("org.jetbrains.kotlin.jvm"); - var extension = project.getExtensions().getByType(JteExtension.class); - extension.getBinaryStaticContent().set(true); - extension.precompile(); + gg.jte.gradle.JteExtension extension = (gg.jte.gradle.JteExtension) project.getExtensions().getByType(onClass("gg.jte.gradle.JteExtension").type()); JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class); SourceSet main = javaPluginExtension.getSourceSets().findByName("main"); From 9670cc8c56cee8413674a5b89ab757e7dd459031 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Sat, 23 Aug 2025 12:53:51 -0700 Subject: [PATCH 3/7] Remove the compile-time dependency on the JTE Gradle plugin. --- build.gradle | 5 ----- .../java/com/diffplug/webtools/jte/JtePlugin.java | 12 +++++++----- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index a5428a8..c491313 100644 --- a/build.gradle +++ b/build.gradle @@ -12,10 +12,6 @@ apply from: 干.file('spotless/java.gradle') apply from: 干.file('base/maven.gradle') apply from: 干.file('base/sonatype.gradle') -repositories { - mavenCentral() - gradlePluginPortal() -} dependencies { // reflection for version decoupling implementation 'org.jooq:joor:0.9.15' @@ -28,7 +24,6 @@ dependencies { api "org.eclipse.jetty:jetty-servlet:$VER_JETTY" // jte codegen String VER_JTE = '3.2.1' - api "gg.jte:jte-gradle-plugin:${VER_JTE}" implementation "gg.jte:jte-runtime:${VER_JTE}" implementation "gg.jte:jte:${VER_JTE}" } diff --git a/src/main/java/com/diffplug/webtools/jte/JtePlugin.java b/src/main/java/com/diffplug/webtools/jte/JtePlugin.java index a70db0f..6d98ab9 100644 --- a/src/main/java/com/diffplug/webtools/jte/JtePlugin.java +++ b/src/main/java/com/diffplug/webtools/jte/JtePlugin.java @@ -15,6 +15,7 @@ */ package com.diffplug.webtools.jte; +import static org.joor.Reflect.on; import static org.joor.Reflect.onClass; import gg.jte.ContentType; @@ -51,21 +52,22 @@ public class JtePlugin implements Plugin { public void apply(Project project) { project.getPlugins().apply("gg.jte.gradle"); project.getPlugins().apply("org.jetbrains.kotlin.jvm"); - gg.jte.gradle.JteExtension extension = (gg.jte.gradle.JteExtension) project.getExtensions().getByType(onClass("gg.jte.gradle.JteExtension").type()); + var extension = on(project.getExtensions().getByType(onClass("gg.jte.gradle.JteExtension").type())); JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class); SourceSet main = javaPluginExtension.getSourceSets().findByName("main"); project.getTasks().named("classes").configure(task -> { - task.getInputs().dir(extension.getSourceDirectory()); + task.getInputs().dir(extension.call("getSourceDirectory")); }); + var jteModelsTask = project.getTasks().register("jteModels", RenderModelClasses.class, task -> { var jteModels = new File(project.getLayout().getBuildDirectory().getAsFile().get(), "jte-models"); main.getJava().srcDir(jteModels); task.getOutputDir().set(jteModels); - task.getInputDir().set(extension.getSourceDirectory().get().toFile()); - task.getPackageName().set(extension.getPackageName()); - task.getContentType().set(extension.getContentType()); + task.getInputDir().set((File) extension.call("getSourceDirectory").call("get").call("toFile").get()); + task.getPackageName().set((Property) extension.call("getPackageName").get()); + task.getContentType().set((Property) extension.call("getContentType").get()); }); project.getTasks().named("compileKotlin").configure(task -> task.dependsOn(jteModelsTask)); } From 25ebe5b2bc5e68f7ae55342a26e07e99916d837b Mon Sep 17 00:00:00 2001 From: ntwigg Date: Sat, 23 Aug 2025 13:12:10 -0700 Subject: [PATCH 4/7] Extract JTE out into a compile-only section. --- build.gradle | 27 +++- .../diffplug/webtools/jte/JteRenderer.java | 141 ++++++++++++++++++ .../com/diffplug/webtools/jte/JtePlugin.java | 136 +---------------- 3 files changed, 173 insertions(+), 131 deletions(-) create mode 100644 src/jte/java/com/diffplug/webtools/jte/JteRenderer.java diff --git a/build.gradle b/build.gradle index c491313..a7553c9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.diffplug.blowdryer' id 'com.diffplug.spotless-changelog' + // id 'io.github.davidburstrom.version-compatibility' version '0.5.0' // https://github.com/davidburstrom/version-compatibility-gradle-plugin?tab=readme-ov-file#releases } apply from: 干.file('base/java.gradle') @@ -12,6 +13,26 @@ apply from: 干.file('spotless/java.gradle') apply from: 干.file('base/maven.gradle') apply from: 干.file('base/sonatype.gradle') +def NEEDS_GLUE = ['jte'] +for (glue in NEEDS_GLUE) { + sourceSets.register(glue) { + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output + java {} + } +} +jar { + for (glue in NEEDS_GLUE) { + from sourceSets.getByName(glue).output.classesDirs + } +} + +spotless { + java { + target 'src/**/*.java' + } +} + dependencies { // reflection for version decoupling implementation 'org.jooq:joor:0.9.15' @@ -24,6 +45,8 @@ dependencies { api "org.eclipse.jetty:jetty-servlet:$VER_JETTY" // jte codegen String VER_JTE = '3.2.1' - implementation "gg.jte:jte-runtime:${VER_JTE}" - implementation "gg.jte:jte:${VER_JTE}" + jteCompileOnly gradleApi() + jteCompileOnly "gg.jte:jte-runtime:${VER_JTE}" + jteCompileOnly "gg.jte:jte:${VER_JTE}" } + diff --git a/src/jte/java/com/diffplug/webtools/jte/JteRenderer.java b/src/jte/java/com/diffplug/webtools/jte/JteRenderer.java new file mode 100644 index 0000000..f039d7b --- /dev/null +++ b/src/jte/java/com/diffplug/webtools/jte/JteRenderer.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2025 DiffPlug + * + * 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.diffplug.webtools.jte; + +import gg.jte.ContentType; +import gg.jte.TemplateConfig; +import gg.jte.compiler.TemplateParser; +import gg.jte.compiler.TemplateParserVisitorAdapter; +import gg.jte.compiler.TemplateType; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import org.gradle.api.file.FileType; +import org.gradle.work.ChangeType; +import org.gradle.work.InputChanges; + +public class JteRenderer { + public static void renderTask(JtePlugin.RenderModelClassesTask task, InputChanges changes) throws IOException { + var templateConfig = new TemplateConfig((ContentType) task.getContentType().get(), task.getPackageName().get()); + var renderer = new JteRenderer(task.getInputDir().getAsFile().get(), templateConfig); + for (var change : changes.getFileChanges(task.getInputDir())) { + if (change.getFileType() == FileType.DIRECTORY) { + return; + } + String name = change.getFile().getName(); + if (!name.endsWith(".jte") && !name.endsWith(".kte")) { + continue; + } + var targetFileJte = task.getOutputDir().file(change.getNormalizedPath()).get().getAsFile().getAbsolutePath(); + var targetFile = new File(targetFileJte.substring(0, targetFileJte.length() - 4) + ".kt"); + if (change.getChangeType() == ChangeType.REMOVED) { + targetFile.delete(); + } else { + targetFile.getParentFile().mkdirs(); + Files.write(targetFile.toPath(), renderer.render(change.getFile()).getBytes(StandardCharsets.UTF_8)); + } + } + } + + static String convertJavaToKotlin(String javaType) { + if (javaType.equals("boolean")) { + return "Boolean"; + } else if (javaType.equals("int")) { + return "Int"; + } else { + // e.g. `@param Result records` -> `val records: Result<*>` + return javaType + .replace("", "<*>") + .replace("java.util.Collection", "Collection") + .replace("java.util.List", "List") + .replace("java.util.Map", "Map") + .replace("java.util.Set", "Set"); + } + } + + final File rootDir; + final TemplateConfig config; + + JteRenderer(File rootDir, TemplateConfig config) { + this.rootDir = rootDir; + this.config = config; + } + + String render(File file) throws IOException { + var pkg = file.getParentFile().getAbsolutePath().substring(rootDir.getAbsolutePath().length() + 1).replace(File.separatorChar, '.'); + var name = file.getName(); + var lastDot = name.lastIndexOf('.'); + name = name.substring(0, lastDot); + String ext = file.getName().substring(lastDot); + + var imports = new LinkedHashSet(); + imports.add("gg.jte.TemplateEngine"); + imports.add("gg.jte.TemplateOutput"); + var params = new LinkedHashMap(); + + new TemplateParser(Files.readString(file.toPath()), TemplateType.Template, new TemplateParserVisitorAdapter() { + @Override + public void onImport(String importClass) { + imports.add(importClass.replace("static ", "")); + } + + @Override + public void onParam(String parameter) { + var idxOfColon = parameter.indexOf(':'); + if (idxOfColon == -1) { // .jte + // lastIndexOf accounts for valid multiple spaces, e.g `Map featureMap` + var spaceIdx = parameter.lastIndexOf(' '); + var type = parameter.substring(0, spaceIdx).trim(); + var name = parameter.substring(spaceIdx + 1).trim(); + if (name.endsWith("Nullable")) { + type += "?"; + } + params.put(name, convertJavaToKotlin(type)); + } else { // .kte + var name = parameter.substring(0, idxOfColon).trim(); + var type = parameter.substring(idxOfColon + 1).trim(); + params.put(name, type); + } + } + }, config).parse(); + + var builder = new StringBuilder(); + builder.append("package " + pkg + "\n"); + builder.append("\n"); + for (var imp : imports) { + builder.append("import " + imp + "\n"); + } + builder.append("\n"); + builder.append("class " + name + "(\n"); + params.forEach((paramName, type) -> { + builder.append("\tval " + paramName + ": " + type + ",\n"); + }); + builder.append("\t) : common.JteModel {\n"); + builder.append("\n"); + builder.append("\toverride fun render(engine: TemplateEngine, output: TemplateOutput) {\n"); + builder.append("\t\tengine.render(\"" + pkg.replace('.', '/') + "/" + name + ext + "\", mapOf(\n"); + params.forEach((paramName, type) -> { + builder.append("\t\t\t\"" + paramName + "\" to " + paramName + ",\n"); + }); + builder.append("\t\t), output)\n"); + builder.append("\t}\n"); + builder.append("}"); + return builder.toString(); + } +} diff --git a/src/main/java/com/diffplug/webtools/jte/JtePlugin.java b/src/main/java/com/diffplug/webtools/jte/JtePlugin.java index 6d98ab9..f5c33ad 100644 --- a/src/main/java/com/diffplug/webtools/jte/JtePlugin.java +++ b/src/main/java/com/diffplug/webtools/jte/JtePlugin.java @@ -18,32 +18,15 @@ import static org.joor.Reflect.on; import static org.joor.Reflect.onClass; -import gg.jte.ContentType; -import gg.jte.TemplateConfig; -import gg.jte.compiler.TemplateParser; -import gg.jte.compiler.TemplateParserVisitorAdapter; -import gg.jte.compiler.TemplateType; import java.io.File; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import org.gradle.api.DefaultTask; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.file.DirectoryProperty; -import org.gradle.api.file.FileType; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.provider.Property; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.InputDirectory; -import org.gradle.api.tasks.OutputDirectory; -import org.gradle.api.tasks.PathSensitive; -import org.gradle.api.tasks.PathSensitivity; -import org.gradle.api.tasks.SourceSet; -import org.gradle.api.tasks.TaskAction; -import org.gradle.work.ChangeType; +import org.gradle.api.tasks.*; import org.gradle.work.Incremental; import org.gradle.work.InputChanges; @@ -61,18 +44,19 @@ public void apply(Project project) { task.getInputs().dir(extension.call("getSourceDirectory")); }); - var jteModelsTask = project.getTasks().register("jteModels", RenderModelClasses.class, task -> { + @SuppressWarnings("unchecked") + var jteModelsTask = project.getTasks().register("jteModels", RenderModelClassesTask.class, task -> { var jteModels = new File(project.getLayout().getBuildDirectory().getAsFile().get(), "jte-models"); main.getJava().srcDir(jteModels); task.getOutputDir().set(jteModels); task.getInputDir().set((File) extension.call("getSourceDirectory").call("get").call("toFile").get()); task.getPackageName().set((Property) extension.call("getPackageName").get()); - task.getContentType().set((Property) extension.call("getContentType").get()); + task.getContentType().set((Property>) extension.call("getContentType").get()); }); project.getTasks().named("compileKotlin").configure(task -> task.dependsOn(jteModelsTask)); } - public static abstract class RenderModelClasses extends DefaultTask { + public static abstract class RenderModelClassesTask extends DefaultTask { @Incremental @PathSensitive(PathSensitivity.RELATIVE) @InputDirectory @@ -85,117 +69,11 @@ public static abstract class RenderModelClasses extends DefaultTask { abstract Property getPackageName(); @Input - abstract Property getContentType(); + abstract Property> getContentType(); @TaskAction public void render(InputChanges changes) throws IOException { - var templateConfig = new TemplateConfig(getContentType().get(), getPackageName().get()); - var renderer = new JteRenderer(getInputDir().getAsFile().get(), templateConfig); - for (var change : changes.getFileChanges(getInputDir())) { - if (change.getFileType() == FileType.DIRECTORY) { - return; - } - String name = change.getFile().getName(); - if (!name.endsWith(".jte") && !name.endsWith(".kte")) { - continue; - } - var targetFileJte = getOutputDir().file(change.getNormalizedPath()).get().getAsFile().getAbsolutePath(); - var targetFile = new File(targetFileJte.substring(0, targetFileJte.length() - 4) + ".kt"); - if (change.getChangeType() == ChangeType.REMOVED) { - targetFile.delete(); - } else { - targetFile.getParentFile().mkdirs(); - Files.write(targetFile.toPath(), renderer.render(change.getFile()).getBytes(StandardCharsets.UTF_8)); - } - } - } - } - - static String convertJavaToKotlin(String javaType) { - if (javaType.equals("boolean")) { - return "Boolean"; - } else if (javaType.equals("int")) { - return "Int"; - } else { - // e.g. `@param Result records` -> `val records: Result<*>` - return javaType - .replace("", "<*>") - .replace("java.util.Collection", "Collection") - .replace("java.util.List", "List") - .replace("java.util.Map", "Map") - .replace("java.util.Set", "Set"); - } - } - - static class JteRenderer { - final File rootDir; - final TemplateConfig config; - - JteRenderer(File rootDir, TemplateConfig config) { - this.rootDir = rootDir; - this.config = config; - } - - String render(File file) throws IOException { - var pkg = file.getParentFile().getAbsolutePath().substring(rootDir.getAbsolutePath().length() + 1).replace(File.separatorChar, '.'); - var name = file.getName(); - var lastDot = name.lastIndexOf('.'); - name = name.substring(0, lastDot); - String ext = file.getName().substring(lastDot); - - var imports = new LinkedHashSet(); - imports.add("gg.jte.TemplateEngine"); - imports.add("gg.jte.TemplateOutput"); - var params = new LinkedHashMap(); - - new TemplateParser(Files.readString(file.toPath()), TemplateType.Template, new TemplateParserVisitorAdapter() { - @Override - public void onImport(String importClass) { - imports.add(importClass.replace("static ", "")); - } - - @Override - public void onParam(String parameter) { - var idxOfColon = parameter.indexOf(':'); - if (idxOfColon == -1) { // .jte - // lastIndexOf accounts for valid multiple spaces, e.g `Map featureMap` - var spaceIdx = parameter.lastIndexOf(' '); - var type = parameter.substring(0, spaceIdx).trim(); - var name = parameter.substring(spaceIdx + 1).trim(); - if (name.endsWith("Nullable")) { - type += "?"; - } - params.put(name, convertJavaToKotlin(type)); - } else { // .kte - var name = parameter.substring(0, idxOfColon).trim(); - var type = parameter.substring(idxOfColon + 1).trim(); - params.put(name, type); - } - } - }, config).parse(); - - var builder = new StringBuilder(); - builder.append("package " + pkg + "\n"); - builder.append("\n"); - for (var imp : imports) { - builder.append("import " + imp + "\n"); - } - builder.append("\n"); - builder.append("class " + name + "(\n"); - params.forEach((paramName, type) -> { - builder.append("\tval " + paramName + ": " + type + ",\n"); - }); - builder.append("\t) : common.JteModel {\n"); - builder.append("\n"); - builder.append("\toverride fun render(engine: TemplateEngine, output: TemplateOutput) {\n"); - builder.append("\t\tengine.render(\"" + pkg.replace('.', '/') + "/" + name + ext + "\", mapOf(\n"); - params.forEach((paramName, type) -> { - builder.append("\t\t\t\"" + paramName + "\" to " + paramName + ",\n"); - }); - builder.append("\t\t), output)\n"); - builder.append("\t}\n"); - builder.append("}"); - return builder.toString(); + onClass("com.diffplug.webtools.jte.JteRenderer").call("renderTask", this, changes); } } } From 8c9bb1bd4be179669636bb94641fa49d070ff476 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Sat, 23 Aug 2025 13:29:09 -0700 Subject: [PATCH 5/7] Document the JTE plugin. --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index 9c53a69..1597b6e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ - [node](#node) - hassle-free `npm install` and `npm run blah` - [static server](#static-server) - a simple static file server +- [jte](#jte) - creates idiomatic Kotlin model classes for `jte` templates (strict nullability & idiomatic collections and generics) ## Node @@ -30,3 +31,38 @@ tasks.register('serve', com.diffplug.webtools.serve.StaticServerTask) { port = 8080 // by default } ``` + +### JTE + +You have to apply `gg.jte.gradle` plugin yourself. We add a task called `jteModels` which creates a Kotlin model classes with strict nullability. Like so: + +```jte +// header.jte +@param String title +@param String createdAtAndBy +@param Long idToImpersonateNullable +@param String loginLinkNullable +``` + +will turn into + +```kotlin +class header( + val title: String, + val createdAtAndBy: String, + val idToImpersonateNullable: Long?, + val loginLinkNullable: String?, + ) : common.JteModel { + + override fun render(engine: TemplateEngine, output: TemplateOutput) { + engine.render("pages/Admin/userShow/header.jte", mapOf( + "title" to title, + "createdAtAndBy" to createdAtAndBy, + "idToImpersonateNullable" to idToImpersonateNullable, + "loginLinkNullable" to loginLinkNullable, + ), output) + } +} +``` + +We also translate Java collections and generics to their Kotlin equivalents. See `JteRenderer.convertJavaToKotlin` for details. From d1ac3d48d732b183d039ce6c20419dbf8415c20e Mon Sep 17 00:00:00 2001 From: ntwigg Date: Sat, 23 Aug 2025 13:35:03 -0700 Subject: [PATCH 6/7] Minor cleanup. --- build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/build.gradle b/build.gradle index a7553c9..a7bb9b3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,6 @@ plugins { id 'com.diffplug.blowdryer' id 'com.diffplug.spotless-changelog' - // id 'io.github.davidburstrom.version-compatibility' version '0.5.0' // https://github.com/davidburstrom/version-compatibility-gradle-plugin?tab=readme-ov-file#releases } apply from: 干.file('base/java.gradle') @@ -49,4 +48,3 @@ dependencies { jteCompileOnly "gg.jte:jte-runtime:${VER_JTE}" jteCompileOnly "gg.jte:jte:${VER_JTE}" } - From 6b658f3cf9ba346ff1f7c96caf938ae2f19ac54f Mon Sep 17 00:00:00 2001 From: ntwigg Date: Sat, 23 Aug 2025 13:40:25 -0700 Subject: [PATCH 7/7] Update changelog. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bd65a1..dcd138c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Webtools releases ## [Unreleased] +### Added +- `com.diffplug.webtools.jte` plugin ([#10](https://github.com/diffplug/webtools/pull/10)) ## [1.2.6] - 2025-08-22 ### Fixed