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 fb0c220 commit 20e7a11
Show file tree
Hide file tree
Showing 8 changed files with 498 additions and 131 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 @@ -17,10 +17,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 @@ -29,10 +29,14 @@ protected ModelAndView doResolveHandlerMethodException(HttpServletRequest reques
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
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,82 +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.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()));
}
}
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());
}
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 @@ -88,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 @@ -9,23 +9,68 @@

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

import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponseHeader.*;

/**
* 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.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()));
}
}
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());
}
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 @@ -39,6 +84,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 @@ -31,7 +31,7 @@ public class HtmxMvcAutoConfiguration implements WebMvcRegistrations, WebMvcConf
private final ObjectFactory<ViewResolver> viewResolverObjectFactory;
private final ObjectFactory<LocaleResolver> localeResolverObjectFactory;
private final ObjectMapper objectMapper;
private final HtmxHandlerMethodAnnotationHandler handlerMethodAnnotationHandler;
private final HtmxHandlerMethodHandler handlerMethodHandler;

HtmxMvcAutoConfiguration(@Qualifier("viewResolver") ObjectFactory<ViewResolver> viewResolverObjectFactory,
ObjectFactory<LocaleResolver> localeResolverObjectFactory) {
Expand All @@ -42,7 +42,7 @@ public class HtmxMvcAutoConfiguration implements WebMvcRegistrations, WebMvcConf
this.viewResolverObjectFactory = viewResolverObjectFactory;
this.localeResolverObjectFactory = localeResolverObjectFactory;
this.objectMapper = JsonMapper.builder().build();
this.handlerMethodAnnotationHandler = new HtmxHandlerMethodAnnotationHandler(this.objectMapper);
this.handlerMethodHandler = new HtmxHandlerMethodHandler(this.objectMapper);
}

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

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

@Override
Expand All @@ -69,7 +69,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 20e7a11

Please sign in to comment.