Skip to content

Commit dad5cf7

Browse files
author
ntwigg
committed
Add JtePlugin to webtools.
1 parent 9177962 commit dad5cf7

File tree

3 files changed

+216
-1
lines changed

3 files changed

+216
-1
lines changed

build.gradle

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

15+
repositories {
16+
mavenCentral()
17+
gradlePluginPortal()
18+
}
1519
dependencies {
1620
// node.js
1721
api 'com.github.eirslett:frontend-maven-plugin:1.15.1'
@@ -20,4 +24,9 @@ dependencies {
2024
String VER_JETTY = '11.0.25'
2125
api "org.eclipse.jetty:jetty-server:$VER_JETTY"
2226
api "org.eclipse.jetty:jetty-servlet:$VER_JETTY"
27+
// jte codegen
28+
String VER_JTE = '3.2.1'
29+
api "gg.jte:jte-gradle-plugin:${VER_JTE}"
30+
implementation "gg.jte:jte-runtime:${VER_JTE}"
31+
implementation "gg.jte:jte:${VER_JTE}"
2332
}

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: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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 gg.jte.gradle.JteExtension;
24+
import gg.jte.gradle.JteGradle;
25+
import java.io.File;
26+
import java.io.IOException;
27+
import java.nio.charset.StandardCharsets;
28+
import java.nio.file.Files;
29+
import java.util.LinkedHashMap;
30+
import java.util.LinkedHashSet;
31+
import org.gradle.api.DefaultTask;
32+
import org.gradle.api.Plugin;
33+
import org.gradle.api.Project;
34+
import org.gradle.api.file.DirectoryProperty;
35+
import org.gradle.api.file.FileType;
36+
import org.gradle.api.plugins.JavaPluginExtension;
37+
import org.gradle.api.provider.Property;
38+
import org.gradle.api.tasks.Input;
39+
import org.gradle.api.tasks.InputDirectory;
40+
import org.gradle.api.tasks.OutputDirectory;
41+
import org.gradle.api.tasks.PathSensitive;
42+
import org.gradle.api.tasks.PathSensitivity;
43+
import org.gradle.api.tasks.SourceSet;
44+
import org.gradle.api.tasks.TaskAction;
45+
import org.gradle.work.ChangeType;
46+
import org.gradle.work.Incremental;
47+
import org.gradle.work.InputChanges;
48+
49+
public class JtePlugin implements Plugin<Project> {
50+
@Override
51+
public void apply(Project project) {
52+
project.getPlugins().apply(JteGradle.class);
53+
project.getPlugins().apply("org.jetbrains.kotlin.jvm");
54+
var extension = project.getExtensions().getByType(JteExtension.class);
55+
extension.getBinaryStaticContent().set(true);
56+
extension.precompile();
57+
58+
JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class);
59+
SourceSet main = javaPluginExtension.getSourceSets().findByName("main");
60+
61+
project.getTasks().named("classes").configure(task -> {
62+
task.getInputs().dir(extension.getSourceDirectory());
63+
});
64+
var jteModelsTask = project.getTasks().register("jteModels", RenderModelClasses.class, task -> {
65+
var jteModels = new File(project.getLayout().getBuildDirectory().getAsFile().get(), "jte-models");
66+
main.getJava().srcDir(jteModels);
67+
task.getOutputDir().set(jteModels);
68+
task.getInputDir().set(extension.getSourceDirectory().get().toFile());
69+
task.getPackageName().set(extension.getPackageName());
70+
task.getContentType().set(extension.getContentType());
71+
});
72+
project.getTasks().named("compileKotlin").configure(task -> task.dependsOn(jteModelsTask));
73+
}
74+
75+
public static abstract class RenderModelClasses extends DefaultTask {
76+
@Incremental
77+
@PathSensitive(PathSensitivity.RELATIVE)
78+
@InputDirectory
79+
abstract DirectoryProperty getInputDir();
80+
81+
@OutputDirectory
82+
abstract DirectoryProperty getOutputDir();
83+
84+
@Input
85+
abstract Property<String> getPackageName();
86+
87+
@Input
88+
abstract Property<ContentType> getContentType();
89+
90+
@TaskAction
91+
public void render(InputChanges changes) throws IOException {
92+
var templateConfig = new TemplateConfig(getContentType().get(), getPackageName().get());
93+
var renderer = new JteRenderer(getInputDir().getAsFile().get(), templateConfig);
94+
for (var change : changes.getFileChanges(getInputDir())) {
95+
if (change.getFileType() == FileType.DIRECTORY) {
96+
return;
97+
}
98+
String name = change.getFile().getName();
99+
if (!name.endsWith(".jte") && !name.endsWith(".kte")) {
100+
continue;
101+
}
102+
var targetFileJte = getOutputDir().file(change.getNormalizedPath()).get().getAsFile().getAbsolutePath();
103+
var targetFile = new File(targetFileJte.substring(0, targetFileJte.length() - 4) + ".kt");
104+
if (change.getChangeType() == ChangeType.REMOVED) {
105+
targetFile.delete();
106+
} else {
107+
targetFile.getParentFile().mkdirs();
108+
Files.write(targetFile.toPath(), renderer.render(change.getFile()).getBytes(StandardCharsets.UTF_8));
109+
}
110+
}
111+
}
112+
}
113+
114+
static String convertJavaToKotlin(String javaType) {
115+
if (javaType.equals("boolean")) {
116+
return "Boolean";
117+
} else if (javaType.equals("int")) {
118+
return "Int";
119+
} else {
120+
// e.g. `@param Result<?> records` -> `val records: Result<*>`
121+
return javaType
122+
.replace("<?>", "<*>")
123+
.replace("java.util.Collection", "Collection")
124+
.replace("java.util.List", "List")
125+
.replace("java.util.Map", "Map")
126+
.replace("java.util.Set", "Set");
127+
}
128+
}
129+
130+
static class JteRenderer {
131+
final File rootDir;
132+
final TemplateConfig config;
133+
134+
JteRenderer(File rootDir, TemplateConfig config) {
135+
this.rootDir = rootDir;
136+
this.config = config;
137+
}
138+
139+
String render(File file) throws IOException {
140+
var pkg = file.getParentFile().getAbsolutePath().substring(rootDir.getAbsolutePath().length() + 1).replace(File.separatorChar, '.');
141+
var name = file.getName();
142+
var lastDot = name.lastIndexOf('.');
143+
name = name.substring(0, lastDot);
144+
String ext = file.getName().substring(lastDot);
145+
146+
var imports = new LinkedHashSet<String>();
147+
imports.add("gg.jte.TemplateEngine");
148+
imports.add("gg.jte.TemplateOutput");
149+
var params = new LinkedHashMap<String, String>();
150+
151+
new TemplateParser(Files.readString(file.toPath()), TemplateType.Template, new TemplateParserVisitorAdapter() {
152+
@Override
153+
public void onImport(String importClass) {
154+
imports.add(importClass.replace("static ", ""));
155+
}
156+
157+
@Override
158+
public void onParam(String parameter) {
159+
var idxOfColon = parameter.indexOf(':');
160+
if (idxOfColon == -1) { // .jte
161+
// lastIndexOf accounts for valid multiple spaces, e.g `Map<String, String> featureMap`
162+
var spaceIdx = parameter.lastIndexOf(' ');
163+
var type = parameter.substring(0, spaceIdx).trim();
164+
var name = parameter.substring(spaceIdx + 1).trim();
165+
if (name.endsWith("Nullable")) {
166+
type += "?";
167+
}
168+
params.put(name, convertJavaToKotlin(type));
169+
} else { // .kte
170+
var name = parameter.substring(0, idxOfColon).trim();
171+
var type = parameter.substring(idxOfColon + 1).trim();
172+
params.put(name, type);
173+
}
174+
}
175+
}, config).parse();
176+
177+
var builder = new StringBuilder();
178+
builder.append("package " + pkg + "\n");
179+
builder.append("\n");
180+
for (var imp : imports) {
181+
builder.append("import " + imp + "\n");
182+
}
183+
builder.append("\n");
184+
builder.append("class " + name + "(\n");
185+
params.forEach((paramName, type) -> {
186+
builder.append("\tval " + paramName + ": " + type + ",\n");
187+
});
188+
builder.append("\t) : common.JteModel {\n");
189+
builder.append("\n");
190+
builder.append("\toverride fun render(engine: TemplateEngine, output: TemplateOutput) {\n");
191+
builder.append("\t\tengine.render(\"" + pkg.replace('.', '/') + "/" + name + ext + "\", mapOf(\n");
192+
params.forEach((paramName, type) -> {
193+
builder.append("\t\t\t\"" + paramName + "\" to " + paramName + ",\n");
194+
});
195+
builder.append("\t\t), output)\n");
196+
builder.append("\t}\n");
197+
builder.append("}");
198+
return builder.toString();
199+
}
200+
}
201+
}

0 commit comments

Comments
 (0)