[BAEL-1502] spring-5-reactive | Validation for Functional Endpoints (#5437)

*  Added validation for functional endpoints scenarios:
* validating in handler explicitly
* created abstract handler with validation steps
* using validation handlers with two implementations

* * added annotated entity to be used with springvalidator

* * added tests and cleaning the code slightly
This commit is contained in:
rozagerardo 2018-10-16 02:22:19 -03:00 committed by maibin
parent a8f3ef312b
commit 45194b5edc
13 changed files with 448 additions and 0 deletions

View File

@ -0,0 +1,12 @@
package com.baeldung.validations.functional;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class FunctionalValidationsApplication {
public static void main(String[] args) {
SpringApplication.run(FunctionalValidationsApplication.class, args);
}
}

View File

@ -0,0 +1,45 @@
package com.baeldung.validations.functional.handlers;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ResponseStatusException;
import reactor.core.publisher.Mono;
public abstract class AbstractValidationHandler<T, U extends Validator> {
private final Class<T> validationClass;
private final U validator;
protected AbstractValidationHandler(Class<T> clazz, U validator) {
this.validationClass = clazz;
this.validator = validator;
}
abstract protected Mono<ServerResponse> processBody(T validBody, final ServerRequest originalRequest);
public final Mono<ServerResponse> handleRequest(final ServerRequest request) {
return request.bodyToMono(this.validationClass)
.flatMap(body -> {
Errors errors = new BeanPropertyBindingResult(body, this.validationClass.getName());
this.validator.validate(body, errors);
if (errors == null || errors.getAllErrors()
.isEmpty()) {
return processBody(body, request);
} else {
return onValidationErrors(errors, body, request);
}
});
}
protected Mono<ServerResponse> onValidationErrors(Errors errors, T invalidBody, final ServerRequest request) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, errors.getAllErrors()
.toString());
}
}

View File

@ -0,0 +1,43 @@
package com.baeldung.validations.functional.handlers;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ResponseStatusException;
import com.baeldung.validations.functional.model.CustomRequestEntity;
import com.baeldung.validations.functional.validators.CustomRequestEntityValidator;
import reactor.core.publisher.Mono;
@Component
public class FunctionalHandler {
public Mono<ServerResponse> handleRequest(final ServerRequest request) {
Validator validator = new CustomRequestEntityValidator();
Mono<String> responseBody = request.bodyToMono(CustomRequestEntity.class)
.map(body -> {
Errors errors = new BeanPropertyBindingResult(body, CustomRequestEntity.class.getName());
validator.validate(body, errors);
if (errors == null || errors.getAllErrors()
.isEmpty()) {
return String.format("Hi, %s [%s]!", body.getName(), body.getCode());
} else {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, errors.getAllErrors()
.toString());
}
});
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(responseBody, String.class);
}
}

View File

@ -0,0 +1,30 @@
package com.baeldung.validations.functional.handlers.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import com.baeldung.validations.functional.handlers.AbstractValidationHandler;
import com.baeldung.validations.functional.model.AnnotatedRequestEntity;
import reactor.core.publisher.Mono;
@Component
public class AnnotatedRequestEntityValidationHandler extends AbstractValidationHandler<AnnotatedRequestEntity, Validator> {
private AnnotatedRequestEntityValidationHandler(@Autowired Validator validator) {
super(AnnotatedRequestEntity.class, validator);
}
@Override
protected Mono<ServerResponse> processBody(AnnotatedRequestEntity validBody, ServerRequest originalRequest) {
String responseBody = String.format("Hi, %s. Password: %s!", validBody.getUser(), validBody.getPassword());
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(responseBody), String.class);
}
}

View File

