Skip to content

Commit 95d9509

Browse files
authored
[Jex generation] Add initial support for htmx (#520)
Currently, it does not handle the case of common routing entry (for normal processing and hx).
1 parent 46956db commit 95d9509

File tree

11 files changed

+305
-34
lines changed

11 files changed

+305
-34
lines changed

http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java

+143-29
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
package io.avaje.http.generator.jex;
22

3-
import static io.avaje.http.generator.core.ProcessingContext.isAssignable2Interface;
4-
import static io.avaje.http.generator.core.ProcessingContext.logError;
5-
import static io.avaje.http.generator.core.ProcessingContext.platform;
6-
7-
import java.io.IOException;
83
import java.util.List;
94

105
import io.avaje.http.generator.core.*;
11-
import io.avaje.http.generator.core.openapi.MediaType;
6+
7+
import javax.lang.model.type.TypeMirror;
8+
9+
import static io.avaje.http.generator.core.ProcessingContext.*;
1210

1311
/**
1412
* Write code to register Web route for a given controller method.
@@ -17,13 +15,15 @@ class ControllerMethodWriter {
1715

1816
private final MethodReader method;
1917
private final Append writer;
18+
private final ControllerReader reader;
2019
private final WebMethod webMethod;
2120
private final boolean instrumentContext;
2221
private final boolean isFilter;
2322

24-
ControllerMethodWriter(MethodReader method, Append writer) {
23+
ControllerMethodWriter(MethodReader method, Append writer, ControllerReader reader) {
2524
this.method = method;
2625
this.writer = writer;
26+
this.reader = reader;
2727
this.webMethod = method.webMethod();
2828
this.instrumentContext = method.instrumentContext();
2929
this.isFilter = webMethod == CoreWebMethod.FILTER;
@@ -47,11 +47,19 @@ void writeRouting() {
4747
} else if (isFilter) {
4848
writer.append(" routing.filter(this::_%s)", method.simpleName());
4949
} else {
50-
writer.append(
51-
" routing.%s(\"%s\", this::_%s)",
52-
webMethod.name().toLowerCase(), fullPath, method.simpleName());
50+
writer.append(" routing.%s(\"%s\", ", webMethod.name().toLowerCase(), fullPath);
51+
var hxRequest = method.hxRequest();
52+
if (hxRequest != null) {
53+
writeHxHandler(hxRequest);
54+
} else {
55+
writer.append("this::_%s)", method.simpleName());
56+
}
5357
}
58+
writeRoles();
59+
writer.append(";").eol();
60+
}
5461

62+
private void writeRoles() {
5563
List<String> roles = method.roles();
5664
if (!roles.isEmpty()) {
5765
writer.append(".withRoles(");
@@ -63,11 +71,88 @@ void writeRouting() {
6371
}
6472
writer.append(")");
6573
}
66-
writer.append(";").eol();
6774
}
6875

69-
void writeHandler(boolean requestScoped) {
76+
private void writeHxHandler(HxRequestPrism hxRequest) {
77+
writer.append("HxHandler.builder(this::_%s)", method.simpleName());
78+
if (hasValue(hxRequest.target())) {
79+
writer.append(".target(\"%s\")", hxRequest.target());
80+
}
81+
if (hasValue(hxRequest.triggerId())) {
82+
writer.append(".trigger(\"%s\")", hxRequest.triggerId());
83+
} else if (hasValue(hxRequest.value())) {
84+
writer.append(".trigger(\"%s\")", hxRequest.value());
85+
}
86+
if (hasValue(hxRequest.triggerName())) {
87+
writer.append(".triggerName(\"%s\")", hxRequest.triggerName());
88+
} else if (hasValue(hxRequest.value())) {
89+
writer.append(".triggerName(\"%s\")", hxRequest.value());
90+
}
91+
writer.append(".build())");
92+
}
7093

94+
private static boolean hasValue(String value) {
95+
return value != null && !value.isBlank();
96+
}
97+
98+
enum ResponseMode {
99+
Void,
100+
Json,
101+
Text,
102+
Templating,
103+
InputStream,
104+
Other
105+
}
106+
107+
ResponseMode responseMode() {
108+
if (method.isVoid() || isFilter) {
109+
return ResponseMode.Void;
110+
}
111+
if (isInputStream(method.returnType())) {
112+
return ResponseMode.InputStream;
113+
}
114+
if (producesJson()) {
115+
return ResponseMode.Json;
116+
}
117+
if (useTemplating()) {
118+
return ResponseMode.Templating;
119+
}
120+
if (producesText()) {
121+
return ResponseMode.Text;
122+
}
123+
return ResponseMode.Other;
124+
}
125+
126+
private boolean isInputStream(TypeMirror type) {
127+
return isAssignable2Interface(type.toString(), "java.io.InputStream");
128+
}
129+
130+
private boolean producesJson() {
131+
return // useJsonB
132+
!disabledDirectWrites()
133+
&& !"byte[]".equals(method.returnType().toString())
134+
&& (method.produces() == null || method.produces().toLowerCase().contains("json"));
135+
}
136+
137+
private boolean producesText() {
138+
return (method.produces() != null && method.produces().toLowerCase().contains("text"));
139+
}
140+
141+
private boolean useContentCache() {
142+
return method.hasContentCache();
143+
}
144+
145+
private boolean useTemplating() {
146+
return reader.html()
147+
&& !"byte[]".equals(method.returnType().toString())
148+
&& (method.produces() == null || method.produces().toLowerCase().contains("html"));
149+
}
150+
151+
private boolean usesFormParams() {
152+
return method.params().stream().anyMatch(p -> p.isForm() || ParamType.FORMPARAM.equals(p.paramType()));
153+
}
154+
155+
void writeHandler(boolean requestScoped) {
71156
if (method.isErrorMethod()) {
72157
writer.append(" private void _%s(Context ctx, %s ex) {", method.simpleName(), method.exceptionShortName());
73158
} else if (isFilter) {
@@ -77,7 +162,6 @@ void writeHandler(boolean requestScoped) {
77162
}
78163

79164
writer.eol();
80-
81165
write(requestScoped);
82166
writer.append(" }").eol().eol();
83167
}
@@ -105,6 +189,23 @@ private void write(boolean requestScoped) {
105189
param.writeValidate(writer);
106190
}
107191
}
192+
final var withFormParams = usesFormParams();
193+
final ResponseMode responseMode = responseMode();
194+
final boolean withContentCache = responseMode == ResponseMode.Templating && useContentCache();
195+
if (withContentCache) {
196+
writer.append(" var key = contentCache.key(ctx");
197+
if (withFormParams) {
198+
writer.append(", ctx.formParamMap()");
199+
}
200+
writer.append(");").eol();
201+
writer.append(" var cacheContent = contentCache.content(key);").eol();
202+
writer.append(" if (cacheContent != null) {").eol();
203+
writeContextReturn(responseMode);
204+
writer.append(" res.send(cacheContent);").eol();
205+
writer.append(" return;").eol();
206+
writer.append(" }").eol();
207+
}
208+
108209
writer.append(" ");
109210
if (!method.isVoid()) {
110211
writer.append("var result = ");
@@ -138,26 +239,39 @@ private void write(boolean requestScoped) {
138239
}
139240
writer.append(";").eol();
140241
if (!method.isVoid()) {
141-
writer.append(" if (result != null) {").eol();
142-
writer.append(" ");
143-
writeContextReturn();
144-
writer.append("result);").eol();
145-
writer.append(" }").eol();
242+
TypeMirror typeMirror = method.returnType();
243+
boolean includeNoContent = !typeMirror.getKind().isPrimitive();
244+
String indent = includeNoContent ? " " : " ";
245+
if (includeNoContent) {
246+
writer.append(" if (result != null) {").eol();
247+
}
248+
if (responseMode == ResponseMode.Templating) {
249+
writer.append(indent).append("var content = renderer.render(result);").eol();
250+
if (withContentCache) {
251+
writer.append(indent).append("contentCache.contentPut(key, content);").eol();
252+
}
253+
writer.append(indent);
254+
writeContextReturn(responseMode);
255+
writer.append("content);").eol();
256+
} else {
257+
writer.append(indent);
258+
writeContextReturn(responseMode);
259+
writer.append("result);").eol();
260+
}
261+
if (includeNoContent) {
262+
writer.append(" }").eol();
263+
}
146264
}
147265
}
148266

149-
private void writeContextReturn() {
267+
private void writeContextReturn(ResponseMode responseMode) {
150268
final var produces = method.produces();
151-
if (produces == null || produces.equalsIgnoreCase(MediaType.APPLICATION_JSON.getValue())) {
152-
writer.append("ctx.json(");
153-
} else if (produces.equalsIgnoreCase(MediaType.TEXT_HTML.getValue())) {
154-
writer.append("ctx.html(");
155-
} else if (produces.equalsIgnoreCase(MediaType.TEXT_PLAIN.getValue())) {
156-
writer.append("ctx.text(");
157-
} else if (JsonBUtil.isJsonMimeType(produces)) {
158-
writer.append("ctx.contentType(\"%s\").json(", produces);
159-
} else {
160-
writer.append("ctx.contentType(\"%s\").write(", produces);
269+
switch (responseMode) {
270+
case Void: break;
271+
case Json: writer.append("ctx.json("); break;
272+
case Text: writer.append("ctx.text("); break;
273+
case Templating: writer.append("ctx.html("); break;
274+
default: writer.append("ctx.contentType(\"%s\").write(", produces);
161275
}
162276
}
163277

http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerWriter.java

+32-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.avaje.http.generator.core.*;
55

66
import java.io.IOException;
7+
import java.util.Objects;
78

89
/**
910
* Write Jex specific Controller WebRoute handling adapter.
@@ -19,12 +20,22 @@ class ControllerWriter extends BaseControllerWriter {
1920
reader.addImportType(API_CONTEXT);
2021
reader.addImportType(API_ROUTING);
2122
reader.addImportType("java.io.IOException");
22-
2323
if (reader.methods().stream()
2424
.map(MethodReader::webMethod)
2525
.anyMatch(w -> CoreWebMethod.FILTER == w)) {
2626
reader.addImportType("io.avaje.jex.FilterChain");
2727
}
28+
if (reader.methods().stream()
29+
.map(MethodReader::hxRequest)
30+
.anyMatch(Objects::nonNull)) {
31+
reader.addImportType("io.avaje.jex.htmx.HxHandler");
32+
}
33+
if (reader.html()) {
34+
reader.addImportType("io.avaje.jex.htmx.TemplateRender");
35+
if (reader.hasContentCache()) {
36+
reader.addImportType("io.avaje.jex.htmx.TemplateContentCache");
37+
}
38+
}
2839
}
2940

3041
void write() {
@@ -50,7 +61,7 @@ private void writeAddRoutes() {
5061
private void writeHandlers() {
5162
for (MethodReader method : reader.methods()) {
5263
if (method.isWebMethod()) {
53-
new ControllerMethodWriter(method, writer).writeHandler(isRequestScoped());
64+
new ControllerMethodWriter(method, writer, reader).writeHandler(isRequestScoped());
5465
if (!reader.isDocHidden()) {
5566
method.buildApiDocumentation();
5667
}
@@ -59,7 +70,7 @@ private void writeHandlers() {
5970
}
6071

6172
private void writeRouting(MethodReader method) {
62-
new ControllerMethodWriter(method, writer).writeRouting();
73+
new ControllerMethodWriter(method, writer, reader).writeRouting();
6374
}
6475

6576
private void writeClassStart() {
@@ -82,7 +93,12 @@ private void writeClassStart() {
8293
if (instrumentContext) {
8394
writer.append(" private final RequestContextResolver resolver;").eol();
8495
}
85-
96+
if (reader.html()) {
97+
writer.append(" private final TemplateRender renderer;").eol();
98+
if (reader.hasContentCache()) {
99+
writer.append(" private final TemplateContentCache contentCache;").eol();
100+
}
101+
}
86102
writer.eol();
87103

88104
writer.append(" public %s$Route(%s %s", shortName, controllerType, controllerName);
@@ -92,6 +108,12 @@ private void writeClassStart() {
92108
if (instrumentContext) {
93109
writer.append(", RequestContextResolver resolver");
94110
}
111+
if (reader.html()) {
112+
writer.append(", TemplateRender renderer");
113+
if (reader.hasContentCache()) {
114+
writer.append(", TemplateContentCache contentCache");
115+
}
116+
}
95117
writer.append(") {").eol();
96118
writer.append(" this.%s = %s;", controllerName, controllerName).eol();
97119
if (reader.isIncludeValidator()) {
@@ -100,6 +122,12 @@ private void writeClassStart() {
100122
if (instrumentContext) {
101123
writer.append(" this.resolver = resolver;").eol();
102124
}
125+
if (reader.html()) {
126+
writer.append(" this.renderer = renderer;").eol();
127+
if (reader.hasContentCache()) {
128+
writer.append(" this.contentCache = contentCache;").eol();
129+
}
130+
}
103131
writer.append(" }").eol().eol();
104132
}
105133

tests/test-jex/pom.xml

+25-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,24 @@
2929
<version>${jex.version}</version>
3030
</dependency>
3131

32+
<dependency>
33+
<groupId>io.avaje</groupId>
34+
<artifactId>avaje-htmx-api</artifactId>
35+
<version>${project.version}</version>
36+
</dependency>
37+
38+
<dependency>
39+
<groupId>io.avaje</groupId>
40+
<artifactId>avaje-jex-htmx</artifactId>
41+
<version>${jex.version}</version>
42+
</dependency>
43+
44+
<dependency>
45+
<groupId>io.jstach</groupId>
46+
<artifactId>jstachio</artifactId>
47+
<version>1.3.6</version>
48+
</dependency>
49+
3250
<dependency>
3351
<groupId>com.fasterxml.jackson.core</groupId>
3452
<artifactId>jackson-databind</artifactId>
@@ -52,7 +70,7 @@
5270
<artifactId>swagger-annotations</artifactId>
5371
<version>${swagger.version}</version>
5472
</dependency>
55-
73+
5674
<dependency>
5775
<groupId>io.avaje</groupId>
5876
<artifactId>avaje-jsonb</artifactId>
@@ -81,6 +99,12 @@
8199
<version>2.3</version>
82100
</dependency>
83101

102+
<dependency>
103+
<groupId>io.jstach</groupId>
104+
<artifactId>jstachio-apt</artifactId>
105+
<version>1.3.6</version>
106+
</dependency>
107+
84108
<!-- test dependencies -->
85109
<dependency>
86110
<groupId>io.avaje</groupId>

0 commit comments

Comments
 (0)