Removing duplication from Spring controllers - java

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.

Related

Implementatio of mutliple ConstraintValidator and their priority (enable/disable by requests endpoint)

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.

How to automatically add Bean Validation when partially updating PATCH Spring Boot MVC

As we all know, there is a big problem with a partial update of the entity. Since the automatic conversion from json strings to the entity, all fields that have not been transferred will be marked null. And as a result, the fields that we did not want to reset will be reset.
I will show the classical scheme:
#RestController
#RequestMapping(EmployeeController.PATH)
public class EmployeeController {
public final static String PATH = "/employees";
#Autowired
private Service service;
#PatchMapping("/{id}")
public Employee update(#RequestBody Employee employee, #PathVariable Long id) {
return service.update(id, employee);
}
}
#Service
public class Service {
#Autowired
private EmployeeRepository repository;
#Override
public Employee update(Long id, Employee entity) {
Optional<T> optionalEntityFromDB = repository.findById(id);
return optionalEntityFromDB
.map(e -> saveAndReturnSavedEntity(entity, e))
.orElseThrow(RuntimeException::new);
}
private T saveAndReturnSavedEntity(Employee entity, Employee entityFromDB) {
entity.setId(entityFromDB.getId());
return repository.save(entity);
}
}
#Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}
and as I have already said that in the current implementation we will not be able to perform a partial update in any way. That is, it is impossible to send an update of only one field in a json line; all fields will be updated, and in null (excepted passed).
The solution to this problem is that you need to perform the conversion from string json to the entity in manual. That is, do not use all the magic from Spring Boot (which is very sad).
I will also give an example of how this can be implemented using merge at the json level:
#RestController
#RequestMapping(EmployeeRawJsonController.PATH)
public class EmployeeRawJsonController {
public final static String PATH = "/raw-json-employees";
#Autowired
private EmployeeRawJsonService service;
#PatchMapping("/{id}")
public Employee update(#RequestBody String json, #PathVariable Long id) {
return service.update(id, json);
}
}
#Service
public class EmployeeRawJsonService {
#Autowired
private EmployeeRepository employeeRepository;
public Employee update(Long id, String json) {
Optional<Employee> optionalEmployee = employeeRepository.findById(id);
return optionalEmployee
.map(e -> getUpdatedFromJson(e, json))
.orElseThrow(RuntimeException::new);
}
private Employee getUpdatedFromJson(Employee employee, String json) {
Long id = employee.getId();
updateFromJson(employee, json);
employee.setId(id);
return employeeRepository.save(employee);
}
private void updateFromJson(Employee employee, String json) {
try {
new ObjectMapper().readerForUpdating(employee).readValue(json);
} catch (IOException e) {
throw new RuntimeException("Cannot update from json", e);
}
}
}
#Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}
With this solution, we eliminate the problem associated with the partial update.
But here another problem arises, that we are losing the automatic addition of validation of beans.
That is, in the first case, validation is enough to add one annotation #Valid:
#PatchMapping("/{id}")
public Employee update(#RequestBody #Valid Employee employee, #PathVariable Long id) {
return service.update(id, employee);
}
But we can't do the same when we perform manual deserialization.
My question is, is there any way to enable automatic validation for the second case?
Or maybe there are other solutions that allow you to use Spring Boot magic for Bean Validation.
What you need is not the normal validation , which can achieved through manual validator call.Let’s now go the manual route and set things up programmatically:
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<User>> violations = validator.validate(object);
for (ConstraintViolation<User> violation : violations) {
log.error(violation.getMessage());
}
To validate a bean, we must first have a Validator object, which is constructed using a ValidatorFactory.
Normal validations on Spring Controllers specified with #Valid annotations are triggered automatically during the DataBinding phase when a request is served.All validators registered with the DataBinder will be executed at that stage. We can't do that for your case, so you can manually trigger the validation like above.

Java Validating a extended Pojo

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");
}
}
}
}

