Skip to content

Commit 102fc58

Browse files
docs & refactoring
1 parent d7864b4 commit 102fc58

File tree

13 files changed

+118
-115
lines changed

13 files changed

+118
-115
lines changed

docs/asciidoc/modules/avaje-validator.adoc

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -167,13 +167,6 @@ import io.jooby.validation.BeanValidator
167167
}
168168
----
169169

170-
[IMPORTANT]
171-
====
172-
Please note, if you are mixing both approaches (MVC/scripts), it's better to avoid using a filter,
173-
as it may lead to double validation on MVC routes. In this case,
174-
it's recommended to use the handler version (see below).
175-
====
176-
177170
`BeanValidator.validate()` behaves identically to validation in MVC routes.
178171
It also supports validating list, array, and map of beans.
179172

@@ -223,6 +216,12 @@ catches `ConstraintViolationException` and transforms it into the following resp
223216
}
224217
----
225218

219+
[NOTE]
220+
====
221+
`AvajeValidatorModule` is compliant with `ProblemDetails`. Therefore, if you enable
222+
the Problem Details feature, the response above will be transformed into an `RFC 7807` compliant format
223+
====
224+
226225
It is possible to override the `title` and `status` code of the response above:
227226

228227
[source, java]
@@ -330,4 +329,4 @@ import io.jooby.avaje.validator.AvajeValidatorModule;
330329
cfg.failFast(true);
331330
}));
332331
}
333-
----
332+
----

docs/asciidoc/modules/hibernate-validator.adoc

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,6 @@ import io.jooby.validation.BeanValidator
130130
}
131131
----
132132

133-
[IMPORTANT]
134-
====
135-
Please note, if you are mixing both approaches (MVC/scripts), it's better to avoid using a filter,
136-
as it may lead to double validation on MVC routes. In this case,
137-
it's recommended to use the handler version (see below).
138-
====
139-
140133
`BeanValidator.validate()` behaves identically to validation in MVC routes.
141134
It also supports validating list, array, and map of beans
142135

@@ -186,6 +179,12 @@ catches `ConstraintViolationException` and transforms it into the following resp
186179
}
187180
----
188181

182+
[NOTE]
183+
====
184+
`HibernateValidatorModule` is compliant with `ProblemDetails`. Therefore, if you enable the Problem Details feature,
185+
the response above will be transformed into an `RFC 7807` compliant format
186+
====
187+
189188
It is possible to override the `title` and `status` code of the response above:
190189

191190
[source, java]
@@ -386,4 +385,4 @@ import io.jooby.hibernate.validator.HibernateValidatorModule;
386385
cfg.failFast(true);
387386
}));
388387
}
389-
----
388+
----

docs/asciidoc/problem-details.adoc

Lines changed: 35 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -10,52 +10,44 @@ Thankfully, there’s a standard called https://www.rfc-editor.org/rfc/rfc7807[I
1010
By adopting `RFC 7807`, API designers don’t have to spend time creating a custom solution, and users benefit by recognizing a familiar format across different APIs.
1111
If it suits the API’s needs, using this standard benefits both designers and users alike.
1212

13-
`Jooby` provides built-in support for `Problem Details`. There are two main entities to work with:
13+
`Jooby` provides built-in support for `Problem Details`.
1414

15-
1. `HttpProblem` - representation of the `RFC 7807` model and the way to instantiate the problem.
15+
=== Set up ProblemDetails
1616

17-
2. `ProblemDetailsHandler` - global error handler that catches all exceptions, transforms them into `Problem Details` compliant format and renders the response based on the `Accept` header value.
18-
It also sets the appropriate content-type in response (e.g. application/problem+json, application/problem+xml)
17+
To enable the `ProblemDetails`, simply add the following line to your configuration:
1918

20-
=== Set up handler
19+
.application.conf
20+
[source, properties]
21+
----
22+
problem.details.enabled = true
23+
----
2124

22-
The bare minimal requirement is to set up an error handler:
25+
This is the bare minimal configuration you need.
26+
It enables a global error handler that catches all exceptions, transforms them into Problem Details compliant format and renders the response based on the Accept header value. It also sets the appropriate content-type in response (e.g. application/problem+json, application/problem+xml)
2327

24-
.Java
25-
[source,java,role="primary"]
26-
----
27-
import io.jooby.problem.ProblemDetailsHandler;
28+
All supported settings include:
2829

