REST request argument Validation In Router Function - spring

In Spring controller approach, We could do REST request argument validation using #Valid with Something like this
#PostMapping(REGISTER)
public ResponseEntity<SomeData> registerSomeData(#RequestBody #Valid final SomeData someData) {
...................
}
public class SomeData {
#Size(min = 2, max = 20)
private String firstname;
#Digits(fraction = 0, integer = 10)
private Integer customerID;
#NotNull
private Customer customer;
}
If the request doesn't match these contraints, then Spring framework would throw Bad Request Exception(400).
With Spring5 router functions, I don't understand how we can do this, because we can't give #Valid in router functions.

It's mildly irritating that this useful functionality does not seem to have been carried over into the functional world but it's really not too hard to implement the validation step yourself. Here's how.
Create a bean to perform the validation:
#Component
public class RequestValidator {
#Inject
Validator validator;
public <T> Mono<T> validate(T obj) {
if (obj == null) {
return Mono.error(new IllegalArgumentException());
}
Set<ConstraintViolation<T>> violations = this.validator.validate(obj);
if (violations == null || violations.isEmpty()) {
return Mono.just(obj);
}
return Mono.error(new ConstraintViolationException(violations));
}
}
Now in your handler function include a step that performs the validation. In this example the FindRequest class is a JSON domain model class that contains validation annotations such as #NotEmpty and #NotNull etc. Adapt how you construct the ServerResponse based on this fictitious example that calls a reactive data repository.
#Component
public class MyHandler {
#Inject
RequestValidator validator;
public Mono<ServerResponse> findAllPeople(ServerRequest request) {
return request.bodyToMono(FindRequest.class)
.flatMap(this.validator::validate)
.flatMap(fr -> ServerResponse
.ok()
.body(this.repo.findAllByName(fr.getName()), Person.class));
}
}
The same approach can be used to extend the functionality to handle Flux as well as Mono.

You can't use annotation-based validation with (functional) Spring Webflux. See this answer.
If you absolutely need annotation-based validation, you should know that you can keep using the traditional Spring MVC with Spring 5 (or non-functional Webflux).

