Cross-field validation: validazione incrociata con JSF e RichFaces 4

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

Come sappiamo, grazie a Gabriele, 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:

<h:form id="validationForm">
<h:panelGrid columns="3" style="margin: 0 auto;">
   <h:outputLabel value="E-mail" for="email"/>
   <h:inputText value="#{crossValidationBB.mail}" id="email" validator="#{crossValidationBB.checkConfirmedMail}" required="true">
      <f:attribute name="confirmedMail" value="#{crossValidationBB.inputText}"></f:attribute>
   </h:inputText>
   <h:message for="email" id="emailMessage" />
   <h:outputLabel value="Ripeti E-mail" for="conf-email"/>
   <h:inputText id="conf-email" binding="#{crossValidationBB.inputText}" required="true"/>
   <h:message for="conf-email" />
   <h:commandButton value="Check" />
</h:panelGrid>
</h:form>

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 tipo HtmlInputText 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 <h:inputText> 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:

<h:form id="graphValidationForm">
<h:panelGrid columns="3" style="margin: 0 auto;">
<rich:graphValidator value="#{crossValidationBB}">
   <h:outputLabel value="E-mail" for="email"/>
   <h:inputText value="#{crossValidationBB.mail}" id="email" required="true" />
   <rich:message for="email" id="emailMessage" />
   <h:outputLabel value="Ripeti E-mail" for="conf-email"/>
   <h:inputText id="conf-email" value="#{crossValidationBB.confirmedMail}" required="true"/>
   <rich:message for="conf-email" />
   <a4j:commandButton value="Check"></a4j:commandButton>
</rich:graphValidator>
</h:panelGrid>
</h:form>

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 Fabio intitolato L’interfaccia Java Cloneable;
  • Il metodo annotato restituisce boolean per cui giustamente comincia con is; se avesse restituito Boolean 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:

<f:attribute name="confirmedMail" value="#{rich:findComponent('conf-email')}"></f:attribute>

Senza quindi aggiungere il binding tra l’HtmlInputText e il backing bean, la funzione RichFaces provvederà a passarlo direttamente al validatore come attributo.

Andrea Como

Sono un software engineer focalizzato nella progettazione e sviluppo di applicazioni web in Java. Presso OmniaGroup ricopro il ruolo di Tech Leader sulle tecnologie legate alla piattaforma Java EE 5 (come WebSphere 7.0, EJB3, JPA 1 (EclipseLink), JSF 1.2 (Mojarra) e RichFaces 3) e Java EE 6 con JBoss AS 7, in particolare di CDI, JAX-RS, nonché di EJB 3.1, JPA2, JSF2 e RichFaces 4. Al momento mi occupo di ECM, in particolar modo sulla customizzazione di Alfresco 4 e sulla sua installazione con tecnologie da devops come Vagrant e Chef. In passato ho lavorato con la piattaforma alternativa alla enterprise per lo sviluppo web: Java SE 6, Tomcat 6, Hibernate 3 e Spring 2.5. Nei ritagli di tempo sviluppo siti web in PHP e ASP. Per maggiori informazioni consulta il mio curriculum pubblico. Follow me on Twitter - LinkedIn profile - Google+