springmvc jsr303 validator co-exist with spring WebDataBinder validator in one action - spring

Since springmvc 3.x now supports jsr303 and old spring style validator, i want to mix them in my sample apps. But there is only one method enabled for a specified controller, is that the limit of spring framework or JSR standard?
Here is my sample code.
User.java, stands for the domain model, uses JSR303 for validation.
public class User{
#Size(max = 16, message = "user loginId max-length is 16")
private String loginId;
//omit getter and setter
}
UserValidator.java, implements the org.springframework.validation.Validator interface to support user validation.
public class UserValidator implements Validator {
private UserService userService;
public boolean supports(Class<?> clazz) {
return User.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
User u = (User) target;
// loginName check for new user
if (u.getUserId() == null && !userService.isLoginIdUnique(u.getLoginId(), null)) {
errors.rejectValue("loginId", "user.loginId.unique", new Object[] { u.getLoginId() }, null);
}
}
#Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
}
UserController.java, uses InitBinder annotation to inject UserValidator into WebDataBinder.
#Controller("jspUserController")
#RequestMapping("/sys/users")
public class UserController {
private UserValidator userValidator;
#Autowired
public void setUserValidator(UserValidator userValidator) {
this.userValidator = userValidator;
}
/*#InitBinder("user")
public void initBinderUser(WebDataBinder binder) {
binder.setValidator(userValidator);
}*/
#RequestMapping(value = "/save")
public String save(#Valid User user, BindingResult bindingResult, Model model, HttpServletRequest request) {
if (bindingResult.hasErrors()) {
return "/sys/user/edit";
}
userService.saveUser(user);
return "redirect:/sys/users/index";
}
}
If I uncomment the #InitBinder("user") in UserController, the JSR303 validation will be disabled. While the current commented code will use JSR validator to do the validation.
Can anyone give me a workaround to mix them in one controller?

You can ADD your validator instead of SETTING it :
#InitBinder("user")
public void initBinderUser(WebDataBinder binder) {
binder.addValidators(userValidator);
}
This will execute the JSR303 validations first and then your custom validator. No need then to call the validator directly in the save method.

You can use your validator directly and let the global LocalValidatorFactoryBean (JSR-303) do its work as well:
#Controller("jspUserController")
#RequestMapping("/sys/users")
public class UserController {
private UserValidator userValidator;
#Autowired
public void setUserValidator(UserValidator userValidator) {
this.userValidator = userValidator;
}
#RequestMapping(value = "/save")
public String save(#Valid User user, BindingResult bindingResult, Model model, HttpServletRequest request) {
this.userValidator.validate(user, bindingResult);
if (bindingResult.hasErrors()) {
return "/sys/user/edit";
}
userService.saveUser(user);
return "redirect:/sys/users/index";
}
}

Related

Hibernate validation - autowired returns null