I created GeneralValidator class which works javax.validation.Validator
#Component
#RequiredArgsConstructor
public class GeneralValidator {
private final Validator validator;
private <T> void validate(T obj) {
if (obj == null) {
throw new IllegalArgumentException();
}
Set<ConstraintViolation<T>> violations = this.validator.validate(obj);
if (violations != null && !violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
/**
* #param obj object we will validate
* #param next Publisher we will call if don't have any validation hits
*/
public <T> Mono<ServerResponse> validateAndNext(T obj, Mono<ServerResponse> next) {
try {
validate(obj);
return next;
} catch (IllegalArgumentException ex) {
return ServerResponse.badRequest()
.body(new ErrorResponse("Request body is empty or unable to deserialize"), ErrorResponse.class);
} catch (ConstraintViolationException ex) {
return ServerResponse.badRequest()
.body(new ValidationErrorResponse(
"Request body failed validation",
ex.getConstraintViolations()
.stream()
.map(v -> "Field '%s' has value %s but %s"
.formatted(v.getPropertyPath(), v.getInvalidValue(),v.getMessage()))
.collect(Collectors.toList())
), ValidationErrorResponse.class);
}
}
}
How to use it:
...
.POST("/", req -> req.bodyToMono(RequestObject.class)
.flatMap(r -> validator.validateAndNext(r,routeFunction.execute(r)))
)
...
routeFunction.execute:
public #NotNull Mono<ServerResponse> execute(RequestObject request) {
//handling body
}

Related

UpdateById method in Spring Reactive Mongo Router Handler

In Spring Reactive Java how can I write an updateById() method using the Router and Handler?
For example, the Router has this code:
RouterFunctions.route(RequestPredicates.PUT("/employees/{id}").and(RequestPredicates.accept(MediaType.APPLICATION_JSON))
.and(RequestPredicates.contentType(MediaType.APPLICATION_JSON)),
employeeHandler::updateEmployeeById);
My question is how to write the employeeHandler::updateEmployeeById() keeping the ID as same but changing the other members of the Employee object?
public Mono<ServerResponse> updateEmployeeById(ServerRequest serverRequest) {
Mono<Employee> employeeMono = serverRequest.bodyToMono(Employee.class);
<And now what??>
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(employeeMono, Employee.class);
}
The Employee class looks like this:
#Document
#Data
#AllArgsConstructor
#NoArgsConstructor
public class Employee {
#Id
int id;
double salary;
}
Thanks for any help.
First of all, you have to add ReactiveMongoRepository in your classpath. You can also read about it here.
#Repository
public interface EmployeeRepository extends ReactiveMongoRepository<Employee, Integer> {
Mono<Employee> findById(Integer id);
}
Then your updateEmployeeById method can have the following structure:
public Mono<ServerResponse> updateEmployeeById(ServerRequest serverRequest) {
return serverRequest
.bodyToMono(Employee.class)
.doOnSubscribe(e -> log.info("update employee request received"))
.flatMap(employee -> {
Integer id = Integer.parseInt(serverRequest.pathVariable("id"));
return employeeRepository
.findById(id)
.switchIfEmpty(Mono.error(new NotFoundException("employee with " + id + " has not been found")))
// what you need to do is to update already found entity with
// new values. Usually map() function is used for that purpose
// because map is about 'transformation' what is setting new
// values in our case
.map(foundEmployee -> {
foundEmployee.setSalary(employee.getSalary());
return foundEmployee;
});
})
.flatMap(employeeRepository::save)
.doOnError(error -> log.error("error while updating employee", error))
.doOnSuccess(e -> log.info("employee [{}] has been updated", e.getId()))
.flatMap(employee -> ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(BodyInserters.fromValue(employee), Employee.class));
}
UPDATE:
Based on Prana's answer, I have updated the code above merging our solutions in one. Logging with a help of Slf4j was added. And switchIfEmpty() functions for the case when the entity was not found.
I would also suggest your reading about global exception handling which will make your API even better. A part of it I can provide here:
/**
* Returns routing function.
*
* #param errorAttributes errorAttributes
* #return routing function
*/
#Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
private HttpStatus getStatus(Throwable error) {
HttpStatus status;
if (error instanceof NotFoundException) {
status = NOT_FOUND;
} else if (error instanceof ValidationException) {
status = BAD_REQUEST;
} else {
status = INTERNAL_SERVER_ERROR;
}
return status;
}
/**
* Custom global error handler.
*
* #param request request
* #return response
*/
private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
Map<String, Object> errorPropertiesMap = getErrorAttributes(request, false);
Throwable error = getError(request);
HttpStatus errorStatus = getStatus(error);
return ServerResponse
.status(errorStatus)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromValue(errorPropertiesMap));
}
A slightly different version of the above worked without any exceptions:
public Mono<ServerResponse> updateEmployeeById(ServerRequest serverRequest) {
Mono<ServerResponse> notFound = ServerResponse.notFound().build();
Mono<Employee> employeeMono = serverRequest.bodyToMono(Employee.class);
Integer employeeId = Integer.parseInt(serverRequest.pathVariable("id"));
employeeMono = employeeMono.flatMap(employee -> employeeRepository.findById(employeeId)
.map(foundEmployee -> {
foundEmployee.setSalary(employee.getSalary());
return foundEmployee;
})
.flatMap(employeeRepository::save));
return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(employeeMono, Employee.class).switchIfEmpty(notFound);
}
Thanks to Stepan Tsybulski.

How to use Method level validation

