Skip to content

Commit a421085

Browse files
committed
MessageSource support for Spring MVC and WebFlux exceptions
See gh-28814
1 parent ff81d64 commit a421085

File tree

31 files changed

+674
-127
lines changed

31 files changed

+674
-127
lines changed

spring-web/src/main/java/org/springframework/web/ErrorResponse.java

+48
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616

1717
package org.springframework.web;
1818

19+
import java.util.Locale;
20+
21+
import org.springframework.context.MessageSource;
1922
import org.springframework.http.HttpHeaders;
2023
import org.springframework.http.HttpStatusCode;
2124
import org.springframework.http.ProblemDetail;
25+
import org.springframework.lang.Nullable;
2226

2327

2428
/**
@@ -59,4 +63,48 @@ default HttpHeaders getHeaders() {
5963
*/
6064
ProblemDetail getBody();
6165

66+
/**
67+
* Return a code to use to resolve the problem "detail" for this exception
68+
* through a {@link org.springframework.context.MessageSource}.
69+
* <p>By default this is initialized via
70+
* {@link #getDefaultDetailMessageCode(Class, String)} but each exception
71+
* overrides this to provide relevant data that that can be expanded into
72+
* placeholders within the message.
73+
*/
74+
default String getDetailMessageCode() {
75+
return getDefaultDetailMessageCode(getClass(), null);
76+
}
77+
78+
/**
79+
* Return the arguments to use to resolve the problem "detail" through a
80+
* {@link MessageSource}.
81+
*/
82+
@Nullable
83+
default Object[] getDetailMessageArguments() {
84+
return null;
85+
}
86+
87+
/**
88+
* Variant of {@link #getDetailMessageArguments()} that uses the given
89+
* {@link MessageSource} to resolve the message arguments.
90+
* <p>By default this delegates to {@link #getDetailMessageArguments()}
91+
* by concrete implementations may override it, for example in order to
92+
* resolve validation errors through a {@code MessageSource}.
93+
*/
94+
@Nullable
95+
default Object[] getDetailMessageArguments(MessageSource messageSource, Locale locale) {
96+
return getDetailMessageArguments();
97+
}
98+
99+
/**
100+
* Build a message code for the given exception type, which consists of
101+
* {@code "problemDetail."} followed by the full {@link Class#getName() class name}.
102+
* @param exceptionType the exception type for which to build a code
103+
* @param suffix an optional suffix, e.g. for exceptions that may have multiple
104+
* error message with different arguments.
105+
*/
106+
static String getDefaultDetailMessageCode(Class<?> exceptionType, @Nullable String suffix) {
107+
return "problemDetail." + exceptionType.getName() + (suffix != null ? "." + suffix : "");
108+
}
109+
62110
}

spring-web/src/main/java/org/springframework/web/ErrorResponseException.java

+36
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ public class ErrorResponseException extends NestedRuntimeException implements Er
4646

4747
private final ProblemDetail body;
4848

49+
private final String messageDetailCode;
50+
51+
@Nullable
52+
private final Object[] messageDetailArguments;
53+
4954

5055
/**
5156
* Constructor with a {@link HttpStatusCode}.
@@ -66,11 +71,32 @@ public ErrorResponseException(HttpStatusCode status, @Nullable Throwable cause)
6671
* subclass of {@code ProblemDetail} with extended fields.
6772
*/
6873
public ErrorResponseException(HttpStatusCode status, ProblemDetail body, @Nullable Throwable cause) {
74+
this(status, body, cause, null, null);
75+
}
76+
77+
/**
78+
* Constructor with a given {@link ProblemDetail}, and a
79+
* {@link org.springframework.context.MessageSource} code and arguments to
80+
* resolve the detail message with.
81+
* @since 6.0
82+
*/
83+
protected ErrorResponseException(
84+
HttpStatusCode status, ProblemDetail body, @Nullable Throwable cause,
85+
@Nullable String messageDetailCode, @Nullable Object[] messageDetailArguments) {
86+
6987
super(null, cause);
7088
this.status = status;
7189
this.body = body;
90+
this.messageDetailCode = initMessageDetailCode(messageDetailCode);
91+
this.messageDetailArguments = messageDetailArguments;
92+
}
93+
94+
private String initMessageDetailCode(@Nullable String messageDetailCode) {
95+
return (messageDetailCode != null ?
96+
messageDetailCode : ErrorResponse.getDefaultDetailMessageCode(getClass(), null));
7297
}
7398