@ -0,0 +1,37 @@
package com.baeldung.validations.functional.handlers.impl;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import com.baeldung.validations.functional.handlers.AbstractValidationHandler;
import com.baeldung.validations.functional.model.CustomRequestEntity;
import com.baeldung.validations.functional.validators.CustomRequestEntityValidator;
import reactor.core.publisher.Mono;
@Component
public class CustomRequestEntityValidationHandler extends AbstractValidationHandler<CustomRequestEntity, CustomRequestEntityValidator> {
private CustomRequestEntityValidationHandler() {
super(CustomRequestEntity.class, new CustomRequestEntityValidator());
}
@Override
protected Mono<ServerResponse> processBody(CustomRequestEntity validBody, ServerRequest originalRequest) {
String responseBody = String.format("Hi, %s [%s]!", validBody.getName(), validBody.getCode());
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(responseBody), String.class);
}
@Override
protected Mono<ServerResponse> onValidationErrors(Errors errors, CustomRequestEntity invalidBody, final ServerRequest request) {
return ServerResponse.badRequest()
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(String.format("Custom message showing the errors: %s", errors.getAllErrors()
.toString())), String.class);
}
}

View File

@ -0,0 +1,28 @@
package com.baeldung.validations.functional.handlers.impl;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import com.baeldung.validations.functional.handlers.AbstractValidationHandler;
import com.baeldung.validations.functional.model.OtherEntity;
import com.baeldung.validations.functional.validators.OtherEntityValidator;
import reactor.core.publisher.Mono;
@Component
public class OtherEntityValidationHandler extends AbstractValidationHandler<OtherEntity, OtherEntityValidator> {
private OtherEntityValidationHandler() {
super(OtherEntity.class, new OtherEntityValidator());
}
@Override
protected Mono<ServerResponse> processBody(OtherEntity validBody, ServerRequest originalRequest) {
String responseBody = String.format("Other object with item %s and quantity %s!", validBody.getItem(), validBody.getQuantity());
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(responseBody), String.class);
}
}

View File

@ -0,0 +1,23 @@
package com.baeldung.validations.functional.model;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class AnnotatedRequestEntity {
@NotNull
private String user;
@NotNull
@Size(min = 4, max = 7)
private String password;
}

View File

@ -0,0 +1,17 @@
package com.baeldung.validations.functional.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class CustomRequestEntity {
private String name;
private String code;
}

View File

@ -0,0 +1,17 @@
package com.baeldung.validations.functional.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class OtherEntity {
private String item;
private Integer quantity;
}

View File

@ -0,0 +1,29 @@
package com.baeldung.validations.functional.routers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import com.baeldung.validations.functional.handlers.FunctionalHandler;
import com.baeldung.validations.functional.handlers.impl.AnnotatedRequestEntityValidationHandler;
import com.baeldung.validations.functional.handlers.impl.CustomRequestEntityValidationHandler;
import com.baeldung.validations.functional.handlers.impl.OtherEntityValidationHandler;
@Configuration
public class ValidationsRouters {
@Bean
public RouterFunction<ServerResponse> responseHeaderRoute(@Autowired CustomRequestEntityValidationHandler dryHandler,
@Autowired FunctionalHandler complexHandler,
@Autowired OtherEntityValidationHandler otherHandler,
@Autowired AnnotatedRequestEntityValidationHandler annotatedEntityHandler) {
return RouterFunctions.route(RequestPredicates.POST("/complex-handler-functional-validation"), complexHandler::handleRequest)
.andRoute(RequestPredicates.POST("/dry-functional-validation"), dryHandler::handleRequest)
.andRoute(RequestPredicates.POST("/other-dry-functional-validation"), otherHandler::handleRequest)
.andRoute(RequestPredicates.POST("/annotated-functional-validation"), annotatedEntityHandler::handleRequest);
}
}

View File

@ -0,0 +1,29 @@
package com.baeldung.validations.functional.validators;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import com.baeldung.validations.functional.model.CustomRequestEntity;
public class CustomRequestEntityValidator implements Validator {
private static final int MINIMUM_CODE_LENGTH = 6;
@Override
public boolean supports(Class<?> clazz) {
return CustomRequestEntity.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "code", "field.required");
CustomRequestEntity request = (CustomRequestEntity) target;
if (request.getCode() != null && request.getCode()
.trim()
.length() < MINIMUM_CODE_LENGTH) {
errors.rejectValue("code", "field.min.length", new Object[] { Integer.valueOf(MINIMUM_CODE_LENGTH) }, "The code must be at least [" + MINIMUM_CODE_LENGTH + "] characters in length.");
}
}
}

