A chi non è mai capitato di compilare una form web che chiedeva di ripetere la password o l’indirizzo mail per conferma? Una richiesta più che legittima quando si stanno inserendo dati importanti. L’implementazione di questa logica è banale, ma quando si lavora con certi framework è bene seguirne le best practice per non ritrovarsi in situazioni spiacevoli: anche cose così semplici certe volte possono rivelare complicazioni inattese. Prendiamo per esempio JSF: come si potrebbe realizzare la validazione incrociata tra i campi? Possiamo scegliere una via semplice e veloce, sperando che non conduca all’inferno, o inerpicarci tra validatori e binding, rendendo le cose un po’ più complesse ma sicuramente più corrette!
La scorciatoia
grazie a , JSF serve ogni richiesta web attraverso un ciclo diviso in fasi ben definite: per validare il valore di un campo, è facile creare un validatore JSF, che verrà chiamato nell’opportuna fase dal framework. Ma quando c’è in gioco una validazione correlata tra campi? La prima cosa che viene in mente sarebbe: “chissenefrega! Salto la fase di validazione e faccio i controlli incrociati nella fase successiva in una action del backing bean!” Basta però poi fare i conti con una serie di cose del tipo: “come segnalo l’errore di validazione all’utente se sono nella fase successiva”? Oppure: “gli altri campi della form sono già stati aggiornati nel backing bean, ma la validazione è fallita! Come torno indietro?”. A queste e altre domande si risponde provando a validare i campi correlati nella fase giusta, cioè quella della validazione: vediamo come.
La retta via
Prendiamo per esempio una pagina che ci chiede di inserire due volte un indirizzo mail per conferma. Eccone il codice JSF:
e il corrispettivo backing bean:
@ManagedBean(name="crossValidationBB") @RequestScoped public class CrossValidationBackingBean implements Serializable { private String mail; private transient HtmlInputText inputText; public void checkConfirmedMail(FacesContext context, UIComponent component, Object value) { HtmlInputText input = (HtmlInputText) component.getAttributes().get("confirmedMail"); if (!value.equals(input.getSubmittedValue())) { FacesMessage message = new FacesMessage(); message.setSeverity(FacesMessage.SEVERITY_ERROR); message.setSummary("Valori diversi"); throw new ValidatorException(message); } else { FacesMessage message = new FacesMessage(); message.setSeverity(FacesMessage.SEVERITY_INFO); message.setSummary("OK!"); context.addMessage("validationForm:emailMessage", message); } } //... Getters & Setters ... }
La parte più interessante sono le linee evidenziate nel codice della pagina:
- il primo campo di input ha come value l’attributo mail del backing bean
CrossValidationBackingBean
e un validatore definito nel backing bean stesso che effettuerà il controllo tra i due campi. Al campo di input viene passato inoltre l’attributo confirmedMail, che è un oggetto di tipoHtmlInputText
come definito nel backing bean; - il secondo campo non ha value, ma definisce solamente il binding con l’attributo inputText. In questo modo si associa tutto il componente
al backing bean, non solo il suo value.
Il vantaggio di impostare la pagina in questo modo lo si vede nel metodo di checkConfirmedMail. Quando viene chiamato infatti siamo in fase di validazione: gli attributi del backing bean non sono stati ancora aggiornati, quindi è necessario recuperare i valori introdotti dall’utente in altro modo. La firma del metodi ci restituisce il value del primo campo di input, mentre dall’attributo confirmedMail che gli abbiamo passato, recuperiamo tutto il campo di tipo HtmlInputText
, al quale poi chiederemo il submittedValue, che sarà il valore inserito nel secondo campo di input!. Abbiamo quindi ottenuto il nostro scopo: riuscire a confrontare i valori dei due campi nella fase di validazione del ciclo di vita.
RichFaces Graph Validator
E poi venne RichFaces: grazie alla bean validation (JSR-303) e RichFaces 4, è possibile semplificare notevolmente l’esempio precedente. Prendiamo per esempio la stessa pagina di prima, questa volta con qualche modifica:
I campi di input interni al tag rich:graphValidator
saranno interessati dalla validazione incrociata. Come funziona? Per preservare i dati sul backing bean, durante la fase di validazione, RichFaces esegue un clone del backing bean specificato dall’attributo value, che poi provvede ad aggiornare realmente nella fase di update model se la validazione viene passata con successo. L’esito della validazione è determinata da un metodo senza argomenti che restituisce un booleano, annotato con javax.validation.constraints.AssertTrue
. Da notare che questo metodo deve seguire la specifica JavaBean, altrimenti una bella eccezione in console ce lo farà ricordare!
@ManagedBean(name="crossValidationBB") @RequestScoped public class CrossValidationBackingBean implements Serializable, Cloneable { private String mail; private String confirmedMail; @AssertTrue(message = "Sono diversi!") public boolean isConfirmed() { return mail.equals(confirmedMail); } // Getters & Setters }
Il backing bean che regola la validazione è estremamente semplice. Da notare un paio di cose:
- Il backing bean estende l’interfaccia
Cloneable
. Se non viene fatto, la validazione funziona ugualmente ma si rischia di ritrovarsi con valori non desiderati sul backing bean in caso di validazioni fallite. Per capire meglio le problematiche degli oggetti clonabili vi consiglio di leggere il post di intitolato ; - Il metodo annotato restituisce
boolean
per cui giustamente comincia con is; se avesse restituitoBoolean
sarebbe cominciato con get.
Conclusioni
Con la bean validation e RichFaces 4 la validazione incrociata diventa estremamente semplice e intuitiva. Le cose si complicano però quando in una form abbiamo più coppie di campi da validare. Se si inseriscono tutti questi campi all’interno dello stesso tag graphValidator e si creano diversi metodi annotati con AssertTrue
, tutto funziona correttamente, e i metodi vengono chiamati uno per uno. Se però non possiamo raggrupparle insieme e inseriamo nella pagina diversi graphValidator che puntano allo stesso backing bean, verranno comunque chiamati tutti i metodi annotati con AssertTrue
per ogni graphValidator, provocando spiacevoli NullPointerException
. Pare infatti che il clone del backing bean venga popolato esclusivamente con i valori interni al graphValidator, ma i metodi di validazione vengono chiamati ugualmente tutti!! Per form piuttosto complesse quindi, forse rimane ancora valida la prima soluzione presentata, che possiamo però semplificare grazie alle funzioni di RichFaces.
Togliamo per esempio il binding al secondo campo di input (conf-email) e modifichiamo l’attributo passato al primo come segue:
Senza quindi aggiungere il binding tra l’HtmlInputText e il backing bean, la funzione RichFaces provvederà a passarlo direttamente al validatore come attributo.
Pingback: ()