Skip to content

Commit

Permalink
Fix #10990
Browse files Browse the repository at this point in the history
Add support for bean validation to reactive routes
Use the newly provided Build Item to retrieve the set of supported constraints.
  • Loading branch information
cescoffier committed Sep 8, 2020
1 parent 3ded180 commit e330f52
Show file tree
Hide file tree
Showing 10 changed files with 768 additions and 57 deletions.
34 changes: 34 additions & 0 deletions docs/src/main/asciidoc/reactive-routes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,40 @@ id: 3

----
=== Using Bean Validation
You can combine reactive routes and Bean Validation.
First, don't forget to add the `quarkus-hibernate-validator` extension to your project.
Then, you can add constraints to your route parameter (annotated with `@Param` or `@Body`):
[source,java]
----
@Route(produces = "application/json")
Person createPerson(@Body @Valid Person person, @NonNull @Param("id") String primaryKey) {
// ...
}
----
If the parameters do not pass the tests, it returns an HTTP 400 response.
If the request accepts JSON payload, the response follows the https://opensource.zalando.com/problem/constraint-violation/[Problem] format.
When returning an object or a `Uni`, you can also use the `@Valid` annotation:
[source,java]
----
@Route(...)
@Valid Uni<Person> createPerson(@Body @Valid Person person, @NonNull @Param("id") String primaryKey) {
// ...
}
----
If the item produced by the route does not pass the validation, it returns a HTTP 500 response.
If the request accepts JSON payload, the response follows the https://opensource.zalando.com/problem/constraint-violation/[Problem] format.
Note that only `@Valid` is supported on the return type.
The returned class can use any constraint.
In the case of `Uni`, it checks the item produced asynchronously.
== Using the Vert.x Web Router
You can also register your route directly on the _HTTP routing layer_ by registering routes directly on the `Router` object.
Expand Down
9 changes: 9 additions & 0 deletions extensions/vertx-web/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx-web</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator-spi</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
Expand All @@ -37,6 +41,11 @@
<artifactId>quarkus-elytron-security-properties-file-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package io.quarkus.vertx.web.deployment;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;

import io.quarkus.hibernate.validator.spi.BeanValidationAnnotationsBuildItem;

