Skip to content

Commit c8bea46

Browse files
wimdeblauwexhaggi
authored andcommitted
[fix] Annotations on exception handler methods do not work
Fixes gh-150
1 parent 1f757ca commit c8bea46

File tree

5 files changed

+389
-205
lines changed

5 files changed

+389
-205
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.github.wimdeblauwe.htmx.spring.boot.mvc;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import jakarta.servlet.http.HttpServletResponse;
5+
import org.springframework.web.method.HandlerMethod;
6+
import org.springframework.web.servlet.ModelAndView;
7+
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
8+
import org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod;
9+
10+
import java.lang.reflect.Method;
11+
12+
/**
13+
* A custom {@link ExceptionHandlerExceptionResolver} that handles htmx annotations
14+
* present on exception handler methods.
15+
*
16+
* @since 3.6.2
17+
*/
18+
public class HtmxExceptionHandlerExceptionResolver extends ExceptionHandlerExceptionResolver {
19+
20+
private final HtmxHandlerMethodAnnotationHandler handlerMethodAnnotationHandler;
21+
22+
public HtmxExceptionHandlerExceptionResolver(HtmxHandlerMethodAnnotationHandler handlerMethodAnnotationHandler) {
23+
this.handlerMethodAnnotationHandler = handlerMethodAnnotationHandler;
24+
}
25+
26+
@Override
27+
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod, Exception exception) {
28+
29+
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
30+
if (exceptionHandlerMethod != null) {
31+
Method method = exceptionHandlerMethod.getMethod();
32+
handlerMethodAnnotationHandler.handleMethod(method, request, response);
33+
}
34+
35+
return super.doResolveHandlerMethodException(request, response, handlerMethod, exception);
36+
}
37+
38+
}

htmx-spring-boot/src/main/java/io/github/wimdeblauwe/htmx/spring/boot/mvc/HtmxHandlerInterceptor.java

Lines changed: 4 additions & 202 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,26 @@
44
import com.fasterxml.jackson.databind.ObjectMapper;
55
import jakarta.servlet.http.HttpServletRequest;
66
import jakarta.servlet.http.HttpServletResponse;
7-
import org.springframework.core.annotation.AnnotatedElementUtils;
87
import org.springframework.http.HttpHeaders;
98
import org.springframework.web.method.HandlerMethod;
109
import org.springframework.web.servlet.HandlerInterceptor;
11-
import org.springframework.web.servlet.ModelAndView;
1210

1311
import java.lang.reflect.Method;
14-
import java.time.Duration;
1512
import java.util.Collection;
1613
import java.util.HashMap;
1714
import java.util.stream.Collectors;
1815

