From d27d7e1d92308317fa96d262e1e5d5dd0ec34563 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 16 May 2024 22:13:56 +1200 Subject: [PATCH 01/10] htmx: Initial add of htmx-api, htmx-nima request support and initial example --- htmx-api/pom.xml | 25 ++++ .../java/io/avaje/htmx/api/DHxRequest.java | 141 ++++++++++++++++++ .../java/io/avaje/htmx/api/HtmxRequest.java | 106 +++++++++++++ .../java/io/avaje/htmx/api/HxRequest.java | 54 +++++++ htmx-api/src/main/java/module-info.java | 6 + htmx-nima/pom.xml | 32 ++++ .../java/io/avaje/htmx/nima/DHxHandler.java | 50 +++++++ .../io/avaje/htmx/nima/DHxHandlerBuilder.java | 38 +++++ .../java/io/avaje/htmx/nima/HxHandler.java | 21 +++ .../java/io/avaje/htmx/nima/HxHeaders.java | 65 ++++++++ .../main/java/io/avaje/htmx/nima/HxReq.java | 46 ++++++ htmx-nima/src/main/java/module-info.java | 8 + http-generator-core/pom.xml | 8 + .../http/generator/core/MethodReader.java | 9 ++ .../http/generator/core/package-info.java | 1 + .../src/main/java/module-info.java | 1 + .../helidon/nima/ControllerMethodWriter.java | 36 +++-- .../helidon/nima/ControllerWriter.java | 6 + pom.xml | 2 + tests/pom.xml | 4 +- tests/test-nima-htmx/pom.xml | 103 +++++++++++++ .../src/main/java/org/example/htmx/Main.java | 15 ++ .../java/org/example/htmx/UIController.java | 43 ++++++ .../java/org/example/htmx/model/Name.java | 15 ++ .../src/main/resources/ui/home.mustache | 13 ++ .../src/main/resources/ui/name.mustache | 6 + .../nest => htmx}/PathNestController.java | 2 +- .../{path/nest => htmx}/package-info.java | 2 +- .../org/example/path/PathTestController.java | 2 +- 29 files changed, 847 insertions(+), 13 deletions(-) create mode 100644 htmx-api/pom.xml create mode 100644 htmx-api/src/main/java/io/avaje/htmx/api/DHxRequest.java create mode 100644 htmx-api/src/main/java/io/avaje/htmx/api/HtmxRequest.java create mode 100644 htmx-api/src/main/java/io/avaje/htmx/api/HxRequest.java create mode 100644 htmx-api/src/main/java/module-info.java create mode 100644 htmx-nima/pom.xml create mode 100644 htmx-nima/src/main/java/io/avaje/htmx/nima/DHxHandler.java create mode 100644 htmx-nima/src/main/java/io/avaje/htmx/nima/DHxHandlerBuilder.java create mode 100644 htmx-nima/src/main/java/io/avaje/htmx/nima/HxHandler.java create mode 100644 htmx-nima/src/main/java/io/avaje/htmx/nima/HxHeaders.java create mode 100644 htmx-nima/src/main/java/io/avaje/htmx/nima/HxReq.java create mode 100644 htmx-nima/src/main/java/module-info.java create mode 100644 tests/test-nima-htmx/pom.xml create mode 100644 tests/test-nima-htmx/src/main/java/org/example/htmx/Main.java create mode 100644 tests/test-nima-htmx/src/main/java/org/example/htmx/UIController.java create mode 100644 tests/test-nima-htmx/src/main/java/org/example/htmx/model/Name.java create mode 100644 tests/test-nima-htmx/src/main/resources/ui/home.mustache create mode 100644 tests/test-nima-htmx/src/main/resources/ui/name.mustache rename tests/test-nima-jsonb/src/main/java/org/example/{path/nest => htmx}/PathNestController.java (92%) rename tests/test-nima-jsonb/src/main/java/org/example/{path/nest => htmx}/package-info.java (61%) diff --git a/htmx-api/pom.xml b/htmx-api/pom.xml new file mode 100644 index 000000000..ff6951637 --- /dev/null +++ b/htmx-api/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + io.avaje + avaje-http-parent + 2.5-RC1 + + + avaje-htmx-api + + + + + + + io.avaje + avaje-lang + 1.0 + + + + diff --git a/htmx-api/src/main/java/io/avaje/htmx/api/DHxRequest.java b/htmx-api/src/main/java/io/avaje/htmx/api/DHxRequest.java new file mode 100644 index 000000000..c213b6e13 --- /dev/null +++ b/htmx-api/src/main/java/io/avaje/htmx/api/DHxRequest.java @@ -0,0 +1,141 @@ +package io.avaje.htmx.api; + +import io.avaje.lang.Nullable; + +final class DHxRequest implements HtmxRequest { + + private final boolean htmxRequest; + + private final boolean boosted; + private final String currentUrl; + private final boolean historyRestoreRequest; + private final String promptResponse; + private final String target; + private final String triggerName; + private final String triggerId; + + DHxRequest() { + this.htmxRequest = false; + this.boosted = false; + this.currentUrl = null; + this.historyRestoreRequest = false; + this.promptResponse = null; + this.target = null; + this.triggerName = null; + this.triggerId = null; + } + + DHxRequest(boolean boosted, String currentUrl, boolean historyRestoreRequest, String promptResponse, String target, String triggerName, String triggerId) { + this.htmxRequest = true; + this.boosted = boosted; + this.currentUrl = currentUrl; + this.historyRestoreRequest = historyRestoreRequest; + this.promptResponse = promptResponse; + this.target = target; + this.triggerName = triggerName; + this.triggerId = triggerId; + } + + @Override + public boolean isHtmxRequest() { + return htmxRequest; + } + + @Override + public boolean isBoosted() { + return boosted; + } + + @Nullable + @Override + public String currentUrl() { + return currentUrl; + } + + @Override + public boolean isHistoryRestoreRequest() { + return historyRestoreRequest; + } + + @Nullable + @Override + public String promptResponse() { + return promptResponse; + } + + @Nullable + @Override + public String target() { + return target; + } + + @Nullable + @Override + public String triggerName() { + return triggerName; + } + + @Nullable + public String triggerId() { + return triggerId; + } + + static final class DBuilder implements Builder { + + private boolean boosted; + private String currentUrl; + private boolean historyRestoreRequest; + private String promptResponse; + private String target; + private String triggerName; + private String triggerId; + + @Override + public DBuilder boosted(boolean boosted) { + this.boosted = boosted; + return this; + } + + @Override + public DBuilder currentUrl(String currentUrl) { + this.currentUrl = currentUrl; + return this; + } + + @Override + public DBuilder historyRestoreRequest(boolean historyRestoreRequest) { + this.historyRestoreRequest = historyRestoreRequest; + return this; + } + + @Override + public DBuilder promptResponse(String promptResponse) { + this.promptResponse = promptResponse; + return this; + } + + @Override + public DBuilder target(String target) { + this.target = target; + return this; + } + + @Override + public DBuilder triggerName(String triggerName) { + this.triggerName = triggerName; + return this; + } + + @Override + public DBuilder triggerId(String triggerId) { + this.triggerId = triggerId; + return this; + } + + @Override + public HtmxRequest build() { + return new DHxRequest(boosted, currentUrl, historyRestoreRequest, promptResponse, target, triggerName, triggerId); + } + } + +} diff --git a/htmx-api/src/main/java/io/avaje/htmx/api/HtmxRequest.java b/htmx-api/src/main/java/io/avaje/htmx/api/HtmxRequest.java new file mode 100644 index 000000000..d8a47a22c --- /dev/null +++ b/htmx-api/src/main/java/io/avaje/htmx/api/HtmxRequest.java @@ -0,0 +1,106 @@ +package io.avaje.htmx.api; + +import io.avaje.lang.Nullable; + +/** + * This class can be used as a controller method argument to access + * the htmx Request Headers. + * + *
{@code
+ *
+ *   @HxRequest
+ *   @Get("/users")
+ *   String users(HtmxRequest htmxRequest) {
+ *     if (htmxRequest.isBoosted()) {
+ *         ...
+ *     }
+ *   }
+ *
+ * }
+ * + * @see Request Headers Reference + */ +public interface HtmxRequest { + + /** + * Represents a non-Htmx request. + */ + HtmxRequest EMPTY = new DHxRequest(); + + /** + * Return a new builder for the HtmxRequest. + */ + static Builder builder() { + return new DHxRequest.DBuilder(); + } + + /** + * Return true if this is an Htmx request. + */ + boolean isHtmxRequest(); + + /** + * Indicates that the request is via an element using hx-boost. + * + * @return true if the request was made via HX-Boost, false otherwise + */ + boolean isBoosted(); + + /** + * Return the current URL of the browser when the htmx request was made. + */ + @Nullable + String currentUrl(); + + /** + * Indicates if the request is for history restoration after a miss in the local history cache + * + * @return true if this request is for history restoration, false otherwise + */ + boolean isHistoryRestoreRequest(); + + /** + * Return the user response to an HX-Prompt. + */ + @Nullable + String promptResponse(); + + /** + * Return the id of the target element if it exists. + */ + @Nullable + String target(); + + /** + * Return the name of the triggered element if it exists. + */ + @Nullable + String triggerName(); + + /** + * Return the id of the triggered element if it exists. + */ + @Nullable + String triggerId(); + + /** + * Builder for {@link HtmxRequest}. + */ + interface Builder { + Builder boosted(boolean boosted); + + Builder currentUrl(String currentUrl); + + Builder historyRestoreRequest(boolean historyRestoreRequest); + + Builder promptResponse(String promptResponse); + + Builder target(String target); + + Builder triggerName(String triggerName); + + Builder triggerId(String triggerId); + + HtmxRequest build(); + } +} diff --git a/htmx-api/src/main/java/io/avaje/htmx/api/HxRequest.java b/htmx-api/src/main/java/io/avaje/htmx/api/HxRequest.java new file mode 100644 index 000000000..e643f31e5 --- /dev/null +++ b/htmx-api/src/main/java/io/avaje/htmx/api/HxRequest.java @@ -0,0 +1,54 @@ +package io.avaje.htmx.api; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Mark a controller method as handling Htmx requests and potentially restrict + * the handler to only be used for specific Htmx target or Htmx trigger. + *

