Skip to content

Commit d7864b4

Browse files
problem details in modules
1 parent e71754f commit d7864b4

File tree

15 files changed

+382
-52
lines changed

15 files changed

+382
-52
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import java.util.function.Supplier;
4242
import java.util.stream.Collectors;
4343

44+
import io.jooby.problem.ProblemDetailsHandler;
4445
import org.slf4j.Logger;
4546
import org.slf4j.LoggerFactory;
4647

@@ -1324,6 +1325,12 @@ public static Jooby createApp(
13241325
return app;
13251326
}
13261327

1328+
public boolean problemDetailsEnabled() {
1329+
var config = getConfig();
1330+
return config.hasPath(ProblemDetailsHandler.ENABLE_KEY)
1331+
&& config.getBoolean(ProblemDetailsHandler.ENABLE_KEY);
1332+
}
1333+
13271334
private static void configurePackage(Package pkg) {
13281335
if (pkg != null) {
13291336
configurePackage(pkg.getName());

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import java.util.stream.IntStream;
3636
import java.util.stream.Stream;
3737

38+
import io.jooby.problem.ProblemDetailsHandler;
3839
import org.slf4j.Logger;
3940
import org.slf4j.LoggerFactory;
4041

@@ -594,10 +595,11 @@ private void pureAscii(String pattern, Consumer<String> consumer) {
594595

595596
@NonNull public Router start(@NonNull Jooby app, @NonNull Server server) {
596597
started = true;
598+
var globalErrHandler = defineGlobalErrorHandler(app);
597599
if (err == null) {
598-
err = ErrorHandler.create();
600+
err = globalErrHandler;
599601
} else {
600-
err = err.then(ErrorHandler.create());
602+
err = err.then(globalErrHandler);
601603
}
602604

603605
ExecutionMode mode = app.getExecutionMode();
@@ -665,6 +667,15 @@ private void pureAscii(String pattern, Consumer<String> consumer) {
665667
return this;
666668
}
667669

670+
private ErrorHandler defineGlobalErrorHandler(Jooby app) {
671+
if (app.problemDetailsEnabled()) {
672+
var problemDetailsConfig = app.getConfig().getConfig(ProblemDetailsHandler.ROOT_CONFIG_PATH);
673+
return ProblemDetailsHandler.fromConfig(problemDetailsConfig);
674+
} else {
675+
return ErrorHandler.create();
676+
}
677+
}
678+
668679
private ExecutionMode forceMode(Route route, ExecutionMode mode) {
669680
if (route.getMethod().equals(Router.WS)) {
670681
// websocket always run in worker executor

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ public Builder error(final Error error) {
243243
return this;
244244
}
245245

246-
public Builder errors(final List<Error> errors) {
246+
public Builder errors(final List<? extends Error> errors) {
247247
this.errors.addAll(errors);
248248
return this;
249249
}

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
package io.jooby.problem;
77

8+
import com.typesafe.config.Config;
89
import edu.umd.cs.findbugs.annotations.NonNull;
910
import io.jooby.*;
1011
import io.jooby.exception.NotAcceptableException;
@@ -15,6 +16,7 @@
1516
import java.util.Map;
1617

1718
import static io.jooby.MediaType.*;
19+
import static io.jooby.SneakyThrows.throwingConsumer;
1820
import static io.jooby.StatusCode.SERVER_ERROR_CODE;
1921

2022
/**
@@ -29,6 +31,9 @@
2931
*/
3032
public class ProblemDetailsHandler extends DefaultErrorHandler {
3133

34+
public static final String ROOT_CONFIG_PATH = "problem.details";
35+
public static final String ENABLE_KEY = ROOT_CONFIG_PATH + ".enable";
36+
3237
private boolean log4xxErrors;
3338

3439
public ProblemDetailsHandler log4xxErrors() {
@@ -192,4 +197,27 @@ private void logProblem(Context ctx, HttpProblem problem, Throwable cause) {
192197
private String buildLogMsg(Context ctx, HttpProblem problem, StatusCode statusCode) {
193198
return "%s | %s".formatted(ErrorHandler.errorMessage(ctx, statusCode), problem.toString());
194199
}
200+
201+
public static ProblemDetailsHandler fromConfig(Config config) {
202+
var handler = new ProblemDetailsHandler();
203+
204+
if(config.hasPath("log4xxErrors")) {
205+
if(config.getBoolean("log4xxErrors")) {
206+
handler.log4xxErrors();
207+
}
208+
}
209+
210+
if(config.hasPath("muteCodes")) {
211+
config.getIntList("muteCodes")
212+
.forEach(code -> handler.mute(StatusCode.valueOf(code)));
213+
}
214+
215+
if(config.hasPath("muteTypes")) {
216+
config.getStringList("muteTypes")
217+
.forEach(throwingConsumer(
218+
className -> handler.mute((Class<? extends Exception>) Class.forName(className))));
219+
}
220+
221+
return handler;
222+
}
195223
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.jooby.validation;
2+
3+
import java.util.List;
4+
import java.util.regex.Matcher;
5+
import java.util.regex.Pattern;
6+
import java.util.stream.Collectors;
7+
8+
public class JsonPointer {
9+
private static final Pattern ARRAY_PATTERN = Pattern.compile("(\\w+)\\[(\\d+)]");
10+
11+
public static String of(String propertyPath) {
12+
return toJsonPointer(propertyPath);
13+
}
14+
15+
private static String toJsonPointer(String path) {
16+
if (path == null || path.isEmpty()) {
17+
return "/";
18+
}
19+
20+
List<String> parts = List.of(path.split("\\."));
21+
22+
return "/" + parts.stream()
23+
.map(JsonPointer::handleArrayIndex)
24+
.collect(Collectors.joining("/"));
25+
}
26+
27+
private static String handleArrayIndex(String part) {
28+
Matcher matcher = ARRAY_PATTERN.matcher(part);
29+
if (matcher.matches()) {
30+
return matcher.group(1) + "/" + matcher.group(2);
31+
}
32+
return part;
33+
}
34+
}

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

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,79 @@
55
*/
66
package io.jooby.validation;
77

8+
import edu.umd.cs.findbugs.annotations.NonNull;
9+
import io.jooby.StatusCode;
10+
import io.jooby.problem.HttpProblem;
11+
import io.jooby.problem.HttpProblemMappable;
12+
13+
import java.util.LinkedList;
814
import java.util.List;
915

10-
public record ValidationResult(String title, int status, List<Error> errors) {
11-
public record Error(String field, List<String> messages, ErrorType type) {}
16+
public class ValidationResult implements HttpProblemMappable {
17+
18+
private String title;
19+
private int status;
20+
private List<Error> errors;
21+
22+
public ValidationResult(){}
23+
24+
public ValidationResult(String title, int status, List<Error> errors) {
25+
this.title = title;
26+
this.status = status;
27+
this.errors = errors;
28+
}
29+
30+
@NonNull
31+
@Override
32+
public HttpProblem toHttpProblem() {
33+
return HttpProblem.builder()
34+
.title(title)
35+
.status(StatusCode.valueOf(status))
36+
.detail(errors.size() + " constraint violation(s) detected")
37+
.errors(convertErrors())
38+
.build();
39+
}
40+
41+
private List<ProblemError> convertErrors() {
42+
List<ProblemError> problemErrors = new LinkedList<>();
43+
for (Error err : errors) {
44+
for (var msg : err.messages()) {
45+
problemErrors.add(new ProblemError(msg, JsonPointer.of(err.field), err.type));
46+
}
47+
}
48+
return problemErrors;
49+
}
50+
51+
public record Error(String field, List<String> messages, ErrorType type) {
52+
}
53+
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+
}
1266

1367
public enum ErrorType {
1468
FIELD,
1569
GLOBAL
1670
}
71+
72+
public String getTitle() {
73+
return title;
74+
}
75+
76+
public int getStatus() {
77+
return status;
78+
}
79+
80+
public List<Error> getErrors() {
81+
return errors;
82+
}
1783
}

modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/AvajeValidatorModule.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,6 @@
55
*/
66
package io.jooby.avaje.validator;
77

8-
import java.time.Duration;
9-
import java.time.temporal.ChronoUnit;
10-
import java.util.*;
11-
import java.util.function.Consumer;
12-
import java.util.function.Function;
13-
148
import com.typesafe.config.Config;
159
import com.typesafe.config.ConfigValueType;
1610
import edu.umd.cs.findbugs.annotations.NonNull;
@@ -22,6 +16,14 @@
2216
import io.jooby.StatusCode;
2317
import io.jooby.validation.BeanValidator;
2418

19+
import java.time.Duration;
20+
import java.time.temporal.ChronoUnit;
21+
import java.util.List;
22+
import java.util.Locale;
23+
import java.util.Optional;
24+
import java.util.function.Consumer;
25+
import java.util.function.Function;
26+
2527
/**
2628
* Avaje Validator Module: https://jooby.io/modules/avaje-validator.
2729
*
@@ -158,7 +160,9 @@ public void install(@NonNull Jooby app) {
158160
app.getServices().put(BeanValidator.class, new BeanValidatorImpl(validator));
159161

160162
if (!disableDefaultViolationHandler) {
161-
app.error(new ConstraintViolationHandler(statusCode, title, logException));
163+
app.error(new ConstraintViolationHandler(
164+
statusCode, title, logException, app.problemDetailsEnabled())
165+
);
162166
}
163167
}
164168

modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/ConstraintViolationHandler.java

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,23 @@
55
*/
66
package io.jooby.avaje.validator;
77

8-
import static io.jooby.validation.ValidationResult.ErrorType.FIELD;
9-
import static io.jooby.validation.ValidationResult.ErrorType.GLOBAL;
10-
import static java.util.stream.Collectors.groupingBy;
11-
12-
import java.util.ArrayList;
13-
import java.util.List;
14-
import java.util.Map;
15-
16-
import org.slf4j.Logger;
17-
import org.slf4j.LoggerFactory;
18-
198
import edu.umd.cs.findbugs.annotations.NonNull;
209
import io.avaje.validation.ConstraintViolation;
2110
import io.avaje.validation.ConstraintViolationException;
2211
import io.jooby.Context;
2312
import io.jooby.ErrorHandler;
2413
import io.jooby.StatusCode;
2514
import io.jooby.validation.ValidationResult;
15+
import org.slf4j.Logger;
16+
import org.slf4j.LoggerFactory;
17+
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
import java.util.Map;
21+
22+
import static io.jooby.validation.ValidationResult.ErrorType.FIELD;
23+
import static io.jooby.validation.ValidationResult.ErrorType.GLOBAL;
24+
import static java.util.stream.Collectors.groupingBy;
2625

2726
/**
2827
* Catches and transform {@link ConstraintViolationException} into {@link ValidationResult}
@@ -62,12 +61,17 @@ public class ConstraintViolationHandler implements ErrorHandler {
6261
private final StatusCode statusCode;
6362
private final String title;
6463
private final boolean logException;
64+
private final boolean problemDetailsEnabled;
6565

6666
public ConstraintViolationHandler(
67-
@NonNull StatusCode statusCode, @NonNull String title, boolean logException) {
67+
@NonNull StatusCode statusCode,
68+
@NonNull String title,
69+
boolean logException,
70+
boolean problemDetailsEnabled) {
6871
this.statusCode = statusCode;
6972
this.title = title;
7073
this.logException = logException;
74+
this.problemDetailsEnabled = problemDetailsEnabled;
7175
}
7276

7377
@Override
@@ -82,7 +86,7 @@ public void apply(@NonNull Context ctx, @NonNull Throwable cause, @NonNull Statu
8286
var errors = collectErrors(groupedByPath);
8387

8488
var result = new ValidationResult(title, statusCode.value(), errors);
85-
ctx.setResponseCode(statusCode).render(result);
89+
renderOrPropagate(ctx, result, code);
8690
}
8791
}
8892

@@ -103,4 +107,12 @@ private List<ValidationResult.Error> collectErrors(
103107
private List<String> extractMessages(List<ConstraintViolation> violations) {
104108
return violations.stream().map(ConstraintViolation::message).toList();
105109
}
110+
111+
private void renderOrPropagate(Context ctx, ValidationResult result, StatusCode code) {
112+
if (problemDetailsEnabled) {
113+
ctx.getRouter().getErrorHandler().apply(ctx, result.toHttpProblem(), code);
114+
} else {
115+
ctx.setResponseCode(statusCode).render(result);
116+
}
117+
}
106118
}

0 commit comments

Comments
 (0)