19-
import static io.github.wimdeblauwe.htmx.spring.boot.mvc.HtmxResponseHeader.*;
20-
2116
/**
2217
* HandlerInterceptor that adds htmx specific headers to the response.
2318
*/
2419
public class HtmxHandlerInterceptor implements HandlerInterceptor {
2520

2621
private final ObjectMapper objectMapper;
22+
private final HtmxHandlerMethodAnnotationHandler handlerMethodAnnotationHandler;
2723

28-
public HtmxHandlerInterceptor(ObjectMapper objectMapper) {
24+
public HtmxHandlerInterceptor(ObjectMapper objectMapper, HtmxHandlerMethodAnnotationHandler handlerMethodAnnotationHandler) {
2925
this.objectMapper = objectMapper;
26+
this.handlerMethodAnnotationHandler = handlerMethodAnnotationHandler;
3027
}
3128

3229
@Override
@@ -78,18 +75,8 @@ public boolean preHandle(HttpServletRequest request,
7875

7976
if (handler instanceof HandlerMethod) {
8077
Method method = ((HandlerMethod) handler).getMethod();
81-
setHxLocation(request, response, method);
82-
setHxPushUrl(request, response, method);
83-
setHxRedirect(request, response, method);
84-
setHxReplaceUrl(request, response, method);
85-
setHxReswap(response, method);
86-
setHxRetarget(response, method);
87-
setHxReselect(response, method);
88-
setHxTrigger(response, method);
89-
setHxTriggerAfterSettle(response, method);
90-
setHxTriggerAfterSwap(response, method);
91-
setHxRefresh(response, method);
9278
setVary(request, response);
79+
handlerMethodAnnotationHandler.handleMethod(method, request, response);
9380
}
9481

9582
return true;
@@ -101,110 +88,6 @@ private void setVary(HttpServletRequest request, HttpServletResponse response) {
10188
}
10289
}
10390

104-
private void setHxLocation(HttpServletRequest request, HttpServletResponse response, Method method) {
105-
HxLocation methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxLocation.class);
106-
if (methodAnnotation != null) {
107-
var location = convertToLocation(methodAnnotation);
108-
if (location.hasContextData()) {
109-
location.setPath(RequestContextUtils.createUrl(request, location.getPath(), methodAnnotation.contextRelative()));
110-
setHeaderJsonValue(response, HtmxResponseHeader.HX_LOCATION, location);
111-
} else {
112-
setHeader(response, HtmxResponseHeader.HX_LOCATION, RequestContextUtils.createUrl(request, location.getPath(), methodAnnotation.contextRelative()));
113-
}
114-
}
115-
}
116-
117-
private void setHxPushUrl(HttpServletRequest request, HttpServletResponse response, Method method) {
118-
HxPushUrl methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxPushUrl.class);
119-
if (methodAnnotation != null) {
120-
if (HtmxValue.TRUE.equals(methodAnnotation.value())) {
121-
setHeader(response, HX_PUSH_URL, getRequestUrl(request));
122-
} else {
123-
setHeader(response, HX_PUSH_URL, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative()));
124-
}
125-
}
126-
}
127-
128-
private void setHxRedirect(HttpServletRequest request, HttpServletResponse response, Method method) {
129-
HxRedirect methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRedirect.class);
130-
if (methodAnnotation != null) {
131-
setHeader(response, HX_REDIRECT, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative()));
132-
}
133-
}
134-
135-
private void setHxReplaceUrl(HttpServletRequest request, HttpServletResponse response, Method method) {
136-
HxReplaceUrl methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReplaceUrl.class);
137-
if (methodAnnotation != null) {
138-
if (HtmxValue.TRUE.equals(methodAnnotation.value())) {
139-
setHeader(response, HX_REPLACE_URL, getRequestUrl(request));
140-
} else {
141-
setHeader(response, HX_REPLACE_URL, RequestContextUtils.createUrl(request, methodAnnotation.value(), methodAnnotation.contextRelative()));
142-
}
143-
}
144-
}
145-
146-
private void setHxReswap(HttpServletResponse response, Method method) {
147-
HxReswap methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReswap.class);
148-
if (methodAnnotation != null) {
149-
setHeader(response, HX_RESWAP, convertToReswap(methodAnnotation));
150-
}
151-
}
152-
153-
private void setHxRetarget(HttpServletResponse response, Method method) {
154-
HxRetarget methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRetarget.class);
155-
if (methodAnnotation != null) {
156-
setHeader(response, HX_RETARGET, methodAnnotation.value());
157-
}
158-
}
159-
160-
private void setHxReselect(HttpServletResponse response, Method method) {
161-
HxReselect methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxReselect.class);
162-
if (methodAnnotation != null) {
163-
setHeader(response, HX_RESELECT, methodAnnotation.value());
164-
}
165-
}
166-
167-
private void setHxTrigger(HttpServletResponse response, Method method) {
168-
HxTrigger methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxTrigger.class);
169-
if (methodAnnotation != null) {
170-
setHeader(response, convertToHeader(methodAnnotation.lifecycle()), methodAnnotation.value());
171-
}
172-
}
173-
174-
private void setHxTriggerAfterSettle(HttpServletResponse response, Method method) {
175-
HxTriggerAfterSettle methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxTriggerAfterSettle.class);
176-
if (methodAnnotation != null) {
177-
setHeader(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SETTLE, methodAnnotation.value());
178-
}
179-
}
180-
181-
private void setHxTriggerAfterSwap(HttpServletResponse response, Method method) {
182-
HxTriggerAfterSwap methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxTriggerAfterSwap.class);
183-
if (methodAnnotation != null) {
184-
setHeader(response, HtmxResponseHeader.HX_TRIGGER_AFTER_SWAP, methodAnnotation.value());
185-
}
186-
}
187-
188-
private void setHxRefresh(HttpServletResponse response, Method method) {
189-
HxRefresh methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, HxRefresh.class);
190-
if (methodAnnotation != null) {
191-
setHeader(response, HX_REFRESH, HtmxValue.TRUE);
192-
}
193-
}
194-
195-
private HtmxResponseHeader convertToHeader(HxTriggerLifecycle lifecycle) {
196-
switch (lifecycle) {
197-
case RECEIVE:
198-
return HX_TRIGGER;
199-
case SETTLE:
200-
return HX_TRIGGER_AFTER_SETTLE;
201-
case SWAP:
202-
return HX_TRIGGER_AFTER_SWAP;
203-
default:
204-
throw new IllegalArgumentException("Unknown lifecycle:" + lifecycle);
205-
}
206-
}
207-
20891
private void setHeaderJsonValue(HttpServletResponse response, HtmxResponseHeader header, Object value) {
20992
try {
21093
response.setHeader(header.getValue(), objectMapper.writeValueAsString(value));
@@ -213,87 +96,6 @@ private void setHeaderJsonValue(HttpServletResponse response, HtmxResponseHeader
21396
}
21497
}
21598

216-
private void setHeader(HttpServletResponse response, HtmxResponseHeader header, String value) {
217-
response.setHeader(header.getValue(), value);
218-
}
219-
220-
private void setHeader(HttpServletResponse response, HtmxResponseHeader header, String[] values) {
221-
response.setHeader(header.getValue(), String.join(",", values));
222-
}
223-
224-
private HtmxLocation convertToLocation(HxLocation annotation) {
225-
var location = new HtmxLocation();
226-
location.setPath(annotation.path());
227-
if (!annotation.source().isEmpty()) {
228-
location.setSource(annotation.source());
229-
}
230-
if (!annotation.event().isEmpty()) {
231-
location.setEvent(annotation.event());
232-
}
233-
if (!annotation.handler().isEmpty()) {
234-
location.setHandler(annotation.handler());
235-
}
236-
if (!annotation.target().isEmpty()) {
237-
location.setTarget(annotation.target());
238-
}
239-
if (!annotation.target().isEmpty()) {
240-
location.setSwap(annotation.swap());
241-
}
242-
if (!annotation.select().isEmpty()) {
243-
location.setSelect(annotation.select());
244-
}
245-
return location;
246-
}
247-
248-
private String convertToReswap(HxReswap annotation) {
249-
250-
var reswap = new HtmxReswap(annotation.value());
251-
if (annotation.swap() != -1) {
252-
reswap.swap(Duration.ofMillis(annotation.swap()));
253-
}
254-
if (annotation.settle() != -1) {
255-
reswap.swap(Duration.ofMillis(annotation.settle()));
256-
}
257-
if (annotation.transition()) {
258-
reswap.transition();
259-
}
260-
if (annotation.focusScroll() != HxReswap.FocusScroll.UNDEFINED) {
261-
reswap.focusScroll(annotation.focusScroll() == HxReswap.FocusScroll.TRUE);
262-
}
263-
if (annotation.show() != HxReswap.Position.UNDEFINED) {
264-
reswap.show(convertToPosition(annotation.show()));
265-
if (!annotation.showTarget().isEmpty()) {
266-
reswap.scrollTarget(annotation.showTarget());
267-
}
268-
}
269-
if (annotation.scroll() != HxReswap.Position.UNDEFINED) {
270-
reswap.scroll(convertToPosition(annotation.scroll()));
271-
if (!annotation.scrollTarget().isEmpty()) {
272-
reswap.scrollTarget(annotation.scrollTarget());
273-
}
274-
}
275-
276-
return reswap.toString();
277-
}
278-
279-
private HtmxReswap.Position convertToPosition(HxReswap.Position position) {
280-
return switch (position) {
281-
case TOP -> HtmxReswap.Position.TOP;
282-
case BOTTOM -> HtmxReswap.Position.BOTTOM;
283-
default -> throw new IllegalStateException("Unexpected value: " + position);
284-
};
285-
}
286-
287-
private String getRequestUrl(HttpServletRequest request) {
288-
String path = request.getRequestURI();
289-
String queryString = request.getQueryString();
290-
291-
if (queryString != null && !queryString.isEmpty()) {
292-
path += "?" + queryString;
293-
}
294-
return path;
295-
}
296-
29799
private void addHxTriggerHeaders(HttpServletResponse response, HtmxResponseHeader headerName, Collection<HtmxTrigger> triggers) {
298100
if (triggers.isEmpty()) {
299101
return;

0 commit comments

Comments
 (0)