Skip to content

Commit 6086593

Browse files
authored
Add our JTE plugin to webtools (#10)
2 parents 9177962 + 6b658f3 commit 6086593

File tree

6 files changed

+291
-1
lines changed

6 files changed

+291
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Webtools releases
22

33
## [Unreleased]
4+
### Added
5+
- `com.diffplug.webtools.jte` plugin ([#10](https://github.com/diffplug/webtools/pull/10))
46

57
## [1.2.6] - 2025-08-22
68
### Fixed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- [node](#node) - hassle-free `npm install` and `npm run blah`
44
- [static server](#static-server) - a simple static file server
5+
- [jte](#jte) - creates idiomatic Kotlin model classes for `jte` templates (strict nullability & idiomatic collections and generics)
56

67
## Node
78

@@ -30,3 +31,38 @@ tasks.register('serve', com.diffplug.webtools.serve.StaticServerTask) {
3031
port = 8080 // by default
3132
}
3233
```
34+
35+
### JTE
36+
37+
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:
38+
39+
```jte
40+
// header.jte
41+
@param String title
42+
@param String createdAtAndBy
43+
@param Long idToImpersonateNullable
44+
@param String loginLinkNullable
45+
```
46+
47+
will turn into
48+
49+
```kotlin
50+
class header(
51+
val title: String,
52+
val createdAtAndBy: String,
53+
val idToImpersonateNullable: Long?,
54+
val loginLinkNullable: String?,
55+
) : common.JteModel {
56+
57+
override fun render(engine: TemplateEngine, output: TemplateOutput) {
58+
engine.render("pages/Admin/userShow/header.jte", mapOf(
59+
"title" to title,
60+
"createdAtAndBy" to createdAtAndBy,
61+
"idToImpersonateNullable" to idToImpersonateNullable,
62+
"loginLinkNullable" to loginLinkNullable,
63+
), output)
64+
}
65+
}
66+
```
67+
68+
We also translate Java collections and generics to their Kotlin equivalents. See `JteRenderer.convertJavaToKotlin` for details.

build.gradle

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,39 @@ apply from: 干.file('spotless/java.gradle')
1212
apply from: 干.file('base/maven.gradle')
1313
apply from: 干.file('base/sonatype.gradle')
1414

15+
def NEEDS_GLUE = ['jte']
16+
for (glue in NEEDS_GLUE) {
17+
sourceSets.register(glue) {
18+
compileClasspath += sourceSets.main.output
19+
runtimeClasspath += sourceSets.main.output
20+
java {}
21+
}
22+
}
23+
jar {
24+
for (glue in NEEDS_GLUE) {
25+
from sourceSets.getByName(glue).output.classesDirs
26+
}
27+
}
28+
29+
spotless {
30+
java {
31+
target 'src/**/*.java'
32+
}
33+
}
34+
1535
dependencies {
36+
// reflection for version decoupling
37+
implementation 'org.jooq:joor:0.9.15'
1638
// node.js
1739
api 'com.github.eirslett:frontend-maven-plugin:1.15.1'
1840
implementation 'com.diffplug.durian:durian-swt.os:5.0.1'
1941
// static file server
2042
String VER_JETTY = '11.0.25'
2143
api "org.eclipse.jetty:jetty-server:$VER_JETTY"
2244
api "org.eclipse.jetty:jetty-servlet:$VER_JETTY"
45+
// jte codegen
46+
String VER_JTE = '3.2.1'
47+
jteCompileOnly gradleApi()
48+
jteCompileOnly "gg.jte:jte-runtime:${VER_JTE}"
49+
jteCompileOnly "gg.jte:jte:${VER_JTE}"
2350
}

gradle.properties

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ org=diffplug
55
license=apache
66
git_url=github.com/diffplug/webtools
77
plugin_tags=node
8-
plugin_list=node
8+
plugin_list=node jte
99

1010
ver_java=17
1111

@@ -15,3 +15,8 @@ plugin_node_id=com.diffplug.webtools.node
1515
plugin_node_impl=com.diffplug.webtools.node.NodePlugin
1616
plugin_node_name=DiffPlug NodeJS
1717
plugin_node_desc=Runs `npm install` and `npm run xx` in an efficient way
18+
19+
plugin_jte_id=com.diffplug.webtools.jte
20+
plugin_jte_impl=com.diffplug.webtools.jte.JtePlugin
21+
plugin_jte_name=DiffPlug JTE
22+
plugin_jte_desc=Runs the JTE plugin and adds typesafe model classes
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright (C) 2025 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.webtools.jte;
17+
18+
import gg.jte.ContentType;
19+
import gg.jte.TemplateConfig;
20+
import gg.jte.compiler.TemplateParser;
21+
import gg.jte.compiler.TemplateParserVisitorAdapter;
22+
import gg.jte.compiler.TemplateType;
23+
import java.io.File;
24+
import java.io.IOException;
25+
import java.nio.charset.StandardCharsets;
26+
import java.nio.file.Files;
27+
import java.util.LinkedHashMap;
28+
import java.util.LinkedHashSet;
29+
import org.gradle.api.file.FileType;
30+
import org.gradle.work.ChangeType;
31+
import org.gradle.work.InputChanges;
32+
33+
public class JteRenderer {
34+
public static void renderTask(JtePlugin.RenderModelClassesTask task, InputChanges changes) throws IOException {
35+
var templateConfig = new TemplateConfig((ContentType) task.getContentType().get(), task.getPackageName().get());
36+
var renderer = new JteRenderer(task.getInputDir().getAsFile().get(), templateConfig);
37+
for (var change : changes.getFileChanges(task.getInputDir())) {
38+
if (change.getFileType() == FileType.DIRECTORY) {
39+
return;
40+
}
41+
String name = change.getFile().getName();
42+
if (!name.endsWith(".jte") && !name.endsWith(".kte")) {
43+
continue;
44+
}
45+
var targetFileJte = task.getOutputDir().file(change.getNormalizedPath()).get().getAsFile().getAbsolutePath();
46+
var targetFile = new File(targetFileJte.substring(0, targetFileJte.length() - 4) + ".kt");
47+
if (change.getChangeType() == ChangeType.REMOVED) {
48+
targetFile.delete();
49+
} else {
50+
targetFile.getParentFile().mkdirs();
51+
Files.write(targetFile.toPath(), renderer.render(change.getFile()).getBytes(StandardCharsets.UTF_8));
52+
}
53+
}
54+
}
55+
56+
static String convertJavaToKotlin(String javaType) {
57+
if (javaType.equals("boolean")) {
58+
return "Boolean";
59+
} else if (javaType.equals("int")) {
60+
return "Int";
61+
} else {
62+
// e.g. `@param Result<?> records` -> `val records: Result<*>`
63+
return javaType
64+
.replace("<?>", "<*>")
65+
.replace("java.util.Collection", "Collection")
66+
.replace("java.util.List", "List")
67+
.replace("java.util.Map", "Map")
68+
.replace("java.util.Set", "Set");
69+
}
70+
}
71+
72+
final File rootDir;
73+
final TemplateConfig config;
74+
75+
JteRenderer(File rootDir, TemplateConfig config) {
76+
this.rootDir = rootDir;
77+
this.config = config;
78+
}
79+
80+
String render(File file) throws IOException {
81+
var pkg = file.getParentFile().getAbsolutePath().substring(rootDir.getAbsolutePath().length() + 1).replace(File.separatorChar, '.');
82+
var name = file.getName();
83+
var lastDot = name.lastIndexOf('.');
84+
name = name.substring(0, lastDot);
85+
String ext = file.getName().substring(lastDot);
86+
87+
var imports = new LinkedHashSet<String>();
88+
imports.add("gg.jte.TemplateEngine");
89+
imports.add("gg.jte.TemplateOutput");
90+
var params = new LinkedHashMap<String, String>();
91+
92+
new TemplateParser(Files.readString(file.toPath()), TemplateType.Template, new TemplateParserVisitorAdapter() {
93+
@Override
94+
public void onImport(String importClass) {
95+
imports.add(importClass.replace("static ", ""));
96+
}
97+
98+
@Override
99+
public void onParam(String parameter) {
100+
var idxOfColon = parameter.indexOf(':');
101+
if (idxOfColon == -1) { // .jte
102+
// lastIndexOf accounts for valid multiple spaces, e.g `Map<String, String> featureMap`
103+
var spaceIdx = parameter.lastIndexOf(' ');
104+
var type = parameter.substring(0, spaceIdx).trim();
105+
var name = parameter.substring(spaceIdx + 1).trim();
106+
if (name.endsWith("Nullable")) {
107+
type += "?";
108+
}
109+
params.put(name, convertJavaToKotlin(type));
110+
} else { // .kte
111+
var name = parameter.substring(0, idxOfColon).trim();
112+
var type = parameter.substring(idxOfColon + 1).trim();
113+
params.put(name, type);
114+
}
115+
}
116+
}, config).parse();
117+
118+
var builder = new StringBuilder();
119+
builder.append("package " + pkg + "\n");
120+
builder.append("\n");
121+
for (var imp : imports) {
122+
builder.append("import " + imp + "\n");
123+
}
124+
builder.append("\n");
125+
builder.append("class " + name + "(\n");
126+
params.forEach((paramName, type) -> {
127+
builder.append("\tval " + paramName + ": " + type + ",\n");
128+
});
129+
builder.append("\t) : common.JteModel {\n");
130+
builder.append("\n");
131+
builder.append("\toverride fun render(engine: TemplateEngine, output: TemplateOutput) {\n");
132+
builder.append("\t\tengine.render(\"" + pkg.replace('.', '/') + "/" + name + ext + "\", mapOf(\n");
133+
params.forEach((paramName, type) -> {
134+
builder.append("\t\t\t\"" + paramName + "\" to " + paramName + ",\n");
135+
});
136+
builder.append("\t\t), output)\n");
137+
builder.append("\t}\n");
138+
builder.append("}");
139+
return builder.toString();
140+
}
141+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright (C) 2025 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.webtools.jte;
17+
18+
import static org.joor.Reflect.on;
19+
import static org.joor.Reflect.onClass;
20+
21+
import java.io.File;
22+
import java.io.IOException;
23+
import org.gradle.api.DefaultTask;
24+
import org.gradle.api.Plugin;
25+
import org.gradle.api.Project;
26+
import org.gradle.api.file.DirectoryProperty;
27+
import org.gradle.api.plugins.JavaPluginExtension;
28+
import org.gradle.api.provider.Property;
29+
import org.gradle.api.tasks.*;
30+
import org.gradle.work.Incremental;
31+
import org.gradle.work.InputChanges;
32+
33+
public class JtePlugin implements Plugin<Project> {
34+
@Override
35+
public void apply(Project project) {
36+
project.getPlugins().apply("gg.jte.gradle");
37+
project.getPlugins().apply("org.jetbrains.kotlin.jvm");
38+
var extension = on(project.getExtensions().getByType(onClass("gg.jte.gradle.JteExtension").type()));
39+
40+
JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class);
41+
SourceSet main = javaPluginExtension.getSourceSets().findByName("main");
42+
43+
project.getTasks().named("classes").configure(task -> {
44+
task.getInputs().dir(extension.call("getSourceDirectory"));
45+
});
46+
47+
@SuppressWarnings("unchecked")
48+
var jteModelsTask = project.getTasks().register("jteModels", RenderModelClassesTask.class, task -> {
49+
var jteModels = new File(project.getLayout().getBuildDirectory().getAsFile().get(), "jte-models");
50+
main.getJava().srcDir(jteModels);
51+
task.getOutputDir().set(jteModels);
52+
task.getInputDir().set((File) extension.call("getSourceDirectory").call("get").call("toFile").get());
53+
task.getPackageName().set((Property<String>) extension.call("getPackageName").get());
54+
task.getContentType().set((Property<Enum<?>>) extension.call("getContentType").get());
55+
});
56+
project.getTasks().named("compileKotlin").configure(task -> task.dependsOn(jteModelsTask));
57+
}
58+
59+
public static abstract class RenderModelClassesTask extends DefaultTask {
60+
@Incremental
61+
@PathSensitive(PathSensitivity.RELATIVE)
62+
@InputDirectory
63+
abstract DirectoryProperty getInputDir();
64+
65+
@OutputDirectory
66+
abstract DirectoryProperty getOutputDir();
67+
68+
@Input
69+
abstract Property<String> getPackageName();
70+
71+
@Input
72+
abstract Property<Enum<?>> getContentType();
73+
74+
@TaskAction
75+
public void render(InputChanges changes) throws IOException {
76+
onClass("com.diffplug.webtools.jte.JteRenderer").call("renderTask", this, changes);
77+
}
78+
}
79+
}

0 commit comments

Comments
 (0)