29-
{
30-
...
31-
error(new ProblemDetailsHandler() // <1>
32-
.log4xxErrors() // <2>
33-
.mute(StatusCode.UNAUTHORIZED) // <3>
34-
);
30+
.application.conf
31+
[source, properties]
32+
----
33+
problem.details {
34+
enabled = true
35+
log4xxErrors = true // <1>
36+
muteCodes = [401, 403] // <2>
37+
muteTypes = ["com.example.MyMutedException"] // <3>
3538
}
3639
----
3740

38-
.Kotlin
39-
[source,kt,role="secondary"]
40-
----
41-
import io.jooby.problem.ProblemDetailsHandler
4241

43-
{
44-
...
45-
error(new ProblemDetailsHandler() // <1>
46-
.log4xxErrors() // <2>
47-
.mute(StatusCode.UNAUTHORIZED) // <3>
48-
)
49-
}
50-
----
42+
<1> By default, only server errors (5xx) will be logged. You can optionally enable the logging of client errors (4xx). If `DEBUG` logging level is enabled, the log will contain a stacktrace as well.
43+
<2> You can optionally mute some status codes completely.
44+
<3> You can optionally mute some exceptions logging completely.
5145

52-
<1> Enable `Problem Details` handler.
53-
Keep in mind, order matters (WYSIWYG), so if you have some specific exception handlers, they should be placed above this line.
54-
<2> By default, only server errors (5xx) will be logged. You can optionally enable the logging of client errors (4xx). If `DEBUG` logging level is enabled, the log will contain a stacktrace as well.
55-
<3> You can optionally mute some status codes or exceptions logging completely.
5646

5747
=== Creating problems
5848

49+
`HttpProblem` class represents the `RFC 7807` model. It is the main entity you need to work with to produce the problem.
50+
5951
==== Static helpers
6052

6153
There are several handy static methods to produce a simple `HttpProblem`:
@@ -70,6 +62,8 @@ Don't overuse it, the problem should have meaningful `title` and `detail` when p
7062
.Java
7163
[source,java,role="primary"]
7264
----
65+
import io.jooby.problem.HttpProblem;
66+
7367
get("/users/{userId}", ctx -> {
7468
var userId = ctx.path("userId").value();
7569
User user = userRepository.findUser(userId);
@@ -87,6 +81,8 @@ get("/users/{userId}", ctx -> {
8781
.Kotlin
8882
[source,kt,role="secondary"]
8983
----
84+
import io.jooby.problem.HttpProblem
85+
9086
get("/users/{userId}") { ctx ->
9187
val userId = ctx.path("userId").value()
9288
val user = userRepository.findUser(userId)
@@ -117,7 +113,7 @@ Resulting response:
117113

118114
==== Builder
119115

120-
Use builder to create rich problem instance with all properties:
116+
Use builder to create a rich problem instance with all properties:
121117

122118
[source,java]
123119
----
@@ -190,8 +186,8 @@ It is basically another extension `errors` on a root level. Adding errors is str
190186
----
191187
throw HttpProblem.builder()
192188
...
193-
.error(new HttpProblem.Error("First name cannot be blank", "#/firstName"))
194-
.error(new HttpProblem.Error("Last name is required", "#/lastName"))
189+
.error(new HttpProblem.Error("First name cannot be blank", "/firstName"))
190+
.error(new HttpProblem.Error("Last name is required", "/lastName"))
195191
.build();
196192
----
197193

@@ -203,11 +199,11 @@ In response:
203199
"errors": [
204200
{
205201
"detail": "First name cannot be blank",
206-
"pointer": "#/firstName"
202+
"pointer": "/firstName"
207203
},
208204
{
209205
"detail": "Last name is required",
210-
"pointer": "#/lastName"
206+
"pointer": "/lastName"
211207
}
212208
]
213209
}
@@ -261,8 +257,7 @@ public class OutOfStockProblem extends HttpProblem {
261257

262258
=== Custom Exception Handlers
263259

264-
All the features described above should give you ability to rely solely on `ProblemDetailsHandler`.
265-
But, in case you still need custom exception handler for some reason, you still can do it:
260+
All the features described above should give you ability to rely solely on built-in global error handler. But, in case you still need custom exception handler for some reason, you still can do it:
266261

267262
[source,java]
268263
----
@@ -275,20 +270,16 @@ But, in case you still need custom exception handler for some reason, you still
275270
276271
ctx.getRouter().getErrorHandler().apply(ctx, problem, code); // <2>
277272
});
278-
279-
// should always go below (WYSIWYG)
280-
error(new ProblemDetailsHandler()); // <3>
281273
}
282274
----
283275

284276
<1> Transform exception to `HttpProblem`
285277
<2> Propagate the problem to `ProblemDetailsHandler`. It will handle the rest.
286-
<3> `ProblemDetailsHandler` should always go below your custom exception handlers
287278

288279
[IMPORTANT]
289280
====
290281
Do not attempt to render `HttpProblem` manually, it is strongly discouraged.
291282
`HttpProblem` is derived from the `RuntimeException` to enable ease of `HttpProblem` throwing.
292283
Thus, thrown `HttpProblem` will also contain a stacktrace, if you render `HttpProblem` as is -
293-
it will be rendered together with stacktrace. It is strongly advised not to expose the stacktrace to the client. Propagate the problem to `ProblemDetailsHandler` and let him take care of the rest.
284+
it will be rendered together with stacktrace. It is strongly advised not to expose the stacktrace to the client system. Propagate the problem to global error handler and let him take care of the rest.
294285
====

jooby/src/main/java/io/jooby/Jooby.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,10 +1325,15 @@ public static Jooby createApp(
13251325
return app;
13261326
}
13271327

1328-
public boolean problemDetailsEnabled() {
1328+
/**
1329+
* Check if {@link ProblemDetailsHandler} is enabled as a global error handler
1330+
*
1331+
* @return boolean flag
1332+
*/
1333+
public boolean problemDetailsIsEnabled() {
13291334
var config = getConfig();
1330-
return config.hasPath(ProblemDetailsHandler.ENABLE_KEY)
1331-
&& config.getBoolean(ProblemDetailsHandler.ENABLE_KEY);
1335+
return config.hasPath(ProblemDetailsHandler.ENABLED_KEY)
1336+
&& config.getBoolean(ProblemDetailsHandler.ENABLED_KEY);
13321337
}
13331338

13341339
private static void configurePackage(Package pkg) {

jooby/src/main/java/io/jooby/internal/RouterImpl.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -667,8 +667,17 @@ private void pureAscii(String pattern, Consumer<String> consumer) {
667667
return this;
668668
}
669669

670+
/**
671+
* Define the global error handler.
672+
* If ProblemDetails is enabled the {@link ProblemDetailsHandler} instantiated
673+
* from configuration settings and returned. Otherwise, {@link DefaultErrorHandler} instance
674+
* returned.
675+
*
676+
* @param app - Jooby application instance
677+
* @return global error handler
678+
*/
670679
private ErrorHandler defineGlobalErrorHandler(Jooby app) {
671-
if (app.problemDetailsEnabled()) {
680+
if (app.problemDetailsIsEnabled()) {
672681
var problemDetailsConfig = app.getConfig().getConfig(ProblemDetailsHandler.ROOT_CONFIG_PATH);
673682
return ProblemDetailsHandler.fromConfig(problemDetailsConfig);
674683
} else {

jooby/src/main/java/io/jooby/problem/ProblemDetailsHandler.java

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@
3131
*/
3232
public class ProblemDetailsHandler extends DefaultErrorHandler {
3333

34+
private static final String MUTE_CODES_KEY = "muteCodes";
35+
private static final String MUTE_TYPES_KEY = "muteTypes";
36+
private static final String LOG_4XX_ERRORS_KEY = "log4xxErrors";
37+
3438
public static final String ROOT_CONFIG_PATH = "problem.details";
35-
public static final String ENABLE_KEY = ROOT_CONFIG_PATH + ".enable";
39+
public static final String ENABLED_KEY = ROOT_CONFIG_PATH + ".enabled";
3640

3741
private boolean log4xxErrors;
3842

@@ -54,7 +58,7 @@ public void apply(@NonNull Context ctx, @NonNull Throwable cause, @NonNull Statu
5458
}
5559

5660
try {
57-
HttpProblem problem = evaluateTheProblem(ctx, cause, code);
61+
HttpProblem problem = evaluateTheProblem(cause, code);
5862

5963
logProblem(ctx, problem, cause);
6064

@@ -87,7 +91,7 @@ private void setResponseType(Context ctx, MediaType type) {
8791
}
8892
}
8993

90-
private HttpProblem evaluateTheProblem(Context ctx, Throwable cause, StatusCode statusCode) {
94+
private HttpProblem evaluateTheProblem(Throwable cause, StatusCode statusCode) {
9195
HttpProblem problem;
9296
if (cause instanceof HttpProblem httpProblem) {
9397
problem = httpProblem;
@@ -201,19 +205,17 @@ private String buildLogMsg(Context ctx, HttpProblem problem, StatusCode statusCo
201205
public static ProblemDetailsHandler fromConfig(Config config) {
202206
var handler = new ProblemDetailsHandler();
203207

204-
if(config.hasPath("log4xxErrors")) {
205-
if(config.getBoolean("log4xxErrors")) {
206-
handler.log4xxErrors();
207-
}
208+
if (config.hasPath(LOG_4XX_ERRORS_KEY) && config.getBoolean(LOG_4XX_ERRORS_KEY)) {
209+
handler.log4xxErrors();
208210
}
209211

210-
if(config.hasPath("muteCodes")) {
211-
config.getIntList("muteCodes")
212+
if (config.hasPath(MUTE_CODES_KEY)) {
213+
config.getIntList(MUTE_CODES_KEY)
212214
.forEach(code -> handler.mute(StatusCode.valueOf(code)));
213215
}
214216

215-
if(config.hasPath("muteTypes")) {
216-
config.getStringList("muteTypes")
217+
if (config.hasPath(MUTE_TYPES_KEY)) {
218+
config.getStringList(MUTE_TYPES_KEY)
217219
.forEach(throwingConsumer(
218220
className -> handler.mute((Class<? extends Exception>) Class.forName(className))));
219221
}

jooby/src/main/java/io/jooby/validation/JsonPointer.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@
55
import java.util.regex.Pattern;
66
import java.util.stream.Collectors;
77

8+
/**
9+
* Transforms hibernate-validator (or avaje-validator) `propertyPath` into
10+
* <a href="https://www.rfc-editor.org/rfc/rfc6901.html">JSON POINTER</a> format.
11+
* For example:
12+
* <p>"person.firstName" -> "/person/firstName"</p>
13+
* <p>"persons[0].firstName" -> "/persons/0/firstName"</p>
14+
*
15+
* @author kliushnichenko
16+
* @since 3.4.2
17+
*/
818
public class JsonPointer {
919
private static final Pattern ARRAY_PATTERN = Pattern.compile("(\\w+)\\[(\\d+)]");
1020

@@ -14,7 +24,7 @@ public static String of(String propertyPath) {
1424

1525
private static String toJsonPointer(String path) {
1626
if (path == null || path.isEmpty()) {
17-
return "/";
27+
return ""; // means the whole document
1828
}
1929

2030
List<String> parts = List.of(path.split("\\."));

jooby/src/main/java/io/jooby/validation/ValidationResult.java

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ public HttpProblem toHttpProblem() {
3838
.build();
3939
}
4040

41-
private List<ProblemError> convertErrors() {
42-
List<ProblemError> problemErrors = new LinkedList<>();
41+
private List<HttpProblem.Error> convertErrors() {
42+
List<HttpProblem.Error> problemErrors = new LinkedList<>();
4343
for (Error err : errors) {
4444
for (var msg : err.messages()) {
45-
problemErrors.add(new ProblemError(msg, JsonPointer.of(err.field), err.type));
45+
problemErrors.add(new HttpProblem.Error(msg, JsonPointer.of(err.field)));
4646
}
4747
}
4848
return problemErrors;
@@ -51,19 +51,6 @@ private List<ProblemError> convertErrors() {
5151
public record Error(String field, List<String> messages, ErrorType type) {
5252
}
5353

54-
public static class ProblemError extends HttpProblem.Error {
55-
private final ErrorType type;
56-
57-
public ProblemError(String detail, String pointer, ErrorType type) {
58-
super(detail, pointer);
59-
this.type = type;
60-
}
61-
62-
public ErrorType getType() {
63-
return type;
64-
}
65-
}
66-
6754
public enum ErrorType {
6855
FIELD,
6956
GLOBAL

0 commit comments

Comments
 (0)