I'm trying to validate some parameters used in a method with javax.validation, but I'm having trouble doing it right.
This is my method:
ServiceResponseInterface getEngineTriage(
#NotNull(message = Constants.MANDATORY_PARAMETERS_MISSING) String riskAssessmentId,
#NotNull(message = Constants.MANDATORY_PARAMETERS_MISSING) String participantId,
#Pattern(regexp = "NEW|RENEWAL|EDIT|OPERATION|RATING", flags = Pattern.Flag.CASE_INSENSITIVE, message = Constants.WRONG_PARAMETERS) String eventType) {
~Some code~
return ServiceResponseNoContent.ServiceResponseNoContentBuilder.build();
}
The class has the #Validated annotation, at this point I'm stuck, how can I check when I call the method if the paramethers are validated?
Basically, if your configuration is right, your method is not executed if any validation error occurs. So you need to handle your method with a simple try-catch block.
I will give an example configuration for method level validation in Spring below.
public interface IValidationService {
public boolean methodLevelValidation(#NotNull String param);
}
#Service
#Validated
public class ValidationService implements IValidationService {
#Override
public boolean methodLevelValidation(String param) {
// some business logic here
return true;
}
}
And you can handle any validation errors like below:
#Test
public void testMethodLevelValidationNotPassAndHandle() {
boolean result = false;
try {
result = validationService.methodLevelValidation(null);
Assert.assertTrue(result);
} catch (ConstraintViolationException e) {
Assert.assertFalse(result);
Assert.assertNotNull(e.getMessage());
logger.info(e.getMessage());
}
}
Note: You need to define your validation annotations in your interface if you have implemented your component from one. Otherwise, you can just put it in your bare spring component:
#Component
#Validated
public class BareValidationService {
public boolean methodLevelValidation(#NotNull String param) {
return true;
}
}
Hope this helps, cheers!

How to inject PathVariable id into RequestBody *before* JSR-303 validation is executed?

I'm stuck in an apparently simple problem: I want to perform some custom validation based on the object id in a PUT request.
#RequestMapping(value="/{id}", method=RequestMethod.PUT)
public ResponseEntity<Void> update(#Valid #RequestBody ClientDTO objDto, #PathVariable Integer id) {
Client obj = service.fromDTO(objDto);
service.update(obj);
return ResponseEntity.noContent().build();
}
I'd like to create a custom validator to output a custom message in case I update some field that can't be the same of another object in my database. Something like this:
public class ClientUpdateValidator implements ConstraintValidator<ClientUpdate, ClientDTO> {
#Autowired
private ClientRepository repo;
#Override
public void initialize(ClientInsert ann) {
}
#Override
public boolean isValid(ClientDTO objDto, ConstraintValidatorContext context) {
Client aux = repo.findByName(objDto.getName());
if (aux != null && !aux.getId().equals(objDto.getId())) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("Already exists")
.addPropertyNode("name").addConstraintViolation();
return false;
}
return true;
}
}
However, the object id comes from #PathVariable, not from #RequestBody. I can't call "objDto.getId()" like I did above.
On the other hand, it doesn't make much sense to obligate to fill up the object id in the request body, because this way the path variable would become meaninless.
How can I solve this problem? Is there a way to inject the id from PathVariable into RequestBody object before bean validation is executed? If not, what would be a viable solution? Thanks.
Try to inject httpServletRequest into the custom validator
public class ClientUpdateValidator implements ConstraintValidator<ClientUpdate, ClientDTO> {
#Autowired
private HttpServletRequest request;
#Autowired
private ClientRepository repo;
#Override
public void initialize(ClientInsert ann) {
}
#Override
public boolean isValid(ClientDTO objDto, ConstraintValidatorContext context) {
// for example your path to put endpoint is /client/{id}
Map map = (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
String id = map.get("id");
Client aux = repo.findByName(objDto.getName());
if (aux != null && !aux.getId().equals(id)) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("Already exists")
.addPropertyNode("name").addConstraintViolation();
return false;
}
return true;
}
}

Spring custom validation annotation + Validator implementation

