Skip to content

Commit

Permalink
[fix] broken HtmxResponse handling since 9cc7331
Browse files Browse the repository at this point in the history
  • Loading branch information
xhaggi committed Dec 5, 2024
1 parent 6a98b8b commit fc37705
Show file tree
Hide file tree
Showing 8 changed files with 484 additions and 116 deletions.
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

0 comments on commit fc37705

Please sign in to comment.