/**
* 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;
}
}