Spring Web Flow : binding e validazione
Prosegue la nostra serie su Spring Web Flow. In questo articolo saranno trattati nuovi concetti di SWF, quali il binding e la validazione dei bean.
Binding
In Spring Web Flow il binding con i model è un’operazione piuttosto semplice e immediata. Nel nostro esempio abbiamo creato 3 nuovi JavaBean, che andremo poi a legare alle nostre pagine jsp attraverso il file di definizione del flusso. I nuovi bean saranno: Book, Author e Address.
package it.study.webflow.library.beans; import java.io.Serializable; public class Book implements Serializable { private String title; private int year; private String publishingHouse; public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public int getYear() { return year; } public void setYear(int year) { this.year = year; } public String getPublishingHouse() { return publishingHouse; } public void setPublishingHouse(String publishingHouse) { this.publishingHouse = publishingHouse; } }
Come si può notare l’oggetto Book è un semplice java bean con attributi private e dei metodi accessori. E’ importante notare una caratteristica del binding: i nomi degli attributi rispecchiano esattamente i parametri che Web Flow troverà come parametri della request, una volta effettuato il submit del form (come visibile dal codice seguente).
... <form method="post"> <div class="label">Titolo:</div> <div class="input"><input type="text" id="title" name="title"></div> <div class="label">Anno:</div> <div class="input"><input type="text" id="year" name="year"></div> <div class="label">Casa Ed.:</div> <div class="input"><input type="text" id="publishingHouse" name="publishingHouse"></div> <div> <input type="submit" name="_eventId_save" value="save"> </div> </form> ...
Per far in modo che i dati del form vengano automaticamente legati al nostro bean, si deve effettuare la configurazione nel file di definizione del flusso
... <var name="book" class="it.study.webflow.library.beans.Book"></var> <var name="author" class="it.study.webflow.library.beans.Author"></var> <view-state id="enterBookData" view="bookData" model="book"> <transition on="save" to="enterAuthorData"></transition> </view-state> <view-state id="enterAuthorData" view="authorForm" model="author"> <transition on="save" to="addressInformation"></transition> </view-state> ...
La prima cosa che notiamo è l’aggiunta dell’attributo model nel view-stateenterBookData. Questo attributo permette di definire l’oggetto che verrà legato alla nostra view, bookData, definizione che troviamo nel file di configurazione di Tiles (vedi prima parte della serie di articoli). SWF per applicare il binding fa uso del metodo shouldBind() della nostra AbstractMvcView, classe base per le View di SpringMVC (sia per le servlet che per le portlet).
Il metodo non farà altro che verificare l’esistenza dell’attributo bind e del suo valore boolean. Nel caso in cui questo attributo non venga aggiunto alla nostra transition (nel nostro esempio la transition “save”), il bind verrà applicato. In caso il boolean sia valorizzato a true, il framework si occuperà di effettuare il binding tra i parametri della request e gli attributi dell’oggetto definito.
Da notare che l’id book utilizzato come valore dell’attributo model, è stato precedentemente definito come variabile visibile all’interno dell’intero flusso attraverso l’uso del tag var. Questo elemento permette di definire una variabile con scope valido per l’intero flusso. Gli oggetti che vengono salvati all’interno dello scope flow devono obbligatoriamente implementare l’interfaccia java.io.Serializable.
Il tag var ci permette di definire il name con cui verrà salvato l’oggetto all’interno del flow scope e la class che indica quale oggetto il framework deve creare nello scope. Oltre a quanto mostrato nell’esempio, è possibile indicare nell’attributo model qualsiasi oggetto all’interno dei seguenti scope:
- flowScope: viene creato nel momento in cui un flow ha inizio e viene distrutto quando termina il flusso
- viewScope: viene creato nel momento in cui il flusso entra in un view-state e termina quando esce dallo stato
- requestScope: viene creato nel momento in cui viene creato uno state del flusso e distrutto al suo ritorno
- flashScope: viene creato nel momento in cui un flow ha inizio, viene ripulito in ogni view renderer, e viene distrutto quando il flusso termina
- conversationScope: viene creato quando il top-level flow ha inizio e termina quando il top-level flow termina. E’ valido anche per l’esecuzione dei sotto flussi.
Se nessuno scope viene definito per l’attributo model, come nel nostro esempio, lo scope viene automaticamente selezionato dal framework che ricerca nel seguente ordine: request, flash, view, flow e conversation. Nel caso non venga trovato l’attributo in nessuno degli scope precendenti, viene sollevata un’eccezione del tipo EvaluationException.
Analogamente a quanto abbiamo fatto per lo state enterBookData, anche per lo state enterAuthorData abbiamo definito il model author che verrà utilizzato per il binding automatico delegato al framework. Come detto in precedenza, le variabili definite attraverso l’uso del tag var hanno una visibilità per tutto il life-cycle del flow scope, pertanto hanno una durata pari alla durata del nostro flusso (ricordate però che non sono visibili all’interno dei sub-flow, per quello va usato lo scope conversationScope).
Questo ci permette di poter accedere ai nostri oggetti salvati nel flow scope, anche in pagine diverse da quelle usate per effettuare il binding. A dimostrazione di ciò, abbiamo opportunamente modificato l’ultima pagina del nostro flusso, la confirmationPage.jsp, per dimostrare come sia possibile accedere alle informazioni inserite in maschere precedenti.
... <div><h1>Book Summary!</h1></div> <div> <b>Book Title:</b> ${book.title } <br> <b>Author Name:</b> ${author.name } <br> <b>Pubbl. Year:</b> ${book.year} <br> <br> </div> ...
Eseguendo il nostro flusso di esempio, possiamo notare come le informazioni salvate nei primi due state, enterBookData e enterAuthorData, le ritroviamo nello state finale confirmation. Questo è possibile perchè abbiamo salvato gli oggetti book e author nello scope del flusso, se le avessimo rese disponibili solo al viewScope, ad esempio, non avremmo potuto accedere in nessun modo, nell’ultima pagina, ai valori degli oggetti Author e Book.
E’ possibile fare in modo che il binding non venga effettuato dal framework, tramite l’attributo bind=”false”. In questo modo, per particolari eventi, come ad esempio un’uscita da una pagina, il binding e la validazione (se presente) non vengono eseguiti. Di default l’attributo vale true.
Validazione
Unitamente al processo di binding, Spring Web Flow mette a disposizione anche la validazione. E’ possibile effettuare la validazione dei nostri model sia implementando un metodo di validazione all’interno del nostro model sia implementando una classe che avrà come compito esclusivo la validazione.
Oltre a queste due possibilità, SWF offre la possibilità di utilizzare qualsiasi framework esterno di validazione. Vediamo come implementare la validazione dei nostri bean Book e Author utilizzando entrambe le tecniche messe a disposizione dal framework.
Implementazione del metodo nel model
In questa prima implementazione andremo a modificare il nostro bean Book. Rispettando delle semplici convenzioni imposte da SWF creiamo un nuovo metodo, all’interno del nostro bean, che si chiamerà validateEnterBookData. La convenzione da seguire per questo tipo di approccio alla validazione con Web Flow, è quella di creare un metodo public void del tipo validate${state}, dove ${state} corrisponde all’id del nostro view-state.
Seguendo questa semplice regola abbiamo creato il nostro metodo come segue
public void validateEnterBookData(MessageContext messageContext) { MessageBuilder builder = new MessageBuilder(); Calendar calendar = Calendar.getInstance(); int year = calendar.get(Calendar.YEAR); if (this.year > year) { messageContext.addMessage( builder.error().source("year") .defaultText("Anno di pubblicazione errato").build()); } if (title.equals("")) { messageContext.addMessage( builder.error().source("title") .defaultText("Il titolo è obbligatorio").build()); } if (publishingHouse.equals("")) { messageContext.addMessage( builder.error().source("publishingHouse") .defaultText("La Casa Editrice è obbligatoria").build()); } }
La classe org.springframework.binding.message.MessageBuilder che abbiamo utilizzato nel codice di validazione, ci permette di creare dei messaggi del tipo: error, warning, fatal e info. E’ possibile utilizzare un testo di default, come nel nostro esempio, attraverso il metodo defaultText(String text) o utilizzare l’internazionalizzazione tramite i metodi code(String code) e codes(String[] codes). La stringa che utilizzeremo come code, sarà la chiave da ricercare nel nostro file .properties d’internazionalizzazione.
Infine, una volta preparato il messaggio, invochiamo il metodo build() che crea il messaggio che verrà poi visualizzato. Tutti i messaggi vengono conservati all’interno di un org.springframework.util.CachingMapDecorator nel MessageContext, fornito al metodo validate dal framework.
Una volta terminato il processo di validazione, nel caso ci siano degli errori presenti all’interno del MessageContext, SWF come view presenta la stessa pagina ricaricata e la gestione dell’errore è demandata allo sviluppatore. SpringMVC mette inoltre a disposizione delle tag library per la gestione degli errori.
... <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> ... <form method="post"> <spring:bind path="book.title"> <div class="label"> Titolo: </div> <div> <input type="text" id="title" name="title" value="${status.value}"> <c:if test="${status.error}"> <span style="color: red"> <c:forEach var="errorMessage" items="${status.errorMessages}"> ${errorMessage} </c:forEach> </span> </c:if> </div> </spring:bind> <div class="label"> Anno: </div> <spring:bind path="book.year"> <div> <input type="text" id="year" name="year" value="${status.value}"> <c:if test="${status.error}"> <span style="color: red"> <c:forEach var="errorMessage" items="${status.errorMessages}"> ${errorMessage} </c:forEach> </span> </c:if> </div> </spring:bind> <div class="label"> Casa Ed.: </div> <spring:bind path="book.publishingHouse"> <div> <input type="text" id="publishingHouse" name="publishingHouse" value="${status.value}"> <c:if test="${status.error}"> <span style="color: red"> <c:forEach var="errorMessage" items="${status.errorMessages}"> ${errorMessage} </c:forEach> </span> </c:if> </div> </spring:bind> <div> <input type="submit" name="_eventId_save" value="save"> </div> </form> ...
Implementazione della classe di validazione
Per il secondo tipo d’implementazione abbiamo creato una nuova classe Validator, il cui compito sarà esclusivamente quello di effettuare la validazione del nostro model. Anche in questo caso si devono seguire delle convenzioni stabilite dal framework.
Per questo tipo d’implementazione si deve creare una classe con il nome del tipo ${model}Validator per essere automaticamente caricato e invocato. La seconda convenzione è quella di creare un metodo per ogni view-state che vogliamo validare. Come nell’implementazione che abbiamo mostrato precedentemente, anche in questo caso il nome del metodo sarà del tipo validate${state}. Pertanto nel nostro esempio il validator avrà il nome AuthorValidator e il nome del metodo sarà validateEnterAuthorData
package it.study.webflow.library.validators; import it.study.webflow.library.beans.Author; import org.springframework.binding.message.MessageBuilder; import org.springframework.binding.message.MessageContext; import org.springframework.stereotype.Component; @Component public class AuthorValidator { public void validateEnterAuthorData(Author author, MessageContext context) { MessageBuilder builder = new MessageBuilder(); if (author.getName().equals("")) { context.addMessage(builder.error() .source("name").defaultText( "Nome obbligatorio").build()); } if (author.getSurname().equals("")) { context.addMessage(builder.error() .source("surname").defaultText( "Cognome obbligatorio").build()); } if (author.getTaxCode().equals("")) { context.addMessage(builder.error() .source("taxCode").defaultText( "Codice fiscale obbligatorio").build()); } else if (author.getTaxCode().length() < 16) { context.addMessage(builder.error() .source("taxCode").defaultText( "Codice fiscale errato").build()); } } }
Come possiamo notare l’implementazione del metodo è del tutto simile a quella vista precedentemente. In questo estratto di codice, due sono le differenze rispetto alla versione precedente di validazione e sono: l’uso dell’annotation @Component e il model che viene passato nel metodo di validazione.
L’annotazione org.springframework.stereotype.Component, ci permette d’indicare al container di Spring che la nostra classe validator è un componente. Tale classe verrà, tramite configurazione, automaticamente individuata e caricata dal container.
... <!-- Attiva la configurazione dei bean attraverso annotation --> <context:annotation-config /> <!-- Cerca tutti i @Component nel package indicato --> <context:component-scan base-package="it.study.webflow.library.validators" /> ...
La seconda differenza, consiste nel model passato nel nostro metodo validate. Questo ci permette di poter accedere ai valori del nostro bean nel codice di validazione e poter verificare gli attributi del model utilizzato per il binding.
Conclusioni
In questo secondo articolo d’introduzione a Spring Web Flow ci siamo occupati di due concetti come binding e validazione che in ogni applicazione sono operazioni di routine e piuttosto ripetitive. SWF rende queste operazioni molto semplici e immediate purchè si seguano alcune piccole convenzioni che rendono la vita più semplice allo sviluppatore che affronta queste tematiche.
Nella versione 2.0.3 di Web Flow, appena uscita, sono state aggiunte alcune piccole novità , come:
- possibilità di poter scegliere quali attributi del model mappare con i parametri di un form
- rendere alcuni attributi obbligatori oppure opzionali
- fare conversioni a un certo tipo di dato direttamente nel processo di binding
- possibilità di evitare la validazione senza escludere il binding
Per ulteriori informazioni vi rimandiamo alla documentazione ufficiale.
Link al primo articolo della serie
Commenti recenti