I have a simple POJO which is considered valid if one of the 2 attributes (ssn / clientId) is populated. I have written a custom Spring Validator to do the validation.
The question I have is, is there a way I can annotate my POJO so that my custom Validator is invoked for validation automatically? I'm trying to avoid manually invoking it from multiple places.
public class QueryCriteria {
#NotNull
private QueryBy queryBy;
private String ssn;
private String clientId;
}
public class QueryCriteriaValidator implements org.springframework.validation.Validator {
#Override
public boolean supports(Class<?> clazz) {
return QueryCriteria.class.equals(clazz);
}
#Override
public void validate(Object target, Errors errors) {
QueryCriteria criteria = (QueryCriteria) target;
switch (criteria.getQueryBy()){
case CLIENT:
//validation logic
break;
case SSN:
//validation logic
break;
default:
break;
}
}
}
With Spring MVC, just add the custom validator to your controller's data binder:
#Autowired
private QueryCriteriaValidator validator;
#InitBinder
public void initializeBinder(WebDataBinder binder) {
binder.addValidators(validator);
}
Otherwise, you'll want to inject the custom validator and call it yourself.
#Autowired
private QueryCriteriaValidator validator;
public void process(QueryCriteria criteria) {
check(validator.validate(criteria));
// Continue processing.
}
Só you can create your own validation. Example :
#RequiredParam
#RegexpValidation("^[\\p{IsLatin}]{2}[\\p{IsLatin}-]* [\\p{IsLatin}]{2}[\\p{IsLatin}- ]*$")
private String name;
Create a class validator e send the form that you want to validate:
Map<String, String> invalidFieldMap = Validator.validate(form);
if (invalidFieldMap.size() > 0)
{
LOGGER.warn("Invalid parameters");
}
Look this gist Validator.java https://gist.github.com/cmsandiga/cb7ab4d537ef4d4691f7

How to validate Spring MVC #PathVariable values?

