Skip to content

Commit

Permalink
problem details in modules
Browse files Browse the repository at this point in the history
  • Loading branch information
kliushnichenko committed Oct 10, 2024
1 parent e71754f commit d7864b4
Show file tree
Hide file tree
Showing 15 changed files with 382 additions and 52 deletions.
7 changes: 7 additions & 0 deletions jooby/src/main/java/io/jooby/Jooby.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import java.util.function.Supplier;
import java.util.stream.Collectors;

import io.jooby.problem.ProblemDetailsHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -1324,6 +1325,12 @@ public static Jooby createApp(
return app;
}

public boolean problemDetailsEnabled() {
var config = getConfig();
return config.hasPath(ProblemDetailsHandler.ENABLE_KEY)
&& config.getBoolean(ProblemDetailsHandler.ENABLE_KEY);
}

private static void configurePackage(Package pkg) {
if (pkg != null) {
configurePackage(pkg.getName());
Expand Down
15 changes: 13 additions & 2 deletions jooby/src/main/java/io/jooby/internal/RouterImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import java.util.stream.IntStream;
import java.util.stream.Stream;

import io.jooby.problem.ProblemDetailsHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

@NonNull public Router start(@NonNull Jooby app, @NonNull Server server) {
started = true;
var globalErrHandler = defineGlobalErrorHandler(app);
if (err == null) {
err = ErrorHandler.create();
err = globalErrHandler;
} else {
err = err.then(ErrorHandler.create());
err = err.then(globalErrHandler);
}

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

private ErrorHandler defineGlobalErrorHandler(Jooby app) {
if (app.problemDetailsEnabled()) {
var problemDetailsConfig = app.getConfig().getConfig(ProblemDetailsHandler.ROOT_CONFIG_PATH);
return ProblemDetailsHandler.fromConfig(problemDetailsConfig);
} else {
return ErrorHandler.create();
}
}

private ExecutionMode forceMode(Route route, ExecutionMode mode) {
if (route.getMethod().equals(Router.WS)) {
// websocket always run in worker executor
Expand Down
2 changes: 1 addition & 1 deletion jooby/src/main/java/io/jooby/problem/HttpProblem.java
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ public Builder error(final Error error) {
return this;
}

public Builder errors(final List<Error> errors) {
public Builder errors(final List<? extends Error> errors) {
this.errors.addAll(errors);
return this;
}
Expand Down
28 changes: 28 additions & 0 deletions jooby/src/main/java/io/jooby/problem/ProblemDetailsHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
package io.jooby.problem;

import com.typesafe.config.Config;
import edu.umd.cs.findbugs.annotations.NonNull;
import io.jooby.*;
import io.jooby.exception.NotAcceptableException;
Expand All @@ -15,6 +16,7 @@
import java.util.Map;

import static io.jooby.MediaType.*;
import static io.jooby.SneakyThrows.throwingConsumer;
import static io.jooby.StatusCode.SERVER_ERROR_CODE;

/**
Expand All @@ -29,6 +31,9 @@
*/
public class ProblemDetailsHandler extends DefaultErrorHandler {

public static final String ROOT_CONFIG_PATH = "problem.details";
public static final String ENABLE_KEY = ROOT_CONFIG_PATH + ".enable";

private boolean log4xxErrors;

public ProblemDetailsHandler log4xxErrors() {
Expand Down Expand Up @@ -192,4 +197,27 @@ private void logProblem(Context ctx, HttpProblem problem, Throwable cause) {
private String buildLogMsg(Context ctx, HttpProblem problem, StatusCode statusCode) {
return "%s | %s".formatted(ErrorHandler.errorMessage(ctx, statusCode), problem.toString());
}

public static ProblemDetailsHandler fromConfig(Config config) {
var handler = new ProblemDetailsHandler();

if(config.hasPath("log4xxErrors")) {
if(config.getBoolean("log4xxErrors")) {
handler.log4xxErrors();
}
}

if(config.hasPath("muteCodes")) {
config.getIntList("muteCodes")
.forEach(code -> handler.mute(StatusCode.valueOf(code)));
}

if(config.hasPath("muteTypes")) {
config.getStringList("muteTypes")
.forEach(throwingConsumer(
className -> handler.mute((Class<? extends Exception>) Class.forName(className))));
}

return handler;
}
}
34 changes: 34 additions & 0 deletions jooby/src/main/java/io/jooby/validation/JsonPointer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.jooby.validation;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class JsonPointer {
private static final Pattern ARRAY_PATTERN = Pattern.compile("(\\w+)\\[(\\d+)]");

public static String of(String propertyPath) {
return toJsonPointer(propertyPath);
}

private static String toJsonPointer(String path) {
if (path == null || path.isEmpty()) {
return "/";
}

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

return "/" + parts.stream()
.map(JsonPointer::handleArrayIndex)
.collect(Collectors.joining("/"));
}

private static String handleArrayIndex(String part) {
Matcher matcher = ARRAY_PATTERN.matcher(part);
if (matcher.matches()) {
return matcher.group(1) + "/" + matcher.group(2);
}
return part;
}
}
70 changes: 68 additions & 2 deletions jooby/src/main/java/io/jooby/validation/ValidationResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,79 @@
*/
package io.jooby.validation;

import edu.umd.cs.findbugs.annotations.NonNull;
import io.jooby.StatusCode;
import io.jooby.problem.HttpProblem;
import io.jooby.problem.HttpProblemMappable;

import java.util.LinkedList;
import java.util.List;

public record ValidationResult(String title, int status, List<Error> errors) {
public record Error(String field, List<String> messages, ErrorType type) {}
public class ValidationResult implements HttpProblemMappable {

private String title;
private int status;
private List<Error> errors;

public ValidationResult(){}

public ValidationResult(String title, int status, List<Error> errors) {
this.title = title;
this.status = status;
this.errors = errors;
}

@NonNull
@Override
public HttpProblem toHttpProblem() {
return HttpProblem.builder()
.title(title)
.status(StatusCode.valueOf(status))
.detail(errors.size() + " constraint violation(s) detected")
.errors(convertErrors())
.build();
}

private List<ProblemError> convertErrors() {
List<ProblemError> problemErrors = new LinkedList<>();
for (Error err : errors) {
for (var msg : err.messages()) {
problemErrors.add(new ProblemError(msg, JsonPointer.of(err.field), err.type));
}
}
return problemErrors;
}

public record Error(String field, List<String> messages, ErrorType type) {
}

public static class ProblemError extends HttpProblem.Error {
private final ErrorType type;

public ProblemError(String detail, String pointer, ErrorType type) {
super(detail, pointer);
this.type = type;
}

public ErrorType getType() {
return type;
}
}

public enum ErrorType {
FIELD,
GLOBAL
}

public String getTitle() {
return title;
}

public int getStatus() {
return status;
}

public List<Error> getErrors() {
return errors;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@
*/
package io.jooby.avaje.validator;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigValueType;
import edu.umd.cs.findbugs.annotations.NonNull;
Expand All @@ -22,6 +16,14 @@
import io.jooby.StatusCode;
import io.jooby.validation.BeanValidator;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;

/**
* Avaje Validator Module: https://jooby.io/modules/avaje-validator.
*
Expand Down Expand Up @@ -158,7 +160,9 @@ public void install(@NonNull Jooby app) {
app.getServices().put(BeanValidator.class, new BeanValidatorImpl(validator));

if (!disableDefaultViolationHandler) {
app.error(new ConstraintViolationHandler(statusCode, title, logException));
app.error(new ConstraintViolationHandler(
statusCode, title, logException, app.problemDetailsEnabled())
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,23 @@
*/
package io.jooby.avaje.validator;

import static io.jooby.validation.ValidationResult.ErrorType.FIELD;
import static io.jooby.validation.ValidationResult.ErrorType.GLOBAL;
import static java.util.stream.Collectors.groupingBy;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import edu.umd.cs.findbugs.annotations.NonNull;
import io.avaje.validation.ConstraintViolation;
import io.avaje.validation.ConstraintViolationException;
import io.jooby.Context;
import io.jooby.ErrorHandler;
import io.jooby.StatusCode;
import io.jooby.validation.ValidationResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static io.jooby.validation.ValidationResult.ErrorType.FIELD;
import static io.jooby.validation.ValidationResult.ErrorType.GLOBAL;
import static java.util.stream.Collectors.groupingBy;

/**
* Catches and transform {@link ConstraintViolationException} into {@link ValidationResult}
Expand Down Expand Up @@ -62,12 +61,17 @@ public class ConstraintViolationHandler implements ErrorHandler {
private final StatusCode statusCode;
private final String title;
private final boolean logException;
private final boolean problemDetailsEnabled;

public ConstraintViolationHandler(
@NonNull StatusCode statusCode, @NonNull String title, boolean logException) {
@NonNull StatusCode statusCode,
@NonNull String title,
boolean logException,
boolean problemDetailsEnabled) {
this.statusCode = statusCode;
this.title = title;
this.logException = logException;
this.problemDetailsEnabled = problemDetailsEnabled;
}

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

var result = new ValidationResult(title, statusCode.value(), errors);
ctx.setResponseCode(statusCode).render(result);
renderOrPropagate(ctx, result, code);
}
}

Expand All @@ -103,4 +107,12 @@ private List<ValidationResult.Error> collectErrors(
private List<String> extractMessages(List<ConstraintViolation> violations) {
return violations.stream().map(ConstraintViolation::message).toList();
}

private void renderOrPropagate(Context ctx, ValidationResult result, StatusCode code) {
if (problemDetailsEnabled) {
ctx.getRouter().getErrorHandler().apply(ctx, result.toHttpProblem(), code);
} else {
ctx.setResponseCode(statusCode).render(result);
}
}
}
Loading

0 comments on commit d7864b4

Please sign in to comment.