/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.jooby.hbv; import static java.util.Objects.requireNonNull; import static javax.validation.Validation.byProvider; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Predicate; import javax.validation.ConstraintValidatorFactory; import javax.validation.ConstraintViolationException; import javax.validation.ValidationException; import javax.validation.Validator; import org.hibernate.validator.HibernateValidator; import org.hibernate.validator.HibernateValidatorConfiguration; import org.jooby.Env; import org.jooby.Jooby; import org.jooby.Parser; import org.jooby.Request; import com.google.inject.Binder; import com.google.inject.TypeLiteral; import com.google.inject.multibindings.Multibinder; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import com.typesafe.config.ConfigValueFactory; /** * Bean validation via Hibernate Validator. * * <h1>exposes</h1> * <ul> * <li>a {@link Validator},</li> * <li>a {@link HibernateValidatorConfiguration} and</li> * <li>a {@link Parser}</li> * </ul> * * <h1>usage</h1> * * <pre> * { * use(new Hbv()); * * get("/", req {@literal ->} { * Validator validator = req.require(Validator.class); * Car car = req.params().to(Car.class); * Set<ConstraintViolation> violations = validator.validate(car); * if (violations.size() {@literal >} 0) { * // handle errors * ... * } * }); * } * </pre> * * <h2>automatic validations of HTTP params and body</h2> * * Previous example demonstrate how to manually validate a bean created via: * {@link Request#params()} or {@link Request#body()}. The boilerplate code can be avoided if you * explicitly tell the validation module which classes require validation. * * The previous example can be rewritten as: * * <pre> * { * use(new Hbv(Car.class)); * * get("/", () {@literal ->} { * Car car = req.params().to(Car.class); * // a valid car is here * ... * }); * } * </pre> * * Here a {@link Parser} will do the boilerplate part and throws * a {@link ConstraintViolationException}. * * <h3>rendering a ConstraintViolationException</h3> * * The default err handler will render the {@link ConstraintViolationException} without problem, but * suppose we have a JavaScript client and want to display the errors in a friendly way. * * <pre> * { * use(new Jackson()); // JSON renderer * * use(new Hbv(Car.class)); // Validate Car objects * * err((req, rsp, err) {@literal ->} { * Throwable cause = err.getCause(); * if (cause instanceof ConstraintViolationException) { * Set<ConstraintViolation<?>> constraints = * ((ConstraintViolationException) cause) .getConstraintViolations(); * * Map<Path, String> errors = constraints.stream() * .collect(Collectors.toMap( * ConstraintViolation::getPropertyPath, * ConstraintViolation::getMessage * )); * rsp.send(errors); * } * }); * * get("/", () {@literal ->} { * Car car = req.params().to(Car.class); * // a valid car is here * ... * }); * } *</pre> * * The call to <code>rsp.send(errors);</code> will be rendered by ```Jackson``` (or any other that * applies) and will produces a more friendly response, here it will be a JavaScript object with the * errors. * * <h2>constraint validator factory</h2> * * {@link ConstraintValidatorFactory} is the extension point for customizing how constraint * validators are instantiated and released. * * In Jooby, a {@link ConstraintValidatorFactory} is powered by Guice. * * <h2>configuration</h2> * * Any property defined at <code>hibernate.validator</code> will be add it automatically: * * application.conf: * * <pre> * hibernate.validator.fail_fast = true * </pre> * * Or programmatically: * * <pre> * { * use(new Hbv().doWith(config {@literal ->} { * config.failFast(true); * })); * } * </pre> * * @author edgar * @since 0.6.0 */ public class Hbv implements Jooby.Module { private Predicate<TypeLiteral<?>> predicate; private BiConsumer<HibernateValidatorConfiguration, Config> configurer; /** * Creates a new {@link Hbv} module. * * @param predicate A predicate to test if a class require validation or not. */ public Hbv(final Predicate<TypeLiteral<?>> predicate) { this.predicate = requireNonNull(predicate, "Predicate is required."); } /** * Creates a new {@link Hbv} module. * * @param classes List of classes that require validation. */ public Hbv(final Class<?>... classes) { this(typeIs(classes)); } /** * Creates a new {@link Hbv} module. No automatic validation is applied. */ public Hbv() { this(none()); } /** * Setup a configurer callback. * * @param configurer Configurer callback. * @return This module. */ public Hbv doWith(final Consumer<HibernateValidatorConfiguration> configurer) { requireNonNull(configurer, "Configurer callback is required."); this.configurer = (hvc, conf) -> { configurer.accept(hvc); }; return this; } /** * Setup a configurer callback. * * @param configurer Configurer callback. * @return This module. */ public Hbv doWith(final BiConsumer<HibernateValidatorConfiguration, Config> configurer) { this.configurer = requireNonNull(configurer, "Configurer callback is required."); return this; } @Override public void configure(final Env env, final Config config, final Binder binder) { HibernateValidatorConfiguration configuration = byProvider(HibernateValidator.class) .configure(); if (config.hasPath("hibernate.validator")) { config.getConfig("hibernate.validator").root().forEach((k, v) -> { configuration.addProperty("hibernate.validator." + k, v.unwrapped().toString()); }); } if (configurer != null) { configurer.accept(configuration, config); } binder.bind(HibernateValidatorConfiguration.class).toInstance(configuration); binder.bind(Validator.class).toProvider(HbvFactory.class).asEagerSingleton(); env.lifeCycle(HbvFactory.class); Multibinder.newSetBinder(binder, Parser.class).addBinding() .toInstance(new HbvParser(predicate)); } @Override public Config config() { return ConfigFactory.empty(Hbv.class.getName()) .withValue("err." + ValidationException.class.getName(), ConfigValueFactory.fromAnyRef(400)); } static Predicate<TypeLiteral<?>> typeIs(final Class<?>[] classes) { return type -> { Class<?> it = type.getRawType(); for (Class<?> klass : classes) { if (it.isAssignableFrom(klass)) { return true; } } return false; }; } static Predicate<TypeLiteral<?>> none() { return type -> false; } }