For a simple RESTful JSON api implemented in Spring MVC, can I use Bean Validation (JSR-303) to validate the path variables passed into the handler method?
For example:
#RequestMapping(value = "/number/{customerNumber}")
#ResponseBody
public ResponseObject searchByNumber(#PathVariable("customerNumber") String customerNumber) {
...
}
Here, I need to validate the customerNumber variables's length using Bean validation. Is this possible with Spring MVC v3.x.x? If not, what's the best approach for this type of validations?
Thanks.
Spring does not support #javax.validation.Valid on #PathVariable annotated parameters in handler methods. There was an Improvement request, but it is still unresolved.
Your best bet is to just do your custom validation in the handler method body or consider using org.springframework.validation.annotation.Validated as suggested in other answers.
You can use like this:
use org.springframework.validation.annotation.Validated to valid RequestParam or PathVariable.
*
* Variant of JSR-303's {#link javax.validation.Valid}, supporting the
* specification of validation groups. Designed for convenient use with
* Spring's JSR-303 support but not JSR-303 specific.
*
step.1 init ValidationConfig
#Configuration
public class ValidationConfig {
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
return processor;
}
}
step.2 Add #Validated to your controller handler class, Like:
#RequestMapping(value = "poo/foo")
#Validated
public class FooController {
...
}
step.3 Add validators to your handler method:
#RequestMapping(value = "{id}", method = RequestMethod.DELETE)
public ResponseEntity<Foo> delete(
#PathVariable("id") #Size(min = 1) #CustomerValidator int id) throws RestException {
// do something
return new ResponseEntity(HttpStatus.OK);
}
final step. Add exception resolver to your context:
#Component
public class BindExceptionResolver implements HandlerExceptionResolver {
#Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
if (ex.getClass().equals(BindException.class)) {
BindException exception = (BindException) ex;
List<FieldError> fieldErrors = exception.getFieldErrors();
return new ModelAndView(new MappingJackson2JsonView(), buildErrorModel(request, response, fieldErrors));
}
}
}
The solution is simple:
#GetMapping(value = {"/", "/{hash:[a-fA-F0-9]{40}}"})
public String request(#PathVariable(value = "hash", required = false) String historyHash)
{
// Accepted requests: either "/" or "/{40 character long hash}"
}
And yes, PathVariables are ment to be validated, like any user input.
Instead of using #PathVariable, you can take advantage of Spring MVC ability to map path variables into a bean:
#RestController
#RequestMapping("/user")
public class UserController {
#GetMapping("/{id}")
public void get(#Valid GetDto dto) {
// dto.getId() is the path variable
}
}
And the bean contains the actual validation rules:
#Data
public class GetDto {
#Min(1) #Max(99)
private long id;
}
Make sure that your path variables ({id}) correspond to the bean fields (id);
#PathVariable is not meant to be validated in order to send back a readable message to the user. As principle a pathVariable should never be invalid. If a pathVariable is invalid the reason can be:
a bug generated a bad url (an href in jsp for example). No #Valid is
needed and no message is needed, just fix the code;
"the user" is manipulating the url.
Again, no #Valid is needed, no meaningful message to the user should
be given.
In both cases just leave an exception bubble up until it is catched by
the usual Spring ExceptionHandlers in order to generate a nice
error page or a meaningful json response indicating the error. In
order to get this result you can do some validation using custom editors.
Create a CustomerNumber class, possibly as immutable (implementing a CharSequence is not needed but allows you to use it basically as if it were a String)
public class CustomerNumber implements CharSequence {
private String customerNumber;
public CustomerNumber(String customerNumber) {
this.customerNumber = customerNumber;
}
#Override
public String toString() {
return customerNumber == null ? null : customerNumber.toString();
}
#Override
public int length() {
return customerNumber.length();
}
#Override
public char charAt(int index) {
return customerNumber.charAt(index);
}
#Override
public CharSequence subSequence(int start, int end) {
return customerNumber.subSequence(start, end);
}
#Override
public boolean equals(Object obj) {
return customerNumber.equals(obj);
}
#Override
public int hashCode() {
return customerNumber.hashCode();
}
}
Create an editor implementing your validation logic (in this case no whitespaces and fixed length, just as an example)
public class CustomerNumberEditor extends PropertyEditorSupport {
#Override
public void setAsText(String text) throws IllegalArgumentException {
if (StringUtils.hasText(text) && !StringUtils.containsWhitespace(text) && text.length() == YOUR_LENGTH) {
setValue(new CustomerNumber(text));
} else {
throw new IllegalArgumentException();
// you could also subclass and throw IllegalArgumentException
// in order to manage a more detailed error message
}
}
#Override
public String getAsText() {
return ((CustomerNumber) this.getValue()).toString();
}
}
Register the editor in the Controller
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(CustomerNumber.class, new CustomerNumberEditor());
// ... other editors
}
Change the signature of your controller method accepting CustomerNumber instead of String (whatever your ResponseObject is ...)
#RequestMapping(value = "/number/{customerNumber}")
#ResponseBody
public ResponseObject searchByNumber(#PathVariable("customerNumber") CustomerNumber customerNumber) {
...
}
You can create the answer you want by using the fields in the ConstraintViolationException with the following method;
#ExceptionHandler(ConstraintViolationException.class)
protected ResponseEntity<Object> handlePathVariableError(final ConstraintViolationException exception) {
log.error(exception.getMessage(), exception);
final List<SisSubError> subErrors = new ArrayList<>();
exception.getConstraintViolations().forEach(constraintViolation -> subErrors.add(generateSubError(constraintViolation)));
final SisError error = generateErrorWithSubErrors(VALIDATION_ERROR, HttpStatus.BAD_REQUEST, subErrors);
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
You need to added an #Validated annotation to Controller class and any validation annotation before path variable field
Path variable may not be linked with any bean in your system. What do you want to annotate with JSR-303 annotations?
To validate path variable you should use this approach Problem validating #PathVariable url on spring 3 mvc
Actually there is a very simple solution to this. Add or override the same controller method with its request mapping not having the placeholder for the path variable and throw ResponseStatusException from it. Code given below
#RequestMapping(value = "/number")
#ResponseBody
public ResponseObject searchByNumber() {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"customer number missing")
}

Resources