diff --git a/.github/dependabot.yml b/.github/dependabot.yml index de2383d88..8178f9917 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,13 +3,16 @@ updates: - package-ecosystem: "maven" directory: "/" schedule: - interval: "weekly" + interval: weekly open-pull-requests-limit: 10 + groups: + dependencies: + patterns: + - "*" labels: - "dependencies" target-branch: "master" - - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/workflows/jdk-ea-stable.yml b/.github/workflows/jdk-ea-stable.yml new file mode 100644 index 000000000..a6c44f00f --- /dev/null +++ b/.github/workflows/jdk-ea-stable.yml @@ -0,0 +1,37 @@ + +name: JDK EA Stable + +on: + push: + pull_request: + workflow_dispatch: + schedule: + - cron: '39 1 * * 1,3,5' + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + - name: Set up Java + uses: oracle-actions/setup-java@v1 + with: + website: jdk.java.net + release: ea + version: stable + - name: Maven cache + uses: actions/cache@v4 + env: + cache-name: maven-cache + with: + path: + ~/.m2 + key: build-${{ env.cache-name }} + - name: Maven version + run: mvn --version + - name: Build with Maven + run: mvn clean verify package -Ptest diff --git a/.github/workflows/jdk-ea.yml b/.github/workflows/jdk-ea.yml index 6dec68f0c..7e3cdc1e3 100644 --- a/.github/workflows/jdk-ea.yml +++ b/.github/workflows/jdk-ea.yml @@ -2,6 +2,7 @@ name: avaje-http EA on: + pull_request: workflow_dispatch: schedule: - cron: '39 1 * * 2,5' diff --git a/README.md b/README.md index bfe8803b0..a47585153 100644 --- a/README.md +++ b/README.md @@ -47,17 +47,13 @@ Use source code generation to adapt annotated REST controllers `@Path, @Get, @Po ``` -### JDK 22+ +### JDK 23+ -In JDK 22+, annotation processors are disabled by default, you will need to add a flag to re-enable. +In JDK 23+, annotation processors are disabled by default, you will need to add a flag to re-enable. ```xml - - org.apache.maven.plugins - maven-compiler-plugin - - -proc:full - - + + full + ``` ## Define a Controller (These APT processors work with both Java and Kotlin) diff --git a/htmx-api/pom.xml b/htmx-api/pom.xml new file mode 100644 index 000000000..8bdc810c5 --- /dev/null +++ b/htmx-api/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + io.avaje + avaje-http-parent + 2.8-RC1 + + + avaje-htmx-api + + + + + + + io.avaje + avaje-lang + 1.1 + + + + diff --git a/htmx-api/src/main/java/io/avaje/htmx/api/ContentCache.java b/htmx-api/src/main/java/io/avaje/htmx/api/ContentCache.java new file mode 100644 index 000000000..56a8b2106 --- /dev/null +++ b/htmx-api/src/main/java/io/avaje/htmx/api/ContentCache.java @@ -0,0 +1,16 @@ +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.RetentionPolicy.RUNTIME; + +/** + * Mark a controller method as using a content cache. + */ +@Target(METHOD) +@Retention(RUNTIME) +public @interface ContentCache { + +} 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/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-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-jstache/pom.xml b/htmx-nima-jstache/pom.xml new file mode 100644 index 000000000..fa0592ddd --- /dev/null +++ b/htmx-nima-jstache/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + io.avaje + avaje-http-parent + 2.8-RC1 + + + avaje-htmx-nima-jstache + + + 21 + 21 + 21 + UTF-8 + false + 1.3.6 + + + + + io.jstach + jstachio + ${io.jstach.version} + + + io.avaje + avaje-htmx-api + ${project.version} + + + io.avaje + avaje-htmx-nima + ${project.version} + + + io.avaje + avaje-inject + 10.3 + provided + true + + + io.avaje + avaje-spi-service + 2.6 + 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..c74e0f617 --- /dev/null +++ b/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/DefaultTemplateProvider.java @@ -0,0 +1,25 @@ +package io.avaje.htmx.nima.jstache; + +import io.avaje.htmx.nima.TemplateContentCache; +import io.avaje.htmx.nima.TemplateRender; +import io.avaje.inject.BeanScopeBuilder; +import io.avaje.inject.spi.InjectPlugin; +import io.avaje.spi.ServiceProvider; + +/** + * Plugin for avaje inject that provides a default TemplateRender instance. + */ +@ServiceProvider +public final class DefaultTemplateProvider implements InjectPlugin { + + @Override + public Class[] provides() { + return new Class[]{TemplateRender.class, TemplateContentCache.class}; + } + + @Override + public void apply(BeanScopeBuilder builder) { + builder.provideDefault(null, TemplateRender.class, JStacheTemplateRender::new); + builder.provideDefault(null, TemplateContentCache.class, SimpleContentCache::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..7b5b70294 --- /dev/null +++ b/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/JStacheTemplateRender.java @@ -0,0 +1,12 @@ +package io.avaje.htmx.nima.jstache; + +import io.avaje.htmx.nima.TemplateRender; +import io.jstach.jstachio.JStachio; + +public final class JStacheTemplateRender implements TemplateRender { + + @Override + public String render(Object viewModel) { + return JStachio.render(viewModel); + } +} diff --git a/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/SimpleContentCache.java b/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/SimpleContentCache.java new file mode 100644 index 000000000..aaa64b6a5 --- /dev/null +++ b/htmx-nima-jstache/src/main/java/io/avaje/htmx/nima/jstache/SimpleContentCache.java @@ -0,0 +1,31 @@ +package io.avaje.htmx.nima.jstache; + +import io.avaje.htmx.nima.TemplateContentCache; +import io.helidon.webserver.http.ServerRequest; + +import java.util.concurrent.ConcurrentHashMap; + +public class SimpleContentCache implements TemplateContentCache { + + private final ConcurrentHashMap localCache = new ConcurrentHashMap<>(); + + @Override + public String key(ServerRequest req) { + return req.requestedUri().path().rawPath(); + } + + @Override + public String key(ServerRequest req, Object formParams) { + return req.requestedUri().path().rawPath() + formParams; + } + + @Override + public String content(String key) { + return localCache.get(key); + } + + @Override + public void contentPut(String key, String content) { + localCache.put(key, 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..88d587079 --- /dev/null +++ b/htmx-nima-jstache/src/main/java/module-info.java @@ -0,0 +1,12 @@ +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 transitive io.avaje.inject; + requires static io.avaje.spi; + + provides io.avaje.inject.spi.InjectExtension with io.avaje.htmx.nima.jstache.DefaultTemplateProvider; +} diff --git a/htmx-nima/pom.xml b/htmx-nima/pom.xml new file mode 100644 index 000000000..0598cec23 --- /dev/null +++ b/htmx-nima/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + io.avaje + avaje-http-parent + 2.8-RC1 + + + avaje-htmx-nima + + + 21 + false + UTF-8 + + + + + io.avaje + avaje-htmx-api + 2.8-RC1 + + + io.helidon.webserver + helidon-webserver + 4.1.0 + + + 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..0c28dbbd5 --- /dev/null +++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/HxHandler.java @@ -0,0 +1,46 @@ +package io.avaje.htmx.nima; + +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/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..2dda4c675 --- /dev/null +++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/HxReq.java @@ -0,0 +1,49 @@ +package io.avaje.htmx.nima; + +import io.avaje.htmx.api.HtmxRequest; +import io.helidon.webserver.http.ServerRequest; + +/** + * Obtain the HtmxRequest for the given Helidon 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/io/avaje/htmx/nima/TemplateContentCache.java b/htmx-nima/src/main/java/io/avaje/htmx/nima/TemplateContentCache.java new file mode 100644 index 000000000..83a8a60e7 --- /dev/null +++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/TemplateContentCache.java @@ -0,0 +1,30 @@ +package io.avaje.htmx.nima; + +import io.helidon.webserver.http.ServerRequest; + +/** + * Defines caching of template content. + */ +public interface TemplateContentCache { + + /** + * Return the key given the request. + */ + String key(ServerRequest req); + + /** + * Return the key given the request with form parameters. + */ + String key(ServerRequest req, Object formParams); + + /** + * Return the content given the key. + */ + String content(String key); + + /** + * Put the content into the cache. + */ + void contentPut(String key, String content); + +} 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..fa69f00f6 --- /dev/null +++ b/htmx-nima/src/main/java/io/avaje/htmx/nima/TemplateRender.java @@ -0,0 +1,12 @@ +package io.avaje.htmx.nima; + +/** + * Template render API for Helidon. + */ +public interface TemplateRender { + + /** + * Render the given template view model to the server response. + */ + String render(Object viewModel); +} 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..8adfb5b45 --- /dev/null +++ b/htmx-nima/src/main/java/module-info.java @@ -0,0 +1,7 @@ +module io.avaje.htmx.nima { + + requires io.avaje.htmx.api; + requires io.helidon.webserver; + + exports io.avaje.htmx.nima; +} diff --git a/http-api-javalin/pom.xml b/http-api-javalin/pom.xml index f38b8839f..9f3b18bd3 100644 --- a/http-api-javalin/pom.xml +++ b/http-api-javalin/pom.xml @@ -4,7 +4,7 @@ io.avaje avaje-http-parent - 2.6-RC1 + 2.8-RC1 .. diff --git a/http-api/pom.xml b/http-api/pom.xml index 704fb8d12..80efe5d32 100644 --- a/http-api/pom.xml +++ b/http-api/pom.xml @@ -4,7 +4,7 @@ io.avaje avaje-http-parent - 2.6-RC1 + 2.8-RC1 .. @@ -24,7 +24,7 @@ io.javalin javalin - 6.1.6 + 6.3.0 provided true diff --git a/http-client-gson-adapter/pom.xml b/http-client-gson-adapter/pom.xml index a2c4ff1d2..099a2b053 100644 --- a/http-client-gson-adapter/pom.xml +++ b/http-client-gson-adapter/pom.xml @@ -4,7 +4,7 @@ io.avaje avaje-http-parent - 2.6-RC1 + 2.8-RC1 avaje-http-client-gson @@ -20,7 +20,7 @@ io.avaje avaje-http-client - 2.6-RC1 + 2.8-RC1 provided diff --git a/http-client-moshi-adapter/pom.xml b/http-client-moshi-adapter/pom.xml index 93173772b..f5cd2899c 100644 --- a/http-client-moshi-adapter/pom.xml +++ b/http-client-moshi-adapter/pom.xml @@ -3,7 +3,7 @@ io.avaje avaje-http-parent - 2.6-RC1 + 2.8-RC1 avaje-http-client-moshi @@ -19,7 +19,7 @@ io.avaje avaje-http-client - 2.6-RC1 + 2.8-RC1 provided diff --git a/http-client/pom.xml b/http-client/pom.xml index 7144e47f2..c7562cc9c 100644 --- a/http-client/pom.xml +++ b/http-client/pom.xml @@ -4,7 +4,7 @@ io.avaje avaje-http-parent - 2.6-RC1 + 2.8-RC1 avaje-http-client @@ -29,21 +29,21 @@ com.fasterxml.jackson.core jackson-databind - 2.17.1 + 2.17.2 true io.avaje avaje-jsonb - 1.11 + 2.1 true io.avaje avaje-inject - 9.12 + 10.3 true @@ -59,14 +59,14 @@ io.javalin javalin - 6.1.6 + 6.3.0 test io.avaje avaje-http-api - 2.6-RC1 + 2.8-RC1 test @@ -99,7 +99,7 @@ io.avaje avaje-inject-generator - 9.12 + 10.3 diff --git a/http-generator-client/pom.xml b/http-generator-client/pom.xml index 305f4aa6e..a662b44a7 100644 --- a/http-generator-client/pom.xml +++ b/http-generator-client/pom.xml @@ -4,7 +4,7 @@ io.avaje avaje-http-parent - 2.6-RC1 + 2.8-RC1 avaje-http-client-generator diff --git a/http-generator-client/src/main/java/io/avaje/http/generator/client/ClientProcessor.java b/http-generator-client/src/main/java/io/avaje/http/generator/client/ClientProcessor.java index bfa894148..2ba487244 100644 --- a/http-generator-client/src/main/java/io/avaje/http/generator/client/ClientProcessor.java +++ b/http-generator-client/src/main/java/io/avaje/http/generator/client/ClientProcessor.java @@ -17,6 +17,7 @@ import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; +import io.avaje.http.generator.core.APContext; import io.avaje.http.generator.core.ClientPrism; import io.avaje.http.generator.core.ControllerReader; import io.avaje.http.generator.core.ImportPrism; @@ -44,6 +45,7 @@ public SourceVersion getSupportedSourceVersion() { public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); this.processingEnv = processingEnv; + APContext.init(processingEnv); ProcessingContext.init(processingEnv, new ClientPlatformAdapter(), false); this.componentWriter = new SimpleComponentWriter(metaData); useJsonB = ProcessingContext.useJsonb(); @@ -51,7 +53,7 @@ public synchronized void init(ProcessingEnvironment processingEnv) { @Override public boolean process(Set annotations, RoundEnvironment round) { - ProcessingContext.findModule(annotations, round); + APContext.setProjectModuleElement(annotations, round); final var platform = platform(); if (!(platform instanceof ClientPlatformAdapter)) { setPlatform(new ClientPlatformAdapter()); @@ -107,7 +109,8 @@ protected String writeClientAdapter(ControllerReader reader) throws IOException private void initialiseComponent() { metaData.initialiseFullName(); if (!metaData.all().isEmpty()) { - ProcessingContext.validateModule(metaData.fullName()); + ProcessingContext.addClientComponent(metaData.fullName()); + ProcessingContext.validateModule(); } try { componentWriter.init(); diff --git a/http-generator-core/pom.xml b/http-generator-core/pom.xml index 31194e098..0b730af17 100644 --- a/http-generator-core/pom.xml +++ b/http-generator-core/pom.xml @@ -4,7 +4,7 @@ io.avaje avaje-http-parent - 2.6-RC1 + 2.8-RC1 avaje-http-generator-core @@ -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/BaseProcessor.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java index 595014b7a..485835fa3 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/BaseProcessor.java @@ -22,6 +22,11 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.util.ElementFilter; +import io.avaje.prism.GenerateAPContext; +import io.avaje.prism.GenerateModuleInfoReader; + +@GenerateAPContext +@GenerateModuleInfoReader @SupportedOptions({"useJavax", "useSingleton", "instrumentRequests","disableDirectWrites"}) public abstract class BaseProcessor extends AbstractProcessor { @@ -42,6 +47,7 @@ public Set getSupportedAnnotationTypes() { @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); + APContext.init(processingEnv); ProcessingContext.init(processingEnv, providePlatformAdapter()); } @@ -51,7 +57,7 @@ public synchronized void init(ProcessingEnvironment processingEnv) { @Override public boolean process(Set annotations, RoundEnvironment round) { var pathElements = round.getElementsAnnotatedWith(typeElement(PathPrism.PRISM_TYPE)); - + APContext.setProjectModuleElement(annotations, round); if (contextPathString == null) { contextPathString = ElementFilter.modulesIn(pathElements).stream() @@ -79,6 +85,7 @@ public boolean process(Set annotations, RoundEnvironment if (round.processingOver()) { writeOpenAPI(); + ProcessingContext.validateModule(); } return false; } 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..2e5cd9dcd 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,10 @@ 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; + /** Set true via {@code @ContentCache} to indicate use of Templating content cache */ + private boolean hasContentCache; private boolean methodHasValid; /** @@ -70,7 +74,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 +177,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 +198,14 @@ String produces() { return producesPrism; } + public boolean html() { + return html; + } + + public boolean hasContentCache() { + return hasContentCache; + } + public TypeElement beanType() { return beanType; } @@ -235,10 +253,11 @@ public void read(boolean withSingleton) { } private void deriveIncludeValidation() { - methodHasValid = methodHasValid(); + methodHasValid = anyMethodHasValid(); + hasContentCache = anyMethodHasContentCache(); } - private boolean methodHasValid() { + private boolean anyMethodHasValid() { for (final MethodReader method : methods) { if (method.hasValid()) { return true; @@ -247,6 +266,15 @@ private boolean methodHasValid() { return false; } + private boolean anyMethodHasContentCache() { + for (final MethodReader method : methods) { + if (method.hasContentCache()) { + return true; + } + } + return false; + } + private void readField(Element element) { if (!requestScope) { final String rawType = element.asType().toString(); diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/JsonBUtil.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/JsonBUtil.java index a47923feb..212600c96 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/JsonBUtil.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/JsonBUtil.java @@ -5,9 +5,14 @@ import java.util.function.Consumer; import java.util.stream.Collectors; -public class JsonBUtil { +public final class JsonBUtil { + private JsonBUtil() {} + public static boolean isJsonMimeType(String producesMimeType) { + return producesMimeType == null || producesMimeType.toLowerCase().contains("application/json"); + } + public static Map jsonTypes(ControllerReader reader) { final Map jsonTypes = new LinkedHashMap<>(); 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..d427b57b9 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 @@ -41,8 +41,10 @@ public class MethodReader { private final List actualParams; private final PathSegments pathSegments; private final boolean hasValid; + private final Optional contentCache; private final List superMethods; private final Optional timeout; + private final HxRequestPrism hxRequest; private WebMethod webMethod; private int statusCode; @@ -77,6 +79,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 -> { @@ -85,10 +88,12 @@ public class MethodReader { }); if (isWebMethod()) { this.hasValid = initValid(); + this.contentCache = initContentCache(); this.instrumentContext = initResolver(); this.pathSegments = PathSegments.parse(Util.combinePath(bean.path(), webMethodPath)); } else { this.hasValid = false; + this.contentCache = Optional.empty(); this.pathSegments = null; this.instrumentContext = false; } @@ -122,7 +127,11 @@ private boolean initValid() { private boolean superMethodHasValid() { return superMethods.stream() - .anyMatch(e -> findAnnotation(ValidPrism::getOptionalOn).isPresent()); + .anyMatch(e -> findAnnotation(ValidPrism::getOptionalOn).isPresent()); + } + + private Optional initContentCache() { + return findAnnotation(ContentCachePrism::getOptionalOn); } @Override @@ -190,6 +199,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; } @@ -404,6 +420,10 @@ boolean hasValid() { return hasValid; } + public boolean hasContentCache() { + return contentCache.isPresent(); + } + public String simpleName() { return element.getSimpleName().toString(); } diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/ProcessingContext.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/ProcessingContext.java index f99a34b16..f15a9ebc0 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/ProcessingContext.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/ProcessingContext.java @@ -1,11 +1,8 @@ package io.avaje.http.generator.core; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; import java.net.URI; import java.nio.file.Paths; -import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -16,7 +13,6 @@ import javax.annotation.processing.Filer; import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; -import javax.annotation.processing.RoundEnvironment; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.ModuleElement; @@ -53,8 +49,7 @@ private static final class Ctx { private final boolean instrumentAllMethods; private final boolean disableDirectWrites; private final boolean javalin6; - private ModuleElement module; - private boolean validated; + private String clientFQN; Ctx(ProcessingEnvironment env, PlatformAdapter adapter, boolean generateOpenAPI) { readAdapter = adapter; @@ -146,7 +141,8 @@ public static JavaFileObject createWriter(String cls, Element origin) throws IOE /** Create a file writer for the META-INF services file. */ public static FileObject createMetaInfWriter(String target) throws IOException { - return CTX.get().filer.createResource(StandardLocation.CLASS_OUTPUT, "", target); + + return filer().createResource(StandardLocation.CLASS_OUTPUT, "", target); } public static JavaFileObject createWriter(String cls) throws IOException { @@ -175,7 +171,7 @@ public static List superMethods(Element element, String metho .filter(type -> !type.toString().contains("java.lang.Object")) .map(superType -> { final var superClass = (TypeElement) types.asElement(superType); - for (final ExecutableElement method : ElementFilter.methodsIn(CTX.get().elementUtils.getAllMembers(superClass))) { + for (final var method : ElementFilter.methodsIn(CTX.get().elementUtils.getAllMembers(superClass))) { if (method.getSimpleName().contentEquals(methodName)) { return method; } @@ -231,48 +227,26 @@ public static boolean isAssignable2Interface(String type, String superType) { static Stream superTypes(Element element) { final Types types = CTX.get().typeUtils; return types.directSupertypes(element.asType()).stream() - .filter(type -> !type.toString().contains("java.lang.Object")) - .map(superType -> (TypeElement) types.asElement(superType)) - .flatMap(e -> Stream.concat(superTypes(e), Stream.of(e))) - .map(Object::toString); - } - - public static void findModule(Set annotations, RoundEnvironment roundEnv) { - if (CTX.get().module == null) { - CTX.get().module = - annotations.stream() - .map(roundEnv::getElementsAnnotatedWith) - .flatMap(Collection::stream) - .findAny() - .map(ProcessingContext::getModuleElement) - .orElse(null); - } - } + .filter(type -> !type.toString().contains("java.lang.Object")) + .map(superType -> (TypeElement) types.asElement(superType)) + .flatMap(e -> Stream.concat(superTypes(e), Stream.of(e))) + .map(Object::toString); + } + + public static void validateModule() { + var module = APContext.getProjectModuleElement(); + if (module != null && !module.isUnnamed()) { + try (var bufferedReader = APContext.getModuleInfoReader()) { + var reader = new ModuleInfoReader(module, bufferedReader); + reader.requires().forEach(r -> { + if (!r.isStatic() && r.getDependency().getQualifiedName().contentEquals("io.avaje.http.api.javalin")) { + logWarn(module, "io.avaje.http.api.javalin only contains SOURCE retention annotations. It should added as `requires static`"); + } + }); + var fqn = CTX.get().clientFQN; + + reader.validateServices("io.avaje.http.client.HttpClient.GeneratedComponent", Set.of(fqn)); - public static void validateModule(String fqn) { - var module = CTX.get().module; - if (module != null && !CTX.get().validated && !module.isUnnamed()) { - - CTX.get().validated = true; - try (var inputStream = - CTX.get() - .filer - .getResource(StandardLocation.SOURCE_PATH, "", "module-info.java") - .toUri() - .toURL() - .openStream(); - var reader = new BufferedReader(new InputStreamReader(inputStream))) { - - var noProvides = reader.lines().map(s -> { - if (s.contains("io.avaje.http.api.javalin") && !s.contains("static")) { - logWarn("io.avaje.http.api.javalin only contains SOURCE retention annotations. It should added as `requires static`"); - } - return s; - }) - .noneMatch(s -> s.contains(fqn)); - if (noProvides && !buildPluginAvailable()) { - logError(module, "Missing `provides io.avaje.http.client.HttpClient.GeneratedComponent with %s;`", fqn); - } } catch (Exception e) { // can't read module } @@ -309,4 +283,8 @@ private static boolean resourceExists(String relativeName) { return false; } } + + public static void addClientComponent(String clientFQN) { + CTX.get().clientFQN = clientFQN; + } } diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/TypeMap.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/TypeMap.java index 1594766f2..037b69507 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/TypeMap.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/TypeMap.java @@ -316,12 +316,12 @@ static class EnumHandler extends ObjectHandler { @Override public String toMethod() { - return "(" + type.shortType() + ") toEnum(" + type.shortType() + ".class, "; + return "(" + type.shortTypeNested() + ") toEnum(" + type.shortTypeNested() + ".class, "; } @Override public String asMethod() { - return "(" + type.shortType() + ") asEnum(" + type.shortType() + ".class, "; + return "(" + type.shortTypeNested() + ") asEnum(" + type.shortTypeNested() + ".class, "; } } diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/UType.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/UType.java index f22658b2b..24738fdb3 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/UType.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/UType.java @@ -31,6 +31,13 @@ static UType parse(String type) { */ String shortType(); + /** + * Return the short type taking nested type into account. + */ + default String shortTypeNested() { + return shortType(); + } + /** * Return the short name. */ @@ -152,6 +159,11 @@ public String shortType() { return Util.shortName(rawType); } + @Override + public String shortTypeNested() { + return Util.shortName(rawType, true); + } + @Override public String shortName() { return Util.initLower(shortType()).replace(".", "$"); diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/MediaType.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/MediaType.java index 331488cfa..e4af9c899 100644 --- a/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/MediaType.java +++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/MediaType.java @@ -4,6 +4,7 @@ public enum MediaType { APPLICATION_JSON("application/json"), TEXT_PLAIN("text/plain"), TEXT_HTML("text/html"), + HTML_UTF8("text/html;charset=UTF8"), UNKNOWN(""); private final String value; 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..5300dda1b 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,9 @@ @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) +@GeneratePrism(value = io.avaje.htmx.api.Html.class, publicAccess = true) +@GeneratePrism(value = io.avaje.htmx.api.ContentCache.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-core/src/test/java/io/avaje/http/generator/core/UTypeTest.java b/http-generator-core/src/test/java/io/avaje/http/generator/core/UTypeTest.java index 12dea5d6d..51e79c70b 100644 --- a/http-generator-core/src/test/java/io/avaje/http/generator/core/UTypeTest.java +++ b/http-generator-core/src/test/java/io/avaje/http/generator/core/UTypeTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; class UTypeTest { @@ -17,4 +18,12 @@ void isJavaLangPackage_expect_false() { assertFalse(UType.isJavaLangPackage("java.lang.other.Foo")); assertFalse(UType.isJavaLangPackage("not.lang.Foo")); } + + @Test + void parseNestedEnum() { + UType uType = UType.parse("my.pack.Foo.NestedEnum"); + assertThat(uType.mainType()).isEqualTo("my.pack.Foo.NestedEnum"); + assertThat(uType.shortType()).isEqualTo("Foo.NestedEnum"); + assertThat(uType.shortTypeNested()).isEqualTo("NestedEnum"); + } } diff --git a/http-generator-helidon/pom.xml b/http-generator-helidon/pom.xml index 0494b1daf..8fb09b39f 100644 --- a/http-generator-helidon/pom.xml +++ b/http-generator-helidon/pom.xml @@ -4,7 +4,7 @@ avaje-http-parent io.avaje - 2.6-RC1 + 2.8-RC1 avaje-http-helidon-generator 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..5afa98747 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; @@ -67,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(); @@ -92,10 +87,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(); @@ -111,7 +131,7 @@ void writeHandler(boolean requestScoped) { writer.append(" res.status(%s);", lookupStatusCode(statusCode)).eol(); } } - + boolean withFormParams = false; final var bodyType = method.bodyType(); if (bodyType != null && !method.isErrorMethod() && !isFilter) { if ("InputStream".equals(bodyType)) { @@ -124,26 +144,41 @@ void writeHandler(boolean requestScoped) { } else { defaultHelidonBodyContent(); } - } else if (usesFormParams()) { - writer.append(" var formParams = req.content().as(Parameters.class);").eol(); + } else { + withFormParams = usesFormParams(); + if (withFormParams) { + writer.append(" var formParams = req.content().as(Parameters.class);").eol(); + } + } + final ResponseMode responseMode = responseMode(); + final boolean withContentCache = responseMode == ResponseMode.Templating && useContentCache(); + if (withContentCache) { + writer.append(" var key = contentCache.key(req"); + if (withFormParams) { + writer.append(", formParams"); + } + writer.append(");").eol(); + writer.append(" var cacheContent = contentCache.content(key);").eol(); + writer.append(" if (cacheContent != null) {").eol(); + writeContextReturn(" "); + writer.append(" res.send(cacheContent);").eol(); + writer.append(" return;").eol(); + writer.append(" }").eol(); } final var segments = method.pathSegments(); if (segments.fullPath().contains("{")) { writer.append(" var pathParams = req.path().pathParameters();").eol(); } - for (final PathSegments.Segment matrixSegment : segments.matrixSegments()) { matrixSegment.writeCreateSegment(writer, platform()); } - final var params = method.params(); for (final MethodParam param : params) { if (!isExceptionOrFilterChain(param)) { param.writeCtxGet(writer, segments); } } - if (method.includeValidate()) { for (final MethodParam param : params) { param.writeValidate(writer); @@ -185,7 +220,7 @@ void writeHandler(boolean requestScoped) { } writer.append(");").eol(); - if (!method.isVoid() && !isFilter) { + if (responseMode != ResponseMode.Void) { TypeMirror typeMirror = method.returnType(); boolean includeNoContent = !typeMirror.getKind().isPrimitive(); if (includeNoContent) { @@ -194,19 +229,29 @@ void writeHandler(boolean requestScoped) { writer.append(" } else {").eol(); } String indent = includeNoContent ? " " : " "; - writeContextReturn(indent); - if (isInputStream(method.returnType())) { - final var uType = UType.parse(method.returnType()); - writer.append(indent).append("result.transferTo(res.outputStream());", uType.shortName()).eol(); - } else if (producesJson()) { - if (returnTypeString()) { - writer.append(indent).append("res.send(result); // send raw JSON").eol(); - } else { - final var uType = UType.parse(method.returnType()); - writer.append(indent).append("%sJsonType.toJson(result, JsonOutput.of(res));", uType.shortName()).eol(); + if (responseMode == ResponseMode.Templating) { + writer.append(indent).append("var content = renderer.render(result);").eol(); + if (withContentCache) { + writer.append(indent).append("contentCache.contentPut(key, content);").eol(); } + writeContextReturn(indent); + writer.append(indent).append("res.send(content);").eol(); + } else { - writer.append(indent).append("res.send(result);").eol(); + writeContextReturn(indent); + if (responseMode == ResponseMode.InputStream) { + final var uType = UType.parse(method.returnType()); + writer.append(indent).append("result.transferTo(res.outputStream());", uType.shortName()).eol(); + } else if (responseMode == ResponseMode.Json) { + if (returnTypeString()) { + writer.append(indent).append("res.send(result); // send raw JSON").eol(); + } else { + final var uType = UType.parse(method.returnType()); + writer.append(indent).append("%sJsonType.toJson(result, JsonOutput.of(res));", uType.shortName()).eol(); + } + } else { + writer.append(indent).append("res.send(result);").eol(); + } } if (includeNoContent) { writer.append(" }").eol(); @@ -215,6 +260,40 @@ void writeHandler(boolean requestScoped) { writer.append(" }").eol().eol(); } + enum ResponseMode { + Void, + Json, + Templating, + InputStream, + Other + } + + ResponseMode responseMode() { + if (method.isVoid() || isFilter) { + return ResponseMode.Void; + } + if (isInputStream(method.returnType())) { + return ResponseMode.InputStream; + } + if (producesJson()) { + return ResponseMode.Json; + } + if (useTemplating()) { + return ResponseMode.Templating; + } + return ResponseMode.Other; + } + + private boolean useContentCache() { + return method.hasContentCache(); + } + + 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()); @@ -280,6 +359,7 @@ private void writeContextReturn(String indent) { final var contentTypeString = "res.headers().contentType(MediaTypes."; writer.append(indent); switch (produces) { + case HTML_UTF8 -> writer.append("res.headers().contentType(HTML_UTF8);").eol(); case APPLICATION_JSON -> writer.append(contentTypeString).append("APPLICATION_JSON);").eol(); case TEXT_HTML -> writer.append(contentTypeString).append("TEXT_HTML);").eol(); case TEXT_PLAIN -> writer.append(contentTypeString).append("TEXT_PLAIN);").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..c2a39d2e8 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,17 @@ 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"); + } + if (reader.html()) { + reader.addImportType("io.avaje.htmx.nima.TemplateRender"); + if (reader.hasContentCache()) { + reader.addImportType("io.avaje.htmx.nima.TemplateContentCache"); + } + } } void write() { @@ -80,7 +92,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(); } @@ -120,16 +132,24 @@ private void writeClassStart() { if (reader.isIncludeValidator()) { writer.append(" private static final HeaderName HEADER_ACCEPT_LANGUAGE = HeaderNames.create(\"Accept-Language\");").eol(); } + if (reader.html()) { + writer.append(" private static final io.helidon.common.media.type.MediaType HTML_UTF8 = MediaTypes.create(\"text/html;charset=UTF8\");").eol(); + } writer.append(" private final %s %s;", controllerType, controllerName).eol(); 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;").eol(); + if (reader.hasContentCache()) { + writer.append(" private final TemplateContentCache contentCache;").eol(); + } + } for (final UType type : jsonTypes.values()) { if (!isInputStream(type.full())) { @@ -146,6 +166,12 @@ private void writeClassStart() { if (useJsonB) { writer.append(", Jsonb jsonb"); } + if (reader.html()) { + writer.append(", TemplateRender renderer"); + if (reader.hasContentCache()) { + writer.append(", TemplateContentCache contentCache"); + } + } if (instrumentContext) { writer.append(", RequestContextResolver resolver"); } @@ -155,6 +181,12 @@ private void writeClassStart() { if (reader.isIncludeValidator()) { writer.append(" this.validator = validator;").eol(); } + if (reader.html()) { + writer.append(" this.renderer = renderer;").eol(); + if (reader.hasContentCache()) { + writer.append(" this.contentCache = contentCache;").eol(); + } + } if (instrumentContext) { writer.append(" this.resolver = resolver;").eol(); } @@ -176,6 +208,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/http-generator-javalin/pom.xml b/http-generator-javalin/pom.xml index ea9a6ae73..1985a057c 100644 --- a/http-generator-javalin/pom.xml +++ b/http-generator-javalin/pom.xml @@ -4,7 +4,7 @@ io.avaje avaje-http-parent - 2.6-RC1 + 2.8-RC1 avaje-http-javalin-generator diff --git a/http-generator-javalin/src/main/java/io/avaje/http/generator/javalin/ControllerMethodWriter.java b/http-generator-javalin/src/main/java/io/avaje/http/generator/javalin/ControllerMethodWriter.java index 3cda06122..799d67763 100644 --- a/http-generator-javalin/src/main/java/io/avaje/http/generator/javalin/ControllerMethodWriter.java +++ b/http-generator-javalin/src/main/java/io/avaje/http/generator/javalin/ControllerMethodWriter.java @@ -4,14 +4,7 @@ import java.util.List; -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.PathSegments; -import io.avaje.http.generator.core.UType; -import io.avaje.http.generator.core.Util; -import io.avaje.http.generator.core.WebMethod; +import io.avaje.http.generator.core.*; import io.avaje.http.generator.core.openapi.MediaType; /** Write code to register Web route for a given controller method. */ @@ -158,8 +151,9 @@ private void writeContextReturn() { } private void writeContextReturn(final String resultVariableName) { - final var produces = method.produces(); - if (produces == null || MediaType.APPLICATION_JSON.getValue().equalsIgnoreCase(produces)) { + var produces = method.produces(); + boolean applicationJson = produces == null || MediaType.APPLICATION_JSON.getValue().equalsIgnoreCase(produces); + if (applicationJson || JsonBUtil.isJsonMimeType(produces)) { if (useJsonB) { var uType = UType.parse(method.returnType()); final boolean isfuture = "java.util.concurrent.CompletableFuture".equals(uType.mainType()); @@ -169,12 +163,19 @@ private void writeContextReturn(final String resultVariableName) { } writer.append(" try {"); } - writer.append(" %sJsonType.toJson(%s, ctx.contentType(\"application/json\").res().getOutputStream());", uType.shortName(), resultVariableName); + if (produces == null) { + produces = MediaType.APPLICATION_JSON.getValue(); + } + writer.append(" %sJsonType.toJson(%s, ctx.contentType(\"%s\").res().getOutputStream());", uType.shortName(), resultVariableName, produces); if (isfuture || method.isErrorMethod()) { writer.append(" } catch (java.io.IOException e) { throw new java.io.UncheckedIOException(e); }"); } } else { - writer.append(" ctx.json(%s);", resultVariableName); + if (applicationJson) { + writer.append(" ctx.json(%s);", resultVariableName); + } else { + writer.append(" ctx.contentType(\"%s\").json(%s);", produces, resultVariableName); + } } } else if (MediaType.TEXT_HTML.getValue().equalsIgnoreCase(produces)) { writer.append(" ctx.html(%s);", resultVariableName); diff --git a/http-generator-jex/pom.xml b/http-generator-jex/pom.xml index 787b49aca..f77bac53a 100644 --- a/http-generator-jex/pom.xml +++ b/http-generator-jex/pom.xml @@ -4,7 +4,7 @@ io.avaje avaje-http-parent - 2.6-RC1 + 2.8-RC1 avaje-http-jex-generator diff --git a/http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java b/http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java index dc663e297..da6d99a86 100644 --- a/http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java +++ b/http-generator-jex/src/main/java/io/avaje/http/generator/jex/ControllerMethodWriter.java @@ -3,12 +3,7 @@ import static io.avaje.http.generator.core.ProcessingContext.platform; import java.util.List; -import io.avaje.http.generator.core.Append; -import io.avaje.http.generator.core.MethodParam; -import io.avaje.http.generator.core.MethodReader; -import io.avaje.http.generator.core.PathSegments; -import io.avaje.http.generator.core.Util; -import io.avaje.http.generator.core.WebMethod; +import io.avaje.http.generator.core.*; import io.avaje.http.generator.core.openapi.MediaType; /** @@ -102,6 +97,8 @@ private void writeContextReturn() { writer.append("ctx.html("); } else if (produces.equalsIgnoreCase(MediaType.TEXT_PLAIN.getValue())) { writer.append("ctx.text("); + } else if (JsonBUtil.isJsonMimeType(produces)) { + writer.append("ctx.contentType(\"%s\").json(", produces); } else { writer.append("ctx.contentType(\"%s\").write(", produces); } diff --git a/http-hibernate-validator/pom.xml b/http-hibernate-validator/pom.xml index 2990065c8..323d53949 100644 --- a/http-hibernate-validator/pom.xml +++ b/http-hibernate-validator/pom.xml @@ -5,12 +5,12 @@ io.avaje avaje-http-hibernate-validator - 3.5-RC3 + 3.5 org.avaje java11-oss - 3.10 + 4.3 @@ -36,14 +36,14 @@ io.avaje avaje-http-api - 2.0-RC1 + 2.6 provided io.avaje avaje-inject - 9.3 + 10.1 provided diff --git a/http-hibernate-validator/src/main/java/io/avaje/http/hibernate/validator/ValidatorProvider.java b/http-hibernate-validator/src/main/java/io/avaje/http/hibernate/validator/ValidatorProvider.java index ce2d3356a..cb8759224 100644 --- a/http-hibernate-validator/src/main/java/io/avaje/http/hibernate/validator/ValidatorProvider.java +++ b/http-hibernate-validator/src/main/java/io/avaje/http/hibernate/validator/ValidatorProvider.java @@ -6,7 +6,7 @@ /** * Plugin for avaje inject that provides a default BeanValidator instance. */ -public final class ValidatorProvider implements io.avaje.inject.spi.Plugin { +public final class ValidatorProvider implements io.avaje.inject.spi.InjectPlugin { @Override public Class[] provides() { diff --git a/http-hibernate-validator/src/main/java/module-info.java b/http-hibernate-validator/src/main/java/module-info.java index dbd4a1f54..2d1c0f6f9 100644 --- a/http-hibernate-validator/src/main/java/module-info.java +++ b/http-hibernate-validator/src/main/java/module-info.java @@ -8,5 +8,5 @@ requires io.avaje.inject; requires jakarta.validation; - provides io.avaje.inject.spi.Plugin with ValidatorProvider; + provides io.avaje.inject.spi.InjectExtension with ValidatorProvider; } diff --git a/http-hibernate-validator/src/main/resources/META-INF/services/io.avaje.inject.spi.Plugin b/http-hibernate-validator/src/main/resources/META-INF/services/io.avaje.inject.spi.InjectExtension similarity index 100% rename from http-hibernate-validator/src/main/resources/META-INF/services/io.avaje.inject.spi.Plugin rename to http-hibernate-validator/src/main/resources/META-INF/services/io.avaje.inject.spi.InjectExtension diff --git a/http-inject-plugin/pom.xml b/http-inject-plugin/pom.xml index fc8de7fac..91e9da057 100644 --- a/http-inject-plugin/pom.xml +++ b/http-inject-plugin/pom.xml @@ -4,7 +4,7 @@ io.avaje avaje-http-parent - 2.6-RC1 + 2.8-RC1 .. @@ -19,17 +19,24 @@ io.avaje avaje-inject - 9.12 + 10.3 provided true io.avaje avaje-http-api - 1.37 + 2.7 true provided + + io.avaje + avaje-spi-service + 2.6 + provided + true + diff --git a/http-inject-plugin/src/main/java/io/avaje/http/inject/DefaultResolverProvider.java b/http-inject-plugin/src/main/java/io/avaje/http/inject/DefaultResolverProvider.java index 56b5ad2f2..b22eb7714 100644 --- a/http-inject-plugin/src/main/java/io/avaje/http/inject/DefaultResolverProvider.java +++ b/http-inject-plugin/src/main/java/io/avaje/http/inject/DefaultResolverProvider.java @@ -3,10 +3,12 @@ import io.avaje.http.api.context.RequestContextResolver; import io.avaje.http.api.context.ThreadLocalRequestContextResolver; import io.avaje.inject.BeanScopeBuilder; -import io.avaje.inject.spi.Plugin; +import io.avaje.inject.spi.InjectPlugin; +import io.avaje.spi.ServiceProvider; /** Plugin for avaje inject that provides a default RequestContextResolver instance. */ -public final class DefaultResolverProvider implements Plugin { +@ServiceProvider +public final class DefaultResolverProvider implements InjectPlugin { @Override public Class[] provides() { diff --git a/http-inject-plugin/src/main/java/module-info.java b/http-inject-plugin/src/main/java/module-info.java index c8149f2fe..e13c5e735 100644 --- a/http-inject-plugin/src/main/java/module-info.java +++ b/http-inject-plugin/src/main/java/module-info.java @@ -1,8 +1,8 @@ module io.avaje.http.plugin { - requires io.avaje.http.api; - requires io.avaje.inject; - - provides io.avaje.inject.spi.Plugin with io.avaje.http.inject.DefaultResolverProvider; + requires io.avaje.http.api; + requires io.avaje.inject; + requires static io.avaje.spi; + provides io.avaje.inject.spi.InjectExtension with io.avaje.http.inject.DefaultResolverProvider; } diff --git a/http-inject-plugin/src/main/resources/META-INF/services/io.avaje.inject.spi.Plugin b/http-inject-plugin/src/main/resources/META-INF/services/io.avaje.inject.spi.Plugin deleted file mode 100644 index d996e4e0c..000000000 --- a/http-inject-plugin/src/main/resources/META-INF/services/io.avaje.inject.spi.Plugin +++ /dev/null @@ -1 +0,0 @@ -io.avaje.http.inject.DefaultResolverProvider diff --git a/pom.xml b/pom.xml index a5f16b9ec..e569bebd2 100644 --- a/pom.xml +++ b/pom.xml @@ -4,12 +4,12 @@ org.avaje java11-oss - 4.1 + 4.4 io.avaje avaje-http-parent - 2.6-RC1 + 2.8-RC1 pom @@ -19,9 +19,9 @@ true - 2.2.22 + 2.2.23 2.14.2 - 1.21 + 1.31 ${project.build.directory}${file.separator}module-info.shade @@ -35,6 +35,7 @@ + htmx-api http-api http-api-javalin http-client @@ -60,12 +61,23 @@ jdk21plus - [21,22] + [21,) + 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 61d79f2b6..cdec44f34 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -4,7 +4,7 @@ avaje-http-parent io.avaje - 2.6-RC1 + 2.8-RC1 tests @@ -12,13 +12,13 @@ true - 5.10.2 - 3.26.0 - 2.17.1 + 5.11.0 + 3.26.3 + 2.17.2 2.5 - 9.12 - 4.0.9 - 6.1.6 + 10.3 + 4.1.0 + 6.3.0 @@ -33,11 +33,12 @@ jdk21plus - [21,22] + [21,) test-nima test-nima-jsonb + test-nima-htmx diff --git a/tests/test-client-generation/pom.xml b/tests/test-client-generation/pom.xml index 049ff4b3c..a1d6ba4da 100644 --- a/tests/test-client-generation/pom.xml +++ b/tests/test-client-generation/pom.xml @@ -4,7 +4,7 @@ io.avaje tests - 2.6-RC1 + 2.8-RC1 test-client-generation diff --git a/tests/test-client/pom.xml b/tests/test-client/pom.xml index d2d60d9da..70285d69d 100644 --- a/tests/test-client/pom.xml +++ b/tests/test-client/pom.xml @@ -6,7 +6,7 @@ io.avaje tests - 2.6-RC1 + 2.8-RC1 test-client diff --git a/tests/test-javalin-jsonb/pom.xml b/tests/test-javalin-jsonb/pom.xml index f45031ffb..d910cbb86 100644 --- a/tests/test-javalin-jsonb/pom.xml +++ b/tests/test-javalin-jsonb/pom.xml @@ -5,7 +5,7 @@ io.avaje tests - 2.6-RC1 + 2.8-RC1 test-javalin-jsonb @@ -13,7 +13,7 @@ true org.example.myapp.Main - 2.2.22 + 2.2.23 1.3.71 @@ -80,13 +80,13 @@ io.avaje avaje-jsonb - 1.11 + 2.1 io.avaje avaje-jsonb-generator - 1.11 + 2.1 provided @@ -101,7 +101,7 @@ io.rest-assured rest-assured - 5.4.0 + 5.5.0 test diff --git a/tests/test-javalin-jsonb/src/main/java/org/example/myapp/web/HelloController.java b/tests/test-javalin-jsonb/src/main/java/org/example/myapp/web/HelloController.java index a918215ed..fb6d8ecf5 100644 --- a/tests/test-javalin-jsonb/src/main/java/org/example/myapp/web/HelloController.java +++ b/tests/test-javalin-jsonb/src/main/java/org/example/myapp/web/HelloController.java @@ -25,6 +25,7 @@ import io.javalin.http.Context; import io.swagger.v3.oas.annotations.Hidden; import jakarta.inject.Inject; +import org.example.myapp.web.other.Foo; /** * Hello resource manager. @@ -82,6 +83,7 @@ List findByName(String name, @QueryParam("my-param") @Default("one") S /** * Simple example post with JSON body response. */ + @Produces(MediaType.APPLICATION_JSON_PATCH_JSON) @Post HelloDto post(HelloDto dto) { dto.name = "posted"; @@ -182,4 +184,10 @@ String controlStatusCode(Context ctx) { ctx.status(201); return "controlStatusCode"; } + + @Produces(value = "text/plain") + @Get("takesNestedEnum") + String takesNestedEnum(Foo.NestedEnum myEnum) { + return "takesNestedEnum-" + myEnum; + } } diff --git a/tests/test-javalin-jsonb/src/main/java/org/example/myapp/web/other/Foo.java b/tests/test-javalin-jsonb/src/main/java/org/example/myapp/web/other/Foo.java new file mode 100644 index 000000000..f3ed0507a --- /dev/null +++ b/tests/test-javalin-jsonb/src/main/java/org/example/myapp/web/other/Foo.java @@ -0,0 +1,7 @@ +package org.example.myapp.web.other; + +public class Foo { + public enum NestedEnum { + A, B, C + } +} diff --git a/tests/test-javalin-jsonb/src/main/resources/public/openapi.json b/tests/test-javalin-jsonb/src/main/resources/public/openapi.json index 087e3dd87..549828b2d 100644 --- a/tests/test-javalin-jsonb/src/main/resources/public/openapi.json +++ b/tests/test-javalin-jsonb/src/main/resources/public/openapi.json @@ -1,7 +1,7 @@ { "openapi" : "3.0.1", "info" : { - "title" : "Example service showing off the Path extension method of controller", + "title" : "Example service", "description" : "Example Javalin controllers with Java and Maven", "version" : "" }, @@ -14,11 +14,11 @@ "tags" : [ { "name" : "tag1", - "description" : "this is added to openapi tags" + "description" : "it's somethin" }, { "name" : "tag1", - "description" : "it's somethin" + "description" : "this is added to openapi tags" } ], "paths" : { @@ -320,7 +320,7 @@ "201" : { "description" : "", "content" : { - "application/json" : { + "application/json-patch+json" : { "schema" : { "$ref" : "#/components/schemas/HelloDto" } @@ -646,6 +646,41 @@ } } }, + "/hello/takesNestedEnum" : { + "get" : { + "tags" : [ + + ], + "summary" : "", + "description" : "", + "parameters" : [ + { + "name" : "myEnum", + "in" : "query", + "schema" : { + "type" : "string", + "enum" : [ + "A", + "B", + "C" + ] + } + } + ], + "responses" : { + "200" : { + "description" : "", + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + } + } + } + } + }, "/hello/withMatrix/{year_segment}/{other}" : { "get" : { "tags" : [ diff --git a/tests/test-javalin-jsonb/src/test/java/io/avaje/http/generator/JavalinProcessorTest.java b/tests/test-javalin-jsonb/src/test/java/io/avaje/http/generator/JavalinProcessorTest.java index cc5441088..b2262c874 100644 --- a/tests/test-javalin-jsonb/src/test/java/io/avaje/http/generator/JavalinProcessorTest.java +++ b/tests/test-javalin-jsonb/src/test/java/io/avaje/http/generator/JavalinProcessorTest.java @@ -25,7 +25,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.avaje.http.generator.javalin.JavalinProcessor; -import io.avaje.jsonb.generator.Processor; +import io.avaje.jsonb.generator.JsonbProcessor; class JavalinProcessorTest { @@ -76,7 +76,7 @@ public void runAnnotationProcessorJsonB() throws Exception { final var task = compiler.getTask( new PrintWriter(System.out), null, null, List.of("--release=11"), null, files); - task.setProcessors(List.of(new JavalinProcessor(), new Processor())); + task.setProcessors(List.of(new JavalinProcessor(), new JsonbProcessor())); assertThat(task.call()).isTrue(); assert Files.readString( @@ -104,7 +104,7 @@ public void runAnnotationProcessorJavax() throws Exception { "-AdisableDirectWrites=true"), null, files); - task.setProcessors(List.of(new JavalinProcessor(), new Processor())); + task.setProcessors(List.of(new JavalinProcessor(), new JsonbProcessor())); // we don't have javax on the cp assertThat(task.call()).isFalse(); @@ -133,7 +133,7 @@ public void runAnnotationProcessorJakarta() throws Exception { "-AdisableDirectWrites=true"), null, files); - task.setProcessors(List.of(new JavalinProcessor(), new Processor())); + task.setProcessors(List.of(new JavalinProcessor(), new JsonbProcessor())); assertThat(task.call()).isTrue(); @@ -163,7 +163,7 @@ public void testOpenAPIGeneration() throws Exception { List.of("--release=11", "-AdisableDirectWrites=true"), null, openAPIController); - task.setProcessors(List.of(new JavalinProcessor(), new Processor())); + task.setProcessors(List.of(new JavalinProcessor(), new JsonbProcessor())); assertThat(task.call()).isTrue(); @@ -197,7 +197,7 @@ public void testInheritableOpenAPIGeneration() throws Exception { List.of("--release=11", "-AdisableDirectWrites=true"), null, openAPIController); - task.setProcessors(List.of(new JavalinProcessor(), new Processor())); + task.setProcessors(List.of(new JavalinProcessor(), new JsonbProcessor())); assertThat(task.call()).isTrue(); diff --git a/tests/test-javalin/pom.xml b/tests/test-javalin/pom.xml index 6307622f1..925384baf 100644 --- a/tests/test-javalin/pom.xml +++ b/tests/test-javalin/pom.xml @@ -4,7 +4,7 @@ io.avaje tests - 2.6-RC1 + 2.8-RC1 test-javalin @@ -12,7 +12,7 @@ true org.example.myapp.Main - 2.2.22 + 2.2.23 1.3.71 @@ -94,7 +94,7 @@ io.rest-assured rest-assured - 5.4.0 + 5.5.0 test diff --git a/tests/test-javalin/src/main/java/org/example/myapp/web/HelloController.java b/tests/test-javalin/src/main/java/org/example/myapp/web/HelloController.java index 4ddd454ba..bfad1c72a 100644 --- a/tests/test-javalin/src/main/java/org/example/myapp/web/HelloController.java +++ b/tests/test-javalin/src/main/java/org/example/myapp/web/HelloController.java @@ -82,6 +82,7 @@ List findByName(String name, @QueryParam("my-param") @Default("one") S /** * Simple example post with JSON body response. */ + @Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8") @Post HelloDto post(HelloDto dto) { dto.name = "posted"; diff --git a/tests/test-javalin/src/main/resources/public/openapi.json b/tests/test-javalin/src/main/resources/public/openapi.json index ff158eb08..76a5ad5b4 100644 --- a/tests/test-javalin/src/main/resources/public/openapi.json +++ b/tests/test-javalin/src/main/resources/public/openapi.json @@ -310,7 +310,7 @@ "201" : { "description" : "", "content" : { - "application/json" : { + "application/json;charset=UTF-8" : { "schema" : { "$ref" : "#/components/schemas/HelloDto" } diff --git a/tests/test-jex/pom.xml b/tests/test-jex/pom.xml index 70714c850..dc6834c5f 100644 --- a/tests/test-jex/pom.xml +++ b/tests/test-jex/pom.xml @@ -4,7 +4,7 @@ io.avaje tests - 2.6-RC1 + 2.8-RC1 test-jex @@ -13,7 +13,7 @@ true org.example.myapp.Main 2.5 - 2.2.22 + 2.2.23 diff --git a/tests/test-nima-htmx/pom.xml b/tests/test-nima-htmx/pom.xml new file mode 100644 index 000000000..30dc21e50 --- /dev/null +++ b/tests/test-nima-htmx/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + io.avaje + tests + 2.8-RC1 + + + test-nima-htmx + + + 21 + UTF-8 + false + 1.3.6 + + + + + io.avaje + avaje-inject + ${avaje-inject.version} + + + io.avaje + avaje-htmx-nima-jstache + ${project.version} + + + 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.13.0 + + 21 + + + io.avaje + avaje-http-helidon-generator + ${project.version} + + + io.avaje + avaje-inject-generator + ${avaje-inject.version} + + + io.avaje + avaje-jsonb-generator + 2.1 + + + 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..482ffe033 --- /dev/null +++ b/tests/test-nima-htmx/src/main/java/org/example/htmx/UIController.java @@ -0,0 +1,32 @@ +package org.example.htmx; + +import io.avaje.htmx.api.Html; +import io.avaje.htmx.api.ContentCache; +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 java.time.Instant; +import java.util.List; + +@Html +@Controller +@Path("/") +public class UIController { + + @ContentCache + @Get + ViewHome index() { + return new ViewHome("Robin3"); + } + + @ContentCache + @HxRequest(target = "name") + @Get("name") + ViewName name() { + var mlist = List.of("one","two","three", "four"); + return new ViewName("JimBolin", Instant.now(), "MoreMeMore", mlist); + } + +} 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/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/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..ec94bf95e --- /dev/null +++ b/tests/test-nima-htmx/src/main/java/org/example/htmx/template/JstacheTemplateRender.java @@ -0,0 +1,14 @@ +package org.example.htmx.template; + +import io.avaje.htmx.nima.TemplateRender; +import io.avaje.inject.Component; +import io.jstach.jstachio.JStachio; + +@Component +public class JstacheTemplateRender implements TemplateRender { + + @Override + public String render(Object viewModel) { + return JStachio.render(viewModel); + } +} diff --git a/tests/test-nima-htmx/src/main/java/org/example/htmx/template/SimpleContentCache.java b/tests/test-nima-htmx/src/main/java/org/example/htmx/template/SimpleContentCache.java new file mode 100644 index 000000000..bf1757496 --- /dev/null +++ b/tests/test-nima-htmx/src/main/java/org/example/htmx/template/SimpleContentCache.java @@ -0,0 +1,33 @@ +package org.example.htmx.template; + +import io.avaje.inject.Component; +import io.avaje.htmx.nima.TemplateContentCache; +import io.helidon.webserver.http.ServerRequest; + +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class SimpleContentCache implements TemplateContentCache { + + private final ConcurrentHashMap localCache = new ConcurrentHashMap<>(); + + @Override + public String key(ServerRequest req) { + return req.requestedUri().path().rawPath(); + } + + @Override + public String key(ServerRequest req, Object formParams) { + return req.requestedUri().path().rawPath() + formParams; + } + + @Override + public String content(String key) { + return localCache.get(key); + } + + @Override + public void contentPut(String key, String content) { + localCache.put(key, content); + } +} 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 new file mode 100644 index 000000000..af4686584 --- /dev/null +++ b/tests/test-nima-htmx/src/main/resources/ui/home.mustache @@ -0,0 +1,8 @@ +{{ + Hi there {{ name }} + + +{{/body}} +{{/fragments/layout}} 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/pom.xml b/tests/test-nima-jsonb/pom.xml index 6b9f309a0..5d0fe3206 100644 --- a/tests/test-nima-jsonb/pom.xml +++ b/tests/test-nima-jsonb/pom.xml @@ -6,7 +6,7 @@ io.avaje tests - 2.6-RC1 + 2.8-RC1 test-nima-jsonb @@ -31,7 +31,7 @@ io.avaje avaje-jsonb - 1.11 + 2.1 io.avaje @@ -90,7 +90,7 @@ io.avaje avaje-jsonb-generator - 1.11 + 2.1 @@ -109,7 +109,7 @@ io.avaje avaje-inject-maven-plugin - 1.2 + 10.3 process-sources 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; diff --git a/tests/test-nima/pom.xml b/tests/test-nima/pom.xml index 3a909037e..ba385f431 100644 --- a/tests/test-nima/pom.xml +++ b/tests/test-nima/pom.xml @@ -6,7 +6,7 @@ io.avaje tests - 2.6-RC1 + 2.8-RC1 test-nima