After looking around, I couldn't find any good solution to this.
My autowired didn't work as expected where it returns null. I've autowired this particular class in other classes and it works so it only doesn't work in constraintvalidator classes.
UserService class
#Service
public class UserService {
#Autowired
private UserRepository userRep;
public void addUser(User user) {
userRep.save(user);
}
public void deleteUser(long userId) {
userRep.deleteById(userId);
}
public List<User> retrieveAllUsers(){
Iterable<User>temp =userRep.findAll();
List<User>allUsers = null;
temp.forEach(allUsers::add);
return allUsers;
}
public boolean searchByEmail(String email) {
return userRep.findByEmail(email);
}
public void updateUser(User user) {
userRep.save(user);
}
}
Annotation interface class
#Target(ElementType.FIELD)
//When will the annotation be processed compilation, runtime etc
#Retention(RetentionPolicy.RUNTIME)
//Where is the logic
#Constraint(validatedBy = EmailValidator.class)
#Documented
public #interface ValidEmail {
//Error message
String message() default "Invalid email";
//Required for annotation
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Annotation logic class. The autowired here returns null
public class EmailValidator implements ConstraintValidator<ValidEmail, String> {
#Autowired
private UserService service;
//Actual place to place the logic to check if the data is valid or not
#Override
public boolean isValid(String email, ConstraintValidatorContext context) {
if (email == null) {
return false;
}
List<User> users = service.retrieveAllUsers();
if (users.size() > 0) {
return Pattern.matches("(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")#(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])", email)
&& service.searchByEmail(email);
}
else {
return Pattern.matches("(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")#(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])", email);
}
}
#Override
public void initialize(ValidEmail validEmail) {
validEmail.message();
}
}
Main
#SpringBootApplication
#ComponentScan(basePackages = {
"com.Alex.Mains", "com.Alex.UserPackage", "com.Alex.Flights", "com.Alex.Security"
})
#EntityScan( basePackages = {"com.Alex.UserPackage", "com.Alex.Flights"})
#EnableJpaRepositories({"com.Alex.UserPackage", "com.Alex.Flights"})
public class JpaApplication {
public static void main(String[] args) {
SpringApplication.run(JpaApplication.class, args);
}
// #Bean
// public Validator validator(final AutowireCapableBeanFactory beanFactory) {
//
// ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
// .configure()
// .constraintValidatorFactory(new SpringConstraintValidatorFactory(beanFactory))
// .buildValidatorFactory();
//
// return validatorFactory.getValidator();
// }
}
Edit: Tried #Componenet
Fixed with adding the following to application.properties. No idea why but it works
spring.jpa.properties.javax.persistence.validation.mode=none
EDIT: My Suggestion
Instead of a custom validator, use the existing #EMail and a unique constraint:
#Entity
public class User {
// ...your properties
#Email
#Column(unique = true)
private String email.
// Rest of class...
}
OLD:
So, first off:
List<User> users = service.retrieveAllUsers();
if (users.size() > 0) {
You are fetching all the Users from the database, just to check whether any users exists? This is very, very inefficient. If you are already using Spring Data, you can just do
#Query("SELECT COUNT(*) > 0 FROM Users")
boolean anyExists();
Furthermore, your Service does not get injected, because EmailValidator is a POJO (plain old java object) and not a Spring managed component. If you annotate it with #Component or #Service Spring will take care of injection.
But I would not recommend that. I'm not sure what your exact use case is, but validators are often used on Entities and as such, they get called when the entity is created or updated. You don't want to issue additional queries in those cases.
Like I said, I don't know what exactly you are trying to achieve, but you could use the existing #Email validator (you can even provide a custom regular expression with the regexp attribute).

Spring boot - Pass argument from interceptor to method in controller

For learning purposes, I have made a custom authentication system where I pass a token from the client to the server through the Authorization header.
In the server side, I'd like to know if it's possible to create in the interceptor, before the request reaches a method in the controller, an User object with the email from the token as a property, and then pass this user object to every request where I require it.
This what I'd like to get, as an example:
#RestController
public class HelloController {
#RequestMapping("/")
public String index(final User user) {
return user.getEmail();
}
}
public class User {
private String email;
}
Where user is an object that I created in the pre-interceptor using the request Authorization header and then I can pass, or not, to any method in the RestController.
Is this possible?
#Recommended solution
I would create a #Bean with #Scope request which would hold the user and then put the appropriate entity into that holder and then take from that holder inside the method.
#Component
#Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class CurrentUser {
private User currentUser;
public User getCurrentUser() {
return currentUser;
}
public void setCurrentUser(User currentUser) {
this.currentUser = currentUser;
}
}
and then
#Component
public class MyInterceptor implements HandlerInterceptor {
private CurrentUser currentUser;
#Autowired
MyInterceptor(CurrentUser currentUser) {
this.currentUser = currentUser;
}
#Override
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
this.currentUser.setCurrentUser(new User("whatever"));
return true;
}
}
and in the Controller
#RestController
public class HelloController {
private CurrentUser currentUser;
#Autowired
HelloController(CurrentUser currentUser) {
this.currentUser = currentUser;
}
#RequestMapping("/")
public String index() {
return currentUser.getCurrentUser().getEmail();
}
}
#Alternative solution
In case your object that you would like to have, only contains one field, you can just cheat on that and add that field to the HttpServletRequest parameters and just see the magic happen.
#Component
public class MyInterceptor implements HandlerInterceptor {
#Override
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//TRY ONE AT THE TIME: email OR user
//BOTH SHOULD WORK BUT SEPARATELY OF COURSE
request.setAttribute("email", "login#domain.com");
request.setAttribute("user", new User("login#domain.com"));
return true;
}
}
You can use a local thread context object as follows - which will be handling one parameter per request thread (thread safe):
public abstract class LoggedUserContext {
private static ThreadLocal<User> currentLoggedUser = new ThreadLocal<>();
public static void setCurrentLoggedUser(User loggedUser) {
if (currentLoggedUser == null) {
currentLoggedUser = new ThreadLocal<>();
}
currentLoggedUser.set(loggedUser);
}
public static User getCurrentLoggedUser() {
return currentLoggedUser != null ? currentLoggedUser.get() : null;
}
public static void clear() {
if (currentLoggedUser != null) {
currentLoggedUser.remove();
}
}
}
Then in the interceptor prehandle function:
LoggedUserContext.setCurrentLoggedUser(loggedUser);
And in the interceptor postHandler function:
LoggedUserContext.clear();
From any other place:
User loggedUser = LoggedUserContext.getCurrentLoggedUser();