99+
74100
@Override
75101
public HttpStatusCode getStatusCode() {
76102
return this.status;
@@ -133,6 +159,16 @@ public final ProblemDetail getBody() {
133159
return this.body;
134160
}
135161

162+
@Override
163+
public String getDetailMessageCode() {
164+
return this.messageDetailCode;
165+
}
166+
167+
@Override
168+
public Object[] getDetailMessageArguments() {
169+
return this.messageDetailArguments;
170+
}
171+
136172
@Override
137173
public String getMessage() {
138174
return this.status + (!this.headers.isEmpty() ? ", headers=" + this.headers : "") + ", " + this.body;

spring-web/src/main/java/org/springframework/web/HttpMediaTypeException.java

+39-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import org.springframework.http.MediaType;
2525
import org.springframework.http.ProblemDetail;
26+
import org.springframework.lang.Nullable;
2627

2728
/**
2829
* Abstract base for exceptions related to media types. Adds a list of supported {@link MediaType MediaTypes}.
@@ -37,23 +38,49 @@ public abstract class HttpMediaTypeException extends ServletException implements
3738

3839
private final ProblemDetail body = ProblemDetail.forStatus(getStatusCode());
3940

41+
private final String messageDetailCode;
42+
43+
@Nullable
44+
private final Object[] messageDetailArguments;
45+
4046

4147
/**
4248
* Create a new HttpMediaTypeException.
4349
* @param message the exception message
50+
* @deprecated as of 6.0
4451
*/
52+
@Deprecated
4553
protected HttpMediaTypeException(String message) {
46-
super(message);
47-
this.supportedMediaTypes = Collections.emptyList();
54+
this(message, Collections.emptyList());
4855
}
4956

5057
/**
5158
* Create a new HttpMediaTypeException with a list of supported media types.
5259
* @param supportedMediaTypes the list of supported media types
60+
* @deprecated as of 6.0
5361
*/
62+
@Deprecated
5463
protected HttpMediaTypeException(String message, List<MediaType> supportedMediaTypes) {
64+
this(message, supportedMediaTypes, null, null);
65+
}
66+
67+
/**
68+
* Create a new HttpMediaTypeException with a list of supported media types.
69+
* @param supportedMediaTypes the list of supported media types
70+
* @param messageDetailCode the code to use to resolve the problem "detail"
71+
* through a {@link org.springframework.context.MessageSource}
72+
* @param messageDetailArguments the arguments to make available when
73+
* resolving the problem "detail" through a {@code MessageSource}
74+
* @since 6.0
75+
*/
76+
protected HttpMediaTypeException(String message, List<MediaType> supportedMediaTypes,
77+
@Nullable String messageDetailCode, @Nullable Object[] messageDetailArguments) {
78+
5579
super(message);
5680
this.supportedMediaTypes = Collections.unmodifiableList(supportedMediaTypes);
81+
this.messageDetailCode = (messageDetailCode != null ?
82+
messageDetailCode : ErrorResponse.getDefaultDetailMessageCode(getClass(), null));
83+
this.messageDetailArguments = messageDetailArguments;
5784
}
5885

5986

@@ -69,4 +96,14 @@ public ProblemDetail getBody() {
6996
return this.body;
7097
}
7198

99+
@Override
100+
public String getDetailMessageCode() {
101+
return this.messageDetailCode;
102+
}
103+
104+
@Override
105+
public Object[] getDetailMessageArguments() {
106+
return this.messageDetailArguments;
107+
}
108+
72109
}

spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotAcceptableException.java

+8-5
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
package org.springframework.web;
1818

19+
import java.util.Collections;
1920
import java.util.List;
20-
import java.util.stream.Collectors;
2121

2222
import org.springframework.http.HttpHeaders;
2323
import org.springframework.http.HttpStatus;
@@ -35,12 +35,16 @@
3535
@SuppressWarnings("serial")
3636
public class HttpMediaTypeNotAcceptableException extends HttpMediaTypeException {
3737

38+
private static final String PARSE_ERROR_DETAIL_CODE =
39+
ErrorResponse.getDefaultDetailMessageCode(HttpMediaTypeNotAcceptableException.class, "parseError");
40+
41+
3842
/**
3943
* Constructor for when the {@code Accept} header cannot be parsed.
4044
* @param message the parse error message
4145
*/
4246
public HttpMediaTypeNotAcceptableException(String message) {
43-
super(message);
47+
super(message, Collections.emptyList(), PARSE_ERROR_DETAIL_CODE, null);
4448
getBody().setDetail("Could not parse Accept header.");
4549
}
4650

@@ -49,9 +53,8 @@ public HttpMediaTypeNotAcceptableException(String message) {
4953
* @param mediaTypes the list of supported media types
5054
*/
5155
public HttpMediaTypeNotAcceptableException(List<MediaType> mediaTypes) {
52-
super("No acceptable representation", mediaTypes);
53-
getBody().setDetail("Acceptable representations: " +
54-
mediaTypes.stream().map(MediaType::toString).collect(Collectors.joining(", ", "'", "'")) + ".");
56+
super("No acceptable representation", mediaTypes, null, new Object[] {mediaTypes});
57+
getBody().setDetail("Acceptable representations: " + mediaTypes + ".");
5558
}
5659

5760

spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.web;
1818

19+
import java.util.Collections;
1920
import java.util.List;
2021

2122
import org.springframework.http.HttpHeaders;
@@ -37,6 +38,10 @@
3738
@SuppressWarnings("serial")
3839
public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException {
3940

41+
private static final String PARSE_ERROR_DETAIL_CODE =
42+
ErrorResponse.getDefaultDetailMessageCode(HttpMediaTypeNotSupportedException.class, "parseError");
43+
44+
4045
@Nullable
4146
private final MediaType contentType;
4247

@@ -49,7 +54,7 @@ public class HttpMediaTypeNotSupportedException extends HttpMediaTypeException {
4954
* @param message the exception message
5055
*/
5156
public HttpMediaTypeNotSupportedException(String message) {
52-
super(message);
57+
super(message, Collections.emptyList(), PARSE_ERROR_DETAIL_CODE, null);
5358
this.contentType = null;
5459
this.httpMethod = null;
5560
getBody().setDetail("Could not parse Content-Type.");
@@ -89,7 +94,7 @@ public HttpMediaTypeNotSupportedException(
8994
public HttpMediaTypeNotSupportedException(@Nullable MediaType contentType,
9095
List<MediaType> supportedMediaTypes, @Nullable HttpMethod httpMethod, String message) {
9196

92-
super(message, supportedMediaTypes);
97+
super(message, supportedMediaTypes, null, new Object[] {contentType, supportedMediaTypes});
9398
this.contentType = contentType;
9499
this.httpMethod = httpMethod;
95100
getBody().setDetail("Content-Type '" + this.contentType + "' is not supported.");

spring-web/src/main/java/org/springframework/web/HttpRequestMethodNotSupportedException.java

+15-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ public class HttpRequestMethodNotSupportedException extends ServletException imp
5252
/**
5353
* Create a new HttpRequestMethodNotSupportedException.
5454
* @param method the unsupported HTTP request method
55+
* @deprecated 6.0 in favor of {@link #HttpRequestMethodNotSupportedException(String, Collection)}
5556
*/
57+
@Deprecated(since = "6.0", forRemoval = true)
5658
public HttpRequestMethodNotSupportedException(String method) {
5759
this(method, (String[]) null);
5860
}
@@ -61,15 +63,17 @@ public HttpRequestMethodNotSupportedException(String method) {
6163
* Create a new HttpRequestMethodNotSupportedException.
6264
* @param method the unsupported HTTP request method
6365
* @param msg the detail message
66+
* @deprecated in favor of {@link #HttpRequestMethodNotSupportedException(String, Collection)}
6467
*/
68+
@Deprecated(since = "6.0", forRemoval = true)
6569
public HttpRequestMethodNotSupportedException(String method, String msg) {
6670
this(method, null, msg);
6771
}
6872

6973
/**
7074
* Create a new HttpRequestMethodNotSupportedException.
7175
* @param method the unsupported HTTP request method
72-
* @param supportedMethods the actually supported HTTP methods (may be {@code null})
76+
* @param supportedMethods the actually supported HTTP methods (possibly {@code null})
7377
*/
7478
public HttpRequestMethodNotSupportedException(String method, @Nullable Collection<String> supportedMethods) {
7579
this(method, (supportedMethods != null ? StringUtils.toStringArray(supportedMethods) : null));
@@ -78,8 +82,10 @@ public HttpRequestMethodNotSupportedException(String method, @Nullable Collectio
7882
/**
7983
* Create a new HttpRequestMethodNotSupportedException.
8084
* @param method the unsupported HTTP request method
81-
* @param supportedMethods the actually supported HTTP methods (may be {@code null})
85+
* @param supportedMethods the actually supported HTTP methods (possibly {@code null})
86+
* @deprecated in favor of {@link #HttpRequestMethodNotSupportedException(String, Collection)}
8287
*/
88+
@Deprecated(since = "6.0", forRemoval = true)
8389
public HttpRequestMethodNotSupportedException(String method, @Nullable String[] supportedMethods) {
8490
this(method, supportedMethods, "Request method '" + method + "' is not supported");
8591
}
@@ -89,7 +95,9 @@ public HttpRequestMethodNotSupportedException(String method, @Nullable String[]
8995
* @param method the unsupported HTTP request method
9096
* @param supportedMethods the actually supported HTTP methods
9197
* @param msg the detail message
98+
* @deprecated in favor of {@link #HttpRequestMethodNotSupportedException(String, Collection)}
9299
*/
100+
@Deprecated(since = "6.0", forRemoval = true)
93101
public HttpRequestMethodNotSupportedException(String method, @Nullable String[] supportedMethods, String msg) {
94102
super(msg);
95103
this.method = method;
@@ -153,4 +161,9 @@ public ProblemDetail getBody() {
153161
return this.body;
154162
}
155163

164+
@Override
165+
public Object[] getDetailMessageArguments() {
166+
return new Object[] {getMethod(), getSupportedHttpMethods()};
167+
}
168+
156169
}

0 commit comments

Comments
 (0)