[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:
parent
a8f3ef312b
commit
45194b5edc
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue