Parfois les validations par annotations peuvent paraître compliquées à mettre en place. C'est pourquoi Spring a rendu possible la création des validateurs manuels.
Validateur manuel dans Spring
Dans le paragraphe précédent on a vu que pour valider les JavaBean dans Spring il suffisait d'utiliser les annotations adéquates. Cependant, ce n'est pas le seul moyen d'accomplir une validation. Dans certains cas on peut écrire le code d'un validateur. Il est important que celui-ci implémente l'interface org.springframework.validation.Validator.
Cette situation-là est un peu différente par rapport à deux précédentes. L'élément validé ne peut plus être accompagné de l'annotation @Valid ou @Validated. L'indication de l'élément à valider doit se faire explicitement dans le code, via DataBinder à qui on montrera quelle instance il faut valider avec quel validateur. Ensuite la récupération des erreurs se fait de la même manière que dans le cas d'une validation par annotations. Regardons cela dans le code d'un contrôleur :
@RequestMapping("/booking") @Controller public class BookingController { // only this method interests us @RequestMapping(value = "/do/{copy}", method = RequestMethod.POST) public String handleDoBooking(@ModelAttribute("booking") Booking booking, @PathVariable long copy, @LoggedUser AuthenticationFrontendUserDetails user, RedirectAttributes redAtt) { // .. DataBinder binder = new DataBinder(booking); CustomDateEditor customDateEditor = new CustomDateEditor(new SimpleDateFormat("dd-MM-yyyy"), true); binder.registerCustomEditor(Date.class, customDateEditor); binder.setValidator(new BookingValidator(bookingService, request, csrfProtector)); binder.validate(); BindingResult results = binder.getBindingResult(); if(results.hasErrors()) { // ... } // ... } }
Le code se comporte comme indiqué précédemment. Passons maintenant à l'analyse du BookinValidator dont l'instance est créée dans la méthode setValidator() :
public class BookingValidator implements Validator { final Logger logger = LoggerFactory.getLogger(BookingValidator.class); private BookingService bookingService; private CSRFProtector csrfProtector; private HttpServletRequest request; public BookingValidator(BookingService bookingService, HttpServletRequest request,CSRFProtector csrfProtector) { this.bookingService = bookingService; this.request = request; this.csrfProtector = csrfProtector; } public boolean supports(Class clazz) { return Booking.class.equals(clazz); } public void validate(Object obj, Errors errors) { logger.info("================================> obj " + obj); Booking booking = (Booking) obj; if (booking.getBookingDate() == null) { errors.rejectValue( "bookingDate", "error.booking.dateEmpty", "Booking date can't be empty" ); } else { logger.info("===========> BOOKINGSERVICE IS " + bookingService); // booking can't be done at the same day Calendar tomorrow = Calendar.getInstance(); tomorrow.add(Calendar.DATE, 1); Calendar bookingDate = Calendar.getInstance(); bookingDate.setTime(booking.getBookingDate()); bookingDate.set(Calendar.AM_PM, tomorrow.get(Calendar.AM_PM)); bookingDate.set(Calendar.HOUR, tomorrow.get(Calendar.HOUR)); bookingDate.set(Calendar.HOUR_OF_DAY, tomorrow.get(Calendar.HOUR_OF_DAY)); bookingDate.set(Calendar.MINUTE, tomorrow.get(Calendar.MINUTE)); bookingDate.set(Calendar.SECOND, tomorrow.get(Calendar.SECOND)); bookingDate.set(Calendar.MILLISECOND , tomorrow.get(Calendar.MILLISECOND)); Borrowing borrowing = new Borrowing(); borrowing.setDateFrom(booking.getBookingDate()); borrowing.setDateTo(borrowing.getDateFrom(), true); logger.info("===> comparing " + bookingDate + " and " + tomorrow); if (bookingDate.before(tomorrow)) { errors.rejectValue( "bookingDate", "error.booking.dateTomorrow", "The farest date is tomorrow" ); } else if (bookingService.hasBooking(booking.getBookCopy(), borrowing.getDateFrom(), borrowing.getDateTo())) { errors.rejectValue( "bookingDate", "error.booking.dateTaken", "Somebody else has already booked the book for this date" ); } } if (bookingService.alreadyBooked(booking.getBookCopy(), booking.getSubscriber())) { errors.rejectValue( "bookCopy", "error.booking.alreadyBooked", "You can't booked the same book copy twice" ); } if (booking.getSubscriber() == null) { errors.rejectValue("subscriber", "entity.empty", "Subscriber was not found"); } else if (booking.getSubscriber() != null && booking.getSubscriber().getBookingNb() == 0) { errors.rejectValue("subscriber", "error.booking.maxLimit", "Subscriber can't book anymore"); } if (booking.getBookCopy() == null) { errors.rejectValue("bookCopy", "error.booking.bookCopyEmpty", "Book copy was not found"); } CSRFConstraintValidator csrfConstraintValidator = new CSRFConstraintValidator(); csrfConstraintValidator.setCSRFProtector(csrfProtector); csrfConstraintValidator.setRequest(request); if (!csrfConstraintValidator.isValid(booking, null)) { errors.rejectValue( "token", "error.csrf.invalid", "An error occured. Please, try again later" ); } } }
Regardons les méthodes une par une. Le constructeur d'abord, il prend en paramètre l'implémentation de l'interface BookingService. Comme on a expliqué dans l'article codage aux interfaces, ce service ne peut pas être injecté directement via @Autowired ou une propriété dans le fichier de configuration. C'est impossible car BookingValidator n'est pas un bean et BookingService doit être passé manuellement en paramètre.
Ensuite on a une méthode qui vérifie si l'objet à valider est supporté par la classe en question. Il s'agit d'une méthode dont le nom est bien explicite, supports().
La dernière méthode implémentée, la plus importante d'ailleurs, contient toute la logique de validation. On vérifie si les éléments passés ne sont pas null ou s'ils correspondent à des critères de validation (date d'emprunt après-demain ou si l'exemplaire n'est pas encore emprunté). On voit qu'à chaque fois où un attribut est invalide, on utilise l'instance de l'objet Errors en appelant la méthode rejectValue(). Cette fonction prend en paramètres (dans l'ordre spécifié dans l'exemple) :
- le nom du champ invalide
- le code du message d'erreur à afficher
- (concerne uniquement le champ bookingDate) une liste d'arguments récupérables par le message d'erreur
- le message par défaut qui sera affiché si le message n'est pas trouvé dans la liste des traductions