Spring MVC - The #SessionAttributes and status.setComplete()

I'm facing a problem I don't really know how to solve.
I am developing a Bug Tracker (learning purposes only). I have a page to create a new issue and one page to edit an issue. Both, for now, have their own controllers.
EditIssueController.java
#Controller
#RequestMapping(value = "/issues/{issueId}")
#SessionAttributes("issuePackage")
public class EditIssueController {
#Autowired
private IssueService issueService;
[...]
#ModelAttribute("issuePackage")
public IssueTagEnvironment populateIssue (#PathVariable("issueId") Integer issueId) {
IssueTagEnvironment issueTagEnv = new IssueTagEnvironment();
issueTagEnv.setIssue(issueService.getIssueById(issueId));
return issueTagEnv;
}
#InitBinder
public void initBinder (WebDataBinder binder) {
[...]
}
#RequestMapping(value = "/edit", method = RequestMethod.GET)
public ModelAndView editIssue (#PathVariable("issueId") Integer issueId,
#ModelAttribute("issuePackage") IssueTagEnvironment issuePackage) {
ModelAndView mav = new ModelAndView("/issues/EditIssue");
[...]
IssueTagEnvironment issueTagEnv = new IssueTagEnvironment();
issueTagEnv.setIssue(issueService.getIssueById(issueId));
[...]
mav.addObject("issuePackage", issueTagEnv);
return mav;
}
#RequestMapping(value = "/edit", method = RequestMethod.POST)
public String updateIssue (#ModelAttribute("issuePackage") IssueTagEnvironment issuePackage,
BindingResult result) {
if (result.hasErrors() == true) {
return "redirect:/issues/{issueId}/edit";
}
issueService.updateIssue(issuePackage.getIssue());
return "redirect:/issues/{issueId}";
}
}
CreateIssueController.java
#Controller
#SessionAttributes("issuePackage")
public class CreateIssueController {
#Autowired
private IssueService issueService;
[...]
#ModelAttribute("issuePackage")
public IssueTagEnvironment populateNewIssue () {
return new IssueTagEnvironment();
}
#InitBinder
public void initBinder (WebDataBinder binder) {
[...]
}
#RequestMapping(value = "/issues/CreateIssue", method = RequestMethod.GET)
public ModelAndView createIssueGet (#ModelAttribute("issuePackage") IssueTagEnvironment issuePackage) {
ModelAndView mav = new ModelAndView("/issues/CreateIssue");
[...]
issuePackage.getIssue().setReporter(SecurityUtils.getCurrentUser());
return mav;
}
#RequestMapping(value = "/issues/CreateIssue", method = RequestMethod.POST)
public String createIssuePost (#ModelAttribute("issuePackage") IssueTagEnvironment issuePackage,
BindingResult result,
SessionStatus status) {
if (result.hasErrors() == true) {
return "redirect:/issues/CreateIssue";
}
[...]
issueService.createIssue(issuePackage.getIssue());
status.setComplete();
return "redirect:/issues/" + issuePackage.getIssue().getId();
}
}
So far everything seems correct (and in indeed works). But here are the dragons:
I am within an "edit" page changing data from an existing issue.
Instead of submitting the changes I decide to press the "Go Back" button from the navigator.
Right after that action (Go Back), I decide to create a new issue and... Here it is! The form to create a new issue isn't empty but filled with the information of the previous "edited-but-not-submitted" issue.
I understand what the problem is: The controller is not completing the session/status by executing status.setComplete().
My question here is, how to solve this problem?
Thanks in advance to the community!
For your current example, it is easy to fix , just change createIssueGet method to :
public ModelAndView createIssueGet () {
ModelAndView mav = new ModelAndView("/issues/CreateIssue");
IssueTagEnvironment issuePackage = new IssueTagEnvironment();
ModelAndView mav = new ModelAndView("/issues/CreateIssue");
mav.addAttribute("issuePackage", issuePackage);
[...]
[...]
}
That way you are sure that you always use a fresh IssueTagEnvironment object in that controller. And Spring will put it in session as you put it in model.
But the problem still remains : if you do not properly call status.setComplete(), you leave in session an object that should not be there, and like you said dragons may be there
I stopped using #SessionAttributes for that reason, and only use a hidden field (for the id) and a Converter from the id to a full object using the service layer, hoping it should be in cache and does not hit the database. Not really nice, but not really worse than that.