/**
* Describe a request handler.
*/
class HandlerDescriptor {

private final MethodInfo method;
private final BeanValidationAnnotationsBuildItem validationAnnotations;

HandlerDescriptor(MethodInfo method) {
HandlerDescriptor(MethodInfo method, BeanValidationAnnotationsBuildItem bvAnnotations) {
this.method = method;
this.validationAnnotations = bvAnnotations;
}

Type getReturnType() {
Expand All @@ -31,6 +36,39 @@ boolean isReturningMulti() {
return method.returnType().name().equals(DotNames.MULTI);
}

/**
* @return {@code true} if the method is annotated with a constraint or {@code @Valid} or any parameter has such kind of
* annotation.
*/
boolean requireValidation() {
if (validationAnnotations == null) {
return false;
}
for (AnnotationInstance annotation : method.annotations()) {
String name = annotation.name().toString();
if (validationAnnotations.getAllAnnotations().contains(name)) {
return true;
}
}
return false;
}

/**
* @return {@code true} if the method is annotated with {@code @Valid}.
*/
boolean isProducedResponseValidated() {
if (validationAnnotations == null) {
return false;
}
for (AnnotationInstance annotation : method.annotations()) {
String name = annotation.name().toString();
if (validationAnnotations.getValidAnnotation().equals(name)) {
return true;
}
}
return false;
}

Type getContentType() {
if (isReturningVoid()) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

import javax.enterprise.context.spi.Context;
Expand All @@ -15,13 +16,17 @@
import io.quarkus.arc.InjectableBean;
import io.quarkus.arc.InjectableContext;
import io.quarkus.arc.InjectableReferenceProvider;
import io.quarkus.gizmo.AssignableResultHandle;
import io.quarkus.gizmo.BranchResult;
import io.quarkus.gizmo.BytecodeCreator;
import io.quarkus.gizmo.FieldCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.vertx.web.runtime.MultiJsonArraySupport;
import io.quarkus.vertx.web.runtime.MultiSseSupport;
import io.quarkus.vertx.web.runtime.MultiSupport;
import io.quarkus.vertx.web.runtime.RouteHandlers;
import io.quarkus.vertx.web.runtime.ValidationSupport;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.groups.UniSubscribe;
Expand Down Expand Up @@ -168,6 +173,24 @@ class Methods {
static final MethodDescriptor OPTIONAL_OF_NULLABLE = MethodDescriptor
.ofMethod(Optional.class, "ofNullable", Optional.class, Object.class);

static final String VALIDATION_VALIDATOR = "javax.validation.Validator";
static final String VALIDATION_CONSTRAINT_VIOLATION_EXCEPTION = "javax.validation.ConstraintViolationException";

static final MethodDescriptor VALIDATION_GET_VALIDATOR = MethodDescriptor.ofMethod(ValidationSupport.class, "getValidator",
"javax.validation.Validator", ArcContainer.class);
static final MethodDescriptor VALIDATION_MAP_VIOLATIONS_TO_JSON = MethodDescriptor
.ofMethod(ValidationSupport.class, "mapViolationsToJson", String.class, Set.class,
HttpServerResponse.class);
static final MethodDescriptor VALIDATION_HANDLE_VIOLATION_EXCEPTION = MethodDescriptor
.ofMethod(ValidationSupport.class.getName(), "handleViolationException",
Void.TYPE.getName(), Methods.VALIDATION_CONSTRAINT_VIOLATION_EXCEPTION,
RoutingContext.class.getName());

static final MethodDescriptor VALIDATOR_VALIDATE = MethodDescriptor
.ofMethod("javax.validation.Validator", "validate", "java.util.Set",
Object.class, Class[].class);
static final MethodDescriptor SET_IS_EMPTY = MethodDescriptor.ofMethod(Set.class, "isEmpty", Boolean.TYPE);

private Methods() {
// Avoid direct instantiation
}
Expand Down Expand Up @@ -204,4 +227,43 @@ static void setContentTypeToJson(ResultHandle response, BytecodeCreator invoke)
branch.invokeInterfaceMethod(MULTIMAP_SET, headers, ct, branch.load("application/json"));
branch.close();
}

/**
* Generate the following code:
*
* <pre>
* String s = null;
* Set<ConstraintViolation<Object>> violations = validator.validate(res);
* if (!violations.isEmpty()) {
* s = ValidationSupport.mapViolationsToJson(violations, response);
* } else {
* s = res.encode()
* }
* </pre>
*/
public static ResultHandle validateProducedItem(ResultHandle response, BytecodeCreator writer, ResultHandle res,
FieldCreator validatorField, ResultHandle owner) {

AssignableResultHandle result = writer.createVariable(String.class);
writer.assign(result, writer.loadNull());

ResultHandle validator = writer.readInstanceField(validatorField.getFieldDescriptor(), owner);
ResultHandle violations = writer.invokeInterfaceMethod(
VALIDATOR_VALIDATE, validator, res, writer.newArray(Class.class, 0));

ResultHandle isEmpty = writer.invokeInterfaceMethod(SET_IS_EMPTY, violations);
BranchResult ifNoViolations = writer.ifTrue(isEmpty);

ResultHandle encoded = ifNoViolations.trueBranch().invokeStaticMethod(JSON_ENCODE, res);
ifNoViolations.trueBranch().assign(result, encoded);
ifNoViolations.trueBranch().close();

ResultHandle json = ifNoViolations.falseBranch().invokeStaticMethod(VALIDATION_MAP_VIOLATIONS_TO_JSON, violations,
response);
ifNoViolations.falseBranch().assign(result, json);
ifNoViolations.falseBranch().close();

return result;

}
}
Loading

0 comments on commit e330f52

Please sign in to comment.