io.jooby.avaje.validator.AvajeValidatorModule Maven / Gradle / Ivy
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
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;
import io.avaje.validation.ConstraintViolationException;
import io.avaje.validation.Validator;
import io.jooby.Context;
import io.jooby.Extension;
import io.jooby.Jooby;
import io.jooby.StatusCode;
import io.jooby.validation.BeanValidator;
/**
* Avaje Validator Module: https://jooby.io/modules/avaje-validator.
*
* {@code
* {
* install(new AvajeValidatorModule());
*
* }
*
* public class Controller {
*
* @POST("/create")
* public void create(@Valid Bean bean) {
* }
*
* }
* }
*
* Supports validation of a single bean, list, array, or map.
*
*
The module also provides a built-in error handler that catches {@link
* ConstraintViolationException} and transforms it into a {@link
* io.jooby.validation.ValidationResult}
*
* @author kliushnichenko
* @author SentryMan
* @since 3.3.1
*/
public class AvajeValidatorModule implements Extension {
private Consumer configurer;
private StatusCode statusCode = StatusCode.UNPROCESSABLE_ENTITY;
private String title = "Validation failed";
private boolean disableDefaultViolationHandler = false;
private boolean logException;
/**
* Setups a configurer callback.
*
* @param configurer Configurer callback.
* @return This module.
*/
public AvajeValidatorModule doWith(@NonNull final Consumer configurer) {
this.configurer = configurer;
return this;
}
/**
* Overrides the default status code for the errors produced by validation. Default code is
* UNPROCESSABLE_ENTITY(422)
*
* @param statusCode new status code
* @return This module.
*/
public AvajeValidatorModule statusCode(@NonNull StatusCode statusCode) {
this.statusCode = statusCode;
return this;
}
/**
* Overrides the default title for the errors produced by validation. Default title is "Validation
* failed"
*
* @param title new title
* @return This module.
*/
public AvajeValidatorModule validationTitle(@NonNull String title) {
this.title = title;
return this;
}
/**
* Ask the error handler to log the exception. Default is: false.
*
* @return This module.
*/
public AvajeValidatorModule logException() {
this.logException = true;
return this;
}
/**
* Disables default constraint violation handler. By default {@link AvajeValidatorModule} provides
* built-in error handler for the {@link ConstraintViolationException} Such exceptions are
* transformed into response of {@link io.jooby.validation.ValidationResult} Use this flag to
* disable default error handler and provide your custom.
*
* @return This module.
*/
public AvajeValidatorModule disableViolationHandler() {
this.disableDefaultViolationHandler = true;
return this;
}
@Override
public void install(@NonNull Jooby app) {
var conf = app.getConfig();
final var builder = Validator.builder();
withProperty(conf, "validation.failFast", conf::getBoolean).ifPresent(builder::failFast);
withProperty(conf, "validation.resourcebundle.names", path -> getStringList(conf, path))
.ifPresent(values -> values.forEach(builder::addResourceBundles));
// Locales from application
Optional.ofNullable(app.getLocales())
.ifPresent(
locales -> {
builder.setDefaultLocale(locales.get(0));
locales.stream().skip(1).forEach(builder::addLocales);
});
withProperty(conf, "validation.locale.default", conf::getString)
.map(Locale::forLanguageTag)
.ifPresent(builder::setDefaultLocale);
withProperty(conf, "validation.locale.addedLocales", path -> getStringList(conf, path))
.orElseGet(List::of)
.stream()
.map(Locale::forLanguageTag)
.forEach(builder::addLocales);
withProperty(conf, "validation.temporal.tolerance.value", conf::getLong)
.ifPresent(
duration -> {
var unit =
withProperty(conf, "validation.temporal.tolerance.chronoUnit", conf::getString)
.map(ChronoUnit::valueOf)
.orElse(ChronoUnit.MILLIS);
builder.temporalTolerance(Duration.of(duration, unit));
});
if (configurer != null) {
configurer.accept(builder);
}
var validator = builder.build();
app.getServices().put(Validator.class, validator);
app.getServices().put(BeanValidator.class, new BeanValidatorImpl(validator));
if (!disableDefaultViolationHandler) {
app.error(new ConstraintViolationHandler(statusCode, title, logException));
}
}
static class BeanValidatorImpl implements BeanValidator {
private final Validator validator;
BeanValidatorImpl(Validator validator) {
this.validator = validator;
}
@Override
public void validate(Context ctx, Object bean) throws ConstraintViolationException {
validator.validate(bean, ctx.locale());
}
}
private static Optional withProperty(Config config, String path, Function fn) {
if (config.hasPath(path)) {
return Optional.of(fn.apply(path));
}
return Optional.empty();
}
private static List getStringList(Config config, String path) {
return config.getValue(path).valueType() == ConfigValueType.STRING
? List.of(config.getString(path))
: config.getStringList(path);
}
}