+ * Controller methods with {@code @HxRequest} require the {@code HX-Request} + * HTTP Header to be set for the handler to process the request. Additionally, + * we can specify {@link #target()}, {@link #triggerId()}, or {@link #triggerName()} + * such that the handler is only invoked specifically for requests with those + * matching headers. + */ +@Target({TYPE, METHOD}) +@Retention(RUNTIME) +public @interface HxRequest { + + /** + * Restricts the mapping to the {@code id} of a specific target element. + * + * @see HX-Target + */ + String target() default ""; + + /** + * Restricts the mapping to the {@code id} of a specific triggered element. + * + * @see HX-Trigger + */ + String triggerId() default ""; + + /** + * Restricts the mapping to the {@code name} of a specific triggered element. + * + * @see HX-Trigger-Name + */ + String triggerName() default ""; + + /** + * Restricts the mapping to the {@code id}, if any, or to the {@code name} of a specific triggered element. + *

+ * If you want to be explicit use {@link #triggerId()} or {@link #triggerName()}. + * + * @see HX-Trigger + * @see HX-Trigger-Name + */ + String value() default ""; +} diff --git a/htmx-api/src/main/java/module-info.java b/htmx-api/src/main/java/module-info.java new file mode 100644 index 000000000..c018025a9 --- /dev/null +++ b/htmx-api/src/main/java/module-info.java @@ -0,0 +1,6 @@ +module io.avaje.htmx.api { + + exports io.avaje.htmx.api; + + requires static io.avaje.lang; +} diff --git a/htmx-nima/pom.xml b/htmx-nima/pom.xml new file mode 100644 index 000000000..9416a2c9f --- /dev/null +++ b/htmx-nima/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + io.avaje + avaje-http-parent + 2.5-RC1 + + + avaje-htmx-nima + + + 21 + false + UTF-8 + + + + + io.avaje + avaje-htmx-api + 2.5-RC1 + + + io.helidon.webserver + helidon-webserver + 4.0.7 + + + diff --git a/htmx-nima/src/main/java/io/avaje/htmx/nima/DHxHandler.java b/htmx-nima/src/main/java/io/avaje/htmx/nima/DHxHandler.java new file mode 100644 index 000000000..6b44530a1 --- /dev/null +++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/DHxHandler.java @@ -0,0 +1,50 @@ +package io.avaje.htmx.nima; + +import io.helidon.http.Header; +import io.helidon.http.ServerRequestHeaders; +import io.helidon.webserver.http.Handler; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +import static io.avaje.htmx.nima.HxHeaders.*; + +final class DHxHandler implements Handler { + + + private final Handler delegate; + private final String target; + private final String trigger; + private final String triggerName; + + DHxHandler(Handler delegate, String target, String trigger, String triggerName) { + this.delegate = delegate; + this.target = target; + this.trigger = trigger; + this.triggerName = triggerName; + } + + @Override + public void handle(ServerRequest req, ServerResponse res) throws Exception { + final var headers = req.headers(); + if (headers.contains(HX_REQUEST) && matched(headers)) { + delegate.handle(req, res); + } else { + res.next(); + } + } + + private boolean matched(ServerRequestHeaders headers) { + if (target != null && notMatched(headers.get(HX_TARGET), target)) { + return false; + } + if (trigger != null && notMatched(headers.get(HX_TRIGGER), trigger)) { + return false; + } + return triggerName == null || !notMatched(headers.get(HX_TRIGGER_NAME), triggerName); + } + + private boolean notMatched(Header header, String matchValue) { + return header == null || !matchValue.equals(header.get()); + } + +} diff --git a/htmx-nima/src/main/java/io/avaje/htmx/nima/DHxHandlerBuilder.java b/htmx-nima/src/main/java/io/avaje/htmx/nima/DHxHandlerBuilder.java new file mode 100644 index 000000000..b9370074c --- /dev/null +++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/DHxHandlerBuilder.java @@ -0,0 +1,38 @@ +package io.avaje.htmx.nima; + +import io.helidon.webserver.http.Handler; + +final class DHxHandlerBuilder implements HxHandler.Builder { + + private final Handler delegate; + private String target; + private String trigger; + private String triggerName; + + DHxHandlerBuilder(Handler delegate) { + this.delegate = delegate; + } + + @Override + public DHxHandlerBuilder target(String target) { + this.target = target; + return this; + } + + @Override + public DHxHandlerBuilder trigger(String trigger) { + this.trigger = trigger; + return this; + } + + @Override + public DHxHandlerBuilder triggerName(String triggerName) { + this.triggerName = triggerName; + return this; + } + + @Override + public Handler build() { + return new DHxHandler(delegate, target, trigger, triggerName); + } +} diff --git a/htmx-nima/src/main/java/io/avaje/htmx/nima/HxHandler.java b/htmx-nima/src/main/java/io/avaje/htmx/nima/HxHandler.java new file mode 100644 index 000000000..a719a793d --- /dev/null +++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/HxHandler.java @@ -0,0 +1,21 @@ +package io.avaje.htmx.nima; + +import io.helidon.webserver.http.Handler; + +public interface HxHandler { + + static Builder builder(Handler delegate) { + return new DHxHandlerBuilder(delegate); + } + + interface Builder { + + Builder target(String target); + + Builder trigger(String trigger); + + Builder triggerName(String triggerName); + + Handler build(); + } +} diff --git a/htmx-nima/src/main/java/io/avaje/htmx/nima/HxHeaders.java b/htmx-nima/src/main/java/io/avaje/htmx/nima/HxHeaders.java new file mode 100644 index 000000000..156798d29 --- /dev/null +++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/HxHeaders.java @@ -0,0 +1,65 @@ +package io.avaje.htmx.nima; + +import io.helidon.http.HeaderName; +import io.helidon.http.HeaderNames; + +/** + * HTMX request headers. + * + * @see Request Headers Reference + */ +public interface HxHeaders { + + /** + * Indicates that the request comes from an element that uses hx-boost. + * + * @see HX-Boosted + */ + HeaderName HX_BOOSTED = HeaderNames.create("HX-Boosted"); + + /** + * The current URL of the browser + * + * @see HX-Current-URL + */ + HeaderName HX_CURRENT_URL = HeaderNames.create("HX-Current-URL"); + + /** + * Indicates if the request is for history restoration after a miss in the local history cache. + * + * @see HX-History-Restore-Request + */ + HeaderName HX_HISTORY_RESTORE_REQUEST = HeaderNames.create("HX-History-Restore-Request"); + + /** + * Contains the user response to a hx-prompt. + * + * @see HX-Prompt + */ + HeaderName HX_PROMPT = HeaderNames.create("HX-Prompt"); + /** + * Only present and {@code true} if the request is issued by htmx. + * + * @see HX-Request + */ + HeaderName HX_REQUEST = HeaderNames.create("HX-Request"); + /** + * The {@code id} of the target element if it exists. + * + * @see HX-Target + */ + HeaderName HX_TARGET = HeaderNames.create("HX-Target"); + /** + * The {@code name} of the triggered element if it exists + * + * @see HX-Trigger-Name + */ + HeaderName HX_TRIGGER_NAME = HeaderNames.create("HX-Trigger-Name"); + /** + * The {@code id} of the triggered element if it exists. + * + * @see HX-Trigger + */ + HeaderName HX_TRIGGER = HeaderNames.create("HX-Trigger"); + +} diff --git a/htmx-nima/src/main/java/io/avaje/htmx/nima/HxReq.java b/htmx-nima/src/main/java/io/avaje/htmx/nima/HxReq.java new file mode 100644 index 000000000..a18bd1124 --- /dev/null +++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/HxReq.java @@ -0,0 +1,46 @@ +package io.avaje.htmx.nima; + +import io.avaje.htmx.api.HtmxRequest; +import io.helidon.webserver.http.ServerRequest; + +public class HxReq { + + /** + * Create given the server request. + */ + public static HtmxRequest of(ServerRequest request) { + final var headers = request.headers(); + if (!headers.contains(HxHeaders.HX_REQUEST)) { + return HtmxRequest.EMPTY; + } + + var builder = HtmxRequest.builder(); + if (headers.contains(HxHeaders.HX_BOOSTED)) { + builder.boosted(true); + } + if (headers.contains(HxHeaders.HX_HISTORY_RESTORE_REQUEST)) { + builder.historyRestoreRequest(true); + } + var currentUrl = headers.get(HxHeaders.HX_CURRENT_URL); + if (currentUrl != null) { + builder.currentUrl(currentUrl.get()); + } + var prompt = headers.get(HxHeaders.HX_PROMPT); + if (prompt != null) { + builder.promptResponse(prompt.get()); + } + var target = headers.get(HxHeaders.HX_TARGET); + if (target != null) { + builder.target(target.get()); + } + var triggerName = headers.get(HxHeaders.HX_TRIGGER_NAME); + if (triggerName != null) { + builder.triggerName(triggerName.get()); + } + var trigger = headers.get(HxHeaders.HX_TRIGGER); + if (trigger != null) { + builder.triggerId(trigger.get()); + } + return builder.build(); + } +} diff --git a/htmx-nima/src/main/java/module-info.java b/htmx-nima/src/main/java/module-info.java new file mode 100644 index 000000000..8b593160d --- /dev/null +++ b/htmx-nima/src/main/java/module-info.java @@ -0,0 +1,8 @@ +module io.avaje.htmx.nima { + + requires io.avaje.htmx.api; + requires io.helidon.webserver; + + exports io.avaje.htmx.nima; + +} diff --git a/http-generator-core/pom.xml b/http-generator-core/pom.xml index ee61471d0..9177006f6 100644 --- a/http-generator-core/pom.xml +++ b/http-generator-core/pom.xml @@ -26,6 +26,14 @@ provided + + io.avaje + avaje-htmx-api + ${project.version} + true + provided + + io.swagger.core.v3 swagger-annotations diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/MethodReader.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/MethodReader.java index 2ffa86e20..e679f267e 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/MethodReader.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/MethodReader.java @@ -43,6 +43,7 @@ public class MethodReader { private final boolean hasValid; private final List superMethods; private final Optional timeout; + private final HxRequestPrism hxRequest; private WebMethod webMethod; private int statusCode; @@ -77,6 +78,7 @@ public class MethodReader { this.securityRequirements = readSecurityRequirements(); this.apiResponses = buildApiResponses(); this.javadoc = buildJavadoc(element); + this.hxRequest = HxRequestPrism.getInstanceOn(element); this.timeout = RequestTimeoutPrism.getOptionalOn(element); timeout.ifPresent( p -> { @@ -190,6 +192,13 @@ private void initSetWebMethod(WebMethod webMethod, ExceptionHandlerPrism excepti bean.addImportType(exType); } + /** + * Return the Htmx request annotation for this method. + */ + public HxRequestPrism hxRequest() { + return hxRequest; + } + public Javadoc javadoc() { return javadoc; } diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java index 129ae8cc7..ed26034b1 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java @@ -34,6 +34,7 @@ @GeneratePrism(value = io.swagger.v3.oas.annotations.Hidden.class, publicAccess = true) @GeneratePrism(value = io.avaje.http.api.Client.Import.class, publicAccess = true) @GeneratePrism(value = io.avaje.http.api.RequestTimeout.class, publicAccess = true) +@GeneratePrism(value = io.avaje.htmx.api.HxRequest.class, publicAccess = true) package io.avaje.http.generator.core; import io.avaje.prism.GeneratePrism; diff --git a/http-generator-core/src/main/java/module-info.java b/http-generator-core/src/main/java/module-info.java index 0d5787e3f..f5142424b 100644 --- a/http-generator-core/src/main/java/module-info.java +++ b/http-generator-core/src/main/java/module-info.java @@ -10,6 +10,7 @@ // SHADED: All content after this line will be removed at package time requires static io.avaje.prism; requires static io.avaje.http.api; + requires static io.avaje.htmx.api; requires static io.swagger.v3.oas.models; requires static io.swagger.v3.oas.annotations; requires static java.validation; diff --git a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java index 8a31e3a51..d335330c3 100644 --- a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java +++ b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java @@ -7,14 +7,7 @@ import java.util.Map; import java.util.Optional; -import io.avaje.http.generator.core.Append; -import io.avaje.http.generator.core.CoreWebMethod; -import io.avaje.http.generator.core.MethodParam; -import io.avaje.http.generator.core.MethodReader; -import io.avaje.http.generator.core.ParamType; -import io.avaje.http.generator.core.PathSegments; -import io.avaje.http.generator.core.UType; -import io.avaje.http.generator.core.WebMethod; +import io.avaje.http.generator.core.*; import io.avaje.http.generator.core.openapi.MediaType; import javax.lang.model.type.TypeMirror; @@ -92,10 +85,35 @@ void writeRule() { } else if (isFilter) { writer.append(" routing.addFilter(this::_%s);", method.simpleName()).eol(); } else { - writer.append(" routing.%s(\"%s\", this::_%s);", webMethod.name().toLowerCase(), method.fullPath().replace("\\", "\\\\"), method.simpleName()).eol(); + writer.append(" routing.%s(\"%s\", ", webMethod.name().toLowerCase(), method.fullPath().replace("\\", "\\\\")); + var hxRequest = method.hxRequest(); + if (hxRequest != null) { + writer.append("HxHandler.builder(this::_%s)", method.simpleName()); + if (hasValue(hxRequest.target())) { + writer.append(".target(\"%s\")", hxRequest.target()); + } + if (hasValue(hxRequest.triggerId())) { + writer.append(".trigger(\"%s\")", hxRequest.triggerId()); + } else if (hasValue(hxRequest.value())) { + writer.append(".trigger(\"%s\")", hxRequest.value()); + } + if (hasValue(hxRequest.triggerName())) { + writer.append(".triggerName(\"%s\")", hxRequest.triggerName()); + } else if (hasValue(hxRequest.value())) { + writer.append(".triggerName(\"%s\")", hxRequest.value()); + } + writer.append(".build());").eol(); + + } else { + writer.append("this::_%s);", method.simpleName()).eol(); + } } } + private static boolean hasValue(String value) { + return value != null && !value.isBlank(); + } + void writeHandler(boolean requestScoped) { if (method.isErrorMethod()) { writer.append(" private void _%s(ServerRequest req, ServerResponse res, %s ex) {", method.simpleName(), method.exceptionShortName()).eol(); diff --git a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java index e192f9d05..25dfc33f9 100644 --- a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java +++ b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Objects; import io.avaje.http.generator.core.BaseControllerWriter; import io.avaje.http.generator.core.Constants; @@ -59,6 +60,11 @@ class ControllerWriter extends BaseControllerWriter { reader.addImportType("io.helidon.webserver.http.RoutingRequest"); reader.addImportType("io.helidon.webserver.http.RoutingResponse"); } + if (reader.methods().stream() + .map(MethodReader::hxRequest) + .anyMatch(Objects::nonNull)) { + reader.addImportType("io.avaje.htmx.nima.HxHandler"); + } } void write() { diff --git a/pom.xml b/pom.xml index 225388f96..7537b4425 100644 --- a/pom.xml +++ b/pom.xml @@ -35,6 +35,8 @@ + htmx-api + htmx-nima http-api http-api-javalin http-client diff --git a/tests/pom.xml b/tests/pom.xml index f0f26628d..0774b40af 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -27,6 +27,7 @@ test-jex test-client test-client-generation + test-nima-htmx @@ -37,7 +38,8 @@ test-nima - test-nima-jsonb + + test-nima-htmx diff --git a/tests/test-nima-htmx/pom.xml b/tests/test-nima-htmx/pom.xml new file mode 100644 index 000000000..ab961d45e --- /dev/null +++ b/tests/test-nima-htmx/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + + io.avaje + tests + 2.5-RC1 + + + test-nima-htmx + + + 21 + UTF-8 + false + 1.3.5 + + + + + io.jstach + jstachio + ${io.jstach.version} + + + io.avaje + avaje-htmx-api + ${project.version} + + + io.avaje + avaje-htmx-nima + ${project.version} + + + io.avaje + avaje-nima + 1.0 + + + + + io.avaje + avaje-nima-test + 1.0 + test + + + + + + + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + + + io.avaje + avaje-http-helidon-generator + ${project.version} + + + io.avaje + avaje-inject-generator + ${avaje-inject.version} + + + io.avaje + avaje-jsonb-generator + 1.11 + + + io.jstach + jstachio-apt + ${io.jstach.version} + + + + + + + + + + diff --git a/tests/test-nima-htmx/src/main/java/org/example/htmx/Main.java b/tests/test-nima-htmx/src/main/java/org/example/htmx/Main.java new file mode 100644 index 000000000..1b6d205ad --- /dev/null +++ b/tests/test-nima-htmx/src/main/java/org/example/htmx/Main.java @@ -0,0 +1,15 @@ +package org.example.htmx; + +import io.avaje.inject.InjectModule; +import io.avaje.nima.Nima; + +@InjectModule(name = "hxTest") +public class Main { + + public static void main(String[] args) { + Nima.builder() + .port(8090) + .build() + .start(); + } +} diff --git a/tests/test-nima-htmx/src/main/java/org/example/htmx/UIController.java b/tests/test-nima-htmx/src/main/java/org/example/htmx/UIController.java new file mode 100644 index 000000000..e57b888c2 --- /dev/null +++ b/tests/test-nima-htmx/src/main/java/org/example/htmx/UIController.java @@ -0,0 +1,43 @@ +package org.example.htmx; + +import io.avaje.htmx.api.HxRequest; +import io.avaje.http.api.Controller; +import io.avaje.http.api.Get; +import io.avaje.http.api.Path; +import io.avaje.http.api.Produces; +import io.jstach.jstache.JStache; +import io.jstach.jstachio.JStachio; +import org.example.htmx.model.Name; + +import java.time.Instant; +import java.util.List; + +@Controller +@Path("/") +@Produces("text/html") +public class UIController { + + @Get + String index() { + return JStachio.render(new Home("Robin2")); + } + + @HxRequest(target = "name") + @Get("name") + String name() { + var mlist = List.of("one","two","three"); + +// var mlist = List.of( +// new Name.Pair("one",23), +// new Name.Pair("two",34), +// new Name.Pair("three",43) +// ); +// +// return "Yo " + Instant.now() + ""; + return JStachio.render(new Name("Jim", Instant.now(), "MoreMeMore", mlist)); + } + + @JStache(path = "ui/home.mustache") + public record Home(String name) {} + +} diff --git a/tests/test-nima-htmx/src/main/java/org/example/htmx/model/Name.java b/tests/test-nima-htmx/src/main/java/org/example/htmx/model/Name.java new file mode 100644 index 000000000..4ec0bdf00 --- /dev/null +++ b/tests/test-nima-htmx/src/main/java/org/example/htmx/model/Name.java @@ -0,0 +1,15 @@ +package org.example.htmx.model; + +import io.jstach.jstache.JStache; + +import java.time.Instant; +import java.util.List; + +@JStache(path = "ui/name.mustache") +public record Name(String name, Instant foo, String more, List mlist) { + public String when() { + return foo.toString(); + } + + public record Pair(String nm, int eg) {} +} diff --git a/tests/test-nima-htmx/src/main/resources/ui/home.mustache b/tests/test-nima-htmx/src/main/resources/ui/home.mustache new file mode 100644 index 000000000..4526fc2a4 --- /dev/null +++ b/tests/test-nima-htmx/src/main/resources/ui/home.mustache @@ -0,0 +1,13 @@ + + + Hi + + + +

Heading

+
+ Hi {{ name }} +
+ + + diff --git a/tests/test-nima-htmx/src/main/resources/ui/name.mustache b/tests/test-nima-htmx/src/main/resources/ui/name.mustache new file mode 100644 index 000000000..4c2f0f851 --- /dev/null +++ b/tests/test-nima-htmx/src/main/resources/ui/name.mustache @@ -0,0 +1,6 @@ +
+Yond {{ name }} its {{ when }} !! and {{ more }} sad + {{#mlist}} + in {{.}} ot + {{/mlist}} +
diff --git a/tests/test-nima-jsonb/src/main/java/org/example/path/nest/PathNestController.java b/tests/test-nima-jsonb/src/main/java/org/example/htmx/PathNestController.java similarity index 92% rename from tests/test-nima-jsonb/src/main/java/org/example/path/nest/PathNestController.java rename to tests/test-nima-jsonb/src/main/java/org/example/htmx/PathNestController.java index 47fb7479c..dcc7aa3ec 100644 --- a/tests/test-nima-jsonb/src/main/java/org/example/path/nest/PathNestController.java +++ b/tests/test-nima-jsonb/src/main/java/org/example/htmx/PathNestController.java @@ -1,4 +1,4 @@ -package org.example.path.nest; +package org.example.htmx; import io.avaje.http.api.Controller; import io.avaje.http.api.Get; diff --git a/tests/test-nima-jsonb/src/main/java/org/example/path/nest/package-info.java b/tests/test-nima-jsonb/src/main/java/org/example/htmx/package-info.java similarity index 61% rename from tests/test-nima-jsonb/src/main/java/org/example/path/nest/package-info.java rename to tests/test-nima-jsonb/src/main/java/org/example/htmx/package-info.java index 73f306ff6..d9695fcaa 100644 --- a/tests/test-nima-jsonb/src/main/java/org/example/path/nest/package-info.java +++ b/tests/test-nima-jsonb/src/main/java/org/example/htmx/package-info.java @@ -1,5 +1,5 @@ @Path("nested") -package org.example.path.nest; +package org.example.htmx; import io.avaje.http.api.Path; diff --git a/tests/test-nima-jsonb/src/main/java/org/example/path/PathTestController.java b/tests/test-nima-jsonb/src/main/java/org/example/path/PathTestController.java index 5d1a3f970..2421bd26e 100644 --- a/tests/test-nima-jsonb/src/main/java/org/example/path/PathTestController.java +++ b/tests/test-nima-jsonb/src/main/java/org/example/path/PathTestController.java @@ -1,6 +1,6 @@ package org.example.path; -import org.example.path.nest.PathNestController.NestedTypeResponse; +import org.example.htmx.PathNestController.NestedTypeResponse; import io.avaje.http.api.Controller; import io.avaje.http.api.Get; From 00e29df0c4e4b2b82305cfadbdcc812d5c8b925b Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Sun, 26 May 2024 22:13:30 +1200 Subject: [PATCH 02/10] Bump parent version --- htmx-api/pom.xml | 2 +- htmx-nima/pom.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/htmx-api/pom.xml b/htmx-api/pom.xml index ff6951637..0fd7315ab 100644 --- a/htmx-api/pom.xml +++ b/htmx-api/pom.xml @@ -6,7 +6,7 @@ io.avaje avaje-http-parent - 2.5-RC1 + 2.6-RC1 avaje-htmx-api diff --git a/htmx-nima/pom.xml b/htmx-nima/pom.xml index 9416a2c9f..62ca69254 100644 --- a/htmx-nima/pom.xml +++ b/htmx-nima/pom.xml @@ -6,7 +6,7 @@ io.avaje avaje-http-parent - 2.5-RC1 + 2.6-RC1 avaje-htmx-nima @@ -21,7 +21,7 @@ io.avaje avaje-htmx-api - 2.5-RC1 + 2.6-RC1 io.helidon.webserver From 78520bea8472531651b8463ed4d7de33ed270e51 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Sun, 26 May 2024 22:18:38 +1200 Subject: [PATCH 03/10] Bump parent version --- tests/test-nima-htmx/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-nima-htmx/pom.xml b/tests/test-nima-htmx/pom.xml index ab961d45e..8edf5bf0a 100644 --- a/tests/test-nima-htmx/pom.xml +++ b/tests/test-nima-htmx/pom.xml @@ -6,7 +6,7 @@ io.avaje tests - 2.5-RC1 + 2.6-RC1 test-nima-htmx From 11e558113f671ed739a8dd1779671d02b61f7646 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Sun, 26 May 2024 22:26:57 +1200 Subject: [PATCH 04/10] Conditionally build htmx-nima JDK 21+ --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 29f6ed3d7..9aa85e2b5 100644 --- a/pom.xml +++ b/pom.xml @@ -36,7 +36,6 @@ htmx-api - htmx-nima http-api http-api-javalin http-client @@ -65,6 +64,7 @@ [21,22] + htmx-nima http-generator-helidon From 016e84d329fd4222892f32004b341901a41c6e1d Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 30 May 2024 19:28:54 +1200 Subject: [PATCH 05/10] Add TemplateRender interface --- .../java/io/avaje/htmx/nima/TemplateRender.java | 10 ++++++++++ .../htmx/template/JstacheTemplateRender.java | 17 +++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 htmx-nima/src/main/java/io/avaje/htmx/nima/TemplateRender.java create mode 100644 tests/test-nima-htmx/src/main/java/org/example/htmx/template/JstacheTemplateRender.java diff --git a/htmx-nima/src/main/java/io/avaje/htmx/nima/TemplateRender.java b/htmx-nima/src/main/java/io/avaje/htmx/nima/TemplateRender.java new file mode 100644 index 000000000..ad95696e5 --- /dev/null +++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/TemplateRender.java @@ -0,0 +1,10 @@ +package io.avaje.htmx.nima; + + +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; + +public interface TemplateRender { + + void render(Object viewModel, ServerRequest req, ServerResponse res); +} diff --git a/tests/test-nima-htmx/src/main/java/org/example/htmx/template/JstacheTemplateRender.java b/tests/test-nima-htmx/src/main/java/org/example/htmx/template/JstacheTemplateRender.java new file mode 100644 index 000000000..4acb8ed00 --- /dev/null +++ b/tests/test-nima-htmx/src/main/java/org/example/htmx/template/JstacheTemplateRender.java @@ -0,0 +1,17 @@ +package org.example.htmx.template; + +import io.avaje.htmx.nima.TemplateRender; +import io.avaje.inject.Component; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; +import io.jstach.jstachio.JStachio; + +@Component +public class JstacheTemplateRender implements TemplateRender { + + @Override + public void render(Object viewModel, ServerRequest req, ServerResponse res) { + String content = JStachio.render(viewModel); + res.send(content); + } +} From 24ccffe24a36b16e7089a6ac0ab3e35b4e720bf0 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 6 Jun 2024 23:10:22 +1200 Subject: [PATCH 06/10] Add Jscahe TemplateRender for @Htmx --- .../src/main/java/io/avaje/htmx/api/Html.java | 18 +++++++ htmx-nima-jstache/pom.xml | 47 +++++++++++++++++++ .../nima/jstache/DefaultTemplateProvider.java | 21 +++++++++ .../nima/jstache/JStacheTemplateRender.java | 15 ++++++ .../src/main/java/module-info.java | 11 +++++ .../services/io.avaje.inject.spi.Plugin | 1 + .../http/generator/core/ControllerReader.java | 18 +++++-- .../http/generator/core/package-info.java | 1 + .../helidon/nima/ControllerMethodWriter.java | 12 ++++- .../helidon/nima/ControllerWriter.java | 17 +++++-- pom.xml | 10 ++++ tests/pom.xml | 2 +- tests/test-nima-htmx/pom.xml | 23 ++++----- .../java/org/example/htmx/UIController.java | 28 +++-------- .../main/java/org/example/htmx/ViewHome.java | 7 +++ .../main/java/org/example/htmx/ViewName.java | 13 +++++ .../java/org/example/htmx/model/Name.java | 15 ------ .../java/org/example/htmx/package-info.java | 4 ++ .../resources/ui/fragments/layout.mustache | 11 +++++ .../src/main/resources/ui/home.mustache | 21 ++++----- 20 files changed, 225 insertions(+), 70 deletions(-) create mode 100644 htmx-api/src/main/java/io/avaje/htmx/api/Html.java create mode 100644 htmx-nima-jstache/pom.xml create mode 100644 htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/DefaultTemplateProvider.java create mode 100644 htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/JStacheTemplateRender.java create mode 100644 htmx-nima-jstache/src/main/java/module-info.java create mode 100644 htmx-nima-jstache/src/main/resources/META-INF/services/io.avaje.inject.spi.Plugin create mode 100644 tests/test-nima-htmx/src/main/java/org/example/htmx/ViewHome.java create mode 100644 tests/test-nima-htmx/src/main/java/org/example/htmx/ViewName.java delete mode 100644 tests/test-nima-htmx/src/main/java/org/example/htmx/model/Name.java create mode 100644 tests/test-nima-htmx/src/main/java/org/example/htmx/package-info.java create mode 100644 tests/test-nima-htmx/src/main/resources/ui/fragments/layout.mustache diff --git a/htmx-api/src/main/java/io/avaje/htmx/api/Html.java b/htmx-api/src/main/java/io/avaje/htmx/api/Html.java new file mode 100644 index 000000000..bedae0ded --- /dev/null +++ b/htmx-api/src/main/java/io/avaje/htmx/api/Html.java @@ -0,0 +1,18 @@ +package io.avaje.htmx.api; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Mark a controller as producing HTML by default and using "Templating" + * meaning that response objects are expected to a "Model View" passed to + * the "Templating" library. + */ +@Target(TYPE) +@Retention(RUNTIME) +public @interface Html { + +} diff --git a/htmx-nima-jstache/pom.xml b/htmx-nima-jstache/pom.xml new file mode 100644 index 000000000..8f30611d1 --- /dev/null +++ b/htmx-nima-jstache/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + io.avaje + avaje-http-parent + 2.6-RC1 + + + avaje-htmx-nima-jstache + + + 21 + 21 + 21 + UTF-8 + false + 1.3.5 + + + + + io.jstach + jstachio + ${io.jstach.version} + + + io.avaje + avaje-htmx-api + ${project.version} + + + io.avaje + avaje-htmx-nima + ${project.version} + + + io.avaje + avaje-inject + 9.12 + provided + true + + + diff --git a/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/DefaultTemplateProvider.java b/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/DefaultTemplateProvider.java new file mode 100644 index 000000000..342e2bd45 --- /dev/null +++ b/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/DefaultTemplateProvider.java @@ -0,0 +1,21 @@ +package io.avaje.htmx.nima.jstache; + +import io.avaje.htmx.nima.TemplateRender; +import io.avaje.inject.BeanScopeBuilder; +import io.avaje.inject.spi.Plugin; + +/** + * Plugin for avaje inject that provides a default TemplateRender instance. + */ +public final class DefaultTemplateProvider implements Plugin { + + @Override + public Class[] provides() { + return new Class[]{TemplateRender.class}; + } + + @Override + public void apply(BeanScopeBuilder builder) { + builder.provideDefault(null, TemplateRender.class, JStacheTemplateRender::new); + } +} diff --git a/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/JStacheTemplateRender.java b/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/JStacheTemplateRender.java new file mode 100644 index 000000000..beb4bff60 --- /dev/null +++ b/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/JStacheTemplateRender.java @@ -0,0 +1,15 @@ +package io.avaje.htmx.nima.jstache; + +import io.avaje.htmx.nima.TemplateRender; +import io.helidon.webserver.http.ServerRequest; +import io.helidon.webserver.http.ServerResponse; +import io.jstach.jstachio.JStachio; + +public final class JStacheTemplateRender implements TemplateRender { + + @Override + public void render(Object viewModel, ServerRequest req, ServerResponse res) { + var content = JStachio.render(viewModel); + res.send(content); + } +} diff --git a/htmx-nima-jstache/src/main/java/module-info.java b/htmx-nima-jstache/src/main/java/module-info.java new file mode 100644 index 000000000..2da4668b1 --- /dev/null +++ b/htmx-nima-jstache/src/main/java/module-info.java @@ -0,0 +1,11 @@ +module io.avaje.htmx.nima.jstache { + + exports io.avaje.htmx.nima.jstache; + + requires transitive io.avaje.htmx.nima; + requires transitive io.helidon.webserver; + requires transitive io.jstach.jstachio; + requires io.avaje.inject; + + provides io.avaje.inject.spi.Plugin with io.avaje.htmx.nima.jstache.DefaultTemplateProvider; +} diff --git a/htmx-nima-jstache/src/main/resources/META-INF/services/io.avaje.inject.spi.Plugin b/htmx-nima-jstache/src/main/resources/META-INF/services/io.avaje.inject.spi.Plugin new file mode 100644 index 000000000..456aa71f2 --- /dev/null +++ b/htmx-nima-jstache/src/main/resources/META-INF/services/io.avaje.inject.spi.Plugin @@ -0,0 +1 @@ +io.avaje.htmx.nima.jstache.DefaultTemplateProvider diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/ControllerReader.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/ControllerReader.java index 3914821cc..12e89fdd6 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/ControllerReader.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/ControllerReader.java @@ -47,6 +47,8 @@ public final class ControllerReader { private final String producesPrism; private final boolean hasValid; + /** Set true via {@code @Html} to indicate use of Templating */ + private final boolean html; private boolean methodHasValid; /** @@ -70,7 +72,8 @@ public ControllerReader(TypeElement beanType, String contextPath) { docHidden = initDocHidden(); } this.hasValid = initHasValid(); - this.producesPrism = initProduces(); + this.html = initHtml(); + this.producesPrism = initProduces(html); this.apiResponses = buildApiResponses(); hasInstrument = instrumentAllWebMethods() @@ -172,8 +175,13 @@ private boolean matchMethod(ExecutableElement interfaceMethod, ExecutableElement return interfaceMethod.toString().equals(element.toString()); } - private String initProduces() { - return findAnnotation(ProducesPrism::getOptionalOn).map(ProducesPrism::value).orElse(null); + private boolean initHtml() { + return findAnnotation(HtmlPrism::getOptionalOn).isPresent(); + } + + private String initProduces(boolean html) { + String defaultProduces = html ? "text/html;charset=UTF8" : null; + return findAnnotation(ProducesPrism::getOptionalOn).map(ProducesPrism::value).orElse(defaultProduces); } private boolean initDocHidden() { @@ -188,6 +196,10 @@ String produces() { return producesPrism; } + public boolean html() { + return html; + } + public TypeElement beanType() { return beanType; } diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java index ed26034b1..efdbc2609 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/package-info.java @@ -35,6 +35,7 @@ @GeneratePrism(value = io.avaje.http.api.Client.Import.class, publicAccess = true) @GeneratePrism(value = io.avaje.http.api.RequestTimeout.class, publicAccess = true) @GeneratePrism(value = io.avaje.htmx.api.HxRequest.class, publicAccess = true) +@GeneratePrism(value = io.avaje.htmx.api.Html.class, publicAccess = true) package io.avaje.http.generator.core; import io.avaje.prism.GeneratePrism; diff --git a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java index d335330c3..ee7bf699b 100644 --- a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java +++ b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerMethodWriter.java @@ -60,8 +60,10 @@ final class ControllerMethodWriter { private final boolean useJsonB; private final boolean instrumentContext; private final boolean isFilter; + private final ControllerReader reader; - ControllerMethodWriter(MethodReader method, Append writer, boolean useJsonB) { + ControllerMethodWriter(MethodReader method, Append writer, boolean useJsonB, ControllerReader reader) { + this.reader = reader; this.method = method; this.writer = writer; this.webMethod = method.webMethod(); @@ -223,6 +225,8 @@ void writeHandler(boolean requestScoped) { final var uType = UType.parse(method.returnType()); writer.append(indent).append("%sJsonType.toJson(result, JsonOutput.of(res));", uType.shortName()).eol(); } + } else if (useTemplating()) { + writer.append(indent).append("renderer.render(result, req, res);").eol(); } else { writer.append(indent).append("res.send(result);").eol(); } @@ -233,6 +237,12 @@ void writeHandler(boolean requestScoped) { writer.append(" }").eol().eol(); } + private boolean useTemplating() { + return reader.html() + && !"byte[]".equals(method.returnType().toString()) + && (method.produces() == null || method.produces().toLowerCase().contains("html")); + } + private static boolean isExceptionOrFilterChain(MethodParam param) { return isAssignable2Interface(param.utype().mainType(), "java.lang.Exception") || "FilterChain".equals(param.shortType()); diff --git a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java index 25dfc33f9..8b58214b6 100644 --- a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java +++ b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java @@ -65,6 +65,9 @@ class ControllerWriter extends BaseControllerWriter { .anyMatch(Objects::nonNull)) { reader.addImportType("io.avaje.htmx.nima.HxHandler"); } + if (reader.html()) { + reader.addImportType("io.avaje.htmx.nima.TemplateRender"); + } } void write() { @@ -86,7 +89,7 @@ protected void writeImports() { private List writerMethods() { return reader.methods().stream() .filter(MethodReader::isWebMethod) - .map(it -> new ControllerMethodWriter(it, writer, useJsonB)) + .map(it -> new ControllerMethodWriter(it, writer, useJsonB, reader)) .toList(); } @@ -132,10 +135,12 @@ private void writeClassStart() { if (reader.isIncludeValidator()) { writer.append(" private final Validator validator;").eol(); } - if (instrumentContext) { writer.append(" private final RequestContextResolver resolver;").eol(); } + if (reader.html()) { + writer.append(" private final TemplateRender renderer; // v5").eol(); + } for (final UType type : jsonTypes.values()) { if (!isInputStream(type.full())) { @@ -152,6 +157,9 @@ private void writeClassStart() { if (useJsonB) { writer.append(", Jsonb jsonb"); } + if (reader.html()) { + writer.append(", TemplateRender renderer"); + } if (instrumentContext) { writer.append(", RequestContextResolver resolver"); } @@ -161,6 +169,9 @@ private void writeClassStart() { if (reader.isIncludeValidator()) { writer.append(" this.validator = validator;").eol(); } + if (reader.html()) { + writer.append(" this.renderer = renderer; // v5").eol(); + } if (instrumentContext) { writer.append(" this.resolver = resolver;").eol(); } @@ -182,6 +193,6 @@ private void writeClassStart() { } private boolean isInputStream(String type) { - return isAssignable2Interface(type.toString(), "java.io.InputStream"); + return isAssignable2Interface(type, "java.io.InputStream"); } } diff --git a/pom.xml b/pom.xml index 9aa85e2b5..5bb9a8477 100644 --- a/pom.xml +++ b/pom.xml @@ -65,9 +65,19 @@ htmx-nima + htmx-nima-jstache http-generator-helidon + + test21 + + htmx-nima + htmx-nima-jstache + http-generator-helidon + tests + + module-info.shade diff --git a/tests/pom.xml b/tests/pom.xml index 23562470b..57d9d2ed4 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -16,7 +16,7 @@ 3.25.3 2.17.1 2.5 - 9.12 + 10.0-RC5 4.0.8 6.1.4 diff --git a/tests/test-nima-htmx/pom.xml b/tests/test-nima-htmx/pom.xml index 8edf5bf0a..239b9906f 100644 --- a/tests/test-nima-htmx/pom.xml +++ b/tests/test-nima-htmx/pom.xml @@ -19,6 +19,16 @@ + + io.avaje + avaje-inject + 10.0-RC5 + + + io.avaje + avaje-htmx-nima-jstache + 2.6-RC1 + io.jstach jstachio @@ -48,19 +58,6 @@ test - - - - - - - - - - - - - diff --git a/tests/test-nima-htmx/src/main/java/org/example/htmx/UIController.java b/tests/test-nima-htmx/src/main/java/org/example/htmx/UIController.java index e57b888c2..05c59ded0 100644 --- a/tests/test-nima-htmx/src/main/java/org/example/htmx/UIController.java +++ b/tests/test-nima-htmx/src/main/java/org/example/htmx/UIController.java @@ -1,43 +1,29 @@ package org.example.htmx; +import io.avaje.htmx.api.Html; import io.avaje.htmx.api.HxRequest; import io.avaje.http.api.Controller; import io.avaje.http.api.Get; import io.avaje.http.api.Path; -import io.avaje.http.api.Produces; -import io.jstach.jstache.JStache; -import io.jstach.jstachio.JStachio; -import org.example.htmx.model.Name; import java.time.Instant; import java.util.List; +@Html @Controller @Path("/") -@Produces("text/html") public class UIController { @Get - String index() { - return JStachio.render(new Home("Robin2")); + ViewHome index() { + return new ViewHome("Robin3"); } @HxRequest(target = "name") @Get("name") - String name() { - var mlist = List.of("one","two","three"); - -// var mlist = List.of( -// new Name.Pair("one",23), -// new Name.Pair("two",34), -// new Name.Pair("three",43) -// ); -// -// return "Yo " + Instant.now() + ""; - return JStachio.render(new Name("Jim", Instant.now(), "MoreMeMore", mlist)); + ViewName name() { + var mlist = List.of("one","two","three", "four"); + return new ViewName("JimBolin", Instant.now(), "MoreMeMore", mlist); } - @JStache(path = "ui/home.mustache") - public record Home(String name) {} - } diff --git a/tests/test-nima-htmx/src/main/java/org/example/htmx/ViewHome.java b/tests/test-nima-htmx/src/main/java/org/example/htmx/ViewHome.java new file mode 100644 index 000000000..607dc690b --- /dev/null +++ b/tests/test-nima-htmx/src/main/java/org/example/htmx/ViewHome.java @@ -0,0 +1,7 @@ +package org.example.htmx; + +import io.jstach.jstache.JStache; + +@JStache(path = "home") +public record ViewHome(String name) { +} diff --git a/tests/test-nima-htmx/src/main/java/org/example/htmx/ViewName.java b/tests/test-nima-htmx/src/main/java/org/example/htmx/ViewName.java new file mode 100644 index 000000000..2cda3b0cf --- /dev/null +++ b/tests/test-nima-htmx/src/main/java/org/example/htmx/ViewName.java @@ -0,0 +1,13 @@ +package org.example.htmx; + +import io.jstach.jstache.JStache; + +import java.time.Instant; +import java.util.List; + +@JStache(path = "name") +public record ViewName(String name, Instant foo, String more, List mlist) { + public String when() { + return foo.toString(); + } +} diff --git a/tests/test-nima-htmx/src/main/java/org/example/htmx/model/Name.java b/tests/test-nima-htmx/src/main/java/org/example/htmx/model/Name.java deleted file mode 100644 index 4ec0bdf00..000000000 --- a/tests/test-nima-htmx/src/main/java/org/example/htmx/model/Name.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.example.htmx.model; - -import io.jstach.jstache.JStache; - -import java.time.Instant; -import java.util.List; - -@JStache(path = "ui/name.mustache") -public record Name(String name, Instant foo, String more, List mlist) { - public String when() { - return foo.toString(); - } - - public record Pair(String nm, int eg) {} -} diff --git a/tests/test-nima-htmx/src/main/java/org/example/htmx/package-info.java b/tests/test-nima-htmx/src/main/java/org/example/htmx/package-info.java new file mode 100644 index 000000000..a0e8d10d8 --- /dev/null +++ b/tests/test-nima-htmx/src/main/java/org/example/htmx/package-info.java @@ -0,0 +1,4 @@ +@JStachePath(prefix = "ui/", suffix = ".mustache") +package org.example.htmx; + +import io.jstach.jstache.JStachePath; diff --git a/tests/test-nima-htmx/src/main/resources/ui/fragments/layout.mustache b/tests/test-nima-htmx/src/main/resources/ui/fragments/layout.mustache new file mode 100644 index 000000000..26a8c9e85 --- /dev/null +++ b/tests/test-nima-htmx/src/main/resources/ui/fragments/layout.mustache @@ -0,0 +1,11 @@ + + + + Hi + + + +

Heading

+{{$body}}Empty body{{/body}} + + diff --git a/tests/test-nima-htmx/src/main/resources/ui/home.mustache b/tests/test-nima-htmx/src/main/resources/ui/home.mustache index 4526fc2a4..af4686584 100644 --- a/tests/test-nima-htmx/src/main/resources/ui/home.mustache +++ b/tests/test-nima-htmx/src/main/resources/ui/home.mustache @@ -1,13 +1,8 @@ - - - Hi - - - -

Heading

-
- Hi {{ name }} -
- - - +{{ + Hi there {{ name }} + + +{{/body}} +{{/fragments/layout}} From f0be661e86f1498ad0dfcb142ba680429e7cb0eb Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Thu, 6 Jun 2024 23:39:19 +1200 Subject: [PATCH 07/10] Javadoc, tests pom fix --- .../java/io/avaje/htmx/nima/HxHandler.java | 25 +++++++++++++++++++ .../main/java/io/avaje/htmx/nima/HxReq.java | 3 +++ .../io/avaje/htmx/nima/TemplateRender.java | 6 +++++ htmx-nima/src/main/java/module-info.java | 3 +-- .../helidon/nima/ControllerWriter.java | 4 +-- tests/pom.xml | 3 +-- 6 files changed, 38 insertions(+), 6 deletions(-) diff --git a/htmx-nima/src/main/java/io/avaje/htmx/nima/HxHandler.java b/htmx-nima/src/main/java/io/avaje/htmx/nima/HxHandler.java index a719a793d..0c28dbbd5 100644 --- a/htmx-nima/src/main/java/io/avaje/htmx/nima/HxHandler.java +++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/HxHandler.java @@ -2,20 +2,45 @@ import io.helidon.webserver.http.Handler; +/** + * Wrap a Handler with filtering for Htmx specific headers. + *

+ * The underlying Handler will not be invoked unless the request + * is a Htmx request and matches the required attributes. + */ public interface HxHandler { + /** + * Create a builder that wraps the underlying handler with Htmx + * specific attribute matching. + */ static Builder builder(Handler delegate) { return new DHxHandlerBuilder(delegate); } + /** + * Build the Htmx request handler. + */ interface Builder { + /** + * Match on the given target. + */ Builder target(String target); + /** + * Match on the given trigger. + */ Builder trigger(String trigger); + /** + * Match on the given trigger name. + */ Builder triggerName(String triggerName); + /** + * Build and return the Handler. + */ Handler build(); } } diff --git a/htmx-nima/src/main/java/io/avaje/htmx/nima/HxReq.java b/htmx-nima/src/main/java/io/avaje/htmx/nima/HxReq.java index a18bd1124..2dda4c675 100644 --- a/htmx-nima/src/main/java/io/avaje/htmx/nima/HxReq.java +++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/HxReq.java @@ -3,6 +3,9 @@ import io.avaje.htmx.api.HtmxRequest; import io.helidon.webserver.http.ServerRequest; +/** + * Obtain the HtmxRequest for the given Helidon ServerRequest. + */ public class HxReq { /** diff --git a/htmx-nima/src/main/java/io/avaje/htmx/nima/TemplateRender.java b/htmx-nima/src/main/java/io/avaje/htmx/nima/TemplateRender.java index ad95696e5..4cc1a62fa 100644 --- a/htmx-nima/src/main/java/io/avaje/htmx/nima/TemplateRender.java +++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/TemplateRender.java @@ -4,7 +4,13 @@ import io.helidon.webserver.http.ServerRequest; import io.helidon.webserver.http.ServerResponse; +/** + * Template render API for Helidon. + */ public interface TemplateRender { + /** + * Render the given template view model to the server response. + */ void render(Object viewModel, ServerRequest req, ServerResponse res); } diff --git a/htmx-nima/src/main/java/module-info.java b/htmx-nima/src/main/java/module-info.java index 8b593160d..8adfb5b45 100644 --- a/htmx-nima/src/main/java/module-info.java +++ b/htmx-nima/src/main/java/module-info.java @@ -3,6 +3,5 @@ requires io.avaje.htmx.api; requires io.helidon.webserver; - exports io.avaje.htmx.nima; - + exports io.avaje.htmx.nima; } diff --git a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java index 8b58214b6..e3254ba39 100644 --- a/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java +++ b/http-generator-helidon/src/main/java/io/avaje/http/generator/helidon/nima/ControllerWriter.java @@ -139,7 +139,7 @@ private void writeClassStart() { writer.append(" private final RequestContextResolver resolver;").eol(); } if (reader.html()) { - writer.append(" private final TemplateRender renderer; // v5").eol(); + writer.append(" private final TemplateRender renderer;").eol(); } for (final UType type : jsonTypes.values()) { @@ -170,7 +170,7 @@ private void writeClassStart() { writer.append(" this.validator = validator;").eol(); } if (reader.html()) { - writer.append(" this.renderer = renderer; // v5").eol(); + writer.append(" this.renderer = renderer;").eol(); } if (instrumentContext) { writer.append(" this.resolver = resolver;").eol(); diff --git a/tests/pom.xml b/tests/pom.xml index 57d9d2ed4..2a9a60998 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -27,7 +27,6 @@ test-jex test-client test-client-generation - test-nima-htmx @@ -38,7 +37,7 @@ test-nima - + test-nima-jsonb test-nima-htmx From 3886c1b7105999d2dd1c4f46643f2d70f31c6579 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 13 Aug 2024 23:14:26 +1200 Subject: [PATCH 08/10] Bump version --- htmx-nima-jstache/pom.xml | 2 +- tests/test-nima-htmx/pom.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/htmx-nima-jstache/pom.xml b/htmx-nima-jstache/pom.xml index 8f30611d1..564363aa9 100644 --- a/htmx-nima-jstache/pom.xml +++ b/htmx-nima-jstache/pom.xml @@ -39,7 +39,7 @@ io.avaje avaje-inject - 9.12 + 10.3 provided true diff --git a/tests/test-nima-htmx/pom.xml b/tests/test-nima-htmx/pom.xml index 239b9906f..ebd8c0797 100644 --- a/tests/test-nima-htmx/pom.xml +++ b/tests/test-nima-htmx/pom.xml @@ -22,12 +22,12 @@ io.avaje avaje-inject - 10.0-RC5 + ${avaje-inject.version} io.avaje avaje-htmx-nima-jstache - 2.6-RC1 + ${project.version} io.jstach From 1ab7dd3d3b75499b96afd49ab61052e66ca30fa1 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 13 Aug 2024 23:26:05 +1200 Subject: [PATCH 09/10] Bump version --- tests/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pom.xml b/tests/pom.xml index db31c428b..f69ede219 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -16,7 +16,7 @@ 3.26.0 2.17.1 2.5 - 10.0-RC5 + 10.3 4.0.9 6.1.6 @@ -38,7 +38,7 @@ test-nima test-nima-jsonb - test-nima-htmx + From a816514974fba955448614909d65e91a024796db Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Tue, 13 Aug 2024 23:31:06 +1200 Subject: [PATCH 10/10] Bump htmlx versions to 2.7 --- htmx-api/pom.xml | 2 +- htmx-nima-jstache/pom.xml | 2 +- htmx-nima/pom.xml | 4 ++-- tests/test-nima-htmx/pom.xml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/htmx-api/pom.xml b/htmx-api/pom.xml index 0fd7315ab..ad4b7e30c 100644 --- a/htmx-api/pom.xml +++ b/htmx-api/pom.xml @@ -6,7 +6,7 @@ io.avaje avaje-http-parent - 2.6-RC1 + 2.7 avaje-htmx-api diff --git a/htmx-nima-jstache/pom.xml b/htmx-nima-jstache/pom.xml index 564363aa9..3305f71c0 100644 --- a/htmx-nima-jstache/pom.xml +++ b/htmx-nima-jstache/pom.xml @@ -6,7 +6,7 @@ io.avaje avaje-http-parent - 2.6-RC1 + 2.7 avaje-htmx-nima-jstache diff --git a/htmx-nima/pom.xml b/htmx-nima/pom.xml index 62ca69254..bc405cfec 100644 --- a/htmx-nima/pom.xml +++ b/htmx-nima/pom.xml @@ -6,7 +6,7 @@ io.avaje avaje-http-parent - 2.6-RC1 + 2.7 avaje-htmx-nima @@ -21,7 +21,7 @@ io.avaje avaje-htmx-api - 2.6-RC1 + 2.7 io.helidon.webserver diff --git a/tests/test-nima-htmx/pom.xml b/tests/test-nima-htmx/pom.xml index ebd8c0797..2316c1bb5 100644 --- a/tests/test-nima-htmx/pom.xml +++ b/tests/test-nima-htmx/pom.xml @@ -6,7 +6,7 @@ io.avaje tests - 2.6-RC1 + 2.7 test-nima-htmx