From be7da3aa4477aadd4909b4c06d49af409c493935 Mon Sep 17 00:00:00 2001 From: xhaggi Date: Sun, 24 Nov 2024 08:30:54 +0100 Subject: [PATCH 1/4] Upgrade to Spring Boot 3.4.0 --- .../boot/mvc/HtmxExceptionHandlerExceptionResolver.java | 4 +++- pom.xml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxExceptionHandlerExceptionResolver.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxExceptionHandlerExceptionResolver.java index bd7cbbe..48d033b 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxExceptionHandlerExceptionResolver.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxExceptionHandlerExceptionResolver.java @@ -2,6 +2,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; @@ -26,7 +27,8 @@ public HtmxExceptionHandlerExceptionResolver(HtmxHandlerMethodAnnotationHandler @Override protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, Exception exception) { - ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception); + ServletWebRequest webRequest = new ServletWebRequest(request, response); + ServletInvocableHandlerMethod exceptionHandlerMethod = this.getExceptionHandlerMethod(handlerMethod, exception, webRequest); if (exceptionHandlerMethod != null) { Method method = exceptionHandlerMethod.getMethod(); handlerMethodAnnotationHandler.handleMethod(method, request, response); diff --git a/pom.xml b/pom.xml index 3c5a253..9b11786 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.2.5 + 3.4.0 From 489d84f6848dd04a3eaf44884775d9511cac7969 Mon Sep 17 00:00:00 2001 From: xhaggi Date: Sun, 24 Nov 2024 09:07:36 +0100 Subject: [PATCH 2/4] Get rid of deprecated classes and methods --- .../boot/mvc/HtmxHandlerInterceptor.java | 23 +- .../HtmxHandlerMethodAnnotationHandler.java | 36 +- .../HtmxHandlerMethodArgumentResolver.java | 4 - .../boot/mvc/HtmxMvcAutoConfiguration.java | 1 - .../htmx/spring/boot/mvc/HtmxRequest.java | 66 +- .../htmx/spring/boot/mvc/HtmxResponse.java | 734 +----------------- ...ResponseHandlerMethodArgumentResolver.java | 16 +- ...sponseHandlerMethodReturnValueHandler.java | 183 ----- .../htmx/spring/boot/mvc/HtmxTrigger.java | 1 - .../HtmxTriggerHeadersRequestCondition.java | 1 - .../htmx/spring/boot/mvc/HxLocation.java | 4 +- .../htmx/spring/boot/mvc/HxTrigger.java | 5 - .../spring/boot/mvc/HxTriggerLifecycle.java | 39 - .../spring/boot/mvc/RequestContextUtils.java | 6 - .../boot/mvc/HtmxHandlerInterceptorIT.java | 35 - .../boot/mvc/HtmxHandlerInterceptorTest.java | 18 +- ...HtmxHandlerMethodArgumentResolverTest.java | 4 +- .../HtmxPartialHandlerInterceptorTest.java | 106 --- ...lerMethodReturnValueHandlerController.java | 169 ---- ...seHandlerMethodReturnValueHandlerTest.java | 203 ----- .../spring/boot/mvc/HtmxResponseTest.java | 429 ---------- .../htmx/spring/boot/mvc/HxGetMapping.java | 8 +- .../spring/boot/mvc/PartialsController.java | 133 ---- .../htmx/spring/boot/mvc/TestController.java | 14 - .../support/PartialXpathResultMatchers.java | 176 ----- 25 files changed, 45 insertions(+), 2369 deletions(-) delete mode 100644 htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandler.java delete mode 100644 htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTriggerLifecycle.java delete mode 100644 htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptorIT.java delete mode 100644 htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxPartialHandlerInterceptorTest.java delete mode 100644 htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandlerController.java delete mode 100644 htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandlerTest.java delete mode 100644 htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseTest.java delete mode 100644 htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/PartialsController.java delete mode 100644 htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/support/PartialXpathResultMatchers.java diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptor.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptor.java index 7c09a72..c7037e8 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptor.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptor.java @@ -31,31 +31,16 @@ public void afterCompletion(HttpServletRequest request, HttpServletResponse resp HtmxResponse htmxResponse = RequestContextUtils.getHtmxResponse(request); if (htmxResponse != null) { - addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER, htmxResponse.getTriggersInternal()); - addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SETTLE, htmxResponse.getTriggersAfterSettleInternal()); - addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SWAP, htmxResponse.getTriggersAfterSwapInternal()); - - if (htmxResponse.getLocation() != null) { - HtmxLocation location = htmxResponse.getLocation(); - if (location.hasContextData()) { - location.setPath(RequestContextUtils.createUrl(request, location.getPath(), htmxResponse.isContextRelative())); - setHeaderJsonValue(response, HtmxResponseHeader.HX_LOCATION, location); - } else { - response.setHeader(HtmxResponseHeader.HX_LOCATION.getValue(), RequestContextUtils.createUrl(request, location.getPath(), htmxResponse.isContextRelative())); - } - } + addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER, htmxResponse.getTriggers()); + addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SETTLE, htmxResponse.getTriggersAfterSettle()); + addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SWAP, htmxResponse.getTriggersAfterSwap()); + if (htmxResponse.getReplaceUrl() != null) { response.setHeader(HtmxResponseHeader.HX_REPLACE_URL.getValue(), RequestContextUtils.createUrl(request, htmxResponse.getReplaceUrl(), htmxResponse.isContextRelative())); } if (htmxResponse.getPushUrl() != null) { response.setHeader(HtmxResponseHeader.HX_PUSH_URL.getValue(), RequestContextUtils.createUrl(request, htmxResponse.getPushUrl(), htmxResponse.isContextRelative())); } - if (htmxResponse.getRedirect() != null) { - response.setHeader(HtmxResponseHeader.HX_REDIRECT.getValue(), RequestContextUtils.createUrl(request, htmxResponse.getRedirect(), htmxResponse.isContextRelative())); - } - if (htmxResponse.isRefresh()) { - response.setHeader(HtmxResponseHeader.HX_REFRESH.getValue(), "true"); - } if (htmxResponse.getRetarget() != null) { response.setHeader(HtmxResponseHeader.HX_RETARGET.getValue(), htmxResponse.getRetarget()); } diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodAnnotationHandler.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodAnnotationHandler.java index 97cb271..83e952e 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodAnnotationHandler.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodAnnotationHandler.java @@ -5,13 +5,10 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.http.HttpHeaders; import java.lang.reflect.Method; import java.time.Duration; -import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponseHeader.*; - /** * A handler for processing htmx annotations present on exception handler methods. * @@ -56,9 +53,9 @@ private void setHxPushUrl(HttpServletRequest request, HttpServletResponse respon HxPushUrl methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxPushUrl.class); if (methodAnnotation != null) { if (HtmxValue.TRUE.equals(methodAnnotation.value())) { - setHeader(response, HX_PUSH_URL, getRequestUrl(request)); + setHeader(response, HtmxResponseHeader.HX_PUSH_URL, getRequestUrl(request)); } else { - setHeader(response, HX_PUSH_URL, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative())); + setHeader(response, HtmxResponseHeader.HX_PUSH_URL, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative())); } } } @@ -66,7 +63,7 @@ private void setHxPushUrl(HttpServletRequest request, HttpServletResponse respon private void setHxRedirect(HttpServletRequest request, HttpServletResponse response, Method method) { HxRedirect methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRedirect.class); if (methodAnnotation != null) { - setHeader(response, HX_REDIRECT, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative())); + setHeader(response, HtmxResponseHeader.HX_REDIRECT, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative())); } } @@ -74,9 +71,9 @@ private void setHxReplaceUrl(HttpServletRequest request, HttpServletResponse res HxReplaceUrl methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReplaceUrl.class); if (methodAnnotation != null) { if (HtmxValue.TRUE.equals(methodAnnotation.value())) { - setHeader(response, HX_REPLACE_URL, getRequestUrl(request)); + setHeader(response, HtmxResponseHeader.HX_REPLACE_URL, getRequestUrl(request)); } else { - setHeader(response, HX_REPLACE_URL, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative())); + setHeader(response, HtmxResponseHeader.HX_REPLACE_URL, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative())); } } } @@ -84,28 +81,28 @@ private void setHxReplaceUrl(HttpServletRequest request, HttpServletResponse res private void setHxReswap(HttpServletResponse response, Method method) { HxReswap methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReswap.class); if (methodAnnotation != null) { - setHeader(response, HX_RESWAP, convertToReswap(methodAnnotation)); + setHeader(response, HtmxResponseHeader.HX_RESWAP, convertToReswap(methodAnnotation)); } } private void setHxRetarget(HttpServletResponse response, Method method) { HxRetarget methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRetarget.class); if (methodAnnotation != null) { - setHeader(response, HX_RETARGET, methodAnnotation.value()); + setHeader(response, HtmxResponseHeader.HX_RETARGET, methodAnnotation.value()); } } private void setHxReselect(HttpServletResponse response, Method method) { HxReselect methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReselect.class); if (methodAnnotation != null) { - setHeader(response, HX_RESELECT, methodAnnotation.value()); + setHeader(response, HtmxResponseHeader.HX_RESELECT, methodAnnotation.value()); } } private void setHxTrigger(HttpServletResponse response, Method method) { HxTrigger methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxTrigger.class); if (methodAnnotation != null) { - setHeader(response, convertToHeader(methodAnnotation.lifecycle()), methodAnnotation.value()); + setHeader(response, HtmxResponseHeader.HX_TRIGGER, methodAnnotation.value()); } } @@ -126,20 +123,7 @@ private void setHxTriggerAfterSwap(HttpServletResponse response, Method method) private void setHxRefresh(HttpServletResponse response, Method method) { HxRefresh methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRefresh.class); if (methodAnnotation != null) { - setHeader(response, HX_REFRESH, HtmxValue.TRUE); - } - } - - private HtmxResponseHeader convertToHeader(HxTriggerLifecycle lifecycle) { - switch (lifecycle) { - case RECEIVE: - return HX_TRIGGER; - case SETTLE: - return HX_TRIGGER_AFTER_SETTLE; - case SWAP: - return HX_TRIGGER_AFTER_SWAP; - default: - throw new IllegalArgumentException("Unknown lifecycle:" + lifecycle); + setHeader(response, HtmxResponseHeader.HX_REFRESH, HtmxValue.TRUE); } } diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodArgumentResolver.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodArgumentResolver.java index 8abe019..950c941 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodArgumentResolver.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodArgumentResolver.java @@ -9,10 +9,6 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; -import java.util.Objects; - -import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxRequestHeader.*; - public class HtmxHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { @Override diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxMvcAutoConfiguration.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxMvcAutoConfiguration.java index 0189fc1..ab40759 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxMvcAutoConfiguration.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxMvcAutoConfiguration.java @@ -63,7 +63,6 @@ public void addArgumentResolvers(List resolvers) @Override public void addReturnValueHandlers(List handlers) { - handlers.add(new HtmxResponseHandlerMethodReturnValueHandler(viewResolverObjectFactory.getObject(), localeResolverObjectFactory, objectMapper)); handlers.add(new HtmxViewMethodReturnValueHandler(viewResolverObjectFactory.getObject(), localeResolverObjectFactory.getObject())); } diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequest.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequest.java index 04cec27..d52b795 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequest.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequest.java @@ -1,10 +1,10 @@ package io.github.wimdeblauwe.htmx.spring.boot.mvc; -import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxRequestHeader.*; - import jakarta.servlet.http.HttpServletRequest; import org.springframework.lang.Nullable; +import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxRequestHeader.*; + /** * This class can be used as a controller method argument to access * the htmx Request Headers. @@ -188,98 +188,36 @@ public static final class Builder { private Builder() { } - /** - * @deprecated use {@link #boosted(boolean)} instead. Will be removed in 4.0. - */ - @Deprecated - public Builder withBoosted(boolean boosted) { - return boosted(boosted); - } - public Builder boosted(boolean boosted) { this.boosted = boosted; return this; } - /** - * @deprecated use {@link #currentUrl(String)} instead. Will be removed in 4.0. - */ - @Deprecated - public Builder withCurrentUrl(String currentUrl) { - this.currentUrl = currentUrl; - return this; - } - public Builder currentUrl(String currentUrl) { this.currentUrl = currentUrl; return this; } - /** - * @deprecated use {@link #historyRestoreRequest(boolean)} instead. Will be removed in 4.0. - */ - @Deprecated - public Builder withHistoryRestoreRequest(boolean historyRestoreRequest) { - this.historyRestoreRequest = historyRestoreRequest; - return this; - } - public Builder historyRestoreRequest(boolean historyRestoreRequest) { this.historyRestoreRequest = historyRestoreRequest; return this; } - /** - * @deprecated use {@link #promptResponse(String)} instead. Will be removed in 4.0. - */ - @Deprecated - public Builder withPromptResponse(String promptResponse) { - this.promptResponse = promptResponse; - return this; - } - public Builder promptResponse(String promptResponse) { this.promptResponse = promptResponse; return this; } - /** - * @deprecated use {@link #target(String)} instead. Will be removed in 4.0. - */ - @Deprecated - public Builder withTarget(String target) { - this.target = target; - return this; - } - public Builder target(String target) { this.target = target; return this; } - /** - * @deprecated use {@link #triggerName(String)} instead. Will be removed in 4.0. - */ - @Deprecated - public Builder withTriggerName(String triggerName) { - this.triggerName = triggerName; - return this; - } - public Builder triggerName(String triggerName) { this.triggerName = triggerName; return this; } - /** - * @deprecated use {@link #triggerId(String)} instead. Will be removed in 4.0. - */ - @Deprecated - public Builder withTriggerId(String triggerId) { - this.triggerId = triggerId; - return this; - } - public Builder triggerId(String triggerId) { this.triggerId = triggerId; return this; diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponse.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponse.java index 3522b2b..f070e54 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponse.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponse.java @@ -1,16 +1,14 @@ package io.github.wimdeblauwe.htmx.spring.boot.mvc; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.util.Assert; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.View; -import java.util.*; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; /** * A holder for htmx-related response headers that can be used as method argument in controllers. - * + * *

Example usage in an {@code @Controller}: *

  * @GetMapping(value = "/user")
@@ -27,34 +25,15 @@
  */
 public final class HtmxResponse {
 
-    private static final Logger LOGGER = LoggerFactory.getLogger(HtmxResponse.class);
-
-    private Set views = new LinkedHashSet<>();
-    private Set triggers = new LinkedHashSet<>();
-    private Set triggersAfterSettle = new LinkedHashSet<>();
-    private Set triggersAfterSwap = new LinkedHashSet<>();
+    private final Set triggers = new LinkedHashSet<>();
+    private final Set triggersAfterSettle = new LinkedHashSet<>();
+    private final Set triggersAfterSwap = new LinkedHashSet<>();
     private String replaceUrl;
     private String reselect;
     private boolean contextRelative = true;
     private String retarget;
-    private boolean refresh;
-    private String redirect;
     private String pushUrl;
     private HtmxReswap reswap;
-    private HtmxLocation location;
-
-    /**
-     * Return a builder to build a {@link HtmxResponse}.
-     *
-     * @return the builder
-     * @deprecated use {@link HtmxResponse} as handler method argument and {@link HtmxView},
-     * {@link HtmxRedirectView} or {@link HtmxLocationRedirectView} as handler method return
-     * type instead.  Will be removed in 4.0.
-     */
-    @Deprecated
-    public static Builder builder() {
-        return new Builder();
-    }
 
     /**
      * Create a new HtmxResponse.
@@ -68,13 +47,10 @@ public HtmxResponse() {
      *
      * @param eventName the event name
      * @see HX-Trigger Response Headers
-     * @deprecated Return value is changed to void in 4.0.
      */
-    @Deprecated
-    public HtmxResponse addTrigger(String eventName) {
+    public void addTrigger(String eventName) {
         Assert.hasText(eventName, "eventName should not be blank");
         triggers.add(new HtmxTrigger(eventName, null));
-        return this;
     }
 
     /**
@@ -238,258 +214,10 @@ public void setRetarget(String cssSelector) {
         this.retarget = cssSelector;
     }
 
-    /**
-     * Append the rendered template or fragment.
-     *
-     * @param template must not be {@literal null} or empty.
-     * @return same HtmxResponse for chaining
-     * @deprecated use {@link Builder#view(String)} instead.  Will be removed in 4.0.
-     */
-    @Deprecated
-    public HtmxResponse addTemplate(String template) {
-        Assert.hasText(template, "template should not be blank");
-        if (!views.stream().anyMatch(mav -> template.equals(mav.getViewName()))) {
-            views.add(new ModelAndView(template));
-        }
-        return this;
-    }
-
-    /**
-     * Append the rendered template or fragment as a resolved {@link View}.
-     *
-     * @param template must not be {@literal null}.
-     * @return same HtmxResponse for chaining
-     * @deprecated use {@link Builder#view(View)} instead.  Will be removed in 4.0.
-     */
-    @Deprecated
-    public HtmxResponse addTemplate(View template) {
-        Assert.notNull(template, "template should not be null");
-        if (!views.stream().anyMatch(mav -> template.equals(mav.getView()))) {
-            views.add(new ModelAndView(template));
-        }
-        return this;
-    }
-
-    /**
-     * Append the rendered template or fragment as a {@link ModelAndView}.
-     *
-     * @param template must not be {@literal null}.
-     * @return same HtmxResponse for chaining
-     * @deprecated use {@link Builder#view(ModelAndView)} instead.  Will be removed in 4.0.
-     */
-    @Deprecated
-    public HtmxResponse addTemplate(ModelAndView template) {
-        Assert.notNull(template, "template should not be null");
-        views.add(template);
-        return this;
-    }
-
-    /**
-     * Set a HX-Trigger (or HX-Trigger-After-Settle or HX-Trigger-After-Swap headers.
-     * Multiple trigger were
-     * automatically be merged into the same header.
-     *
-     * @param eventName   must not be {@literal null} or empty.
-     * @param eventDetail can be {@literal null}.
-     * @param step        must not be {@literal null} or empty.
-     * @return same HtmxResponse for chaining
-     * @see HX-Trigger Response Headers
-     * @deprecated use {@link Builder#trigger(String, Object)} instead.  Will be removed in 4.0.
-     */
-    @Deprecated
-    public HtmxResponse addTrigger(String eventName, String eventDetail, HxTriggerLifecycle step) {
-        Assert.hasText(eventName, "eventName should not be blank");
-        switch (step) {
-            case RECEIVE:
-                triggers.add(new HtmxTrigger(eventName, eventDetail));
-                break;
-            case SETTLE:
-                triggersAfterSettle.add(new HtmxTrigger(eventName, eventDetail));
-                break;
-            case SWAP:
-                triggersAfterSwap.add(new HtmxTrigger(eventName, eventDetail));
-                break;
-            default:
-                throw new IllegalArgumentException("Unknown step " + step);
-        }
-        return this;
-    }
-
-    /**
-     * Pushes a new url into the history stack
-     *
-     * @param url must not be {@literal null} or empty. {@literal false} prevents the browser history from being updated
-     * @return same HtmxResponse for chaining
-     * @see HX-Push Response Header documentation
-     * @deprecated use {@link Builder#pushUrl(String)} instead.  Will be removed in 4.0.
-     */
-    @Deprecated
-    public HtmxResponse pushHistory(String url) {
-        Assert.hasText(url, "url should not be blank");
-        this.pushUrl = url;
-        return this;
-    }
-
-    /**
-     * Can be used to do a client-side redirect to a new location
-     *
-     * @param url can be a relative or an absolute url
-     * @return same HtmxResponse for chaining
-     * @deprecated use {@link Builder#redirect(String)} instead.  Will be removed in 4.0.
-     */
-    @Deprecated
-    public HtmxResponse browserRedirect(String url) {
-        Assert.hasText(url, "url should not be blank");
-        this.redirect = url;
-        return this;
-    }
-
-    /**
-     * If set to "true" the client side will do a full refresh of the page
-     *
-     * @param refresh boolean to indicate full refresh or not.
-     * @return same HtmxResponse for chaining
-     * @deprecated use {@link Builder#refresh()} instead.  Will be removed in 4.0.
-     */
-    @Deprecated
-    public HtmxResponse browserRefresh(boolean refresh) {
-        this.refresh = refresh;
-        return this;
-    }
-
-    /**
-     * Set a CSS selector that updates the target of the content update to a different element on the page
-     *
-     * @param cssSelector must not be {@literal null} or empty.
-     * @return same HtmxResponse for chaining
-     * @deprecated use {@link Builder#retarget(String)} instead.  Will be removed in 4.0.
-     */
-    @Deprecated
-    public HtmxResponse retarget(String cssSelector) {
-        Assert.hasText(cssSelector, "cssSelector should not be blank");
-        this.retarget = cssSelector;
-        return this;
-    }
-
-    /**
-     * Set a new swap to specify how the response will be swapped
-     *
-     * @param swapType must not be {@literal null}.
-     * @return same HtmxResponse for chaining
-     * @deprecated use {@link Builder#reswap(HxSwapType)} instead.  Will be removed in 4.0.
-     */
-    @Deprecated
-    public HtmxResponse reswap(HxSwapType swapType) {
-        Assert.notNull(swapType, "swapType should not be null");
-        this.reswap = new HtmxReswap(swapType);
-        return this;
-    }
-
-    /**
-     * @param otherResponse Another HtmxResponse that will be merged into this response.
-     * @return this for chaining
-     * @deprecated use {@link Builder#and(HtmxResponse)} instead.  Will be removed in 4.0.
-     */
-    @Deprecated
-    public HtmxResponse and(HtmxResponse otherResponse) {
-        otherResponse.views.forEach(otherTemplate -> {
-            if (this.views.stream().anyMatch(mav -> Builder.same(otherTemplate, mav))) {
-                LOGGER.warn("Duplicate template '{}' found while merging HtmxResponse", otherTemplate);
-            } else {
-                views.add(otherTemplate);
-            }
-        });
-        Builder.mergeTriggers(this.triggers, otherResponse.triggers);
-        Builder.mergeTriggers(this.triggersAfterSettle, otherResponse.triggersAfterSettle);
-        Builder.mergeTriggers(this.triggersAfterSwap, otherResponse.triggersAfterSwap);
-
-        if (otherResponse.getPushUrl() != null) {
-            this.pushUrl = otherResponse.getPushUrl();
-        }
-        if (otherResponse.getRedirect() != null) {
-            this.redirect = otherResponse.getRedirect();
-        }
-        if (otherResponse.isRefresh()) {
-            this.refresh = true;
-        }
-        if (otherResponse.getRetarget() != null) {
-            this.retarget = otherResponse.getRetarget();
-        }
-        if (otherResponse.getReswap() != null) {
-            this.reswap = otherResponse.reswap;
-        }
-
-        return this;
-    }
-
-    /**
-     * @deprecated use {@link #getRetarget()} instead. Will be removed in 4.0.
-     */
-    @Deprecated
-    String getHeaderRetarget() {
-        return retarget;
-    }
-
-    /**
-     * @deprecated use {@link #isRefresh()} instead. Will be removed in 4.0.
-     */
-    @Deprecated
-    boolean getHeaderRefresh() {
-        return refresh;
-    }
-
-    /**
-     * @deprecated use {@link #getRedirect()} instead. Will be removed in 4.0.
-     */
-    @Deprecated
-    String getHeaderRedirect() {
-        return redirect;
-    }
-
-    /**
-     * @deprecated use {@link #getPushUrl()} instead. Will be removed in 4.0.
-     */
-    @Deprecated
-    String getHeaderPushHistory() {
-        return pushUrl;
-    }
-
-    /**
-     * @deprecated use {@link #getReswap()} instead. Will be removed in 4.0.
-     */
-    @Deprecated
-    public String getHeaderReswap() {
-        return reswap != null ? reswap.getType().getValue() : null;
-    }
-
-    /**
-     * @deprecated use {@link #getViews()} instead. Will be removed in 4.0.
-     */
-    @Deprecated
-    public Collection getTemplates() {
-        return Collections.unmodifiableCollection(views);
-    }
-
-    /**
-     * @deprecated Replaced by {@link HtmxLocationRedirectView}. Will be removed in 4.0.
-     */
-    @Deprecated
-    public HtmxLocation getLocation() {
-        return location;
-    }
-
     public String getPushUrl() {
         return pushUrl;
     }
 
-    /**
-     * @deprecated Replaced by {@link HtmxRedirectView}. Will be removed in 4.0.
-     */
-    @Deprecated
-    public String getRedirect() {
-        return redirect;
-    }
-
     public String getReplaceUrl() {
         return replaceUrl;
     }
@@ -506,454 +234,20 @@ public String getRetarget() {
         return retarget;
     }
 
-    public Map getTriggers() {
-        return getTriggersAsMap(this.triggers);
-    }
-
-    Collection getTriggersInternal() {
-        return Collections.unmodifiableCollection(this.triggers);
-    }
-
-    public Map getTriggersAfterSettle() {
-        return getTriggersAsMap(this.triggersAfterSettle);
-    }
-
-    Collection getTriggersAfterSettleInternal() {
-        return Collections.unmodifiableCollection(this.triggersAfterSettle);
+    public Collection getTriggers() {
+        return this.triggers;
     }
 
-    public Map getTriggersAfterSwap() {
-        return getTriggersAsMap(this.triggersAfterSwap);
+    public Collection getTriggersAfterSettle() {
+        return this.triggersAfterSettle;
     }
 
-    Collection getTriggersAfterSwapInternal() {
-        return Collections.unmodifiableCollection(this.triggersAfterSwap);
-    }
-
-    /**
-     * @deprecated Replaced by {@link HtmxView}. Will be removed in 4.0.
-     */
-    @Deprecated
-    public Collection getViews() {
-        return Collections.unmodifiableCollection(views);
-    }
-
-    /**
-     * @deprecated Replaced by {@link HtmxRefreshView}. Will be removed in 4.0.
-     */
-    @Deprecated
-    public boolean isRefresh() {
-        return refresh;
+    public Collection getTriggersAfterSwap() {
+        return this.triggersAfterSwap;
     }
 
     public boolean isContextRelative() {
         return contextRelative;
     }
 
-    /**
-     * @deprecated will be removed in 4.0.
-     */
-    @Deprecated
-    private Map getTriggersAsMap(Collection triggers) {
-        var map = new HashMap();
-        for (HtmxTrigger trigger : triggers) {
-            map.put(trigger.getEventName(), Objects.toString(trigger.getEventDetail(), null));
-        }
-        return Collections.unmodifiableMap(map);
-    }
-
-    /**
-     * @deprecated use {@link HtmxResponse} as handler method argument
-     * and {@link HtmxView}, {@link HtmxRedirectView} or {@link HtmxLocationRedirectView}
-     * as handler method return type instead.  Will be removed in 4.0.
-     */
-    @Deprecated
-    public static final class Builder {
-
-        private Set views = new LinkedHashSet<>();
-        private Set triggers = new LinkedHashSet<>();
-        private Set triggersAfterSettle = new LinkedHashSet<>();
-        private Set triggersAfterSwap = new LinkedHashSet<>();
-        private HtmxLocation location;
-        private String pushUrl;
-        private String redirect;
-        private boolean refresh;
-        private String replaceUrl;
-        private HtmxReswap reswap;
-        private String retarget;
-        private String reselect;
-        private boolean contextRelative = true;
-
-        /**
-         * Merges another {@link HtmxResponse} into this builder.
-         *
-         * @param otherResponse Another HtmxResponse that will be merged into this response.
-         * @return the builder
-         */
-        public Builder and(HtmxResponse otherResponse) {
-
-            otherResponse.views.forEach(otherTemplate -> {
-                if (this.views.stream().anyMatch(mav -> same(otherTemplate, mav))) {
-                    LOGGER.warn("Duplicate template '{}' found while merging HtmxResponse", otherTemplate);
-                } else {
-                    views.add(otherTemplate);
-                }
-            });
-
-            mergeTriggers(this.triggers, otherResponse.triggers);
-            mergeTriggers(this.triggersAfterSettle, otherResponse.triggersAfterSettle);
-            mergeTriggers(this.triggersAfterSwap, otherResponse.triggersAfterSwap);
-
-            if (otherResponse.location != null) {
-                this.location = otherResponse.location;
-            }
-            if (otherResponse.pushUrl != null) {
-                this.pushUrl = otherResponse.pushUrl;
-            }
-            if (otherResponse.redirect != null) {
-                this.redirect = otherResponse.redirect;
-            }
-            if (otherResponse.refresh) {
-                this.refresh = true;
-            }
-            if (otherResponse.replaceUrl != null) {
-                this.replaceUrl = otherResponse.replaceUrl;
-            }
-            if (otherResponse.reswap != null) {
-                this.reswap = otherResponse.reswap;
-            }
-            if (otherResponse.retarget != null) {
-                this.retarget = otherResponse.retarget;
-            }
-            if (otherResponse.reselect != null) {
-                this.reselect = otherResponse.reselect;
-            }
-
-            return this;
-        }
-
-        /**
-         * @deprecated use {@link HtmxResponse} as handler method argument
-         * and {@link HtmxView}, {@link HtmxRedirectView} or {@link HtmxLocationRedirectView}
-         * as handler method return type instead.  Will be removed in 4.0.
-         */
-        @Deprecated
-        public HtmxResponse build() {
-            var htmxResponse = new HtmxResponse();
-            htmxResponse.views = views;
-            htmxResponse.triggers = triggers;
-            htmxResponse.triggersAfterSettle = triggersAfterSettle;
-            htmxResponse.triggersAfterSwap = triggersAfterSwap;
-            htmxResponse.retarget = retarget;
-            htmxResponse.refresh = refresh;
-            htmxResponse.redirect = redirect;
-            htmxResponse.pushUrl = pushUrl;
-            htmxResponse.replaceUrl = replaceUrl;
-            htmxResponse.reselect = reselect;
-            htmxResponse.reswap = reswap;
-            htmxResponse.location = location;
-            htmxResponse.contextRelative = contextRelative;
-
-            return htmxResponse;
-        }
-
-        /**
-         * Set whether URLs used in the htmx response that starts with a slash ("/") should be interpreted as
-         * relative to the current ServletContext, i.e. as relative to the web application root.
-         * Default is "true": A URL that starts with a slash will be interpreted as relative to
-         * the web application root, i.e. the context path will be prepended to the URL.
-         *
-         * @param contextRelative whether to interpret URLs in the htmx response as relative to the current ServletContext
-         * @return the builder
-         */
-        public Builder contextRelative(boolean contextRelative) {
-            this.contextRelative = contextRelative;
-            return this;
-        }
-
-        /**
-         * Allows you to do a client-side redirect that does not do a full page reload.
-         *
-         * @param path the path
-         * @return the builder
-         * @see HX-Location Response Header
-         */
-        public Builder location(String path) {
-            this.location = new HtmxLocation(path);
-            return this;
-        }
-
-        /**
-         * Allows you to do a client-side redirect that does not do a full page reload.
-         *
-         * @param location the location
-         * @return the builder
-         * @see HX-Location Response Header
-         */
-        public Builder location(HtmxLocation location) {
-            this.location = location;
-            return this;
-        }
-
-        /**
-         * Prevents the browser history stack from being updated.
-         *
-         * @return the builder
-         * @see HX-Push Response Header documentation
-         * @see HX-Replace-Url Response Header
-         */
-        public Builder preventHistoryUpdate() {
-            this.pushUrl = "false";
-            this.replaceUrl = null;
-            return this;
-        }
-
-        /**
-         * Pushes a new URL into the history stack of the browser.
-         * 

- * If you want to prevent the history stack from being updated, use {@link #preventHistoryUpdate()}. - * - * @param url the URL to push into the history stack. The URL can be any URL in the same origin as the current URL. - * @return the builder - * @see HX-Push Response Header documentation - * @see history.pushState() - */ - public Builder pushUrl(String url) { - Assert.hasText(url, "url should not be blank"); - this.pushUrl = url; - this.replaceUrl = null; - return this; - } - - /** - * Can be used to do a client-side redirect to a new location - * - * @param url the URL. Can be a relative or an absolute url - * @return the builder - */ - public Builder redirect(String url) { - Assert.hasText(url, "url should not be blank"); - this.redirect = url; - return this; - } - - /** - * If set to "true" the client side will do a full refresh of the page - * - * @return the builder - */ - public Builder refresh() { - this.refresh = true; - return this; - } - - /** - * Allows you to replace the most recent entry, i.e. the current URL, in the browser history stack. - *

- * If you want to prevent the history stack from being updated, use {@link #preventHistoryUpdate()}. - * - * @param url the URL to replace in the history stack. The URL can be any URL in the same origin as the current URL. - * @return the builder - * @see HX-Replace-Url Response Header - * @see history.replaceState() - */ - public Builder replaceUrl(String url) { - this.replaceUrl = url; - this.pushUrl = null; - return this; - } - - /** - * Set a new swap to specify how the response will be swapped. - * - * @param reswap the reswap options. - * @return the builder - */ - public Builder reswap(HtmxReswap reswap) { - Assert.notNull(reswap, "reswap should not be null"); - this.reswap = reswap; - return this; - } - - /** - * Set a CSS selector that updates the target of the content update to a different element on the page - * - * @param cssSelector the CSS selector - * @return the builder - */ - public Builder retarget(String cssSelector) { - Assert.hasText(cssSelector, "cssSelector should not be blank"); - this.retarget = cssSelector; - return this; - } - - /** - * Set a CSS selector that allows you to choose which part of the response is used to be swapped in. - * Overrides an existing hx-select on the triggering element. - * - * @param cssSelector the CSS selector - * @return the builder - */ - public Builder reselect(String cssSelector) { - Assert.hasText(cssSelector, "cssSelector should not be blank"); - this.reselect = cssSelector; - return this; - } - - /** - * Adds an event that will be triggered once the response is received. - *

Multiple trigger were automatically be merged into the same header. - * - * @param eventName the event name - * @return the builder - * @see HX-Trigger Response Headers - */ - public Builder trigger(String eventName) { - Assert.hasText(eventName, "eventName should not be blank"); - return trigger(eventName, null); - } - - /** - * Adds an event that will be triggered once the response is received. - *

Multiple trigger were automatically be merged into the same header. - * - * @param eventName the event name - * @param eventDetail details along with the event - * @return the builder - * @see HX-Trigger Response Headers - */ - public Builder trigger(String eventName, Object eventDetail) { - Assert.hasText(eventName, "eventName should not be blank"); - triggers.add(new HtmxTrigger(eventName, eventDetail)); - return this; - } - - /** - * Adds an event that will be triggered after the settling step. - *

Multiple triggers were automatically be merged into the same header. - * - * @param eventName the event name - * @return the builder - * @see HX-Trigger Response Headers - */ - public Builder triggerAfterSettle(String eventName) { - Assert.hasText(eventName, "eventName should not be blank"); - return triggerAfterSettle(eventName, null); - } - - /** - * Adds an event that will be triggered after the settling step. - *

Multiple triggers were automatically be merged into the same header. - * - * @param eventName the event name - * @param eventDetail details along with the event - * @return the builder - * @see HX-Trigger Response Headers - */ - public Builder triggerAfterSettle(String eventName, Object eventDetail) { - Assert.hasText(eventName, "eventName should not be blank"); - triggersAfterSettle.add(new HtmxTrigger(eventName, eventDetail)); - return this; - } - - /** - * Adds an event that will be triggered after the swap step. - *

Multiple triggers were automatically be merged into the same header. - * - * @param eventName the event name - * @return the builder - * @see HX-Trigger Response Headers - */ - public Builder triggerAfterSwap(String eventName) { - Assert.hasText(eventName, "eventName should not be blank"); - return triggerAfterSwap(eventName, null); - } - - /** - * Adds an event that will be triggered after the swap step. - *

Multiple triggers were automatically be merged into the same header. - * - * @param eventName the event name - * @param eventDetail details along with the event - * @return the builder - * @see HX-Trigger Response Headers - */ - public Builder triggerAfterSwap(String eventName, Object eventDetail) { - Assert.hasText(eventName, "eventName should not be blank"); - triggersAfterSwap.add(new HtmxTrigger(eventName, eventDetail)); - return this; - } - - /** - * Append a view name to be resolved with {@code ViewResolver} implementations and used together with the implicit model. - * - * @param viewName the name of the view. - * @return the builder - */ - public Builder view(String viewName) { - Assert.hasText(viewName, "viewName should not be blank"); - if (!views.stream().anyMatch(mav -> viewName.equals(mav.getViewName()))) { - views.add(new ModelAndView(viewName)); - } - return this; - } - - /** - * Append a {@link View} instance to use for rendering together with the implicit model. - * - * @param view the view - * @return the builder - */ - public Builder view(View view) { - Assert.notNull(view, "view should not be null"); - if (!views.stream().anyMatch(mav -> view.equals(mav.getView()))) { - views.add(new ModelAndView(view)); - } - return this; - } - - /** - * Append a {@link ModelAndView} instance to use for rendering. - * - * @param modelAndView the model and view - * @return the builder - */ - public Builder view(ModelAndView modelAndView) { - Assert.notNull(modelAndView, "modelAndView should not be null"); - views.add(modelAndView); - return this; - } - - private static void mergeTriggers(Collection triggers, Collection otherTriggers) { - for (HtmxTrigger otherTrigger : otherTriggers) { - if (LOGGER.isWarnEnabled()) { - Optional otrigger = triggers.stream() - .filter(t -> t.getEventName().equals(otherTrigger.getEventName())) - .findFirst(); - - if (otrigger.isPresent()) { - LOGGER.warn("Duplicate trigger event '{}' found. Details '{}' will be overwritten by with '{}'", otherTrigger.getEventName(), otrigger.get().getEventDetail(), otherTrigger.getEventDetail()); - } - } - triggers.add(otherTrigger); - } - } - - private static boolean same(ModelAndView one, ModelAndView two) { - if (one == two) { - return true; - } - if (one == null || two == null) { - return false; - } - if (one.getViewName() != null && one.getViewName().equals(two.getViewName())) { - return true; - } - if (one.getView() != null && one.getView().equals(two.getView())) { - return true; - } - return false; - } - - } } diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodArgumentResolver.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodArgumentResolver.java index be01a02..defa583 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodArgumentResolver.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodArgumentResolver.java @@ -12,8 +12,7 @@ public class HtmxResponseHandlerMethodArgumentResolver implements HandlerMethodA @Override public boolean supportsParameter(MethodParameter parameter) { - return (parameter.getParameterType() == HtmxResponse.class || - parameter.getParameterType() == HtmxResponse.Builder.class); + return parameter.getParameterType() == HtmxResponse.class; } @Override @@ -24,15 +23,10 @@ public Object resolveArgument(MethodParameter parameter, HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); - if (parameter.getParameterType() == HtmxResponse.class) { - var htmxResponse = new HtmxResponse(); - request.setAttribute(RequestContextUtils.HTMX_RESPONSE_CONTEXT_ATTRIBUTE, htmxResponse); - return htmxResponse; - } else { - HtmxResponse.Builder htmxResponseBuilder = HtmxResponse.builder(); - request.setAttribute(RequestContextUtils.HTMX_RESPONSE_CONTEXT_ATTRIBUTE, htmxResponseBuilder); - return htmxResponseBuilder; - } + var htmxResponse = new HtmxResponse(); + request.setAttribute(RequestContextUtils.HTMX_RESPONSE_CONTEXT_ATTRIBUTE, htmxResponse); + + return htmxResponse; } } diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandler.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandler.java deleted file mode 100644 index 5469f3c..0000000 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandler.java +++ /dev/null @@ -1,183 +0,0 @@ -package io.github.wimdeblauwe.htmx.spring.boot.mvc; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.beans.factory.ObjectFactory; -import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; -import org.springframework.ui.ModelMap; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.HandlerMethodReturnValueHandler; -import org.springframework.web.method.support.ModelAndViewContainer; -import org.springframework.web.servlet.LocaleResolver; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.View; -import org.springframework.web.servlet.ViewResolver; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import org.springframework.web.util.ContentCachingResponseWrapper; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * @deprecated Using {@link HtmxResponse} as handler method return value is deprecated, - * use {@link HtmxView}, {@link HtmxRedirectView} or {@link HtmxLocationRedirectView} instead. - * Will be removed in 4.0.0. - */ -@Deprecated -public class HtmxResponseHandlerMethodReturnValueHandler implements HandlerMethodReturnValueHandler { - private final ViewResolver views; - private final ObjectFactory locales; - private final ObjectMapper objectMapper; - - public HtmxResponseHandlerMethodReturnValueHandler(ViewResolver views, - ObjectFactory locales, - ObjectMapper objectMapper) { - this.views = views; - this.locales = locales; - this.objectMapper = objectMapper; - } - - @Override - public boolean supportsReturnType(MethodParameter returnType) { - return returnType.getParameterType().equals(HtmxResponse.class); - } - - @Override - public void handleReturnValue(Object returnValue, - MethodParameter returnType, - ModelAndViewContainer mavContainer, - NativeWebRequest webRequest) throws Exception { - - HtmxResponse htmxResponse = (HtmxResponse) returnValue; - mavContainer.setView(toView(htmxResponse)); - - HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); - HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); - - addHxHeaders(htmxResponse, request, response, mavContainer); - } - - View toView(HtmxResponse htmxResponse) { - - Assert.notNull(htmxResponse, "HtmxResponse must not be null!"); - - return (model, request, response) -> { - Locale locale = locales.getObject().resolveLocale(request); - ContentCachingResponseWrapper wrapper = new ContentCachingResponseWrapper(response); - for (ModelAndView modelAndView : htmxResponse.getViews()) { - View view = modelAndView.getView(); - if (view == null) { - view = views.resolveViewName(modelAndView.getViewName(), locale); - } - for (String key : model.keySet()) { - if (!modelAndView.getModel().containsKey(key)) { - modelAndView.getModel().put(key, model.get(key)); - } - } - Assert.notNull(view, "Template '" + modelAndView + "' could not be resolved"); - view.render(modelAndView.getModel(), request, wrapper); - } - wrapper.copyBodyToResponse(); - }; - } - - void addHxHeaders(HtmxResponse htmxResponse, HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndViewContainer mavContainer) { - addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER, htmxResponse.getTriggersInternal()); - addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SETTLE, htmxResponse.getTriggersAfterSettleInternal()); - addHxTriggerHeaders(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SWAP, htmxResponse.getTriggersAfterSwapInternal()); - - if (htmxResponse.getLocation() != null) { - HtmxLocation location = htmxResponse.getLocation(); - if (mavContainer != null) { - saveFlashAttributes(mavContainer, request, response, location.getPath()); - } - if (location.hasContextData()) { - location.setPath(RequestContextUtils.createUrl(request, location.getPath(), htmxResponse.isContextRelative())); - setHeaderJsonValue(response, HtmxResponseHeader.HX_LOCATION.getValue(), location); - } else { - response.setHeader(HtmxResponseHeader.HX_LOCATION.getValue(), RequestContextUtils.createUrl(request, location.getPath(), htmxResponse.isContextRelative())); - } - } - if (htmxResponse.getReplaceUrl() != null) { - response.setHeader(HtmxResponseHeader.HX_REPLACE_URL.getValue(), RequestContextUtils.createUrl(request, htmxResponse.getReplaceUrl(), htmxResponse.isContextRelative())); - } - if (htmxResponse.getPushUrl() != null) { - response.setHeader(HtmxResponseHeader.HX_PUSH_URL.getValue(), RequestContextUtils.createUrl(request, htmxResponse.getPushUrl(), htmxResponse.isContextRelative())); - } - if (htmxResponse.getRedirect() != null) { - if (mavContainer != null) { - saveFlashAttributes(mavContainer, request, response, htmxResponse.getRedirect()); - } - response.setHeader(HtmxResponseHeader.HX_REDIRECT.getValue(), RequestContextUtils.createUrl(request, htmxResponse.getRedirect(), htmxResponse.isContextRelative())); - } - if (htmxResponse.isRefresh()) { - response.setHeader(HtmxResponseHeader.HX_REFRESH.getValue(), "true"); - } - if (htmxResponse.getRetarget() != null) { - response.setHeader(HtmxResponseHeader.HX_RETARGET.getValue(), htmxResponse.getRetarget()); - } - if (htmxResponse.getReselect() != null) { - response.setHeader(HtmxResponseHeader.HX_RESELECT.getValue(), htmxResponse.getReselect()); - } - if (htmxResponse.getReswap() != null) { - response.setHeader(HtmxResponseHeader.HX_RESWAP.getValue(), htmxResponse.getReswap().toHeaderValue()); - } - } - - private void addHxTriggerHeaders(HttpServletResponse response, HtmxResponseHeader headerName, Collection triggers) { - if (triggers.isEmpty()) { - return; - } - - // separate event names by commas if no additional details are available - if (triggers.stream().allMatch(t -> t.getEventDetail() == null)) { - String value = triggers.stream() - .map(HtmxTrigger::getEventName) - .collect(Collectors.joining(",")); - - response.setHeader(headerName.getValue(), value); - return; - } - - // multiple events with or without details - var triggerMap = new HashMap(); - for (HtmxTrigger trigger : triggers) { - triggerMap.put(trigger.getEventName(), trigger.getEventDetail()); - } - setHeaderJsonValue(response, headerName.getValue(), triggerMap); - } - - private void setHeaderJsonValue(HttpServletResponse response, String name, Object value) { - try { - response.setHeader(name, objectMapper.writeValueAsString(value)); - } catch (JsonProcessingException e) { - throw new IllegalArgumentException("Unable to set header " + name + " to " + value, e); - } - } - - private void saveFlashAttributes(ModelAndViewContainer mavContainer, HttpServletRequest request, HttpServletResponse response, String location) { - mavContainer.setRedirectModelScenario(true); - ModelMap model = mavContainer.getModel(); - - if (model instanceof RedirectAttributes redirectAttributes) { - Map flashAttributes = redirectAttributes.getFlashAttributes(); - if (!CollectionUtils.isEmpty(flashAttributes)) { - if (request != null) { - org.springframework.web.servlet.support.RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes); - if (response != null) { - org.springframework.web.servlet.support.RequestContextUtils.saveOutputFlashMap(location, request, response); - } - } - } - } - } - -} diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxTrigger.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxTrigger.java index 543fa8a..fdff1e8 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxTrigger.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxTrigger.java @@ -1,6 +1,5 @@ package io.github.wimdeblauwe.htmx.spring.boot.mvc; -import java.util.Map; import java.util.Objects; /** diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxTriggerHeadersRequestCondition.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxTriggerHeadersRequestCondition.java index 8777359..423af71 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxTriggerHeadersRequestCondition.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxTriggerHeadersRequestCondition.java @@ -1,7 +1,6 @@ package io.github.wimdeblauwe.htmx.spring.boot.mvc; import jakarta.servlet.http.HttpServletRequest; - import org.springframework.web.cors.CorsUtils; import org.springframework.web.servlet.mvc.condition.RequestCondition; diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxLocation.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxLocation.java index da86612..7c22c0c 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxLocation.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxLocation.java @@ -1,12 +1,12 @@ package io.github.wimdeblauwe.htmx.spring.boot.mvc; +import org.springframework.core.annotation.AliasFor; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.springframework.core.annotation.AliasFor; - /** * Annotation to do a client side redirect that does not do a full page reload. *

diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTrigger.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTrigger.java index a093112..61da7db 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTrigger.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTrigger.java @@ -21,9 +21,4 @@ */ String[] value(); - /** - * @deprecated use annotation {@link HxTriggerAfterSettle} or {@link HxTriggerAfterSwap} instead. - */ - @Deprecated - HxTriggerLifecycle lifecycle() default HxTriggerLifecycle.RECEIVE; } diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTriggerLifecycle.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTriggerLifecycle.java deleted file mode 100644 index 418de34..0000000 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTriggerLifecycle.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.github.wimdeblauwe.htmx.spring.boot.mvc; - -/** - * Represents the HX-Trigger Response Headers. - * - * @see HX-Trigger Response Headers - * @deprecated use annotation {@link HxTriggerAfterSettle} or {@link HxTriggerAfterSwap} instead. - */ -@Deprecated -public enum HxTriggerLifecycle { - /** - * Trigger events as soon as the response is received. - * - * @see HX-Trigger - */ - RECEIVE("HX-Trigger"), - /** - * Trigger events after the settling step. - * - * @see HX-Trigger-After-Settle - */ - SETTLE("HX-Trigger-After-Settle"), - /** - * Trigger events after the swap step. - * - * @see HX-Trigger-After-Swap - */ - SWAP("HX-Trigger-After-Swap"); - - private final String headerName; - - HxTriggerLifecycle(String headerName) { - this.headerName = headerName; - } - - public String getHeaderName() { - return this.headerName; - } -} diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/RequestContextUtils.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/RequestContextUtils.java index 7a3e90f..a94d8af 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/RequestContextUtils.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/RequestContextUtils.java @@ -32,16 +32,10 @@ static HtmxResponse getHtmxResponse(HttpServletRequest request) { Object contextAttribute = request.getAttribute(HTMX_RESPONSE_CONTEXT_ATTRIBUTE); if (contextAttribute instanceof HtmxResponse response) { return response; - } else if (contextAttribute instanceof HtmxResponse.Builder builder) { - return builder.build(); } return null; } - static HtmxResponse.Builder getHtmxResponseBuilder(HttpServletRequest request) { - return (HtmxResponse.Builder) request.getAttribute(HTMX_RESPONSE_CONTEXT_ATTRIBUTE); - } - private static String getContextPath(HttpServletRequest request) { String contextPath = request.getContextPath(); while (contextPath.startsWith("//")) { diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptorIT.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptorIT.java deleted file mode 100644 index 3b10da9..0000000 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptorIT.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.github.wimdeblauwe.htmx.spring.boot.mvc; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.http.ResponseEntity; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@EnableAutoConfiguration(exclude = {SecurityAutoConfiguration.class}) // Security is on by default -@MockBean(PartialsController.TodoRepository.class) -class HtmxHandlerInterceptorIT { - - @Autowired - protected TestRestTemplate restTemplate; - - @LocalServerPort - protected int port; - - @Test - public void testHeaderIsSetOnResponseIfHxTriggerIsPresent() { - final ResponseEntity response - = restTemplate.getForEntity("http://localhost:" + port + "/with-trigger", String.class); - - // Passes if preHandle is used, fails if postHandle is used (HtmxHandlerInterceptor) - assertThat(response.getHeaders().get("HX-Trigger")).isNotNull(); - assertThat(response.getHeaders().get("HX-Trigger")).asList().containsOnly("eventTriggered"); - } -} diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptorTest.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptorTest.java index 4ac8e9d..cf296ee 100644 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptorTest.java +++ b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptorTest.java @@ -6,11 +6,11 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.collection.IsIterableContainingInRelativeOrder.containsInRelativeOrder; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.hamcrest.collection.IsIterableContainingInRelativeOrder.containsInRelativeOrder; -import static org.hamcrest.Matchers.not; @WebMvcTest(TestController.class) @WithMockUser @@ -33,20 +33,6 @@ public void testHeaderIsSetOnResponseWithMultipleEventsIfHxTriggerIsPresent() th .andExpect(header().string("HX-Trigger", "event1,event2")); } - @Test - public void testAfterSettleHeaderIsSetOnResponseIfHxTriggerIsPresent() throws Exception { - mockMvc.perform(get("/with-trigger-settle")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Trigger-After-Settle", "eventTriggered")); - } - - @Test - public void testAfterSwapHeaderIsSetOnResponseIfHxTriggerIsPresent() throws Exception { - mockMvc.perform(get("/with-trigger-swap")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Trigger-After-Swap", "eventTriggered")); - } - @Test public void testAfterSettleHeaderIsSetOnResponseIfHxTriggerAfterSettleIsPresent() throws Exception { mockMvc.perform(get("/with-trigger-after-settle")) diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodArgumentResolverTest.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodArgumentResolverTest.java index 222e51a..5908013 100644 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodArgumentResolverTest.java +++ b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodArgumentResolverTest.java @@ -4,8 +4,8 @@ import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; import static org.assertj.core.api.Assertions.assertThat; @@ -19,7 +19,7 @@ class HtmxHandlerMethodArgumentResolverTest { @Autowired private MockMvc mockMvc; - @MockBean + @MockitoBean private TestService service; @Test diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxPartialHandlerInterceptorTest.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxPartialHandlerInterceptorTest.java deleted file mode 100644 index e4aa656..0000000 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxPartialHandlerInterceptorTest.java +++ /dev/null @@ -1,106 +0,0 @@ -package io.github.wimdeblauwe.htmx.spring.boot.mvc; - -import static io.github.wimdeblauwe.htmx.spring.boot.mvc.support.PartialXpathResultMatchers.partialXpath; -import static org.hamcrest.core.StringContains.containsString; -import static org.mockito.Mockito.when; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.xpath; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.result.MockMvcResultHandlers; - -@WebMvcTest(PartialsController.class) -@WithMockUser -class HtmxPartialHandlerInterceptorTest { - - @MockBean - private PartialsController.TodoRepository todoRepository; - - @Autowired - private MockMvc mockMvc; - - @Test - public void testASinglePartialCanBeReturned() throws Exception { - mockMvc.perform(get("/partials/first")) - .andDo(MockMvcResultHandlers.print()) - .andExpect(status().isOk()) - .andExpect(xpath("/ul").exists()) - .andExpect(xpath("/ul[@hx-swap-oob='true']").doesNotExist()); - } - - @Test - public void testASingleViewCanBeReturned() throws Exception { - mockMvc.perform(get("/partials/view")) - .andDo(MockMvcResultHandlers.print()) - .andExpect(status().isOk()) - .andExpect(xpath("/ul").exists()); - } - - @Test - public void testASingleModelAndViewCanBeReturned() throws Exception { - mockMvc.perform(get("/partials/mav")) - .andDo(MockMvcResultHandlers.print()) - .andExpect(status().isOk()) - .andExpect(partialXpath("/span[@id='item']").exists()) - .andExpect(content().string(containsString("Foo"))); - } - - @Test - public void testAMainChange() throws Exception { - mockMvc.perform(get("/partials/main-and-partial")) - .andDo(MockMvcResultHandlers.print()) - .andExpect(status().isOk()) - //Note xpath can't be used on partials because the document doesn't have a single root - .andExpect(partialXpath("//*[@id='userCount'][@hx-swap-oob]").exists()) - .andExpect(partialXpath("/ul").exists()) - .andExpect(partialXpath("/ul[@hx-swap-oob]").doesNotExist()); - } - - @Test - public void testHeaders() throws Exception { - mockMvc.perform(get("/partials/triggers")) - .andDo(MockMvcResultHandlers.print()) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Trigger", "usersCounted")) - .andExpect(header().string("HX-Trigger-After-Settle", "{\"usersCountedSettle1\":\"aDetail\",\"usersCountedSettle2\":null}")) - .andExpect(header().string("HX-Trigger-After-Swap", "{\"usersCountedSwap\":\"swap detail\"}")) - .andExpect(header().string("HX-Push-Url", "/a/newHistory")) - .andExpect(header().string("HX-Redirect", "/a/redirect")) - .andExpect(header().string("HX-Refresh", "true")) - .andExpect(header().string("HX-Retarget", "#newTarget")); - } - @Test - public void testExtension() throws Exception { - mockMvc.perform(get("/partials/extension")) - .andDo(MockMvcResultHandlers.print()) - .andExpect(status().isOk()) - .andExpect(partialXpath("//*[@id='alert'][@hx-swap-oob]").exists()) - .andExpect(header().string("HX-Trigger", "alertSent")); - } - - @Test - public void testPostTodo() throws Exception { - when(todoRepository.getNumberOfActiveItems()).thenReturn(0); - - mockMvc.perform(post("/partials/add-todo") - .param("name", "A Todo Name") - .with(csrf())) - .andDo(MockMvcResultHandlers.print()) - .andExpect(status().isOk()) - //Note xpath can't be used on partials because the document doesn't have a single root - .andExpect(partialXpath("//*[@id='active-items-count'][@hx-swap-oob]").exists()) - .andExpect(partialXpath("/span[@id='item']").exists()) - .andExpect(partialXpath("/span[@id='item'][@hx-swap-oob]").doesNotExist()); - } - -} diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandlerController.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandlerController.java deleted file mode 100644 index cc0159f..0000000 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandlerController.java +++ /dev/null @@ -1,169 +0,0 @@ -package io.github.wimdeblauwe.htmx.spring.boot.mvc; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -import java.time.Duration; -import java.util.Map; -import java.util.TreeMap; - -@Controller -@RequestMapping("/hvhi") -public class HtmxResponseHandlerMethodReturnValueHandlerController { - - @GetMapping("/hx-location-with-context-data") - public HtmxResponse hxLocationWithContextData() { - var location = new HtmxLocation(); - location.setPath("/path"); - location.setSource("source"); - location.setEvent("event"); - location.setHandler("handler"); - location.setTarget("target"); - location.setSwap("swap"); - location.setSelect("select"); - location.setValues(new TreeMap<>(Map.of("value1", "v1", "value2", "v2"))); - location.setHeaders(new TreeMap<>(Map.of("header1", "v1", "header2", "v2"))); - - return HtmxResponse.builder().location(location).build(); - } - - @GetMapping("/hx-location-without-context-data") - public HtmxResponse hxLocationWithoutContextData() { - return HtmxResponse.builder().location("/path").build(); - } - - @GetMapping("/hx-location-with-flash-attributes") - public HtmxResponse hxLocationWithoutContextData(RedirectAttributes redirectAttributes) { - redirectAttributes.addFlashAttribute("flash", "test"); - return HtmxResponse.builder().location("/path").build(); - } - - @GetMapping("/hx-push-url") - public HtmxResponse hxPushUrl() { - return HtmxResponse.builder().pushUrl("/path").build(); - } - - @GetMapping("/hx-redirect") - public HtmxResponse hxRedirect() { - return HtmxResponse.builder().redirect("/path").build(); - } - - @GetMapping("/hx-redirect-with-flash-attributes") - public HtmxResponse hxRedirectWithFlashAttributes(RedirectAttributes redirectAttributes) { - redirectAttributes.addFlashAttribute("flash", "test"); - return HtmxResponse.builder().redirect("/path").build(); - } - - @GetMapping("/hx-refresh") - public HtmxResponse hxRefresh() { - return HtmxResponse.builder().refresh().build(); - } - - @GetMapping("/hx-replace-url") - public HtmxResponse hxReplaceUrl() { - return HtmxResponse.builder().replaceUrl("/path").build(); - } - - @GetMapping("/hx-reselect") - public HtmxResponse hxReselect() { - return HtmxResponse.builder().reselect("#target").build(); - } - - @GetMapping("/hx-reswap") - public HtmxResponse hxReswapOuterHtmlWithSwap() { - var reswap = HtmxReswap.outerHtml() - .settle(Duration.ofMillis(300)) - .swap(Duration.ofMillis(100)) - .show(HtmxReswap.Position.TOP) - .showTarget("#target") - .scroll(HtmxReswap.Position.BOTTOM) - .scrollTarget("#target") - .transition() - .focusScroll(true); - - return HtmxResponse.builder().reswap(reswap).build(); - } - - @GetMapping("/hx-retarget") - public HtmxResponse hxRetarget() { - return HtmxResponse.builder().retarget("#target").build(); - } - - @GetMapping("/hx-trigger-after-settle-with-details") - public HtmxResponse hxTriggerAfterSettleWithDetails() { - return HtmxResponse.builder() - .triggerAfterSettle("event1") - .triggerAfterSettle("event2", Map.of("var", "value")) - .build(); - } - - @GetMapping("/hx-trigger-after-settle-without-details") - public HtmxResponse hxTriggerAfterSettleWithoutDetails() { - return HtmxResponse.builder() - .triggerAfterSettle("event1") - .triggerAfterSettle("event2") - .build(); - } - - @GetMapping("/hx-trigger-after-swap-with-details") - public HtmxResponse hxTriggerAfterSwapWithDetails() { - return HtmxResponse.builder() - .triggerAfterSwap("event1") - .triggerAfterSwap("event2", Map.of("var", "value")) - .build(); - } - - @GetMapping("/hx-trigger-after-swap-without-details") - public HtmxResponse hxTriggerAfterSwapWithoutDetails() { - return HtmxResponse.builder() - .triggerAfterSwap("event1") - .triggerAfterSwap("event2") - .build(); - } - - @GetMapping("/hx-trigger-with-details") - public HtmxResponse hxTriggerWithDetails() { - return HtmxResponse.builder() - .trigger("event1") - .trigger("event2", Map.of("var", "value")) - .build(); - } - - @GetMapping("/hx-trigger-without-details") - public HtmxResponse hxTriggerWithoutDetails() { - return HtmxResponse.builder() - .trigger("event1") - .trigger("event2") - .build(); - } - - @GetMapping("/prevent-history-update") - public HtmxResponse preventHistoryUpdate() { - return HtmxResponse.builder().preventHistoryUpdate().build(); - } - - @GetMapping("/exception") - public void throwException() { - throw new RuntimeException("Fake exception"); - } - - @GetMapping("/argument") - public String argument(HtmxResponse.Builder htmxResponse) { - htmxResponse.trigger("event1"); - return "argument"; - } - - - - @ExceptionHandler(Exception.class) - public HtmxResponse handleError(Exception ex) { - return HtmxResponse.builder() - .reswap(HtmxReswap.none()) - .view(new ModelAndView("fragments :: error-message", Map.of("message", ex.getMessage()))) - .build(); - } -} diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandlerTest.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandlerTest.java deleted file mode 100644 index e8c2e21..0000000 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseHandlerMethodReturnValueHandlerTest.java +++ /dev/null @@ -1,203 +0,0 @@ -package io.github.wimdeblauwe.htmx.spring.boot.mvc; - - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.web.servlet.MockMvc; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@WebMvcTest(HtmxResponseHandlerMethodReturnValueHandlerController.class) -@WithMockUser -public class HtmxResponseHandlerMethodReturnValueHandlerTest { - - @Autowired - private MockMvc mockMvc; - - @Test - public void testHxLocationWithContextData() throws Exception { - mockMvc.perform(get("/hvhi/hx-location-with-context-data")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Location", "{\"path\":\"/path\",\"source\":\"source\",\"event\":\"event\",\"handler\":\"handler\",\"target\":\"target\",\"swap\":\"swap\",\"select\":\"select\",\"values\":{\"value1\":\"v1\",\"value2\":\"v2\"},\"headers\":{\"header1\":\"v1\",\"header2\":\"v2\"}}")); - } - - @Test - public void testHxLocationWithContextDataPathShouldPrependContextPath() throws Exception { - mockMvc.perform(get("/test/hvhi/hx-location-with-context-data").contextPath("/test")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Location", "{\"path\":\"/test/path\",\"source\":\"source\",\"event\":\"event\",\"handler\":\"handler\",\"target\":\"target\",\"swap\":\"swap\",\"select\":\"select\",\"values\":{\"value1\":\"v1\",\"value2\":\"v2\"},\"headers\":{\"header1\":\"v1\",\"header2\":\"v2\"}}")); - } - - @Test - public void testHxLocationWithoutContextData() throws Exception { - mockMvc.perform(get("/hvhi/hx-location-without-context-data")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Location", "/path")); - } - - @Test - public void testHxLocationWithoutContextDataShouldPrependContextPath() throws Exception { - mockMvc.perform(get("/test/hvhi/hx-location-without-context-data").contextPath("/test")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Location", "/test/path")); - } - - @Test - public void testHxLocationWithFlashAttributes() throws Exception { - mockMvc.perform(get("/hvhi/hx-location-with-flash-attributes")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Location", "/path")) - .andExpect(flash().attribute("flash", "test")); - } - - @Test - public void testHxPushUrl() throws Exception { - mockMvc.perform(get("/hvhi/hx-push-url")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Push-Url", "/path")); - } - - @Test - public void testHxPushUrlShouldPrependContextPath() throws Exception { - mockMvc.perform(get("/test/hvhi/hx-push-url").contextPath("/test")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Push-Url", "/test/path")); - } - - @Test - public void testHxRedirect() throws Exception { - mockMvc.perform(get("/hvhi/hx-redirect")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Redirect", "/path")); - } - - @Test - public void testHxRedirectShouldPrependContextPath() throws Exception { - mockMvc.perform(get("/test/hvhi/hx-redirect").contextPath("/test")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Redirect", "/test/path")); - } - - @Test - public void testHxRedirectWithFlashAttributes() throws Exception { - mockMvc.perform(get("/hvhi/hx-redirect-with-flash-attributes")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Redirect", "/path")) - .andExpect(flash().attribute("flash", "test")); - } - - @Test - public void testHxRefresh() throws Exception { - mockMvc.perform(get("/hvhi/hx-refresh")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Refresh", "true")); - } - - @Test - public void testHxReplaceUrl() throws Exception { - mockMvc.perform(get("/hvhi/hx-replace-url")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Replace-Url", "/path")); - } - - @Test - public void testHxReplaceUrlShouldPrependContextPath() throws Exception { - mockMvc.perform(get("/test/hvhi/hx-replace-url").contextPath("/test")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Replace-Url", "/test/path")); - } - - @Test - public void testHxReselect() throws Exception { - mockMvc.perform(get("/hvhi/hx-reselect")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Reselect", "#target")); - } - - @Test - public void testHxReswap() throws Exception { - mockMvc.perform(get("/hvhi/hx-reswap")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Reswap", "outerHTML transition:true focus-scroll:true swap:100ms settle:300ms scroll:#target:bottom show:#target:top")); - } - - @Test - public void testHxRetarget() throws Exception { - mockMvc.perform(get("/hvhi/hx-retarget")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Retarget", "#target")); - } - - @Test - public void testHxTriggerAfterSettleWithDetails() throws Exception { - mockMvc.perform(get("/hvhi/hx-trigger-after-settle-with-details")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Trigger-After-Settle", "{\"event1\":null,\"event2\":{\"var\":\"value\"}}")); - } - - @Test - public void testHxTriggerAfterSettleWithoutDetails() throws Exception { - mockMvc.perform(get("/hvhi/hx-trigger-after-settle-without-details")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Trigger-After-Settle", "event1,event2")); - } - - @Test - public void testHxTriggerAfterSwapWithDetails() throws Exception { - mockMvc.perform(get("/hvhi/hx-trigger-after-swap-with-details")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Trigger-After-Swap", "{\"event1\":null,\"event2\":{\"var\":\"value\"}}")); - } - - @Test - public void testHxTriggerAfterSwapWithoutDetails() throws Exception { - mockMvc.perform(get("/hvhi/hx-trigger-after-swap-without-details")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Trigger-After-Swap", "event1,event2")); - } - - @Test - public void testHxTriggerWithDetails() throws Exception { - mockMvc.perform(get("/hvhi/hx-trigger-with-details")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Trigger", "{\"event1\":null,\"event2\":{\"var\":\"value\"}}")); - } - - @Test - public void testHxTriggerWithoutDetails() throws Exception { - mockMvc.perform(get("/hvhi/hx-trigger-without-details")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Trigger", "event1,event2")); - } - - @Test - public void testPreventHistoryUpdate() throws Exception { - mockMvc.perform(get("/hvhi/prevent-history-update")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Push-Url", "false")) - .andExpect(header().doesNotExist("HX-Replace-Url")); - } - - @Test - public void testException() throws Exception { - String html = mockMvc.perform(get("/hvhi/exception")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Reswap", "none")) - .andReturn().getResponse().getContentAsString(); - assertThat(html).contains(""" - - Fake exception - """); - } - - @Test - public void testHxTriggerArgument() throws Exception { - mockMvc.perform(get("/hvhi/argument")) - .andExpect(status().isOk()) - .andExpect(header().string("HX-Trigger", "event1")); - } -} diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseTest.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseTest.java deleted file mode 100644 index f6c51ca..0000000 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponseTest.java +++ /dev/null @@ -1,429 +0,0 @@ -package io.github.wimdeblauwe.htmx.spring.boot.mvc; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.Map; - -public class HtmxResponseTest { - - @Test - public void testEmptyResponse() { - var response = HtmxResponse.builder().build(); - - assertThat(response.getViews()).isEmpty(); - assertThat(response.getTriggers()).isEmpty(); - assertThat(response.getTriggersAfterSettle()).isEmpty(); - assertThat(response.getTriggersAfterSwap()).isEmpty(); - } - - @Test - public void testAddingView() { - String view = "my-view"; - - var response = HtmxResponse.builder() - .view(view) - .build(); - - assertThat(response.getViews()) - .extracting(mav -> mav.getViewName()) - .containsExactly(view); - } - - @Test - public void testAddingViewMultipleTimesShouldBeIgnored() { - String view = "my-view"; - String viewWithMarkupSelector = "my-view :: fragment"; - - var response = HtmxResponse.builder() - .view(view) - .view(viewWithMarkupSelector) - .view(view) - .build(); - - assertThat(response.getViews()) - .extracting(mav -> mav.getViewName()) - .containsExactly(view, viewWithMarkupSelector); - } - - @Test - public void testAddingTrigger() { - HtmxResponse response = HtmxResponse.builder() - .trigger("event") - .build(); - - assertThat(response.getTriggersInternal()) - .extracting(HtmxTrigger::getEventName) - .containsExactly("event"); - } - - @Test - public void testAddingTriggerWithDetails() { - var eventDetail = Map.of("detail1", "message1", "detail2", "message2"); - HtmxResponse response = HtmxResponse.builder() - .trigger("event", eventDetail) - .build(); - - assertThat(response.getTriggersInternal()) - .containsExactly(new HtmxTrigger("event", eventDetail)); - } - - @Test - public void testAddingTriggerAfterSwap() { - HtmxResponse response = HtmxResponse.builder() - .triggerAfterSwap("event") - .build(); - - assertThat(response.getTriggersAfterSwapInternal()) - .extracting(HtmxTrigger::getEventName) - .containsExactly("event"); - } - - @Test - public void testAddingTriggerAfterSwapWithDetails() { - var eventDetail = Map.of("detail1", "message1", "detail2", "message2"); - HtmxResponse response = HtmxResponse.builder() - .triggerAfterSwap("event", eventDetail) - .build(); - - assertThat(response.getTriggersAfterSwapInternal()) - .containsExactly(new HtmxTrigger("event", eventDetail)); - } - - @Test - public void testAddingTriggerAfterSettle() { - HtmxResponse response = HtmxResponse.builder() - .triggerAfterSettle("event") - .build(); - - assertThat(response.getTriggersAfterSettleInternal()) - .extracting(HtmxTrigger::getEventName) - .containsExactly("event"); - } - - @Test - public void testAddingTriggerAfterSettleWithDetails() { - var eventDetail = Map.of("detail1", "message1", "detail2", "message2"); - HtmxResponse response = HtmxResponse.builder() - .triggerAfterSettle("event", eventDetail) - .build(); - - assertThat(response.getTriggersAfterSettleInternal()) - .containsExactly(new HtmxTrigger("event", eventDetail)); - } - - @Test - public void testAddingResponseToExistingOneShouldMergeTemplatesAndTriggers() { - var response1 = HtmxResponse.builder() - .view("view1") - .trigger("trigger1") - .view("view2") - .trigger("trigger2") - .build(); - - var response2 = HtmxResponse.builder() - .view("view1") - .trigger("trigger1") - .and(response1) - .build(); - - assertThat(response2).satisfies(response -> { - assertThat(response.getViews()) - .extracting(m -> m.getViewName()) - .containsExactly("view1", "view2"); - - assertThat(response.getTriggersInternal()) - .extracting(HtmxTrigger::getEventName) - .containsExactly("trigger1", "trigger2"); - }); - } - - @Test - public void testAddingResponseToExistingOneShouldOverrideProperties() { - var response1 = HtmxResponse.builder() - .location("location1") - .pushUrl("url1") - .redirect("url1") - .replaceUrl("url1") - .reswap(HtmxReswap.innerHtml()) - .retarget("selector1"); - - var response2 = HtmxResponse.builder() - .location("location2") - .pushUrl("url2") - .redirect("url2") - .replaceUrl("url2") - .reswap(HtmxReswap.outerHtml()) - .retarget("selector2") - .refresh(); - - response1.and(response2.build()); - - assertThat(response1.build()).satisfies(response -> { - assertThat(response.getLocation()).isEqualTo(new HtmxLocation("location2")); - assertThat(response.getPushUrl()).isEqualTo(null); - assertThat(response.getRedirect()).isEqualTo("url2"); - assertThat(response.getReplaceUrl()).isEqualTo("url2"); - assertThat(response.getReswap()).isEqualTo(HtmxReswap.outerHtml()); - assertThat(response.getRetarget()).isEqualTo("selector2"); - assertThat(response.isRefresh()).isEqualTo(true); - }); - } - - @Test - public void testResponseHeaderProperties() { - var response = HtmxResponse.builder() - .trigger("my-trigger") - .pushUrl("/a/history") - .redirect("/a/new/page") - .refresh() - .retarget("#theThing") - .reswap(HtmxReswap.afterBegin()) - .build(); - - assertThat(response.getTriggers()) - .containsOnlyKeys("my-trigger"); - assertThat(response.getPushUrl()).isEqualTo("/a/history"); - assertThat(response.getRedirect()).isEqualTo("/a/new/page"); - assertThat(response.isRefresh()).isTrue(); - assertThat(response.getRetarget()).isEqualTo("#theThing"); - assertThat(response.getReswap()).isEqualTo(HtmxReswap.afterBegin()); - } - - /** - * The order of templates can play a role in some scenarios in HTMX, - * see https://github.com/bigskysoftware/htmx/issues/1198 - */ - @Test - public void testAddingViewsShouldPreserveOrder() { - String view1 = "view1"; - String view2 = "view2"; - - var response = HtmxResponse.builder() - .view(view1) - .view(view2) - .build(); - - assertThat(response.getViews()) - .extracting(mav -> mav.getViewName()) - .containsExactly(view1, view2); - } - - @Test - public void testAddingResponseToExistingOneShouldPreserveViewOrder() { - var response2 = HtmxResponse.builder() - .view("view3") - .view("view4") - .build(); - - var response1 = HtmxResponse.builder() - .view("view1") - .view("view2") - .and(response2) - .build(); - - assertThat(response1.getViews()) - .extracting(mav -> mav.getViewName()) - .hasSize(4) - .containsExactly("view1", "view2", "view3", "view4"); - } - - @Deprecated - @Nested - @DisplayName("Tests for deprecated methods") - class DeprecatedTest { - @Test - public void testEmptyResponse() { - var response = new HtmxResponse(); - - assertThat(response.getTemplates()).isEmpty(); - assertThat(response.getTriggers()).isEmpty(); - assertThat(response.getTriggersAfterSettle()).isEmpty(); - assertThat(response.getTriggersAfterSwap()).isEmpty(); - } - - @Test - public void testAddingTemplate() { - String template = "my-template"; - - var response = new HtmxResponse() - .addTemplate(template); - - assertThat(response.getTemplates()) - .extracting(mav -> mav.getViewName()) - .containsExactly(template); - } - - @Test - public void testAddingTemplateMultipleTimesShouldBeIgnored() { - String template = "my-template"; - String templateWithMarkupSelector = "my-template :: fragment"; - - var response = new HtmxResponse() - .addTemplate(template) - .addTemplate(templateWithMarkupSelector) - .addTemplate(template); - - assertThat(response.getViews()) - .extracting(mav -> mav.getViewName()) - .containsExactly(template, templateWithMarkupSelector); - } - - @Test - public void testAddingTrigger() { - var response = new HtmxResponse() - .addTrigger("event"); - - assertThat(response.getTriggers()) - .containsOnlyKeys("event"); - } - - @Test - public void testAddingTriggerWithDetails() { - var response = new HtmxResponse() - .addTrigger("event", "message", HxTriggerLifecycle.RECEIVE); - - assertThat(response.getTriggers()) - .containsExactly(Map.entry("event", "message")); - } - - @Test - public void testAddingTriggerAfterSwap() { - var response = new HtmxResponse() - .addTrigger("event", null, HxTriggerLifecycle.SWAP); - - assertThat(response.getTriggersAfterSwap()) - .containsOnlyKeys("event"); - } - - @Test - public void testAddingTriggerAfterSwapWithDetails() { - var response = new HtmxResponse() - .addTrigger("event", "message", HxTriggerLifecycle.SWAP); - - assertThat(response.getTriggersAfterSwap()) - .containsExactly(Map.entry("event", "message")); - } - - @Test - public void testAddingTriggerAfterSettle() { - var response = new HtmxResponse() - .addTrigger("event", null, HxTriggerLifecycle.SETTLE); - - assertThat(response.getTriggersAfterSettle()) - .containsOnlyKeys("event"); - } - - @Test - public void testAddingTriggerAfterSettleWithDetails() { - var response = new HtmxResponse() - .addTrigger("event", "message", HxTriggerLifecycle.SETTLE); - - assertThat(response.getTriggersAfterSettle()) - .containsExactly(Map.entry("event", "message")); - } - - @Test - public void testAddingResponseToExistingOneShouldMergeTemplatesAndTriggers() { - var response1 = new HtmxResponse() - .addTemplate("view1") - .addTrigger("trigger1") - .addTemplate("view2") - .addTrigger("trigger2"); - - var response2 = new HtmxResponse() - .addTemplate("view1") - .addTrigger("trigger1") - .and(response1); - - assertThat(response2).satisfies(response -> { - assertThat(response.getViews()) - .extracting(m -> m.getViewName()) - .containsExactly("view1", "view2"); - - assertThat(response.getTriggers()) - .containsOnlyKeys("trigger1", "trigger2"); - }); - } - - @Test - public void testAddingResponseToExistingOneShouldOverrideProperties() { - var response1 = new HtmxResponse() - .retarget("selector1") - .reswap(HxSwapType.INNER_HTML) - .browserRedirect("url1") - .browserRefresh(false) - .pushHistory("url1"); - - var response2 = new HtmxResponse() - .retarget("selector2") - .reswap(HxSwapType.OUTER_HTML) - .browserRedirect("url2") - .browserRefresh(true) - .pushHistory("url2"); - - response1.and(response2); - - assertThat(response1).satisfies(response -> { - assertThat(response.getHeaderRetarget()).isEqualTo("selector2"); - assertThat(response.getHeaderReswap()).isEqualTo(HxSwapType.OUTER_HTML.getValue()); - assertThat(response.getHeaderRedirect()).isEqualTo("url2"); - assertThat(response.getHeaderRefresh()).isEqualTo(true); - assertThat(response.getHeaderPushHistory()).isEqualTo("url2"); - }); - } - - @Test - public void testResponseHeaderProperties() { - var response = new HtmxResponse() - .addTrigger("my-trigger") - .pushHistory("/a/history") - .browserRedirect("/a/new/page") - .browserRefresh(true) - .retarget("#theThing") - .reswap(HxSwapType.AFTER_BEGIN); - - assertThat(response.getTriggers()) - .containsOnlyKeys("my-trigger"); - assertThat(response.getHeaderPushHistory()).isEqualTo("/a/history"); - assertThat(response.getHeaderRedirect()).isEqualTo("/a/new/page"); - assertThat(response.getHeaderRefresh()).isTrue(); - assertThat(response.getHeaderRetarget()).isEqualTo("#theThing"); - assertThat(response.getHeaderReswap()).isEqualTo(HxSwapType.AFTER_BEGIN.getValue()); - } - - @Test - public void testAddingTemplatesShouldPreserveOrder() { - String template1 = "template1"; - String template2 = "template2"; - - var response = new HtmxResponse() - .addTemplate(template1) - .addTemplate(template2); - - assertThat(response.getViews()) - .extracting(mav -> mav.getViewName()) - .containsExactly(template1, template2); - } - - @Test - public void testAddingResponseToExistingOneShouldPreserveTemplateOrder() { - var response2 = new HtmxResponse() - .addTemplate("view3") - .addTemplate("view4"); - - var response1 = new HtmxResponse() - .addTemplate("view1") - .addTemplate("view2") - .and(response2); - - assertThat(response1.getViews()) - .extracting(mav -> mav.getViewName()) - .hasSize(4) - .containsExactly("view1", "view2", "view3", "view4"); - } - } -} diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxGetMapping.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxGetMapping.java index 9f980ef..d2e1618 100644 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxGetMapping.java +++ b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxGetMapping.java @@ -1,14 +1,14 @@ package io.github.wimdeblauwe.htmx.spring.boot.mvc; +import org.springframework.core.annotation.AliasFor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.springframework.core.annotation.AliasFor; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; - @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @HxRequest diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/PartialsController.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/PartialsController.java deleted file mode 100644 index eb8fff8..0000000 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/PartialsController.java +++ /dev/null @@ -1,133 +0,0 @@ -package io.github.wimdeblauwe.htmx.spring.boot.mvc; - -import java.util.Map; - -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.view.AbstractView; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -@Controller -public class PartialsController { - - private final TodoRepository todoRepository; - - public PartialsController(TodoRepository todoRepository) { - this.todoRepository = todoRepository; - } - - public interface TodoRepository { - public int getNumberOfActiveItems(); - } - - public static class TodoItem { - String name; - - public TodoItem(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - @GetMapping("/partials/first") - public HtmxResponse getFirstPartials() { - return HtmxResponse.builder().view("users :: list").build(); - } - - @GetMapping("/partials/view") - public HtmxResponse getFirstView() { - var view = new AbstractView() { - @Override - protected void renderMergedOutputModel(Map model, HttpServletRequest request, - HttpServletResponse response) throws Exception { - response.getWriter().write("

  • A list
"); - } - - }; - - return HtmxResponse.builder().view(view).build(); - } - - @GetMapping("/partials/mav") - public HtmxResponse getFirstModelAndView() { - return HtmxResponse.builder() - .view(new ModelAndView("fragments :: todoItem", Map.of("item", new TodoItem("Foo")))) - .build(); - } - - @GetMapping("/partials/main-and-partial") - public HtmxResponse getMainAndPartial(Model model) { - model.addAttribute("userCountOob", true); - model.addAttribute("userCount", 5); - - return HtmxResponse.builder() - .view("users :: list") - .view("users :: count") - .build(); - } - - @PostMapping("/partials/add-todo") - public HtmxResponse htmxAddTodoItem(TodoItem item, Model model) { - model.addAttribute("item", item); - model.addAttribute("itemCountSwap", "true"); - model.addAttribute("numberOfActiveItems", todoRepository.getNumberOfActiveItems()); - - return HtmxResponse.builder() - .view("fragments :: todoItem") - .view("fragments :: active-items-count") - .build(); - } - - @GetMapping("/partials/triggers") - public HtmxResponse getPartialsAndTriggers(Model model) { - model.addAttribute("userCountOob", true); - model.addAttribute("userCount", 5); - - return HtmxResponse.builder() - .view("users :: list") - .view("users :: count") - .trigger("usersCounted") - .triggerAfterSwap("usersCountedSwap", "swap detail") - .triggerAfterSettle("usersCountedSettle1", "aDetail") - .triggerAfterSettle("usersCountedSettle2") - .retarget("#newTarget") - .pushUrl("/a/newHistory") - .refresh() - .redirect("/a/redirect") - .build(); - } - - - @GetMapping("/partials/extension") - public HtmxResponse getPartialsViaExtension(Model model) { - model.addAttribute("userCountOob", true); - model.addAttribute("userCount", 5); - model.addAttribute("alertText", "Warning! Odium approaches!"); - - return HtmxResponse.builder() - .view("users :: list") - .view("users :: count") - .view("users :: alert") - .trigger("alertSent") - .build(); - } - - - @GetMapping("/partials/expressionUtility") - public String getWithExpressionUtility() { - return "htmxRequest"; - } - -} diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/TestController.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/TestController.java index 8e96705..23aeee9 100644 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/TestController.java +++ b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/TestController.java @@ -29,20 +29,6 @@ public String methodWithHxTriggerAndMultipleEvents() { return ""; } - @GetMapping("/with-trigger-settle") - @HxTrigger(value = "eventTriggered", lifecycle = HxTriggerLifecycle.SETTLE) - @ResponseBody - public String methodWithHxTriggerAndLifecycleSettle() { - return ""; - } - - @GetMapping("/with-trigger-swap") - @HxTrigger(value = "eventTriggered", lifecycle = HxTriggerLifecycle.SWAP) - @ResponseBody - public String methodWithHxTriggerAndLifecycleSwap() { - return ""; - } - @GetMapping("/with-trigger-after-settle") @HxTriggerAfterSettle("eventTriggered") @ResponseBody diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/support/PartialXpathResultMatchers.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/support/PartialXpathResultMatchers.java deleted file mode 100644 index 98a58d1..0000000 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/support/PartialXpathResultMatchers.java +++ /dev/null @@ -1,176 +0,0 @@ -package io.github.wimdeblauwe.htmx.spring.boot.mvc.support; - -import org.hamcrest.Matcher; -import org.springframework.lang.Nullable; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.test.util.XpathExpectationsHelper; -import org.springframework.test.web.servlet.ResultMatcher; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import javax.xml.xpath.XPathExpressionException; -import java.io.UnsupportedEncodingException; -import java.util.Map; - -/** - * Factory for assertions on the response content using XPath expressions when using HtmxPartials - * - *

An instance of this class is typically accessed via - * {@link PartialXpathResultMatchers#partialXpath}. - * - */ -public class PartialXpathResultMatchers { - - private final XpathExpectationsHelper xpathHelper; - - public static PartialXpathResultMatchers partialXpath(String expression, Object... args) throws XPathExpressionException { - return new PartialXpathResultMatchers("//htmxPartial"+expression, null, args); - } - - - /** - * Protected constructor, not for direct instantiation. Use - * {@link PartialXpathResultMatchers#partialXpath(String, Object...)} or - * {@link PartialXpathResultMatchers#partialXpath(String, Map, Object...)}. - * @param expression the XPath expression - * @param namespaces the XML namespaces referenced in the XPath expression, or {@code null} - * @param args arguments to parameterize the XPath expression with using the - * formatting specifiers defined in {@link String#format(String, Object...)} - */ - protected PartialXpathResultMatchers(String expression, @Nullable Map namespaces, Object ... args) - throws XPathExpressionException { - - this.xpathHelper = new XpathExpectationsHelper(expression, namespaces, args); - } - - private byte[] wrapContentWithHtmxRoot(MockHttpServletResponse response) throws UnsupportedEncodingException { - String wrappedContent = ""+response.getContentAsString()+""; - return wrappedContent.getBytes(); - } - - - /** - * Evaluate the XPath and assert the {@link Node} content found with the - * given Hamcrest {@link Matcher}. - */ - public ResultMatcher node(Matcher matcher) { - return result -> { - MockHttpServletResponse response = result.getResponse(); - this.xpathHelper.assertNode(wrapContentWithHtmxRoot(response), getDefinedEncoding(response), matcher); - }; - } - - /** - * Evaluate the XPath and assert the {@link NodeList} content found with the - * given Hamcrest {@link Matcher}. - * @since 5.2.2 - */ - public ResultMatcher nodeList(Matcher matcher) { - return result -> { - MockHttpServletResponse response = result.getResponse(); - this.xpathHelper.assertNodeList(wrapContentWithHtmxRoot(response), getDefinedEncoding(response), matcher); - }; - } - - /** - * Get the response encoding if explicitly defined in the response, {code null} otherwise. - */ - @Nullable - private String getDefinedEncoding(MockHttpServletResponse response) { - return (response.isCharset() ? response.getCharacterEncoding() : null); - } - - /** - * Evaluate the XPath and assert that content exists. - */ - public ResultMatcher exists() { - return result -> { - MockHttpServletResponse response = result.getResponse(); - this.xpathHelper.exists(wrapContentWithHtmxRoot(response), getDefinedEncoding(response)); - }; - } - - /** - * Evaluate the XPath and assert that content doesn't exist. - */ - public ResultMatcher doesNotExist() { - return result -> { - MockHttpServletResponse response = result.getResponse(); - this.xpathHelper.doesNotExist(wrapContentWithHtmxRoot(response), getDefinedEncoding(response)); - }; - } - - /** - * Evaluate the XPath and assert the number of nodes found with the given - * Hamcrest {@link Matcher}. - */ - public ResultMatcher nodeCount(Matcher matcher) { - return result -> { - MockHttpServletResponse response = result.getResponse(); - this.xpathHelper.assertNodeCount(wrapContentWithHtmxRoot(response), getDefinedEncoding(response), matcher); - }; - } - - /** - * Evaluate the XPath and assert the number of nodes found. - */ - public ResultMatcher nodeCount(int expectedCount) { - return result -> { - MockHttpServletResponse response = result.getResponse(); - this.xpathHelper.assertNodeCount(wrapContentWithHtmxRoot(response), getDefinedEncoding(response), expectedCount); - }; - } - - /** - * Apply the XPath and assert the {@link String} value found with the given - * Hamcrest {@link Matcher}. - */ - public ResultMatcher string(Matcher matcher) { - return result -> { - MockHttpServletResponse response = result.getResponse(); - this.xpathHelper.assertString(wrapContentWithHtmxRoot(response), getDefinedEncoding(response), matcher); - }; - } - - /** - * Apply the XPath and assert the {@link String} value found. - */ - public ResultMatcher string(String expectedValue) { - return result -> { - MockHttpServletResponse response = result.getResponse(); - this.xpathHelper.assertString(response.getContentAsByteArray(), getDefinedEncoding(response), expectedValue); - }; - } - - /** - * Evaluate the XPath and assert the {@link Double} value found with the - * given Hamcrest {@link Matcher}. - */ - public ResultMatcher number(Matcher matcher) { - return result -> { - MockHttpServletResponse response = result.getResponse(); - this.xpathHelper.assertNumber(response.getContentAsByteArray(), getDefinedEncoding(response), matcher); - }; - } - - /** - * Evaluate the XPath and assert the {@link Double} value found. - */ - public ResultMatcher number(Double expectedValue) { - return result -> { - MockHttpServletResponse response = result.getResponse(); - this.xpathHelper.assertNumber(response.getContentAsByteArray(), getDefinedEncoding(response), expectedValue); - }; - } - - /** - * Evaluate the XPath and assert the {@link Boolean} value found. - */ - public ResultMatcher booleanValue(Boolean value) { - return result -> { - MockHttpServletResponse response = result.getResponse(); - this.xpathHelper.assertBoolean(response.getContentAsByteArray(), getDefinedEncoding(response), value); - }; - } - -} From e9c2d78f3acaf161ba2ba51ddfbfca29354be622 Mon Sep 17 00:00:00 2001 From: xhaggi Date: Sun, 24 Nov 2024 09:21:25 +0100 Subject: [PATCH 3/4] Move controllers and service into associated test classes --- .../boot/mvc/HtmxHandlerInterceptorTest.java | 180 +++++++++++++++++- ...HtmxHandlerMethodArgumentResolverTest.java | 64 ++++++- ...rMethodArgumentResolverTestController.java | 54 ------ .../HtmxRequestMappingHandlerMappingTest.java | 83 +++++++- ...stMappingHandlerMappingTestController.java | 80 -------- .../htmx/spring/boot/mvc/TestController.java | 178 ----------------- .../htmx/spring/boot/mvc/TestService.java | 10 - 7 files changed, 321 insertions(+), 328 deletions(-) delete mode 100644 htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodArgumentResolverTestController.java delete mode 100644 htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequestMappingHandlerMappingTestController.java delete mode 100644 htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/TestController.java delete mode 100644 htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/TestService.java diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptorTest.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptorTest.java index cf296ee..fa610f4 100644 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptorTest.java +++ b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptorTest.java @@ -4,7 +4,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.stereotype.Controller; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; import static org.hamcrest.Matchers.not; import static org.hamcrest.collection.IsIterableContainingInRelativeOrder.containsInRelativeOrder; @@ -12,7 +17,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(TestController.class) +@WebMvcTest(HtmxHandlerInterceptorTest.TestController.class) +@ContextConfiguration(classes = HtmxHandlerInterceptorTest.TestController.class) @WithMockUser class HtmxHandlerInterceptorTest { @@ -231,4 +237,176 @@ public void testHxReselect() throws Exception { .andExpect(header().string("HX-Reselect", "#target")); } + @Controller + @RequestMapping("/") + static class TestController { + + @GetMapping("/without-trigger") + @ResponseBody + public String methodWithoutAnnotations() { + return ""; + } + + @GetMapping("/with-trigger") + @HxTrigger("eventTriggered") + @ResponseBody + public String methodWithHxTrigger() { + return ""; + } + + @GetMapping("/with-trigger-multiple-events") + @HxTrigger({ "event1", "event2" }) + @ResponseBody + public String methodWithHxTriggerAndMultipleEvents() { + return ""; + } + + @GetMapping("/with-trigger-after-settle") + @HxTriggerAfterSettle("eventTriggered") + @ResponseBody + public String methodWithHxTriggerAfterSettle() { + return ""; + } + + @GetMapping("/with-trigger-after-settle-multiple-events") + @HxTriggerAfterSettle({ "event1", "event2" }) + @ResponseBody + public String methodWithHxTriggerAfterSettleAndMultipleEvents() { + return ""; + } + + @GetMapping("/with-trigger-after-swap") + @HxTriggerAfterSwap("eventTriggered") + @ResponseBody + public String methodWithHxTriggerAfterSwap() { + return ""; + } + + @GetMapping("/with-trigger-after-swap-multiple-events") + @HxTriggerAfterSwap({ "event1", "event2" }) + @ResponseBody + public String methodWithHxTriggerAfterSwapAndMultipleEvents() { + return ""; + } + + @GetMapping("/with-triggers") + @HxTrigger({ "event1", "event2" }) + @HxTriggerAfterSettle({ "event1", "event2" }) + @HxTriggerAfterSwap({ "event1", "event2" }) + @ResponseBody + public String methodWithHxTriggers() { + return ""; + } + + @GetMapping("/updates-sidebar") + @HxUpdatesSidebar + @ResponseBody + public String updatesSidebar() { + return ""; + } + + @GetMapping("/hx-trigger-alias-for") + @HxTriggerWithAliasFor(event = "updateTrigger") + @ResponseBody + public String hxTriggerWithAliasForOverride() { + return ""; + } + + @GetMapping("/hx-refresh") + @HxRefresh + @ResponseBody + public String hxRefresh() { + return ""; + } + + @GetMapping("/hx-vary") + @ResponseBody + public String hxVary() { + return ""; + } + + @GetMapping("/hx-location-without-context-data") + @HxLocation("/path") + @ResponseBody + public String hxLocationWithoutContextData() { + return ""; + } + + @GetMapping("/hx-location-with-context-data") + @HxLocation(path = "/path", source = "source", event = "event", handler = "handler", target = "target", swap = "swap", select = "select") + @ResponseBody + public String hxLocationWithContextData() { + return ""; + } + + @GetMapping("/hx-push-url-path") + @HxPushUrl("/path") + @ResponseBody + public String hxPushUrlPath() { + return ""; + } + + @GetMapping("/hx-push-url") + @HxPushUrl + @ResponseBody + public String hxPushUrl() { + return ""; + } + + @GetMapping("/hx-push-url-false") + @HxPushUrl(HtmxValue.FALSE) + @ResponseBody + public String hxPushUrlFalse() { + return ""; + } + + @GetMapping("/hx-redirect") + @HxRedirect("/path") + @ResponseBody + public String hxRedirect() { + return ""; + } + + @GetMapping("/hx-replace-url-path") + @HxReplaceUrl("/path") + @ResponseBody + public String hxReplaceUrlPath() { + return ""; + } + @GetMapping("/hx-replace-url") + @HxReplaceUrl + @ResponseBody + public String hxReplaceUrl() { + return ""; + } + @GetMapping("/hx-replace-url-false") + @HxReplaceUrl(HtmxValue.FALSE) + @ResponseBody + public String hxReplaceUrlFalse() { + return ""; + } + + @GetMapping("/hx-reswap") + @HxReswap(value = HxSwapType.INNER_HTML, swap = 300) + @ResponseBody + public String hxReswap() { + return ""; + } + + @GetMapping("/hx-retarget") + @HxRetarget("#target") + @ResponseBody + public String hxRetarget() { + return ""; + } + + @GetMapping("/hx-reselect") + @HxReselect("#target") + @ResponseBody + public String hxReselect() { + return ""; + } + + } + } diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodArgumentResolverTest.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodArgumentResolverTest.java index 5908013..d9d2e20 100644 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodArgumentResolverTest.java +++ b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodArgumentResolverTest.java @@ -5,8 +5,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.stereotype.Controller; +import org.springframework.stereotype.Service; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.verify; @@ -14,7 +20,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; -@WebMvcTest(HtmxHandlerMethodArgumentResolverTestController.class) +@WebMvcTest(HtmxHandlerMethodArgumentResolverTest.TestController.class) +@ContextConfiguration(classes = HtmxHandlerMethodArgumentResolverTest.TestController.class) @WithMockUser class HtmxHandlerMethodArgumentResolverTest { @Autowired @@ -165,7 +172,6 @@ void testHxRequestAnnotation() throws Exception { mockMvc.perform(get("/method-arg-resolver/users") .header("HX-Request", "true")) .andExpect(view().name("users :: list")); - ; } @Test @@ -173,13 +179,63 @@ void testHxRequestAnnotationInheritance() throws Exception { mockMvc.perform(get("/method-arg-resolver/users/inherited") .header("HX-Request", "true")) .andExpect(view().name("users :: list")); - ; } @Test void testHxRequestSameUrlNoAnnotation() throws Exception { mockMvc.perform(get("/method-arg-resolver/users")) .andExpect(view().name("users")); - ; } + + @Controller + @RequestMapping("/method-arg-resolver") + static class TestController { + + @Autowired + private TestService service; + + @GetMapping + @ResponseBody + public String htmxRequestDetails(HtmxRequest details) { + service.doSomething(details); + + return ""; + } + + @GetMapping("/users") + @HxRequest + public String htmxRequest(HtmxRequest details) { + service.doSomething(details); + + return "users :: list"; + } + + @GetMapping("/users") + public String normalRequest(HtmxRequest details) { + service.doSomething(details); + + return "users"; + } + + @HxGetMapping("/users/inherited") + public String htmxRequestInheritance(HtmxRequest details) { + service.doSomething(details); + + return "users :: list"; + } + + @GetMapping("/users/inherited") + public String normalRequestInheritance(HtmxRequest details) { + service.doSomething(details); + + return "users"; + } + } + + @Service + public class TestService { + void doSomething(HtmxRequest details) { + } + } + } diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodArgumentResolverTestController.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodArgumentResolverTestController.java deleted file mode 100644 index d85acd1..0000000 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerMethodArgumentResolverTestController.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.github.wimdeblauwe.htmx.spring.boot.mvc; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -@Controller -@RequestMapping("/method-arg-resolver") -public class HtmxHandlerMethodArgumentResolverTestController { - - private final TestService service; - - public HtmxHandlerMethodArgumentResolverTestController(TestService service) { - this.service = service; - } - - @GetMapping - @ResponseBody - public String htmxRequestDetails(HtmxRequest details) { - service.doSomething(details); - - return ""; - } - - @GetMapping("/users") - @HxRequest - public String htmxRequest(HtmxRequest details) { - service.doSomething(details); - - return "users :: list"; - } - - @GetMapping("/users") - public String normalRequest(HtmxRequest details) { - service.doSomething(details); - - return "users"; - } - - @HxGetMapping("/users/inherited") - public String htmxRequestInheritance(HtmxRequest details) { - service.doSomething(details); - - return "users :: list"; - } - - @GetMapping("/users/inherited") - public String normalRequestInheritance(HtmxRequest details) { - service.doSomething(details); - - return "users"; - } -} diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequestMappingHandlerMappingTest.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequestMappingHandlerMappingTest.java index 8381498..2f22f7b 100644 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequestMappingHandlerMappingTest.java +++ b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequestMappingHandlerMappingTest.java @@ -4,14 +4,19 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.stereotype.Controller; +import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxRequestHeader.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@WebMvcTest(HtmxRequestMappingHandlerMappingTestController.class) +@WebMvcTest(HtmxRequestMappingHandlerMappingTest.TestController.class) +@ContextConfiguration(classes = HtmxRequestMappingHandlerMappingTest.TestController.class) @WithMockUser public class HtmxRequestMappingHandlerMappingTest { @@ -132,4 +137,80 @@ void testHxRequestValueWithHxHeaderTriggerFoo() throws Exception { .andExpect(content().string("foo")); } + + @Controller + static class TestController { + + @HxRequest + @GetMapping("/hx-request") + @ResponseBody + public String hxRequest() { + return "hx-request"; + } + + @HxRequest(boosted = false) + @GetMapping("/hx-request-ignore-boosted") + @ResponseBody + public String hxRequestIgnoreBoosted() { + return "boosted-ignored"; + } + + @HxRequest(target = "bar") + @GetMapping("/hx-request-target") + @ResponseBody + public String hxRequestTargetBar() { + return "bar"; + } + + @HxRequest(target = "foo") + @GetMapping("/hx-request-target") + @ResponseBody + public String hxRequestTargetFoo() { + return "foo"; + } + + @HxRequest(triggerId = "bar") + @GetMapping("/hx-request-trigger") + @ResponseBody + public String hxRequestTriggerIdBar() { + return "bar"; + } + + @HxRequest(triggerId = "foo") + @GetMapping("/hx-request-trigger") + @ResponseBody + public String hxRequestTriggerIdFoo() { + return "foo"; + } + + @HxRequest(triggerName = "bar") + @GetMapping("/hx-request-trigger") + @ResponseBody + public String hxRequestTriggerNameBar() { + return "bar"; + } + + @HxRequest(triggerName = "foo") + @GetMapping("/hx-request-trigger") + @ResponseBody + public String hxRequestTriggerNameFoo() { + return "foo"; + } + + @HxRequest("bar") + @GetMapping("/hx-request-value") + @ResponseBody + public String hxRequestValueBar() { + return "bar"; + } + + @HxRequest("foo") + @GetMapping("/hx-request-value") + @ResponseBody + public String hxRequestValueFoo() { + return "foo"; + } + + } + } diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequestMappingHandlerMappingTestController.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequestMappingHandlerMappingTestController.java deleted file mode 100644 index aaaba6c..0000000 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxRequestMappingHandlerMappingTestController.java +++ /dev/null @@ -1,80 +0,0 @@ -package io.github.wimdeblauwe.htmx.spring.boot.mvc; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -@Controller -public class HtmxRequestMappingHandlerMappingTestController { - - @HxRequest - @GetMapping("/hx-request") - @ResponseBody - public String hxRequest() { - return "hx-request"; - } - - @HxRequest(boosted = false) - @GetMapping("/hx-request-ignore-boosted") - @ResponseBody - public String hxRequestIgnoreBoosted() { - return "boosted-ignored"; - } - - @HxRequest(target = "bar") - @GetMapping("/hx-request-target") - @ResponseBody - public String hxRequestTargetBar() { - return "bar"; - } - - @HxRequest(target = "foo") - @GetMapping("/hx-request-target") - @ResponseBody - public String hxRequestTargetFoo() { - return "foo"; - } - - @HxRequest(triggerId = "bar") - @GetMapping("/hx-request-trigger") - @ResponseBody - public String hxRequestTriggerIdBar() { - return "bar"; - } - - @HxRequest(triggerId = "foo") - @GetMapping("/hx-request-trigger") - @ResponseBody - public String hxRequestTriggerIdFoo() { - return "foo"; - } - - @HxRequest(triggerName = "bar") - @GetMapping("/hx-request-trigger") - @ResponseBody - public String hxRequestTriggerNameBar() { - return "bar"; - } - - @HxRequest(triggerName = "foo") - @GetMapping("/hx-request-trigger") - @ResponseBody - public String hxRequestTriggerNameFoo() { - return "foo"; - } - - @HxRequest("bar") - @GetMapping("/hx-request-value") - @ResponseBody - public String hxRequestValueBar() { - return "bar"; - } - - @HxRequest("foo") - @GetMapping("/hx-request-value") - @ResponseBody - public String hxRequestValueFoo() { - return "foo"; - } - -} diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/TestController.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/TestController.java deleted file mode 100644 index 23aeee9..0000000 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/TestController.java +++ /dev/null @@ -1,178 +0,0 @@ -package io.github.wimdeblauwe.htmx.spring.boot.mvc; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseBody; - -@Controller -@RequestMapping("/") -public class TestController { - - @GetMapping("/without-trigger") - @ResponseBody - public String methodWithoutAnnotations() { - return ""; - } - - @GetMapping("/with-trigger") - @HxTrigger("eventTriggered") - @ResponseBody - public String methodWithHxTrigger() { - return ""; - } - - @GetMapping("/with-trigger-multiple-events") - @HxTrigger({ "event1", "event2" }) - @ResponseBody - public String methodWithHxTriggerAndMultipleEvents() { - return ""; - } - - @GetMapping("/with-trigger-after-settle") - @HxTriggerAfterSettle("eventTriggered") - @ResponseBody - public String methodWithHxTriggerAfterSettle() { - return ""; - } - - @GetMapping("/with-trigger-after-settle-multiple-events") - @HxTriggerAfterSettle({ "event1", "event2" }) - @ResponseBody - public String methodWithHxTriggerAfterSettleAndMultipleEvents() { - return ""; - } - - @GetMapping("/with-trigger-after-swap") - @HxTriggerAfterSwap("eventTriggered") - @ResponseBody - public String methodWithHxTriggerAfterSwap() { - return ""; - } - - @GetMapping("/with-trigger-after-swap-multiple-events") - @HxTriggerAfterSwap({ "event1", "event2" }) - @ResponseBody - public String methodWithHxTriggerAfterSwapAndMultipleEvents() { - return ""; - } - - @GetMapping("/with-triggers") - @HxTrigger({ "event1", "event2" }) - @HxTriggerAfterSettle({ "event1", "event2" }) - @HxTriggerAfterSwap({ "event1", "event2" }) - @ResponseBody - public String methodWithHxTriggers() { - return ""; - } - - @GetMapping("/updates-sidebar") - @HxUpdatesSidebar - @ResponseBody - public String updatesSidebar() { - return ""; - } - - @GetMapping("/hx-trigger-alias-for") - @HxTriggerWithAliasFor(event = "updateTrigger") - @ResponseBody - public String hxTriggerWithAliasForOverride() { - return ""; - } - - @GetMapping("/hx-refresh") - @HxRefresh - @ResponseBody - public String hxRefresh() { - return ""; - } - - @GetMapping("/hx-vary") - @ResponseBody - public String hxVary() { - return ""; - } - - @GetMapping("/hx-location-without-context-data") - @HxLocation("/path") - @ResponseBody - public String hxLocationWithoutContextData() { - return ""; - } - - @GetMapping("/hx-location-with-context-data") - @HxLocation(path = "/path", source = "source", event = "event", handler = "handler", target = "target", swap = "swap", select = "select") - @ResponseBody - public String hxLocationWithContextData() { - return ""; - } - - @GetMapping("/hx-push-url-path") - @HxPushUrl("/path") - @ResponseBody - public String hxPushUrlPath() { - return ""; - } - - @GetMapping("/hx-push-url") - @HxPushUrl - @ResponseBody - public String hxPushUrl() { - return ""; - } - - @GetMapping("/hx-push-url-false") - @HxPushUrl(HtmxValue.FALSE) - @ResponseBody - public String hxPushUrlFalse() { - return ""; - } - - @GetMapping("/hx-redirect") - @HxRedirect("/path") - @ResponseBody - public String hxRedirect() { - return ""; - } - - @GetMapping("/hx-replace-url-path") - @HxReplaceUrl("/path") - @ResponseBody - public String hxReplaceUrlPath() { - return ""; - } - @GetMapping("/hx-replace-url") - @HxReplaceUrl - @ResponseBody - public String hxReplaceUrl() { - return ""; - } - @GetMapping("/hx-replace-url-false") - @HxReplaceUrl(HtmxValue.FALSE) - @ResponseBody - public String hxReplaceUrlFalse() { - return ""; - } - - @GetMapping("/hx-reswap") - @HxReswap(value = HxSwapType.INNER_HTML, swap = 300) - @ResponseBody - public String hxReswap() { - return ""; - } - - @GetMapping("/hx-retarget") - @HxRetarget("#target") - @ResponseBody - public String hxRetarget() { - return ""; - } - - @GetMapping("/hx-reselect") - @HxReselect("#target") - @ResponseBody - public String hxReselect() { - return ""; - } - -} diff --git a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/TestService.java b/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/TestService.java deleted file mode 100644 index 1025a6d..0000000 --- a/htmx-spring-boot/src/test/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/TestService.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.github.wimdeblauwe.htmx.spring.boot.mvc; - -import org.springframework.stereotype.Service; - -@Service -public class TestService { - void doSomething(HtmxRequest details) { - - } -} From 7a47bb237fd52b0acb0af1a07887c8e4e60c5784 Mon Sep 17 00:00:00 2001 From: xhaggi Date: Sun, 24 Nov 2024 09:28:43 +0100 Subject: [PATCH 4/4] Mark HtmxView as deprecated in favor of HTML Fragments support in Spring Framework 6.2 See https://docs.spring.io/spring-framework/reference/web/webmvc-view/mvc-fragments.html --- README.md | 30 +++------ .../boot/mvc/HtmxMvcAutoConfiguration.java | 18 +----- .../htmx/spring/boot/mvc/HtmxResponse.java | 4 +- .../htmx/spring/boot/mvc/HtmxView.java | 2 + .../mvc/HtmxViewMethodReturnValueHandler.java | 63 ++----------------- 5 files changed, 16 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index 7ef9f0c..a1e3c77 100644 --- a/README.md +++ b/README.md @@ -175,8 +175,9 @@ public String users() { ### HTML Fragments In Spring MVC, view rendering typically involves specifying one view and one model. However, in htmx a common capability is to send multiple HTML fragments that -htmx can use to update different parts of the page, which is called [Out Of Band Swaps](https://htmx.org/docs/#oob_swaps). For this, controller methods can return -[HtmxView](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxView.html) +htmx can use to update different parts of the page, which is called [Out Of Band Swaps](https://htmx.org/docs/#oob_swaps). Spring offers the ability to return +multiple HTML fragments using `Collection` or `FragmentsRendering` as return type of controller. Further information on this can be found in the +Spring Framework documentation under [HTML Fragments](https://docs.spring.io/spring-framework/reference/web/webmvc-view/mvc-fragments.html). ```java @HxRequest @@ -185,30 +186,13 @@ public View users(Model model) { model.addAttribute("users", userRepository.findAll()); model.addAttribute("count", userRepository.count()); - var view = new HtmxView(); - view.add("users/list"); - view.add("users/count"); - - return view; + return FragmentsRendering + .with("users/list") + .fragment("users/count") + .build(); } ``` -An `HtmxView` can be formed from view names, as above, or fully resolved `View` instances, if the controller knows how -to do that, or from `ModelAndView` instances (resolved or unresolved). Each fragment can have its own model, which is merged with the controller model before rendering. - -```java -@HxRequest -@GetMapping("/users") -public View users(Model model) { - var view = new HtmxView(); - view.add("users/list", Map.of("users", userRepository.findAll())); - view.add("users/count", Map.of("count", userRepository.count())); - - return view; -} -``` - - ### Exceptions It is also possible to use `HtmxRequest` and `HtmxResponse` as method argument in handler methods annotated with `@ExceptionHandler`. diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxMvcAutoConfiguration.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxMvcAutoConfiguration.java index ab40759..72a252f 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxMvcAutoConfiguration.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxMvcAutoConfiguration.java @@ -2,8 +2,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; -import org.springframework.beans.factory.ObjectFactory; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -11,12 +9,9 @@ import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; import org.springframework.context.annotation.Bean; import org.springframework.core.Ordered; -import org.springframework.util.Assert; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; -import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.View; -import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; @@ -28,19 +23,10 @@ @ConditionalOnWebApplication public class HtmxMvcAutoConfiguration implements WebMvcRegistrations, WebMvcConfigurer { - private final ObjectFactory viewResolverObjectFactory; - private final ObjectFactory localeResolverObjectFactory; private final ObjectMapper objectMapper; private final HtmxHandlerMethodAnnotationHandler handlerMethodAnnotationHandler; - HtmxMvcAutoConfiguration(@Qualifier("viewResolver") ObjectFactory viewResolverObjectFactory, - ObjectFactory localeResolverObjectFactory) { - - Assert.notNull(viewResolverObjectFactory, "viewResolverObjectFactory must not be null!"); - Assert.notNull(localeResolverObjectFactory, "localeResolverObjectFactory must not be null!"); - - this.viewResolverObjectFactory = viewResolverObjectFactory; - this.localeResolverObjectFactory = localeResolverObjectFactory; + HtmxMvcAutoConfiguration() { this.objectMapper = JsonMapper.builder().build(); this.handlerMethodAnnotationHandler = new HtmxHandlerMethodAnnotationHandler(this.objectMapper); } @@ -63,7 +49,7 @@ public void addArgumentResolvers(List resolvers) @Override public void addReturnValueHandlers(List handlers) { - handlers.add(new HtmxViewMethodReturnValueHandler(viewResolverObjectFactory.getObject(), localeResolverObjectFactory.getObject())); + handlers.add(new HtmxViewMethodReturnValueHandler()); } @Override diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponse.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponse.java index f070e54..50b107e 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponse.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxResponse.java @@ -2,9 +2,7 @@ import org.springframework.util.Assert; -import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.Set; +import java.util.*; /** * A holder for htmx-related response headers that can be used as method argument in controllers. diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxView.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxView.java index 20d6cdd..c910f02 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxView.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxView.java @@ -17,7 +17,9 @@ * this class. * * @since 3.6.0 + * @deprecated since 4.0.0 for removal in 4.1.0 in favor of HTML Fragments support. */ +@Deprecated public class HtmxView { private final Set views = new LinkedHashSet<>(); diff --git a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxViewMethodReturnValueHandler.java b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxViewMethodReturnValueHandler.java index 28de723..10f4c8a 100644 --- a/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxViewMethodReturnValueHandler.java +++ b/htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxViewMethodReturnValueHandler.java @@ -1,45 +1,20 @@ package io.github.wimdeblauwe.htmx.spring.boot.mvc; -import com.fasterxml.jackson.core.JsonProcessingException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; -import org.springframework.ui.ModelMap; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.method.support.ModelAndViewContainer; -import org.springframework.web.servlet.LocaleResolver; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.View; -import org.springframework.web.servlet.ViewResolver; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import org.springframework.web.util.ContentCachingResponseWrapper; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.stream.Collectors; +import org.springframework.web.servlet.view.FragmentsRendering; /** * Handles return values that are of type {@link HtmxView}. * * @since 3.6.0 + * @deprecated since 4.0.0 for removal in 4.1.0 in favor of HTML Fragments support. */ +@Deprecated public class HtmxViewMethodReturnValueHandler implements HandlerMethodReturnValueHandler { - private final ViewResolver viewResolver; - private final LocaleResolver localeResolver; - - public HtmxViewMethodReturnValueHandler(ViewResolver viewResolver, LocaleResolver localeResolver) { - this.viewResolver = viewResolver; - this.localeResolver = localeResolver; - } - @Override public boolean supportsReturnType(MethodParameter returnType) { return HtmxView.class.isAssignableFrom(returnType.getParameterType()); @@ -52,40 +27,10 @@ public void handleReturnValue(Object returnValue, NativeWebRequest webRequest) throws Exception { if (returnValue instanceof HtmxView htmxView) { - mavContainer.setView(toView(htmxView)); + mavContainer.setView(FragmentsRendering.with(htmxView.getViews()).build()); } else if (returnValue != null) { throw new UnsupportedOperationException("Unexpected return type: " + returnType.getParameterType().getName() + " in method: " + returnType.getMethod()); } } - private View toView(HtmxView htmxView) { - - return (model, request, response) -> { - Locale locale = localeResolver.resolveLocale(request); - ContentCachingResponseWrapper wrapper = new ContentCachingResponseWrapper(response); - - for (ModelAndView mav : htmxView.getViews()) { - View view; - if (mav.isReference()) { - view = viewResolver.resolveViewName(mav.getViewName(), locale); - if (view == null) { - throw new IllegalArgumentException("Could not resolve view with name '" + mav.getViewName() + "'."); - } - } else { - view = mav.getView(); - } - - for (String key : model.keySet()) { - if (!mav.getModel().containsKey(key)) { - mav.getModel().put(key, model.get(key)); - } - } - - view.render(mav.getModel(), request, wrapper); - } - - wrapper.copyBodyToResponse(); - }; - } - }