Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions htmx-spring-boot/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
*/
public class HtmxExceptionHandlerExceptionResolver extends ExceptionHandlerExceptionResolver {

private final HtmxHandlerMethodAnnotationHandler handlerMethodAnnotationHandler;
private final HtmxHandlerMethodHandler htmxHandlerMethodHandler;

public HtmxExceptionHandlerExceptionResolver(HtmxHandlerMethodAnnotationHandler handlerMethodAnnotationHandler) {
this.handlerMethodAnnotationHandler = handlerMethodAnnotationHandler;
public HtmxExceptionHandlerExceptionResolver(HtmxHandlerMethodHandler htmxHandlerMethodHandler) {
this.htmxHandlerMethodHandler = htmxHandlerMethodHandler;
}

@Override
Expand All @@ -31,10 +31,14 @@ protected ModelAndView doResolveHandlerMethodException(HttpServletRequest reques
ServletInvocableHandlerMethod exceptionHandlerMethod = this.getExceptionHandlerMethod(handlerMethod, exception, webRequest);
if (exceptionHandlerMethod != null) {
Method method = exceptionHandlerMethod.getMethod();
handlerMethodAnnotationHandler.handleMethod(method, request, response);
htmxHandlerMethodHandler.handleMethodAnnotations(method, request, response);
}

return super.doResolveHandlerMethodException(request, response, handlerMethod, exception);
ModelAndView modelAndView = super.doResolveHandlerMethodException(request, response, handlerMethod, exception);

htmxHandlerMethodHandler.handleMethodArgument(request, response);

return modelAndView;
}

}
Original file line number Diff line number Diff line change
@@ -1,67 +1,39 @@
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.http.HttpHeaders;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.stream.Collectors;

/**
* HandlerInterceptor that adds htmx specific headers to the response.
*/
public class HtmxHandlerInterceptor implements HandlerInterceptor {

private final ObjectMapper objectMapper;
private final HtmxHandlerMethodAnnotationHandler handlerMethodAnnotationHandler;
private final HtmxHandlerMethodHandler htmxHandlerMethodHandler;

public HtmxHandlerInterceptor(ObjectMapper objectMapper, HtmxHandlerMethodAnnotationHandler handlerMethodAnnotationHandler) {
this.objectMapper = objectMapper;
this.handlerMethodAnnotationHandler = handlerMethodAnnotationHandler;
public HtmxHandlerInterceptor(HtmxHandlerMethodHandler htmxHandlerMethodHandler) {
this.htmxHandlerMethodHandler = htmxHandlerMethodHandler;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

HtmxResponse htmxResponse = RequestContextUtils.getHtmxResponse(request);
if (htmxResponse != null) {
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.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());
}
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
htmxHandlerMethodHandler.handleMethodArgument(request, response);
}

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

if (handler instanceof HandlerMethod) {
Method method = ((HandlerMethod) handler).getMethod();
setVary(request, response);
handlerMethodAnnotationHandler.handleMethod(method, request, response);
setVary(request, response);

if (handler instanceof HandlerMethod handlerMethod) {
htmxHandlerMethodHandler.handleMethodAnnotations(handlerMethod.getMethod(), request, response);
}

return true;
Expand All @@ -73,35 +45,4 @@ private void setVary(HttpServletRequest request, HttpServletResponse response) {
}
}

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 addHxTriggerHeaders(HttpServletResponse response, HtmxResponseHeader headerName, Collection<HtmxTrigger> 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<String, Object>();
for (HtmxTrigger trigger : triggers) {
triggerMap.put(trigger.getEventName(), trigger.getEventDetail());
}
setHeaderJsonValue(response, headerName, triggerMap);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,55 @@
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 java.util.Collection;
import java.util.HashMap;
import java.util.stream.Collectors;

/**
* A handler for processing htmx annotations present on exception handler methods.
* A handler for processing {@link HtmxResponse} and annotations present on handler methods.
*
* @since 3.6.2
*/
class HtmxHandlerMethodAnnotationHandler {
class HtmxHandlerMethodHandler {

private final ObjectMapper objectMapper;

public HtmxHandlerMethodAnnotationHandler(ObjectMapper objectMapper) {
public HtmxHandlerMethodHandler(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

public void handleMethod(Method method, HttpServletRequest request, HttpServletResponse response) {
public void handleMethodArgument(HttpServletRequest request, HttpServletResponse response) {

HtmxResponse htmxResponse = RequestContextUtils.getHtmxResponse(request);
if (htmxResponse != null) {
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.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());
}
}
}

public void handleMethodAnnotations(Method method, HttpServletRequest request, HttpServletResponse response) {

setHxLocation(request, response, method);
setHxPushUrl(request, response, method);
setHxRedirect(request, response, method);
Expand All @@ -36,6 +67,29 @@ public void handleMethod(Method method, HttpServletRequest request, HttpServletR
setHxRefresh(response, method);
}

private void addHxTriggerHeaders(HttpServletResponse response, HtmxResponseHeader headerName, Collection<HtmxTrigger> 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<String, Object>();
for (HtmxTrigger trigger : triggers) {
triggerMap.put(trigger.getEventName(), trigger.getEventDetail());
}
setHeaderJsonValue(response, headerName, triggerMap);
}

private void setHxLocation(HttpServletRequest request, HttpServletResponse response, Method method) {
HxLocation methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxLocation.class);
if (methodAnnotation != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@
public class HtmxMvcAutoConfiguration implements WebMvcRegistrations, WebMvcConfigurer {

private final ObjectMapper objectMapper;
private final HtmxHandlerMethodAnnotationHandler handlerMethodAnnotationHandler;
private final HtmxHandlerMethodHandler handlerMethodHandler;

HtmxMvcAutoConfiguration() {
this.objectMapper = JsonMapper.builder().build();
this.handlerMethodAnnotationHandler = new HtmxHandlerMethodAnnotationHandler(this.objectMapper);
this.handlerMethodHandler = new HtmxHandlerMethodHandler(this.objectMapper);
}

@Override
Expand All @@ -38,7 +38,7 @@ public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {

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

@Override
Expand All @@ -54,7 +54,7 @@ public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handler

@Override
public ExceptionHandlerExceptionResolver getExceptionHandlerExceptionResolver() {
return new HtmxExceptionHandlerExceptionResolver(handlerMethodAnnotationHandler);
return new HtmxExceptionHandlerExceptionResolver(handlerMethodHandler);
}

@Bean
Expand Down
Loading
Loading