View File

@ -0,0 +1,27 @@
package com.baeldung.validations.functional.validators;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import com.baeldung.validations.functional.model.OtherEntity;
public class OtherEntityValidator implements Validator {
private static final int MIN_ITEM_QUANTITY = 1;
@Override
public boolean supports(Class<?> clazz) {
return OtherEntity.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "item", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "quantity", "field.required");
OtherEntity request = (OtherEntity) target;
if (request.getQuantity() != null && request.getQuantity() < MIN_ITEM_QUANTITY) {
errors.rejectValue("quantity", "field.min.length", new Object[] { Integer.valueOf(MIN_ITEM_QUANTITY) }, "There must be at least one item");
}
}
}

View File

@ -0,0 +1,111 @@
package com.baeldung.validations.functional;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec;
import com.baeldung.validations.functional.model.AnnotatedRequestEntity;
import com.baeldung.validations.functional.model.CustomRequestEntity;
import reactor.core.publisher.Mono;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class FunctionalEndpointValidationsLiveTest {
private static final String BASE_URL = "http://localhost:8080";
private static final String COMPLEX_EP_URL = BASE_URL + "/complex-handler-functional-validation";
private static final String DRY_EP_URL = BASE_URL + "/dry-functional-validation";
private static final String ANNOTATIONS_EP_URL = BASE_URL + "/annotated-functional-validation";
private static WebTestClient client;
@BeforeAll
public static void setup() {
client = WebTestClient.bindToServer()
.baseUrl(BASE_URL)
.build();
}
@Test
public void whenRequestingDryEPWithInvalidBody_thenObtainBadRequest() {
CustomRequestEntity body = new CustomRequestEntity("name", "123");
ResponseSpec response = client.post()
.uri(DRY_EP_URL)
.body(Mono.just(body), CustomRequestEntity.class)
.exchange();
response.expectStatus()
.isBadRequest();
}
@Test
public void whenRequestingComplexEPWithInvalidBody_thenObtainBadRequest() {
CustomRequestEntity body = new CustomRequestEntity("name", "123");
ResponseSpec response = client.post()
.uri(COMPLEX_EP_URL)
.body(Mono.just(body), CustomRequestEntity.class)
.exchange();
response.expectStatus()
.isBadRequest();
}
@Test
public void whenRequestingAnnotatedEPWithInvalidBody_thenObtainBadRequest() {
AnnotatedRequestEntity body = new AnnotatedRequestEntity("user", "passwordlongerthan7digits");
ResponseSpec response = client.post()
.uri(ANNOTATIONS_EP_URL)
.body(Mono.just(body), AnnotatedRequestEntity.class)
.exchange();
response.expectStatus()
.isBadRequest();
}
@Test
public void whenRequestingDryEPWithValidBody_thenObtainBadRequest() {
CustomRequestEntity body = new CustomRequestEntity("name", "1234567");
ResponseSpec response = client.post()
.uri(DRY_EP_URL)
.body(Mono.just(body), CustomRequestEntity.class)
.exchange();
response.expectStatus()
.isOk();
}
@Test
public void whenRequestingComplexEPWithValidBody_thenObtainBadRequest() {
CustomRequestEntity body = new CustomRequestEntity("name", "1234567");
ResponseSpec response = client.post()
.uri(COMPLEX_EP_URL)
.body(Mono.just(body), CustomRequestEntity.class)
.exchange();
response.expectStatus()
.isOk();
}
@Test
public void whenRequestingAnnotatedEPWithValidBody_thenObtainBadRequest() {
AnnotatedRequestEntity body = new AnnotatedRequestEntity("user", "12345");
ResponseSpec response = client.post()
.uri(ANNOTATIONS_EP_URL)
.body(Mono.just(body), AnnotatedRequestEntity.class)
.exchange();
response.expectStatus()
.isOk();
}
}