Using Spring MVC Validator in controller using multiple domain objects

I have a Spring MVC controller that handles requests to do with user management including a method to save user details and a method to allow a user to reset their password. I want to use a validator to ensure that the user typed the same password twice.
My controller
#Controller
public class UserDetails {
...
#InitBinder
public void binder(WebDataBinder binder) {
binder.addValidators(new PasswordValidator());
}
...
#RequestMapping(value="/saveUserDetails", method=RequestMethod.POST)
public String saveUserDetails(
#ModelAttribute User user) {
...
}
...
#RequestMapping(value="/resetPassword", method=RequestMethod.POST)
public String resetPassword(
#Validated PasswordPair password, BindingResult result) {
...
}
And the Validator
private final static class PasswordValidator implements Validator {
private final static int MIN_LEN=5;
#Override
public boolean supports(Class<?> clazz) {
return PasswordPair.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
PasswordPair pair = (PasswordPair)target;
//1 impose password rules
if (pair.getPassword().length()<MIN_LEN) {
errors.rejectValue("password", "Too short", "Password must bne at least "+MIN_LEN+" chars");
}
if (!pair.getPassword().equals(pair.getConfirmPassword())) {
errors.rejectValue("confirmPassword", "mustMatch", "passwords must match");
}
}
}
The problem is that when saveUserDetails is called spring is trying to validate the user with the password validator.
I have tried changing #InitBinder to #InitBinder("password") or #InitBinder("PasswordPair") but in that case nothing is validated at all.
How can I make it validate only the correct parameters?
Your approach of using #InitBinder(withname) should work , my guess is that you have just provided the wrong name.
Can you try this name:
#InitBinder("passwordPair")
Another option will to explicitly give your ModelAttribute a name, this way and use that name in the InitBinder:
public String resetPassword(
#ModelAttribute("password") #Valid PasswordPair password, BindingResult result) {
...
}
#InitBinder("password")
On a related note, I have a similar issue recorded with Spring Jira and have a pull request to change this behavior in place. Please vote it up if possible - https://jira.springsource.org/browse/SPR-11429
Add password and confirmPassword field to the User object instead of having PasswordPair object. Change resetPassword method to have user as inparameter.
And change the validator to validate the user. Here i suppose that you would have password validation even when create a new user?
Ex:
#Controller
public class UserDetails {
#InitBinder
public void binder(WebDataBinder binder) {
binder.addValidators(new PasswordValidator());
}
#RequestMapping(value="/saveUserDetails", method=RequestMethod.POST)
public String saveUserDetails(#ModelAttribute User user) {
...
}
#RequestMapping(value="/resetPassword", method=RequestMethod.POST)
public String resetPassword(#Valid #ModelAttribute User user, BindingResult result) {
...
}
And the Validator
private final static class UserValidator implements Validator {
private final static int MIN_LEN=5;
#Override
public boolean supports(Class<?> clazz) {
return PasswordPair.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
User user = (User)target;
//1 impose password rules
if (user.getPassword().length()<MIN_LEN) {
errors.rejectValue("password", "Too short", "Password must bne at least "+MIN_LEN+" chars");
}
if (!user.getPassword().equals(user.getConfirmPassword())) {
errors.rejectValue("confirmPassword", "mustMatch", "passwords must match");
}
}
}
or you could look at this post, maybe it'll help you:
Handling password confirmations on spring-mvc

Categories

Resources