Validazione dei form con Spring MVC

Proseguiamo lo sviluppo della nostra fantastica SuperRubricaTelefonica!

Dopo aver affrontato il tema della gestione dei form con Spring MVC, è il momento di scoprire come validare i dati inviati al Controller.

Mettiamoci subito all’opera!

Validazione lato server

Allo stato attuale, la nostra applicazione non prevede un controllo sui dati immessi tramite form. Ovviamente, però, questo è un comportamento inammissibile se stiamo lavorando ad un progetto reale.

Benché in un’applicazione web che si rispetti la validazione dei dati avviene preventivamente lato client, il server non può ignorare la possibilità di ricevere dati “sporchi”. Valori non ammessi potrebbero essere inviati dall’utente per disattenzione o, in alcuni casi, con intento malevolo.

L’annotazione @Valid

Spring utilizza l’annotazione @Valid, definita dalla specifica Java EE, per identificare quali oggetti di input devono essere validati.

È importante notare che la validazione per i parametri annotati con @Valid sarà abilitata solo se all’interno del classpath è presente un validation provider. Se stiamo utilizzando Spring Boot basterà includere la dipendenza spring-boot-starter-web, oppure,come nel nostro caso, spring-boot-starter-thymeleaf per importare implicitamente anche Hibernate Validator, che si occuperà di processare le nostre regole di validazione.

Piccola nota La dipendenza hibernate-validator è in effetti inclusa nello starter package spring-boot-starter-web. Il pacchetto spring-boot-starter-thymeleaf, che implementa l’integrazione con Thymeleaf, si basa su spring-boot-starter-web, di conseguenza include tutte le dipendenze di quest’ultimo.

Riprendiamo dunque il nostro ContactController e aggiungiamo qualche elemento al metodo submitNewContact:

@PostMapping("/new")
public String submitNewContact(@Valid @ModelAttribute ContactForm contactForm
        , BindingResult bindingResult, Model model) {

  if (bindingResult.hasErrors()) {
    return "contact-form";
  }

  ContactDetails details = new ContactDetails();
  details.setFirstName(contactForm.getFirstName());
  details.setLastName(contactForm.getLastName());
  details.setPhone(contactForm.getPhone());
  details.setEmail(contactForm.getEmail());

  model.addAttribute("contact", details);
  return "contact-details";
}

Le due novità sono:

  • la già citata annotazione @Valid, che ci serve a contrassegnare contactForm come oggetto da validare;
  • il secondo argomento del metodo, di tipo BindingResult, che rappresenta l’esito della validazione.

L’oggetto bindingResult conterrà la lista degli eventuali errori riscontrati nei campi di contactForm. Come potete notare, l’esito della validazione è verificabile invocando il metodo hasErrors.

Nel caso in cui la validazione non andasse a buon fine l’handler rimanderà alla pagina di partenza, in cui verranno evidenziati gli errori rilevati.

Piccola nota IMPORTANTE: ricordate di dichiarare sempre il parametro di tipo BindingResult immediatamente dopo l’oggetto da validare. Se non doveste rispettare questo ordine, Spring potrebbe scatenare errori inaspettati e spesso fuorvianti. Io vi ho avvisati… :-P

Specifichiamo le regole di validazione

Una volta predisposto il Controller, è necessario spiegare a Spring come un certo oggetto deve essere validato.

Il nostro command object (o backing object), contactForm, dovrà fornire a Spring indicazioni su quali valori sono ammessi per i suoi campi. Anche in questo caso le Java annotation ci tornano molto utili:

public class ContactForm {
  @NotEmpty
  @Size(max=50)
  private String firstName;
  @NotEmpty
  @Size(max=100)
  private String lastName;
  @NotEmpty
  @Size(max=20)
  private String phone;
  @NotEmpty
  @Email
  @Size(max=100)
  private String email;

  // getters + setters

}

Ho riportato un estratto della classe ContactForm in cui, oltre ad aver aggiunto il campo email, ho annotato tutti i campi da validare.

Hibernate Validator e Java mettono a disposizione diverse annotazioni per specificare vincoli sui valori ammessi dai campi di una classe. Nella classe ContactForm ho utilizzato:

  • @NotEmpty, per marcare i campi obbligatori;
  • @Size per indicare vincoli sulla lunghezza delle stringhe;
  • @Email per verificare il formato dell’indirizzo email inserito.

Perfetto! A questo dobbiamo visualizzare gli errori…

Visualizzare gli errori

Per far sì che gli errori di validazione vengano visualizzati all’interno della pagina, la nostra contact-form necessita di un’aggiustatina.

Per comprendere meglio le modifiche da effettuare, per il momento concentriamoci sul campo firstName:

<div th:classappend="${#fields.hasErrors('firstName')}? has-error">
  <label>Nome</label> <input th:field="*{firstName}" type="text"/>
  <div th:if="${#fields.hasErrors('firstName')}" class="form-error">
    <span th:errors="*{firstName}" class="text-danger">
  </div>
</div>

L’espressione #fields.hasErrors() ci consente di verificare se per un determinato campo esistono errori di validazione.

Come vedete, nel mio caso utilizzo l’espressione nell’attributo th:classappend per assegnare in maniera condizionale la classe di Bootstrap has-error, utile per evidenziare i campi con valore errato. La stessa espressione la utilizzo inoltre, tramite th:if, per visualizzare i messaggi d’errore, quando necessario.

Il risultato finale è pressappoco questo:

<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:th="http://www.thymeleaf.org">
<head>
<title>Nuovo contatto</title>
</head>
<body>
  <form th:action="@{/contacts/new}" th:object="${contactForm}"
    method="post">
    <h2>Nuovo contatto</h2>
    <div class="form-group"
      th:classappend="${#fields.hasErrors('firstName')}? has-error">
      <label class="control-label">Nome</label> <input
        th:field="*{firstName}" type="text" class="form-control" />
      <div th:if="${#fields.hasErrors('firstName')}" class="form-error">
        <span th:errors="*{firstName}" class="text-danger">
      </div>
    </div>
    <div class="form-group"
      th:classappend="${#fields.hasErrors('lastName')}? has-error">
      <label class="control-label">Cognome</label> <input
        th:field="*{lastName}" type="text" class="form-control" />
      <div th:if="${#fields.hasErrors('lastName')}" class="form-error">
        <span th:errors="*{lastName}" class="text-danger">
      </div>
    </div>
    <div class="form-group"
      th:classappend="${#fields.hasErrors('phone')}? has-error">
      <label class="control-label">Telefono</label> <input
        th:field="*{phone}" type="text" class="form-control" />
      <div th:if="${#fields.hasErrors('phone')}" class="form-error">
        <span th:errors="*{phone}" class="text-danger">
      </div>
    </div>
    <div class="form-group"
      th:classappend="${#fields.hasErrors('email')}? has-error">
      <label class="control-label">Email</label> <input th:field="*{email}"
        type="text" class="form-control" />
      <div th:if="${#fields.hasErrors('email')}" class="form-error">
        <span th:errors="*{email}" class="text-danger">
      </div>
    </div>
    <div class="form-group">
      <input class="btn btn-primary" type="submit" value="Salva" />
    </div>
  </form>
</body>
</html>

Bene, vediamo il tutto in azione!

Se proviamo ad inserire un nuovo contatto senza valorizzare nessuno dei campi, otterremo qualcosa del genere:

Errori validazione server

Noterete che i messaggi di default sono in inglese, il che non ci piace molto (senza offesa per gli inglesi…) Per personalizzare e tradurre i messaggi in italiano dobbiamo fare un altro piccolo passo.

Nella cartella /resources andremo a creare due file: il primo, vuoto, si chiamerà messages.properties e conterrà i messaggi nella lingua di default, ovvero l’inglese; il secondo, quello che interessa a noi, lo chiameremo invece messages_it.properties. In quest’ultimo possiamo definire i messaggi in italiano, ad esempio:

NotEmpty=Campo obbligatorio
Email=Inserire un indirizzo email corretto
Size=Inserire massimo {1} caratteri

A questo punto, lanciando nuovamente l’applicazione, dovreste vedere i messaggi tradotti nella nostra lingua.

(Appendice) Validazione lato client

Come già accennato all’inizio di questo post, è sempre buona norma effettuare la validazione dell’input prima dell’invio dei dati al server (quindi lato client).

Questo approccio, oltre a migliorare l’esperienza d’uso da parte dell’utente, evita di appesantire il server con un ulteriore carico di lavoro che può essere evitato facilmente.

Generalmente, per implementare la validazione lato client è sufficiente una piccola dose di JavaScript.

In questo breve addendum, però, ci accontentiamo di aggiungere ai nostri campi input degli utili attributi che permettono di ottenere (nella maggior parte dei moderni browser) una rudimentale verifica dei dati immessi.

In particolare, aggiungendo l’attributo required otterremo una verifica dei campi obbligatori, mentre sostituendo l’attributo type="text" con type="email" nel campo relativo all’indirizzo email avremo un controllo sul formato della stringa immessa.

Utilizzando Chrome, ad esempio, visualizzeremo qualcosa del genere:

Errori validazione client

In conclusione

La validazione dei dati inviati al server è un aspetto fondamentale di ogni web application, ma a volte sottovalutato.

Spring permette di definire regole di verifica dei dati immessi tramite form, mentre l’integrazione tra Spring MVC e Thymeleaf ci consente d’implementare nelle nostre View i meccanismi di notifica degli errori rilevati. Niente male!

Anche per oggi è tutto, statemi bene e alla prossima! ;-)

David