Skip to content

Commit

Permalink
Merge pull request #118 from xhaggi/missing-response-header-annotatio…
Browse files Browse the repository at this point in the history
…n-support

Add missing annotations for response headers
  • Loading branch information
wimdeblauwe authored May 5, 2024
2 parents da75e77 + eb12958 commit a8b65ab
Show file tree
Hide file tree
Showing 17 changed files with 747 additions and 17 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,17 @@ The first is to use annotations, e.g. `@HxTrigger`, and the second is to use the
See [Response Headers Reference](https://htmx.org/reference/#response_headers) for the related htmx documentation.

The following annotations are currently supported:
* [@HxLocation](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxLocation.html)
* [@HxPushUrl](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxPushUrl.html)
* [@HxRedirect](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxRedirect.html)
* [@HxRefresh](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxRefresh.html)
* [@HxReplaceUrl](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxReplaceUrl.html)
* [@HxReselect](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxReselect.html)
* [@HxReswap](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxReswap.html)
* [@HxRetarget](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxRetarget.html)
* [@HxTrigger](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTrigger.html)
* [@HxTriggerAfterSettle](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTriggerAfterSettle.html)
* [@HxTriggerAfterSwap](https://javadoc.io/doc/io.github.wimdeblauwe/htmx-spring-boot/latest/io/github/wimdeblauwe/htmx/spring/boot/mvc/HxTriggerAfterSwap.html)

>**Note** Please refer to the related Javadoc to learn more about the available options.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package io.github.wimdeblauwe.htmx.spring.boot.mvc;

import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponseHeader.HX_REFRESH;
import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponseHeader.HX_TRIGGER;
import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponseHeader.HX_TRIGGER_AFTER_SETTLE;
import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponseHeader.HX_TRIGGER_AFTER_SWAP;
import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponseHeader.*;

import java.lang.reflect.Method;
import java.time.Duration;

import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.http.HttpHeaders;
Expand All @@ -15,14 +13,34 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class HtmxHandlerInterceptor implements HandlerInterceptor {

private final ObjectMapper objectMapper;

public HtmxHandlerInterceptor(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
HttpServletResponse response,
Object handler) {

if (handler instanceof HandlerMethod) {
Method method = ((HandlerMethod) handler).getMethod();
setHxLocation(response, method);
setHxPushUrl(response, method);
setHxRedirect(response, method);
setHxReplaceUrl(response, method);
setHxReswap(response, method);
setHxRetarget(response, method);
setHxReselect(response, method);
setHxTrigger(response, method);
setHxTriggerAfterSettle(response, method);
setHxTriggerAfterSwap(response, method);
setHxRefresh(response, method);
setVary(request, response);
}
Expand All @@ -36,30 +54,175 @@ private void setVary(HttpServletRequest request, HttpServletResponse response) {
}
}

private void setHxLocation(HttpServletResponse response, Method method) {
HxLocation methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxLocation.class);
if (methodAnnotation != null) {
var location = convertToLocation(methodAnnotation);
if (location.hasContextData()) {
setHeaderJsonValue(response, HtmxResponseHeader.HX_LOCATION, location);
} else {
setHeader(response, HtmxResponseHeader.HX_LOCATION, location.getPath());
}
}
}

private void setHxPushUrl(HttpServletResponse response, Method method) {
HxPushUrl methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxPushUrl.class);
if (methodAnnotation != null) {
setHeader(response, HX_PUSH_URL, methodAnnotation.value());
}
}

private void setHxRedirect(HttpServletResponse response, Method method) {
HxRedirect methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRedirect.class);
if (methodAnnotation != null) {
setHeader(response, HX_REDIRECT, methodAnnotation.value());
}
}

private void setHxReplaceUrl(HttpServletResponse response, Method method) {
HxReplaceUrl methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReplaceUrl.class);
if (methodAnnotation != null) {
setHeader(response, HX_REPLACE_URL, methodAnnotation.value());
}
}

private void setHxReswap(HttpServletResponse response, Method method) {
HxReswap methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReswap.class);
if (methodAnnotation != null) {
setHeader(response, 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());
}
}

private void setHxReselect(HttpServletResponse response, Method method) {
HxReselect methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReselect.class);
if (methodAnnotation != null) {
setHeader(response, HX_RESELECT, methodAnnotation.value());
}
}

private void setHxTrigger(HttpServletResponse response, Method method) {
HxTrigger methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxTrigger.class);
if (methodAnnotation != null) {
response.setHeader(getHeaderName(methodAnnotation.lifecycle()), methodAnnotation.value());
setHeader(response, convertToHeader(methodAnnotation.lifecycle()), methodAnnotation.value());
}
}

private void setHxTriggerAfterSettle(HttpServletResponse response, Method method) {
HxTriggerAfterSettle methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxTriggerAfterSettle.class);
if (methodAnnotation != null) {
setHeader(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SETTLE, methodAnnotation.value());
}
}

private void setHxTriggerAfterSwap(HttpServletResponse response, Method method) {
HxTriggerAfterSwap methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxTriggerAfterSwap.class);
if (methodAnnotation != null) {
setHeader(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SWAP, methodAnnotation.value());
}
}

private void setHxRefresh(HttpServletResponse response, Method method) {
HxRefresh methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRefresh.class);
if (methodAnnotation != null) {
response.setHeader(HX_REFRESH.getValue(), "true");
setHeader(response, HX_REFRESH, HtmxValue.TRUE);
}
}

