I am building project on spring boot and want to add validation that are easy to integrate.
I have Pojo for my project as below:
public class Employee{
#JsonProperty("employeeInfo")
private EmployeeInfo employeeInfo;
}
EmployeeInfo class is as below:
public class EmployeeInfo extends Info {
#JsonProperty("empName")
private String employeeName;
}
Info class is as below:
#JsonIgnoreProperties(ignoreUnknown = true)
public class Info {
#JsonProperty("requestId")
protected String requestId;
}
How to I validate if request Id is not blank with javax.validation
My controller class is as below:
#RequestMapping(value = "/employee/getinfo", method = RequestMethod.GET, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<> getEmployee(#RequestBody Employee employee) {
//need to validate input request here
//for e.g to check if requestId is not blank
}
Request :
{
"employeeInfo": {
"requestId": "",
}
}
Considering you are making use of validation-api:
Please try using below to validate if your String is not null or not containing any whitespace
#NotBlank
In order to validate request parameters in controller methods, you can either use builtin validators or custom one(where you can add any type of validations with custom messages.)
Details on how to use custom validations in spring controller, Check how to validate request parameters with validator like given below:
#Component
public class YourValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
return clazz.isAssignableFrom(Employee.class);
}
#Override
public void validate(Object target, Errors errors) {
if (target instanceof Employee) {
Employee req = (Employee) target;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "employeeInfo.requestId", "YourCustomErrorCode", "yourCustomErrorMessage");
//Or above validation can also be done as
if(req.getEmployeeInfo().getRequestId == null){
errors.rejectValue("employeeInfo.requestId", "YourCustomErrorCode", "YourCustomErrorMessage");
}
}
}
}
Related
I am trying to validate the string email to check if it already appears within my MYSQL database, when I execute with an email thats already used I get the following error:
java.lang.IllegalStateException: Invalid target for Validator [co2103.hw2.controller.TestResultsValidator#62b41c6]: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'testResults' on field 'email': rejected value [abc#le.ac.uk]; codes [email.testResults.email,email.email,email.java.lang.String,email]; arguments []; default message [is already provided by a different user! Please user another one!]
Here is the validator code
public class TestResultsValidator implements Validator{
private TestResultsRepository TrRepo;
private HomeTestRepository HTRepo;
public TestResultsValidator (TestResultsRepository TrRepo, HomeTestRepository HTRepo) {
this.TrRepo = TrRepo;
this.HTRepo = HTRepo;
}
#Override
public boolean supports(Class<?> clazz) {
return TestResults.class.equals(clazz);
}
#Override
public void validate(Object target, Errors errors) {
TestResults tr = (TestResults) target;
for(TestResults t : TrRepo.findAll()) {
//SAME EMAIL
if (tr.getEmail().equals(t.getEmail())) {
errors.rejectValue("email", "email", "is already provided by a different user! Please user another one!");
System.out.println("Email is already taken by a different user, please try another username");
break;
}
The controller code
//Add new results
#RequestMapping(value = "/addResults",method = {RequestMethod.POST , RequestMethod.GET})
public String newHotel(#Valid #ModelAttribute TestResults results, BindingResult result, Model model) {
if (result.hasErrors()) {
model.addAttribute("errors", result);
return "start";
}
else {
trRepo.save(results);
return "Submitted";
}}
You need to register you validator to spring.
First ad Component Annotation to your validtor.
#Component
public class TestResultsValidator implements Validator{
.....
}
Register it in the controller.
#Controller
class TestResultController {
#Autowired
TestResultsValidator testResultsValidator ;
#InitBinder("testResultsValidator")
protected void initMessageBinder(WebDataBinder binder) {
binder.addValidators(testResultsValidator );
}
}
Lets say I have an Object with two fields which should be validated:
public class AnyRQ {
#MerchantAccountValidation
#JsonProperty(value = "merchant-account")
private MerchantAccount merchantAccount;
#RequestIdValidation
#JsonProperty(value = "request-id")
private String requestId;
}
Both of the Annotations #MerchantAccountValidation and #RequestIdValidation implements a ConstraintValidator and including the rules to be valid or not. (Just show one class)
public class RequestIdValidator
implements ConstraintValidator<RequestIdValidation, String> {
#Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null && value.length() > 10;
}
}
Now I have a Controller with two endpoints. Endpoint 1 should validate both Fields but Request 2 should just validate requestId.
#RestController
#RequestMapping("/validate")
public class ValidController {
#PostMapping("/endpoint1")
public ResponseEntity<?> register(#Valid #RequestBody AnyRQ req, Errors errors) {
if (errors.hasErrors()) {
}
return null;
}
#PostMapping("/endpoint2")
public ResponseEntity<?> authorization(#Valid #RequestBody AnyRQ req, Errors errors) {
if (errors.hasErrors()) {
}
return null;
}
}
Is there any way to achive a kind of priority or inheritance to get this working? I was thinking about to have the Validation Annotation on the method level of the endpoints. But unfortunately this is not working.
Patrick!
To achieve the desired outcome you can use #GroupSequence. It mostly meant for ordering validations (no need to check that entity exists in database, if id is null f.e.), but would work for your task.
Let's say you have 2 validation groups (better names are required :) ):
public interface InitialValidation {
}
#GroupSequence(InitialValidation.class)
public interface InitialValidationGroup {
}
public interface FullValidation {
}
#GroupSequence(FullValidation.class)
public interface FullValidationGroup {
}
Specify them in the DTO:
public class AnyRQ {
#MerchantAccountValidation(groups = FullValidation.class)
#JsonProperty(value = "merchant-account")
private MerchantAccount merchantAccount;
#RequestIdValidation(groups = {InitialValidation.class, FullValidation.class})
#JsonProperty(value = "request-id")
private String requestId;
}
And in the controller use #Validated instead of #Valid to provide corresponding group:
#RestController
#RequestMapping("/validate")
public class ValidController {
#PostMapping("/endpoint1")
public ResponseEntity<?> register(#Validated(FullValidationGroup.class) #RequestBody AnyRQ req, Errors errors) {
if (errors.hasErrors()) {
}
return null;
}
#PostMapping("/endpoint2")
public ResponseEntity<?> authorization(#Validated(InitialValidationGroup.class) #RequestBody AnyRQ req, Errors errors) {
if (errors.hasErrors()) {
}
return null;
}
}
The other option is to keep one group in DTO, but specify two groups in controller for #Validated.
I'm building a REST API that uses a #PathParameter for a parent PK and a #RequestBody for the child form parameters. Next I need to validate the #RequestBody values against regex values stored in a database using the param key from the #RequestBody and the parent pk from the #PathParameter, however I've been unable to figure out a good way to add the #PathParameter pk id to the #RequestBody child object before #Valid is called without using #ModelAttribute.
Using #ModelAttribute, I've been able to add the #PathParameter to the #RequestBody object and then validate the #RequestBody object using #Valid. However I found while using #ModelAttribute, Spring no longer throws the MethodArgumentNotValidException therefore eliminating the ability to use a global exception handler.
I found adding BindingResult to the controller handler followed by throwing a new MethodArgumentNotValidException when errors exist the global exception handler could be triggered.
It's my understanding the ModelAttribute is for Spring MVC and not so much RestController since I'm not using the view, I'm wondering if this is a correct approach or whether there's a better solution. Here's a sample of my code.
HTTP Post
localhost:8072/api/clover/graph_run/2
[
{
"graphKey" : "DATA_DIRECTORY",
"graphValue" : "/data/clover-prod"
},
{
"graphKey" : "DATAOUT_DIR",
"graphValue" : "/data/clover-prod/94l"
},
{
"graphKey" : "DELAY_MS",
"graphValue" : "0"
}
]
RestController
#Slf4j
#RestController
#RequiredArgsConstructor
#RequestMapping("/api/clover")
public class GraphRunController {
final CloverServerService cloverServerService;
#PostMapping("/graph_run/{graphId}")
public String graphRun(#ModelAttribute("graphId") #Valid GraphJobDTO graphJobDTO,
BindingResult bindingResult ) throws MethodArgumentNotValidException {
log.debug("GraphRun {}", graphJobDTO);
if (bindingResult.hasErrors()) {
throw new MethodArgumentNotValidException(null, bindingResult);
}
return cloverServerService.runGraph(graphJobDTO);
}
}
ModelAttribute in ControllerAdvise
#Slf4j
#RequiredArgsConstructor
#ControllerAdvice( assignableTypes = {GraphRunController.class})
public class GraphRunControllerAdvise {
final GraphJobRepository graphJobRepository;
#ModelAttribute("graphId")
public GraphJobDTO addGraphId(#PathVariable(value = "graphId") Long graphId,
#RequestBody List<GraphJobPropertyDTO> graphProperties) {
log.debug("ModelAttribute graphId {}", graphId);
//Query database for the graph job and all it's parameters
GraphJob graphJob = graphJobRepository.findById(graphId)
.orElseThrow(() -> new HRINotFoundException("No such job execution."
+ graphId));
GraphJobDTO graphJobDTO = new GraphJobDTO();
graphJobDTO.setGraph(graphJob.getGraph());
graphJobDTO.setGraphProperties(graphProperties);
//Create a map from the database with the key being the GraphKey contained in both the request and the database
Map<String, GraphJobProperty> jobMap = graphJob.getJobProperties().stream().collect(
Collectors.toMap(s -> s.getGraphKey().toUpperCase(), Function.identity()));
graphProperties.forEach(v -> {
String graphKey = v.getGraphKey();
//If the graphKey in the request cannot be found in the database, throw an exception. We will handle the exception
//in th exception handler
if(!jobMap.containsKey(graphKey)) {
throw new HRINotFoundException(String.format("%s is an invalid job parameter.", v.getGraphKey()));
}
//Within the database record is the validation message as well as the regex used to validate the incoming value,
//we set the message and regex in the GraphPropertyDTO object for cross validation later on in the validator.
GraphJobProperty jobProperty = jobMap.get(graphKey);
v.setValidationMessage(jobProperty.getValidationMessage());
v.setValidationRegex(jobProperty.getValidationRegex());
});
//Return the graphJobDTO object for validation
return graphJobDTO;
}
}
Constraint Validator
#Slf4j
public class GraphRegexValidator implements ConstraintValidator<GraphRegex, GraphJobPropertyDTO> {
#Override
public void initialize(final GraphRegex graphRegex) {
}
#Override
public boolean isValid(final GraphJobPropertyDTO dto, final ConstraintValidatorContext context) {
String regex = dto.getValidationRegex();
String value = dto.getGraphValue();
if(regex != null && value != null && !Pattern.matches(regex, value)) {
log.debug("isValid {} - {}", false, dto);
addConstraintViolation(context, getMessage(dto, context));
return false;
}
log.debug("isValid {} - {}", true, dto);
return true;
}
private void addConstraintViolation(ConstraintValidatorContext context, String message) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message).addPropertyNode("graphKey").addConstraintViolation();
}
private String getMessage(GraphJobPropertyDTO dto, ConstraintValidatorContext context) {
return dto.getValidationMessage() != null ? String.format(dto.getValidationMessage(), dto.getGraphValue()) :
context.getDefaultConstraintMessageTemplate();
}
}
DTOs
#Data
public class GraphJobDTO {
#NotNull
Long graphId;
#NotNull
String graph;
#Valid
List<GraphJobPropertyDTO> graphProperties;
}
#Data
#GraphRegex
public class GraphJobPropertyDTO {
String graphKey;
String graphValue;
String validationMessage;
String validationRegex;
}
How to i can discriminate command objects in spring-mvc Controller? For example, i have following bean-classes used as form object:
public class CreateServiceFormBean {
#NotBlank
#Length(min = 3, max = 120)
private String name;
}
public class CreateDependedServiceFormBean extends CreateServiceFormBean {
#NotNull
private Short parentServiceId;
}
Getter's and Setter's is cutted out.
#RequestMapping(method = RequestMethod.POST)
public String createService(CreateServiceFormBean form) {
if (form instanceof CreateServiceFormBean) {
System.out.println("create Service");
//new ServiceEntity(form.getName())
} else if (form instanceof CreateDependedServiceFormBean) {
System.out.println("create depended Service");
parentService = ... get parent service entity...
//new DependedServiceEntity(form.getName(), parentService)
}
return null;
}
How do this? I think about create abstract controller for this two dto's, but is not elegant..maybe can handled in one method..
And how to correct get parent service entity? Some like method ModelAttribute annotation whose return entity by id?
Thanks for Replies!
I have been looking for a way to somehow reduce the amount of code that is duplicated with subtle variance in my Spring MVC controllers, but searching through the SO questions so far has only yielded some questions without any satisfactory answers.
One example of duplication that I want to remove is this, where the user creation page and the role creation page share similarities:
#RequestMapping(value = "user/create", method = RequestMethod.GET)
public String create(#ModelAttribute("user") User user, BindingResult errors) {
LOG.debug("Displaying user creation page.");
return "user/create";
}
#RequestMapping(value = "role/create", method = RequestMethod.GET)
public String create(#ModelAttribute("role") Role role, BindingResult errors) {
LOG.debug("Displaying role creation page.");
return "role/create";
}
A slightly more involved variant of duplication that I would like to remove is the one for posting the create form:
#RequestMapping(value = "user/create", method = RequestMethod.POST)
public String save(#ModelAttribute("user") User user, BindingResult errors) {
LOG.debug("Entering save ({})", user);
validator.validate(user, errors);
validator.validatePassword(user, errors);
validator.validateUsernameAvailable(user, errors);
String encodedPassword = encoder.encode(user.getPassword());
user.setPassword(encodedPassword);
if (errors.hasErrors()) {
return create(user, errors);
} else {
service.save(user);
}
return "redirect:/user/index/1";
}
#RequestMapping(value = "role/create", method = RequestMethod.POST)
public String save(#ModelAttribute("role") Role role, BindingResult errors) {
LOG.debug("Entering save({})", role);
validator.validate(role, errors);
if (errors.hasErrors()) {
return create(role, errors);
} else {
service.save(role);
}
return "redirect:/index";
}
This example includes a validate then save if correct and a redirect to the error page if things don't go as planned.
How to remove this duplication?
Spring uses your handler method parameter types to create class instances from the request parameters or body. As such, there is no way to create a handler (#RequestMapping) method that could take an Object and check if it is either a Role or a User. (Technically you could have both parameters and just check which one isn't null, but that is terrible design).
Consequently, you need a handler method for each. This makes sense since, even through the logic is similar, it is still specific to the exact type of model object you are trying to create. You perform different validation, call a different service method, and return a different view name.
I say your code is fine.
Thought I would provide the solution that I settled on in the hope that it might help someone. My gf suggested that I use the name of the entity as a path variable for the controller, and this has proved to provide a very nice solution for the problem at hand.
The two methods now look like this:
#RequestMapping(value = "{entityName}/create", method = RequestMethod.GET)
public String create(#PathVariable("entityName") String entityName, #ModelAttribute("entity") BaseEntity entity, BindingResult errors) {
LOG.debug("Displaying create page for entity named: [{}]", entityName);
return handlerFactory.getHandler(entityName).getCreateView();
}
#RequestMapping(value = "{entityName}/create", method = RequestMethod.POST)
public String save(#PathVariable("entityName") String entityName, #ModelAttribute("entity") BaseEntity entity, BindingResult errors) {
LOG.debug("Saving entity of type {}", entityName);
CrudHandler handler = handlerFactory.getHandler(entityName);
handler.getCreateValidator().validate(entity, errors);
if (errors.hasErrors()) {
return create(entityName, entity, errors);
}
handler.preSave(entity);
handler.getService().save(entity);
return "redirect:" + DASHBOARD_URL;
}
The CrudHandler interface has implementations for each entity, and provides the controller with the entity specific classes that it needs, such as service and validator. A sample CrudHandler implementation looks like this for me:
#Component
public class RoleCrudHandler implements CrudHandler {
private static final String ENTITY_NAME = "role";
public static final String CREATE_VIEW = "role/create";
public static final String EDIT_VIEW = "role/edit";
#Resource
private RoleService roleService;
#Resource
private RoleValidator validator;
#Resource
private CrudHandlerFactory handlerFactory;
#PostConstruct
public void init() {
handlerFactory.register(ENTITY_NAME, this);
}
#Override
public GenericService getService() {
return roleService;
}
#Override
public Validator getCreateValidator() {
return validator;
}
#Override
public Validator getUpdateValidator() {
return validator;
}
#Override
public BaseEntity createEntity() {
return new Role();
}
#Override
public void preSave(BaseEntity entity) {
}
#Override
public String getCreateView() {
return CREATE_VIEW;
}
#Override
public String getUpdateView() {
return EDIT_VIEW;
}
}
If someone sees some ways to improve this, feel free to share. Hope this will be of use for someone.