Skip to content

Commit 5d50ef5

Browse files
authored
Add htmx content cache feature (#484)
1 parent 8a57e69 commit 5d50ef5

File tree

15 files changed

+240
-39
lines changed

15 files changed

+240
-39
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.avaje.htmx.api;
2+
3+
import java.lang.annotation.Retention;
4+
import java.lang.annotation.Target;
5+
6+
import static java.lang.annotation.ElementType.METHOD;
7+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
8+
9+
/**
10+
* Mark a controller method as using a content cache.
11+
*/
12+
@Target(METHOD)
13+
@Retention(RUNTIME)
14+
public @interface ContentCache {
15+
16+
}

Diff for: htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/DefaultTemplateProvider.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.avaje.htmx.nima.jstache;
22

3+
import io.avaje.htmx.nima.TemplateContentCache;
34
import io.avaje.htmx.nima.TemplateRender;
45
import io.avaje.inject.BeanScopeBuilder;
56
import io.avaje.inject.spi.Plugin;
@@ -11,11 +12,12 @@ public final class DefaultTemplateProvider implements Plugin {
1112

1213
@Override
1314
public Class<?>[] provides() {
14-
return new Class<?>[]{TemplateRender.class};
15+
return new Class<?>[]{TemplateRender.class, TemplateContentCache.class};
1516
}
1617

1718
@Override
1819
public void apply(BeanScopeBuilder builder) {
1920
builder.provideDefault(null, TemplateRender.class, JStacheTemplateRender::new);
21+
builder.provideDefault(null, TemplateContentCache.class, SimpleContentCache::new);
2022
}
2123
}
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
package io.avaje.htmx.nima.jstache;
22

33
import io.avaje.htmx.nima.TemplateRender;
4-
import io.helidon.webserver.http.ServerRequest;
5-
import io.helidon.webserver.http.ServerResponse;
64
import io.jstach.jstachio.JStachio;
75

86
public final class JStacheTemplateRender implements TemplateRender {
97

108
@Override
11-
public void render(Object viewModel, ServerRequest req, ServerResponse res) {
12-
var content = JStachio.render(viewModel);
13-
res.send(content);
9+
public String render(Object viewModel) {
10+
return JStachio.render(viewModel);
1411
}
1512
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package io.avaje.htmx.nima.jstache;
2+
3+
import io.avaje.htmx.nima.TemplateContentCache;
4+
import io.helidon.webserver.http.ServerRequest;
5+
6+
import java.util.concurrent.ConcurrentHashMap;
7+
8+
public class SimpleContentCache implements TemplateContentCache {
9+
10+
private final ConcurrentHashMap<String,String> localCache = new ConcurrentHashMap<>();
11+
12+
@Override
13+
public String key(ServerRequest req) {
14+
return req.requestedUri().path().rawPath();
15+
}
16+
17+
@Override
18+
public String key(ServerRequest req, Object formParams) {
19+
return req.requestedUri().path().rawPath() + formParams;
20+
}
21+
22+
@Override
23+
public String content(String key) {
24+
return localCache.get(key);
25+
}
26+
27+
@Override
28+
public void contentPut(String key, String content) {
29+
localCache.put(key, content);
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package io.avaje.htmx.nima;
2+
3+
import io.helidon.webserver.http.ServerRequest;
4+
5+
/**
6+
* Defines caching of template content.
7+
*/
8+
public interface TemplateContentCache {
9+
10+
/**
11+
* Return the key given the request.
12+
*/
13+
String key(ServerRequest req);
14+
15+
/**
16+
* Return the key given the request with form parameters.
17+
*/
18+
String key(ServerRequest req, Object formParams);
19+
20+
/**
21+
* Return the content given the key.
22+
*/
23+
String content(String key);
24+
25+
/**
26+
* Put the content into the cache.
27+
*/
28+
void contentPut(String key, String content);
29+
30+
}
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
package io.avaje.htmx.nima;
22

3-
4-
import io.helidon.webserver.http.ServerRequest;
5-
import io.helidon.webserver.http.ServerResponse;
6-
73
/**
84
* Template render API for Helidon.
95
*/
@@ -12,5 +8,5 @@ public interface TemplateRender {
128
/**
139
* Render the given template view model to the server response.
1410
*/
15-
void render(Object viewModel, ServerRequest req, ServerResponse res);
11+
String render(Object viewModel);
1612
}

Diff for: http-generator-core/src/main/java/io/avaje/http/generator/core/ControllerReader.java

+18-2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ public final class ControllerReader {
4949
private final boolean hasValid;
5050
/** Set true via {@code @Html} to indicate use of Templating */
5151
private final boolean html;
52+
/** Set true via {@code @ContentCache} to indicate use of Templating content cache */
53+
private boolean hasContentCache;
5254
private boolean methodHasValid;
5355

5456
/**
@@ -200,6 +202,10 @@ public boolean html() {
200202
return html;
201203
}
202204

205+
public boolean hasContentCache() {
206+
return hasContentCache;
207+
}
208+
203209
public TypeElement beanType() {
204210
return beanType;
205211
}
@@ -247,10 +253,11 @@ public void read(boolean withSingleton) {
247253
}
248254

249255
private void deriveIncludeValidation() {
250-
methodHasValid = methodHasValid();
256+
methodHasValid = anyMethodHasValid();
257+
hasContentCache = anyMethodHasContentCache();
251258
}
252259

253-
private boolean methodHasValid() {
260+
private boolean anyMethodHasValid() {
254261
for (final MethodReader method : methods) {
255262
if (method.hasValid()) {
256263
return true;
@@ -259,6 +266,15 @@ private boolean methodHasValid() {
259266
return false;
260267
}
261268

269+
private boolean anyMethodHasContentCache() {
270+
for (final MethodReader method : methods) {
271+
if (method.hasContentCache()) {
272+
return true;
273+
}
274+
}
275+
return false;
276+
}
277+
262278
private void readField(Element element) {
263279
if (!requestScope) {
264280
final String rawType = element.asType().toString();

Diff for: http-generator-core/src/main/java/io/avaje/http/generator/core/MethodReader.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public class MethodReader {
4141
private final List<? extends TypeMirror> actualParams;
4242
private final PathSegments pathSegments;
4343
private final boolean hasValid;
44+
private final Optional<ContentCachePrism> contentCache;
4445
private final List<ExecutableElement> superMethods;
4546
private final Optional<RequestTimeoutPrism> timeout;
4647
private final HxRequestPrism hxRequest;
@@ -87,10 +88,12 @@ public class MethodReader {
8788
});
8889
if (isWebMethod()) {
8990
this.hasValid = initValid();
91+
this.contentCache = initContentCache();
9092
this.instrumentContext = initResolver();
9193
this.pathSegments = PathSegments.parse(Util.combinePath(bean.path(), webMethodPath));
9294
} else {
9395
this.hasValid = false;
96+
this.contentCache = Optional.empty();
9497
this.pathSegments = null;
9598
this.instrumentContext = false;
9699
}
@@ -124,7 +127,11 @@ private boolean initValid() {
124127

125128
private boolean superMethodHasValid() {
126129
return superMethods.stream()
127-
.anyMatch(e -> findAnnotation(ValidPrism::getOptionalOn).isPresent());
130+
.anyMatch(e -> findAnnotation(ValidPrism::getOptionalOn).isPresent());
131+
}
132+
133+
private Optional<ContentCachePrism> initContentCache() {
134+
return findAnnotation(ContentCachePrism::getOptionalOn);
128135
}
129136

130137
@Override
@@ -413,6 +420,10 @@ boolean hasValid() {
413420
return hasValid;
414421
}
415422

423+
public boolean hasContentCache() {
424+
return contentCache.isPresent();
425+
}
426+
416427
public String simpleName() {
417428
return element.getSimpleName().toString();
418429
}

Diff for: http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/MediaType.java

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ public enum MediaType {
44
APPLICATION_JSON("application/json"),
55
TEXT_PLAIN("text/plain"),
66
TEXT_HTML("text/html"),
7+
HTML_UTF8("text/html;charset=UTF8"),
78
UNKNOWN("");
89

910
private final String value;

Diff for: http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
@GeneratePrism(value = io.avaje.http.api.RequestTimeout.class, publicAccess = true)
3737
@GeneratePrism(value = io.avaje.htmx.api.HxRequest.class, publicAccess = true)
3838
@GeneratePrism(value = io.avaje.htmx.api.Html.class, publicAccess = true)
39+
@GeneratePrism(value = io.avaje.htmx.api.ContentCache.class, publicAccess = true)
3940
package io.avaje.http.generator.core;
4041

4142
import io.avaje.prism.GeneratePrism;

Diff for: http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java

+72-20
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ void writeHandler(boolean requestScoped) {
131131
writer.append(" res.status(%s);", lookupStatusCode(statusCode)).eol();
132132
}
133133
}
134-
134+
boolean withFormParams = false;
135135
final var bodyType = method.bodyType();
136136
if (bodyType != null && !method.isErrorMethod() && !isFilter) {
137137
if ("InputStream".equals(bodyType)) {
@@ -144,26 +144,41 @@ void writeHandler(boolean requestScoped) {
144144
} else {
145145
defaultHelidonBodyContent();
146146
}
147-
} else if (usesFormParams()) {
148-
writer.append(" var formParams = req.content().as(Parameters.class);").eol();
147+
} else {
148+
withFormParams = usesFormParams();
149+
if (withFormParams) {
150+
writer.append(" var formParams = req.content().as(Parameters.class);").eol();
151+
}
152+
}
153+
final ResponseMode responseMode = responseMode();
154+
final boolean withContentCache = responseMode == ResponseMode.Templating && useContentCache();
155+
if (withContentCache) {
156+
writer.append(" var key = contentCache.key(req");
157+
if (withFormParams) {
158+
writer.append(", formParams");
159+
}
160+
writer.append(");").eol();
161+
writer.append(" var cacheContent = contentCache.content(key);").eol();
162+
writer.append(" if (cacheContent != null) {").eol();
163+
writeContextReturn(" ");
164+
writer.append(" res.send(cacheContent);").eol();
165+
writer.append(" return;").eol();
166+
writer.append(" }").eol();
149167
}
150168

151169
final var segments = method.pathSegments();
152170
if (segments.fullPath().contains("{")) {
153171
writer.append(" var pathParams = req.path().pathParameters();").eol();
154172
}
155-
156173
for (final PathSegments.Segment matrixSegment : segments.matrixSegments()) {
157174
matrixSegment.writeCreateSegment(writer, platform());
158175
}
159-
160176
final var params = method.params();
161177
for (final MethodParam param : params) {
162178
if (!isExceptionOrFilterChain(param)) {
163179
param.writeCtxGet(writer, segments);
164180
}
165181
}
166-
167182
if (method.includeValidate()) {
168183
for (final MethodParam param : params) {
169184
param.writeValidate(writer);
@@ -205,7 +220,7 @@ void writeHandler(boolean requestScoped) {
205220
}
206221
writer.append(");").eol();
207222

208-
if (!method.isVoid() && !isFilter) {
223+
if (responseMode != ResponseMode.Void) {
209224
TypeMirror typeMirror = method.returnType();
210225
boolean includeNoContent = !typeMirror.getKind().isPrimitive();
211226
if (includeNoContent) {
@@ -214,21 +229,29 @@ void writeHandler(boolean requestScoped) {
214229
writer.append(" } else {").eol();
215230
}
216231
String indent = includeNoContent ? " " : " ";
217-
writeContextReturn(indent);
218-
if (isInputStream(method.returnType())) {
219-
final var uType = UType.parse(method.returnType());
220-
writer.append(indent).append("result.transferTo(res.outputStream());", uType.shortName()).eol();
221-
} else if (producesJson()) {
222-
if (returnTypeString()) {
223-
writer.append(indent).append("res.send(result); // send raw JSON").eol();
224-
} else {
225-
final var uType = UType.parse(method.returnType());
226-
writer.append(indent).append("%sJsonType.toJson(result, JsonOutput.of(res));", uType.shortName()).eol();
232+
if (responseMode == ResponseMode.Templating) {
233+
writer.append(indent).append("var content = renderer.render(result);").eol();
234+
if (withContentCache) {
235+
writer.append(indent).append("contentCache.contentPut(key, content);").eol();
227236
}
228-
} else if (useTemplating()) {
229-
writer.append(indent).append("renderer.render(result, req, res);").eol();
237+
writeContextReturn(indent);
238+
writer.append(indent).append("res.send(content);").eol();
239+
230240
} else {
231-
writer.append(indent).append("res.send(result);").eol();
241+
writeContextReturn(indent);
242+
if (responseMode == ResponseMode.InputStream) {
243+
final var uType = UType.parse(method.returnType());
244+
writer.append(indent).append("result.transferTo(res.outputStream());", uType.shortName()).eol();
245+
} else if (responseMode == ResponseMode.Json) {
246+
if (returnTypeString()) {
247+
writer.append(indent).append("res.send(result); // send raw JSON").eol();
248+
} else {
249+
final var uType = UType.parse(method.returnType());
250+
writer.append(indent).append("%sJsonType.toJson(result, JsonOutput.of(res));", uType.shortName()).eol();
251+
}
252+
} else {
253+
writer.append(indent).append("res.send(result);").eol();
254+
}
232255
}
233256
if (includeNoContent) {
234257
writer.append(" }").eol();
@@ -237,6 +260,34 @@ void writeHandler(boolean requestScoped) {
237260
writer.append(" }").eol().eol();
238261
}
239262

263+
enum ResponseMode {
264+
Void,
265+
Json,
266+
Templating,
267+
InputStream,
268+
Other
269+
}
270+
271+
ResponseMode responseMode() {
272+
if (method.isVoid() || isFilter) {
273+
return ResponseMode.Void;
274+
}
275+
if (isInputStream(method.returnType())) {
276+
return ResponseMode.InputStream;
277+
}
278+
if (producesJson()) {
279+
return ResponseMode.Json;
280+
}
281+
if (useTemplating()) {
282+
return ResponseMode.Templating;
283+
}
284+
return ResponseMode.Other;
285+
}
286+
287+
private boolean useContentCache() {
288+
return method.hasContentCache();
289+
}
290+
240291
private boolean useTemplating() {
241292
return reader.html()
242293
&& !"byte[]".equals(method.returnType().toString())
@@ -308,6 +359,7 @@ private void writeContextReturn(String indent) {
308359
final var contentTypeString = "res.headers().contentType(MediaTypes.";
309360
writer.append(indent);
310361
switch (produces) {
362+
case HTML_UTF8 -> writer.append("res.headers().contentType(HTML_UTF8);").eol();
311363
case APPLICATION_JSON -> writer.append(contentTypeString).append("APPLICATION_JSON);").eol();
312364
case TEXT_HTML -> writer.append(contentTypeString).append("TEXT_HTML);").eol();
313365
case TEXT_PLAIN -> writer.append(contentTypeString).append("TEXT_PLAIN);").eol();

0 commit comments

Comments
 (0)