private String getHeaderName(HxTriggerLifecycle lifecycle) {
private HtmxResponseHeader convertToHeader(HxTriggerLifecycle lifecycle) {
switch (lifecycle) {
case RECEIVE:
return HX_TRIGGER.getValue();
return HX_TRIGGER;
case SETTLE:
return HX_TRIGGER_AFTER_SETTLE.getValue();
return HX_TRIGGER_AFTER_SETTLE;
case SWAP:
return HX_TRIGGER_AFTER_SWAP.getValue();
return HX_TRIGGER_AFTER_SWAP;
default:
throw new IllegalArgumentException("Unknown lifecycle:" + lifecycle);
}
}

private void setHeaderJsonValue(HttpServletResponse response, HtmxResponseHeader header, Object value) {
try {
response.setHeader(header.getValue(), objectMapper.writeValueAsString(value));
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Unable to set header " + header.getValue() + " to " + value, e);
}
}

private void setHeader(HttpServletResponse response, HtmxResponseHeader header, String value) {
response.setHeader(header.getValue(), value);
}

private void setHeader(HttpServletResponse response, HtmxResponseHeader header, String[] values) {
response.setHeader(header.getValue(), String.join(",", values));
}

private HtmxLocation convertToLocation(HxLocation annotation) {
var location = new HtmxLocation();
location.setPath(annotation.path());
if (!annotation.source().isEmpty()) {
location.setSource(annotation.source());
}
if (!annotation.event().isEmpty()) {
location.setEvent(annotation.event());
}
if (!annotation.handler().isEmpty()) {
location.setHandler(annotation.handler());
}
if (!annotation.target().isEmpty()) {
location.setTarget(annotation.target());
}
if (!annotation.target().isEmpty()) {
location.setSwap(annotation.swap());
}
return location;
}

private String convertToReswap(HxReswap annotation) {

var reswap = new HtmxReswap(annotation.value());
if (annotation.swap() != -1) {
reswap.swap(Duration.ofMillis(annotation.swap()));
}
if (annotation.settle() != -1) {
reswap.swap(Duration.ofMillis(annotation.settle()));
}
if (annotation.transition()) {
reswap.transition();
}
if (annotation.focusScroll() != HxReswap.FocusScroll.UNDEFINED) {
reswap.focusScroll(annotation.focusScroll() == HxReswap.FocusScroll.TRUE);
}
if (annotation.show() != HxReswap.Position.UNDEFINED) {
reswap.show(convertToPosition(annotation.show()));
if (!annotation.showTarget().isEmpty()) {
reswap.scrollTarget(annotation.showTarget());
}
}
if (annotation.scroll() != HxReswap.Position.UNDEFINED) {
reswap.scroll(convertToPosition(annotation.scroll()));
if (!annotation.scrollTarget().isEmpty()) {
reswap.scrollTarget(annotation.scrollTarget());
}
}

return reswap.toString();
}

private HtmxReswap.Position convertToPosition(HxReswap.Position position) {
return switch (position) {
case TOP -> HtmxReswap.Position.TOP;
case BOTTOM -> HtmxReswap.Position.BOTTOM;
default -> throw new IllegalStateException("Unexpected value: " + position);
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HtmxHandlerInterceptor());
registry.addInterceptor(new HtmxHandlerInterceptor(objectMapper));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.github.wimdeblauwe.htmx.spring.boot.mvc;

/**
* Holder for constant values.
*/
public class HtmxValue {

/**
* Constant for use in annotations that support a {@code false} value to disable functions.
*/
public static final String FALSE = "false";

/**
* Constant for use in annotations that support a {@code true} value to enable functions.
*/
public static final String TRUE = "true";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package io.github.wimdeblauwe.htmx.spring.boot.mvc;

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.
* <p>
* Note that this annotation does not provide support for specifying {@code values} or {@code headers}.
* If you want to do this, use {@link HtmxResponse} instead.
*
* @see <a href="https://htmx.org/headers/hx-location/">HX-Location Response Header</a>
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HxLocation {

/**
* The url path to make the redirect.
* <p>This is an alias for {@link #path}. For example,
* {@code @HxLocation("/foo")} is equivalent to
* {@code @HxLocation(path="/foo")}.
*/
@AliasFor("path")
String value() default "";
/**
* The url path to make the redirect.
*/
String path() default "";
/**
* The source element of the request
*/
String source() default "";
/**
* An event that "triggered" the request
*/
String event() default "";
/**
* A callback that will handle the response HTML.
*/
String handler() default "";
/**
* The target to swap the response into.
*/
String target() default "";
/**
* How the response will be swapped in relative to the target
*/
String swap() default "";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.github.wimdeblauwe.htmx.spring.boot.mvc;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotation to push a URL into the browser
* <a href="https://developer.mozilla.org/en-US/docs/Web/API/History_API">location history</a>.
* <p>
* The possible values are:
* <ul>
* <li>{@link HtmxValue#TRUE}, which pushes the fetched URL into history.</li>
* <li>{@link HtmxValue#FALSE}, which disables pushing the fetched URL if it would otherwise be pushed.</li>
* <li>A URL to be pushed into the location bar. This may be relative or absolute,
* as per <a href="https://developer.mozilla.org/en-US/docs/Web/API/History/pushState">history.pushState()</a>.</li>
* </ul>
*
* @see <a href="https://htmx.org/headers/hx-push-url/">HX-Push-Url Response Header</a>
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HxPushUrl {

/**
* The value for the {@code HX-Push-Url} response header.
*/
String value() default HtmxValue.TRUE;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.github.wimdeblauwe.htmx.spring.boot.mvc;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotation to do a client-side redirect to a new location.
*
* @see <a href="https://htmx.org/reference/#response_headers">HX-Redirect</a>
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HxRedirect {

/**
* The URL to use to do a client-side redirect to a new location.
*/
String value();

}
Loading

0 comments on commit a8b65ab

Please sign in to comment.