spring-data-rest: Validator not being invoked

I am using springboot 2.0.1.RELEASE with spring-data-rest and followed the workaround mentioned here and my Validator is still not being invoked. Here are the details:
ValidatorRegistrar: Workaround for a bug
#Configuration
public class ValidatorRegistrar implements InitializingBean {
private static final List<String> EVENTS;
static {
List<String> events = new ArrayList<String>();
events.add("beforeCreate");
events.add("afterCreate");
events.add("beforeSave");
events.add("afterSave");
events.add("beforeLinkSave");
events.add("afterLinkSave");
events.add("beforeDelete");
events.add("afterDelete");
EVENTS = Collections.unmodifiableList(events);
}
#Autowired
ListableBeanFactory beanFactory;
#Autowired
ValidatingRepositoryEventListener validatingRepositoryEventListener;
#Override
public void afterPropertiesSet() throws Exception {
Map<String, Validator> validators = beanFactory.getBeansOfType(Validator.class);
for (Map.Entry<String, Validator> entry : validators.entrySet()) {
EVENTS.stream().filter(p -> entry.getKey().startsWith(p)).findFirst()
.ifPresent(p -> validatingRepositoryEventListener.addValidator(p, entry.getValue()));
}
}
}
Validator class:
#Component("beforeSaveBidValidator")
public class BeforeSaveBidValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
return Bid.class.equals(clazz);
}
#Override
public void validate(Object target, Errors errors) {
Bid bid = (Bid)target;
if (!bid.getAddendaAcknowledged()) {
errors.rejectValue("addendaAcknowledged",
"addendaAcknowledged is not true");
}
}
}
Custom RestController for Bids:
#RestController
#RequestMapping(path = "/bids")
public class BidController {
private BidRepository bidRepository;
#Autowired
public BidController(
BidRepository bidRepository) {
this.bidRepository = bidRepository;
}
#PutMapping("{id}")
public Bid update(#RequestBody #Valid Bid bid) {
return bidRepository.save(bid);
}
}
Rest Client Test Code:
Bid bid = new Bid()
...
bid.setAddendaAcknowledged(false)
Map<String, String> uriVariables = new HashMap<String, String>()
uriVariables.put("id", bid.id)
HttpHeaders headers = new HttpHeaders()
headers.setContentType(MediaType.APPLICATION_JSON)
HttpEntity<Bid> entity = new HttpEntity<>(bid, headers)
ResponseEntity<String> response = restTemplate.exchange(
"/bids/{id}", HttpMethod.PUT, entity, Bid.class, bid.id)
// Expected: response.statusCode == HttpStatus.BAD_REQUEST
// Found: response.statusCode == HttpStatus.OK
// Debugger showed that Validator was never invoked.
Any idea what I am missing?
You are trying to use your validator with custom controller, not SDR controller. In this case you can just add it to your controller with #InitBinder annotation:
#RestController
#RequestMapping("/bids")
public class BidController {
//...
#InitBinder("bid") // add this parameter to apply this binder only to request parameters with this name
protected void bidValidator(WebDataBinder binder) {
binder.addValidators(new BidValidator());
}
#PutMapping("/{id}")
public Bid update(#RequestBody #Valid Bid bid) {
return bidRepository.save(bid);
}
}
#Component annotation on your validator is not necessary as well as ValidatorRegistrar class.
How to use validators with SDR controllers you can read in my another answer.

