I'm (new in spring development) creating REST API for my application, CRUD operations are implemented successfully but now I want to implement server side validation. I've also read that there are several ways through which validation could be implemented.
Using given annotations -> #notempty, #email, etc...
Using custom validation -> extending validators
I want to implement both of them in my application, With reference to that,
is it good approach to follow?
OR
Is there any other ways through which validation can be implemented?
Controller
#RestController
public class EmployeeController {
#Autowired
DataServices dataServices;
#Autowired
EmployeeValidator employeeValidator;
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(employeeValidator);
}
#RequestMapping(value = "/employee/", method = RequestMethod.POST)
public ResponseEntity<Object> createUser(
#Valid #RequestBody Employee employee,
UriComponentsBuilder ucBuilder) throws Exception,
DataIntegrityViolationException {
if (dataServices.addEmployee(employee) == 0) {
Error error = new Error(1, "Data integrity violation",
"Email id is already exists.");
return new ResponseEntity<Object>(error, HttpStatus.CONFLICT);
}
HttpHeaders headers = new HttpHeaders();
headers.setLocation(ucBuilder.path("/employee/{id}")
.buildAndExpand(employee.getId()).toUri());
Status status = new Status(1, "Employee has been added successfully.");
return new ResponseEntity<Object>(status, headers, HttpStatus.CREATED);
}
}
Error Handler
#ControllerAdvice
public class RestErrorHandler {
private static final Logger logger = Logger
.getLogger(RestErrorHandler.class);
private MessageSource messageSource;
#Autowired
public RestErrorHandler(MessageSource messageSource) {
this.messageSource = messageSource;
}
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public ValidationErrorDTO processValidationError(
MethodArgumentNotValidException ex) {
logger.debug("Handling form validation error");
BindingResult result = ex.getBindingResult();
List<FieldError> fieldErrors = result.getFieldErrors();
return processFieldErrors(fieldErrors);
}
private ValidationErrorDTO processFieldErrors(List<FieldError> fieldErrors) {
ValidationErrorDTO dto = new ValidationErrorDTO();
for (FieldError fieldError : fieldErrors) {
String localizedErrorMessage = resolveLocalizedErrorMessage(fieldError);
dto.addFieldError(fieldError.getField(), localizedErrorMessage,
fieldError.getDefaultMessage());
}
return dto;
}
private String resolveLocalizedErrorMessage(FieldError fieldError) {
Locale currentLocale = LocaleContextHolder.getLocale();
String localizedErrorMessage = messageSource.getMessage(fieldError,
currentLocale);
// If a message was not found, return the most accurate field error code
// instead.
// You can remove this check if you prefer to get the default error
// message.
if (localizedErrorMessage.equals(fieldError.getDefaultMessage())) {
String[] fieldErrorCodes = fieldError.getCodes();
localizedErrorMessage = fieldErrorCodes[0];
}
return localizedErrorMessage;
}
}
Validator
#Component
public class EmployeeValidator implements Validator {
public boolean supports(Class clazz) {
return Employee.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", errors
.getFieldError().getCode(), "First name is required.");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", errors
.getFieldError().getCode(),
"Last name is required.");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email", errors
.getFieldError().getCode(),
"Email is required.");
}
}
Model
#Entity
#Table(name = "employee")
#JsonIgnoreProperties({ "hibernateLazyInitializer", "handler" })
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue
#Column(name = "id")
private long id;
// #NotEmpty(message = "Please enter first name")
#Column(name = "first_name")
private String firstName;
// #NotEmpty(message = "Please enter last name")
#Column(name = "last_name")
private String lastName;
// #NotEmpty(message = "Please enter email address")
#Email(message = "Please enter valid email address")
#Column(name = "email", unique = true)
private String email;
#NotEmpty(message = "Please enter mobile number")
#Size(min = 10, message = "Please enter valid mobile number")
#Column(name = "phone")
private String phone;
//Getter and Setter
}
In your aproach you are using Server side validations but only in the controller layer. Have you tryied to use Bussines layer validations, like Hibernate Validation API http://hibernate.org/validator/
I've used it in a recent project and form me it's a great way to keep data consistent. Some tweaks and utils were needed to make it work as we wanted but it was not too difficult. For example, this validations, by default, are only checked just after persisting a Object in database, but in our controller we needed to make this validations earlier, so you we had to implement a way to call validation mechanism that relies on hibernate validation mechanism. Or, as another example, we had to develop a similar system on a web service to return errors when incoming data was not valid.
One way to use validations when needed is to implement it on all your bussines objects. They can inherit for a class like this:
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.xml.bind.annotation.XmlTransient;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.fasterxml.jackson.annotation.JsonIgnore;
public abstract class BusinessObject implements Serializable, IObjectWithReport, IBusinessObject {
private static Log log = LogFactory.getLog(BusinessObject.class.getName());
private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
#JsonIgnore
private Set<ConstraintViolation<BusinessObject>> errors;
/* Validation methods */
public final boolean valid() {
preValidate();
errors = new HashSet<ConstraintViolation<BusinessObject>>();
errors = validator.validate(this);
postValidate();
return errors.isEmpty();
}
/**
* Method to be overwriten in subclases so any BO can make some arrangement before checking valid
*/
protected void preValidate() {
log.trace("Generic prevalidate of " + this.getClass().getName());
}
/**
* Method to be overwriten in subclases so any BO can make some arrangement once validation has been made
*/
protected void postValidate() {
log.trace("Generic postValidate of " + this.getClass().getName());
}
public Set<ConstraintViolation<BusinessObject>> getErrors() {
return errors;
}
public boolean hasErrors() {
return errors != null && !errors.isEmpty();
}
}
Note that i use standard javax.validation.Validation API (check references here JPA 2.0 : what is javax.validation.* package?). But the implementation i use is the one from Hibernate.
Pros:
Validations are placed in one single layer, not spread along various layers. So they are easier to maintain.
Better model consistency because of that data is always validated in the same way, independently of how it was generated (user input, web service, pulled from other systems, etc).
Cons:
You need to develop some utils so you can use Model Validations in other layers, but it's not very dificult.
May be overkill if you have a simple project, whithout complexities like many info sources (user input, webservices input, rest services, other database systemas, etc) or interactions.
Related
I have a spring boot project in which I'm facing a data validation issue while doing a post request.
Problem
While doing a post request I'm mapping the request body to a POJO which does not have any javax validations in itself, but the class has fields of another two POJOs which have data validations in them. How can I trigger data validations in the inner POJOs programmatically and throw relevant exceptions if required. I'm using Spring boot v2.5.2.
In the post request I have:
#PostMapping("/signup")
public ResponseEntity<String> addNewUser(#RequestBody #Valid NewUserDetailsPojo newUserDetailsPojo) {
log.debug("Adding new User: {}", newUserDetailsPojo);
Integer userId = userService.addNewUser(newUserDetailsPojo);
if (userId == null) {
return ResponseEntity.badRequest().build();
} else {
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(userId).toUri();
return ResponseEntity.created(location).build();
}
}
Where NewUserDetailsPojo is a simple POJO class having structure:
public class NewUserDetailsPojo {
private BasicDetailsPojo basicDetailsPojo;
private DoctorPojo doctorPojo;
// constructor
public NewUserDetailsPojo(BasicDetailsPojo basicDetailsPojo, DoctorPojo doctorPojo) {
/** before mapping the incoming data to the fields, I want to validate the data
* with my predefined javax.validations constraints declared in the respective
* classes
*/
this.basicDetailsPojo = basicDetailsPojo;
this.doctorPojo = doctorPojo;
}
}
So, as I mentioned earlier, NewUserDetailsPojo does not have any data validations in itself, but its two fields which are of the class
BasicDetailsPojo
DoctorPojo
have data validations in them. I want to invoke javax validations in the constructor of NewUserDetailsPojo and throw suitable exceptions if nessecery.
I'm giving the structures of BasicDetailsPojo and DoctorPojo below:
public class BasicDetailsPojo {
#Size(min = 5, message = "Name should be at least 5 characters long")
private String name;
#Email
private String email;
#Size(min = 10, message = "Contact number must be of 10 digits")
private String contactNo;
private String role;
#NotNull
#Size(min = 8, message = "Password must be 8 characters long")
private String password;
}
This is the structure of DoctorPojo:
public class DoctorPojo extends BasicDetailsPojo {
#NotNull
private String regNo;
#NotNull
private String degree;
#NotNull
private String specialization;
private String experience;
}
I think your problem will be fixed by putting #Valid annotation in the NewUserDetailsPojo class over the basicDetailsPojo and doctorPojo atributes
https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/?v=5.3#section-object-graph-validation
I recently came up to an issue related to validation. Typically, I am building a REST api that allow users to create their account including avatars. All of the information should be submitted when user clicks to Register button. So, my server will then receive a request that includes some fields like name (string), birthday (datetime), ... and avatar (multipart file). So, the question is how to validate the received file is a truly image and has an allowed size and simultaneously validate that the others (email, password) are also valid.
For the case that all fields is text, we can easily validate them using the combination of annotations like this
Controller
#PostMapping(path = "")
public ResponseEntity<?> createNewAccount(#RequestBody #Valid RegisterRequest registerRequest) {
Long resourceId = service.createNewCoderAccount(registerRequest);
return ResponseEntity.created(location(resourceId)).build();
}
Request DTO
#ConfirmedPassword
public class RegisterRequest extends BaseRequest implements ShouldConfirmPassword {
#NotBlank(message = "Field 'email' is required but not be given")
#Email
#Unique(message = "Email has been already in use", service = UserValidatorService.class, column = "email")
private String email;
#NotBlank(message = "Field 'password' is required but not be given")
#Size(min = 6, message = "Password should contain at least 6 characters")
private String password;
#NotBlank(message = "Field 'confirmPassword' is required but not be given")
private String confirmPassword;
#NotBlank(message = "Field 'firstName' is required but not be given")
private String firstName;
#NotBlank(message = "Field 'lastName' is required but not be given")
private String lastName;
}
Or in case that the request containing only file(s), we can absolutely do like this
Controller
#PostMapping(path = "/{id}")
public ResponseEntity<?> editChallengeMetadata(
#ModelAttribute ChallengeMetadataRequest request,
BindingResult bindingResult,
#PathVariable("id") Long id,
#CurrentUser User user
) throws BindException {
challengeMetadataRequestValidator.validate(request, bindingResult);
if (bindingResult.hasErrors()) {
throw new BindException(bindingResult);
}
Long challengeId = service.updateChallengeMetadata(id, request, user);
return ResponseEntity.ok(RestResponse.build(challengeId, HttpStatus.OK));
}
Validator
public class ChallengeMetadataRequestValidator implements Validator {
#Override
public boolean supports(#NonNull Class<?> aClass) {
return ChallengeMetadataRequest.class.isAssignableFrom(aClass);
}
#Override
public void validate(#NonNull Object o, #NonNull Errors errors) {
ChallengeMetadataRequest request = (ChallengeMetadataRequest) o;
if (request.getBanner() != null && !request.getBanner().isEmpty()) {
if (!List.of("image/jpeg", "image/png").contains(request.getBanner().getContentType())) {
errors.rejectValue("banner", "challenge.mime-type.not-supported", new String[]{request.getBanner().getContentType()}, "Mime-type is not supported");
}
}
}
}
As you seen above, if I wrap all data (including avatar) in a DTO class, I definitely write its own validator. But what will happen if then I have to write manually hundreds validators like that.
So, do anyone have any idea about it, typically, make the multipart/form-data request becomes simalar with application/json request ?
Thanks and regards,
User model:
#Entity
#Table(name="user")
public class User {
#Id #GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
#NotBlank
#Column(name="username")
private String username;
#NotEmpty
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name="user_role", joinColumns = {#JoinColumn(name="user_id")},
inverseJoinColumns = {#JoinColumn(name="role_id")})
private Set<Role> roles;
}
Controller:
#RequestMapping(value = {"/users/edit/{id}"}, method = RequestMethod.POST)
public String editUser(ModelMap model, #Valid #ModelAttribute("user") User user, BindingResult result) {
if(result.hasErrors()) {
return "AddUserView";
}
return "redirect:/users";
}
Test with MockMVC:
#Test
public void performUpdateUserTest() throws Throwable {
mockMvc.perform(post("/users/edit/{id}", user.getId())
.param("username", "User"));
}
Well, fine, I can pass a param username as always using param(). But what should I do with ROLES? This field is a separate object. I can't pass it using param(). Then how is it possible to pass it in the test?
The only way out I found is to create an entity and pass it using .flashAttr():
#Test
public void performUpdateUserTest() throws Throwable {
User user = new User("User", new HashSet<Role>(Arrays.asList(new Role("USER"))));
mockMvc.perform(post("/users/edit/{id}", user.getId())
.flashAttr("user", user));
}
But then, what if I need to test that user can't be updated because of binding error in the ROLES field(ROLES can't be null, and suppose, it was set as null)? Thus, I'm not able to create user(and use it with .flashAttr) already with a binding error as the exception will be thrown. And I still have to pass it separately.
Well, after a long time of searching, I found out that I should add a converter to the MockMVC. What converter is you can read HERE, for instance.
I had it already in my project but didn't realize that it didn't work with MockMVC.
So, you can add the converter to MockMVC like that:
#Autowired
private StringToRoleConverter stringToRoleConverter;
#Before
public void init() {
FormattingConversionService cs = new FormattingConversionService();
cs.addConverter(stringToRoleConverter);
mockMvc = MockMvcBuilders.standaloneSetup(userController)
.setConversionService(cs)
.build();
}
Converter itself:
#Component
public class StringToRoleConverter implements Converter<String, Role> {
#Autowired
private RoleService roleService;
#Override
public Role convert(String id) {
Role role = roleService.findById(Integer.valueOf(id));
return role;
}
}
And then I can add param like that:
mockMvc.perform(post("/users/edit/{id}", user.getId())
.param("roles", "2"))
though I'm passing a string there, it will be converter to Role with the help of Spring converter.
I'm trying to implement the server side validation using spring. but its not validating. Here is my code sample.
#RestController
#RequestMapping("/api/v1/note")
public class NoteController {
#Autowired
private final NoteService noteService;
#PostMapping
public ResponseEntity<String> create(#Valid #RequestBody final NoteDto noteDto){
noteService.create(noteDto);
return new ResponseEntity<>("sucess", HttpStatus.CREATED);
}
}
POJO..
#Data
#JsonInclude(value = Include.NON_NULL)
public class NoteDto {
#NotEmpty(message = "Building No can't be empty!")
private String buildingNo;
private String buildingName;
#NotEmpty(message = "Street can't be empty!")
}
What am missing here
#Valid annotation that triggers validations on the NoteDto (in this case #NotNull and #Future). These annotations could come from different JSR-303 providers (e.g, Hibernate, Spring..etc).
Example
static class NoteDto {
#NotNull #Future
private Date date;
}
And Remove final.
The annotated spring validation on fields of a POJO works when it is created from json request body. However, when I create the same object manually (using setters) and want to trigger validation, I'm not sure how to do that.
Here is the Registration class, which has Builder inner class that can build the object. In the build method I would like to trigger spring validation. Please scroll to the bottom and check Builder.build() and Builder.valiate() methods to see current implementation. I'm using javax.validation.Validator to trigger validation, but I prefer to leverage spring validation if possible.
package com.projcore.dao;
import com.projcore.util.ToString;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.hibernate.validator.constraints.NotEmpty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.constraints.Size;
import java.util.List;
import java.util.Set;
/**
* The data transfer object that contains the information of a Registration
* and validation rules for attributes.
*/
#JsonIgnoreProperties(ignoreUnknown = true)
public final class Registration {
private static final Logger LOGGER = LoggerFactory.getLogger(Registration.class);
private String id;
#NotEmpty
#Size(max = 255)
private String messageId;
#NotEmpty
#Size(max = 255)
private String version;
#Size(max = 255)
private String system;
public Registration() {
}
private Registration(Builder builder) {
this.id = builder.id;
this.messageId = builder.messageId;
this.version = builder.version;
this.system = builder.system;
}
public static Builder getBuilder() {
return new Builder();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getMessageId() {
return messageId;
}
public void setMessageId(String messageId) {
this.messageId = messageId;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getSystem() {
return system;
}
public void setSystem(String system) {
this.system = system;
}
#Override
public String toString() {
return ToString.create(this);
}
/**
* Builder pattern makes the object easier to construct in one line.
*/
public static class Builder {
private String id;
private String messageId;
private String version;
private String system;
private Builder() {}
public Builder id(String id) {
this.id = id;
return this;
}
public Builder messageId(String messageId) {
this.messageId = messageId;
return this;
}
public Builder version(String version) {
this.version = version;
return this;
}
public Builder system(String system) {
this.system = system;
return this;
}
public Registration build() {
Registration entry = new Registration(this);
// *** Would like to trigger spring validation here ***
Set violations = validate(entry);
if (violations.isEmpty())
return entry;
else
throw new RuntimeException(violations.toString());
}
private Set validate(Registration entry) {
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<Registration>> constraintViolations = validator.validate(entry);
return constraintViolations;
}
}
}
Validation works fine here:
#RequestMapping(method = RequestMethod.POST)
#ResponseStatus(HttpStatus.CREATED)
Registration create(#RequestBody #Valid Registration registration)
Solution:
Removed Registraion.Builder.validate(). Updated Registraion.Builder.build() to:
public Registration build() {
Registration entry = new Registration(this);
return (Registration) ValidatorUtil.validate(entry);
}
ValidationUtil.java
package projcore.util;
import com.ericsson.admcore.error.InvalidDataException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;
public class ValidatorUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ValidatorUtil.class);
private static final Validator javaxValidator = Validation.buildDefaultValidatorFactory().getValidator();
private static final SpringValidatorAdapter validator = new SpringValidatorAdapter(javaxValidator);
public static Object validate(Object entry) {
Errors errors = new BeanPropertyBindingResult(entry, entry.getClass().getName());
validator.validate(entry, errors);
if (errors == null || errors.getAllErrors().isEmpty())
return entry;
else {
LOGGER.error(errors.toString());
throw new InvalidDataException(errors.getAllErrors().toString(), errors);
}
}
}
InvalidDataException.java
package projcore.error;
import org.springframework.validation.Errors;
/**
* This exception is thrown when the dao has invalid data.
*/
public class InvalidDataException extends RuntimeException {
private Errors errors;
public InvalidDataException(String msg, Errors errors) {
super(msg);
setErrors(errors);
}
public Errors getErrors() {
return errors;
}
public void setErrors(Errors errors) {
this.errors = errors;
}
}
Spring provides full support for the JSR-303 Bean Validation API. This includes convenient support for bootstrapping a JSR-303 implementation as a Spring bean. This allows a javax.validation.Validator to be injected wherever validation is needed in your application.
Use the LocalValidatorFactoryBean to configure a default JSR-303 Validator as a Spring bean:
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />
The basic configuration above will trigger JSR-303 to initialize using its default bootstrap mechanism. A JSR-303 provider, such as Hibernate Validator, is expected to be present in the classpath and will be detected automatically.
5.7.2.1 Injecting a Validator
LocalValidatorFactoryBean implements both javax.validation.Validator and org.springframework.validation.Validator. You may inject a reference to one of these two interfaces into beans that need to invoke validation logic.
Inject a reference to javax.validation.Validator if you prefer to work with the JSR-303 API directly:
// JSR-303 Validator
import javax.validation.Validator;
#Service
public class MyService {
#Autowired
private Validator validator;
}
Inject a reference to org.springframework.validation.Validator if your bean requires the Spring Validation API:
// Spring Validator
import org.springframework.validation.Validator;
#Service
public class MyService {
#Autowired
private Validator validator;
}
Here is a well exaplained example
Using JSR 303 with "classic" Spring Validators (enter the SpringValidatorAdapter)
This link is very helpful. Wrapping javax.validation.Validator in
org.springframework.validation.beanvalidation.SpringValidatorAdapter
helped deal with errors consistently. Can you add this as an answer so
that I can accept it
and
Spring doc here