Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add missing annotations for response headers #118

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 "";
xhaggi marked this conversation as resolved.
Show resolved Hide resolved

}
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