Issue in calling validator automic with #Valid in Spring controller

I am trying to call validator from controller using #Valid annotation, but control is not going to validator and proceeding without validating.
Controller
#Controller
#RequestMapping(value="/event")
public class EventController {
#Autowired
private EventService eventService;
#Autowired
EventValidator eventValidator;
#InitBinder
private void initBinder(WebDataBinder binder) {
binder.setValidator(eventValidator);
}
#RequestMapping(value="/add_event",method = RequestMethod.POST,produces=MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public ResponseEntity<AjaxJSONResponse> postAddEventForm(#Valid #RequestPart("event") Event event, MultipartHttpServletRequest request) {
Boolean inserted = eventService.addEvent(event);
String contextPath = request.getContextPath();
String redirectURL = StringUtils.isEmpty(contextPath)?"/event":contextPath+"/event";
return new ResponseEntity<AjaxJSONResponse>(new AjaxJSONResponse(inserted,"Event Added Successfully",redirectURL), HttpStatus.OK);
}
}
Validator
#Component
public class EventValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
return Event.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
Event event = (Event)target;
if (event.getEventName() == null ||!StringUtils.hasText(event.getEventName())) {
errors.rejectValue("eventName", "", "Event Name is empty");
}
}
}
Please help on this.
Thank in advance

How to combine JSR-303 and Spring Validator class in a service layer?

I have some model class
public class Account {
#Email
private String email;
#NotNull
private String rule;
}
and spring-validator
public class AccountValidator implements Validator {
#Override
public boolean supports(Class aClass) {
return Account.class.equals(aClass);
}
#Override
public void validate(Object obj, Errors errors) {
Account account = (Account) obj;
ValidationUtils.rejectIfEmpty(errors, "email", "email.required");
ValidationUtils.rejectIfEmpty(errors, "rule", "rule.required");
complexValidateRule(account.getRule(), errors);
}
private void complexValidateRule(String rule, Errors errors) {
// ...
}
}
I run in my service
AccountValidator validator = new AccountValidator();
Errors errors = new BeanPropertyBindingResult(account, "account");
validator.validate(account, errors);
Can I add to my validation process constraints #Email, #NotNull (JSR-303) and don't describe these rules in AccountValidator?
I know how works #Valid in spring-controllers, but what's about service layer? Is it possible? How to do such kind of validation in a proper way? May I should use Hibernate Validator?
Spring provides an Adapter to merge both validation APIs.
See the current Spring JavaDoc for more information.
An possible implementation would be
public class AccountValidator implements Validator {
private final SpringValidatorAdapter validator;
public AccountValidator(SpringValidatorAdapter validator) {
super();
this.validator = validator;
}
#Override
public boolean supports(Class aClass) {
return Account.class.equals(aClass);
}
#Override
public void validate(Object obj, Errors errors) {
//jsr303
validator.validate(obj, errors);
//custom rules
Account account = (Account) obj;
complexValidateRule(account.getRule(), errors);
}
private void complexValidateRule(String rule, Errors errors) {
// ...
}
}

Resources