Ajax e JSON con JSF2

JSF2 ha imparato molto dall’esperienza della versione precedente e dai framework che gli ruotavano attorno a tal punto da averne integrati alcuni. Un esempio è Facelets, ma sicuramente quella più utile è stata l’integrazione nativa con Ajax, prendendo spunto da Ajax4JSF (a4j), confluito in RichFaces da molti anni ormai. Una cosa però manca all’implementazione nativa di JSF2, ovvero l’attributo data che possiedono tutti i componenti a4j che eseguono chiamate ajax. Cos’è quest’attributo data? Vediamo allora di fare un passo indietro.

JSON e Ajax4JSF

Solitamente quando si lavora con JSF è difficile che si metta mano a codice Javascript. Questo almeno può essere vero per applicazioni semplici, ma per applicazioni mediamente complesse ormai non si può prescindere dal giocare un po’ anche con Javascript, meglio ancora se con jQuery ;). Se però abbiamo bisogno di maneggiare informazioni ricevute dal server dopo una chiamata ajax come si può fare? L’attributo data dei componenti a4j serve proprio a questo! Basta bindarlo ad un attributo del backing bean per ritrovarselo serializzato in formato JSON sull’evento oncomplete della chiamata ajax all’interno della variabile javascript data. Con questa tecnica quindi a4j (e quindi RichFaces) risolvono il problema di come inviare informazioni dal server al client “masticabili” in Javascript sulla pagina web. Tutto questo però in JSF2 non esiste senza il supporto di un framework di terze parti come RichFaces o PrimeFaces, o almeno non out-of-the-box! Vediamo quindi come risolvere il problema con le nostre sole forze!

Ajax e JSON in azione

L’unico modo quindi che abbiamo per inviare le informazioni che vogliamo al client al termine di una chiamata ajax è riuscire a scrivere all’interno dell’xml generato in risposta dal server. Prendendo spunto (un grosso spunto 😉 ) dalla tecnica usata da PrimeFaces, non è poi così difficile mettere le mani a quell’xml e scriverci quello che vogliamo.
L’idea quindi è quella di avere una classe che permetta di gestire lato server gli oggetti che vogliamo mandare al client e una variabile Javascript che ospiterà gli oggetti serializzati in formato JSON.
Cominciamo quindi col definire un ResponseContext, legato al thread corrente, che ospiterà gli oggetti che vogliamo mandare al client all’interno di una mappa, che sarà possibile usare da un backing bean così:

ResponseContext.getCurrentInstance().addResponseObject("simbleBoolean", true);
ResponseContext.getCurrentInstance().addResponseObject("simpleString", "simpleValue");
ResponseContext.getCurrentInstance().addResponseObject("myObject", new MyObject("test1"));
ResponseContext.getCurrentInstance().addResponseObject("myList", Arrays.asList(new MyObject("test1"), new MyObject("test2")));

E’ possibile quindi inviare al client tipi semplici (come stringhe o booleani), oggetti o persino collezioni di oggetti! L’xml generato in risposta ad una normale chiamata ajax JSF sarà del tipo:

<partial-response>
   <changes>
      <update id="javax.faces.ViewState">XXXXXXXXX</update>
      <extension id="it.cosenonjaviste.data">{"simbleBoolean":true,"simpleString":"simpleValue","myObject":{"myParam":"test1"},"myList":[{"myParam":"test1"},{"myParam":"test2"}]}</extension>
   </changes>
</partial-response>

dove ai normali tag xml, troviamo una extension. A questo punto lato client è facile trasformare il contenuto del tag extension in un oggetto javascript da poter usare alla fine della chiamata ajax. Vediamo quindi cosa serve per ottenere tutto ciò.

Personalizziamo l’ajax response di JSF2!

Lato Server

Cominciamo quindi con la classe ResponseContext

public class ResponseContext {
   
   private static ThreadLocal<ResponseContext> responseContext = new ThreadLocal<ResponseContext>();
   
   private Map<String, Object> responseObjects = new HashMap<String, Object>();

   private ResponseContext() {}
   
   public static ResponseContext getCurrentInstance() {
      if (responseContext.get() == null) {
         responseContext.set(new ResponseContext());
      }
      return responseContext.get();
   }
   
   public void addResponseObject(String key, Object value) {
      responseObjects.put(key, value);
   }
   
   Map<String, Object> getResponseObjects() {
      return Collections.unmodifiableMap(responseObjects);
   }

   void reset() {
      responseObjects.clear();
   }
	
   boolean hasResponseObjects() {
      return !responseObjects.isEmpty();
   }   
}

Ogni richiesta web avrà quindi il suo ResponseContext in un thread local a cui può aggiungere gli oggetti che vuole direttamente da un backing bean tramite il metodo addResponseObject, come visto poco fa. Come fanno però gli oggetti ad andare a finire serializzati sul client? C’è bisogno di “decorare” il PartialResponseWriter di JSF aggiungendo la gestione del nostro ResponseContext sul’xml della risposta ajax:

public class JSONResponseWriter extends PartialResponseWriter {

   private PartialResponseWriter wrapped;

   public JSONResponseWriter(ResponseWriter writer) {
      super(writer);
      wrapped = (PartialResponseWriter) writer;
   }

   @Override
   public void endDocument() throws IOException {
      Map<String, String> extensionAttrs = new HashMap<String, String>(1);
      extensionAttrs.put("id", "it.cosenonjaviste.data");
      startExtension(extensionAttrs);

      ResponseContext.getCurrentInstance().addResponseObject("validationFailed", FacesContext.getCurrentInstance().isValidationFailed());
      write(new Gson().toJson(ResponseContext.getCurrentInstance().getResponseObjects()));
      ResponseContext.getCurrentInstance().reset();

      endExtension();
      wrapped.endDocument();
   }

//Atri metodi delegati...
}

Il codice non è completo. Per brevità ho omesso i metodi da delegare alla variabile wrapped che sono:

  • delete
  • endError
  • endEval
  • endExtension
  • endInsert
  • endUpdate
  • redirect
  • startDocument
  • startError
  • startEval
  • startExtension
  • startInsertAfter
  • startInsertBefore
  • startUpdate
  • updateAttributes

facilmente ricreabili con pochi click in Eclipse.
In pratica quindi l’unico metodo sovrascritto è endDocument, il cui contenuto è piuttosto semplice:

  1. Nel thread local viene sempre inserito l’esito della validazione della richiesta corrente (come fa PrimeFaces… comodo no?), viene aperto un tag extension con id = it.cosenonjaviste.data (definito tramite la mappa extensionAttrs). Il nome è arbitrario, ma mantenere il nome del dominio assicura una certa univocità.
  2. Viene serializzata in un unico oggetto JSON la mappa di oggetti con Google GSon (o con qualsiasi altro framework che preferite).
  3. Prima di chiudere il tag extension, ricordarsi di svuotare la mappa! Gli Application Server infatti usano dei thread pool per servire le richieste per cui se non svuotiamo i valori un altro utente se li può ritrovare impostati senza accorgersene!

Lato server rimane solo da “agganciare” il nostro ResponseWriter al ciclo di vita di JSF. Per far questo occorre:

  • Un contesto che inizializzi il nostro JSONResponseWriter:
    public class JSONPartialViewContextWrapper extends PartialViewContextWrapper {
    
       private PartialViewContext wrapped;
       private PartialResponseWriter writer = null;
       
       public JSONPartialViewContextWrapper(PartialViewContext wrapped) {
          this.wrapped = wrapped;
       }
       
       @Override
       public PartialViewContext getWrapped() {
          return this.wrapped;
       }
    
       @Override
       public void setPartialRequest(boolean value) {
           getWrapped().setPartialRequest(value);
       }
    
       @Override
       public PartialResponseWriter getPartialResponseWriter() {
           if (writer == null) {
               PartialResponseWriter parentWriter = getWrapped().getPartialResponseWriter();
               writer = new JSONResponseWriter(parentWriter);
           }
           return writer;
       }
    }
    
  • Una factory del nuovo contesto:
    public class JSONPartialViewContextFactory extends PartialViewContextFactory {
    
       private PartialViewContextFactory parent;
    
       public JSONPartialViewContextFactory(PartialViewContextFactory parent) {
          this.parent = parent;
       }
    
       @Override
       public PartialViewContextFactory getWrapped() {
          return this.parent;
       }
    
       @Override
       public PartialViewContext getPartialViewContext(FacesContext fc) {
          PartialViewContext parentContext = getWrapped().getPartialViewContext(
                fc);
          return new JSONPartialViewContextWrapper(parentContext);
       }
    }
    
  • Registrare la factory nel faces-config.xml:
    <factory>
       <partial-view-context-factory>
          it.cosenonjaviste.web.response.JSONPartialViewContextFactory
       </partial-view-context-factory>
    </factory>
    

Lato Client

A quest punto vediamo cosa succede sul client. La famosa chiamata ajax può essere scatenata da un bottone del tipo:

<h:commandButton value="Button" id="myButton" action="#{myBackingBean.myAction()}">
   <f:ajax execute="@form" onevent="doOnEvent" onerror="doOnError" render="myForm:myDiv" />
</h:commandButton>

Qual è quindi il posto giusto per recuperare l’oggetto JSON inviato dal server?
Sicuramente negli unici posti lato client dove si ha controllo, ovvero nelle funzioni collegate agli eventi onevent (doOnEvent in questo caso) e onerror (doOnError) che ricevono come argomento l’oggetto javascript evento. Ogni chiamata ajax infatti distingue 3 fasi:

  1. begin: viene chiamata la funzione doOnEvent prima della chiamata ajax;
  2. complete viene chiamata la funzione doOnEvent al termine della chiamata ajax, indipendentemente dall’esito;
  3. success/error viene chiamata la funzione doOnEvent in caso di esito corretto oppure viene chiamata la funzione doOnError.

Scegliamo per esempio l’evento complete perché sempre chiamato indipendentemente dall’esito. Possiamo quindi inventarci una funzione onComplete da usare così:

<h:commandButton value="Button" id="myButton" action="#{myBackingBean.myAction()}">
   <f:ajax execute="@form" onevent="function(e) {onComplete(e, myCallbackOnComplete)}" render="myForm:myDiv" />
</h:commandButton>

var onComplete = function(e, callback) {
   if (e.status == 'complete' && e.responseXML) {
      var extension = e.responseXML.getElementsByTagName('extension');
      if (extension && extension.length > 0) {
         for (var i = 0; i < extension.length; i++) {
            if (extension.item(i).getAttribute('id') == 'it.cosenonjaviste.data') {
               var data = jQuery.parseJSON(extension.item(i).childNodes[0].nodeValue);
               callback(data);
            }
         }
      }
   }
};
var myCallbackOnComplete = function(data) {
   console.debug(data);
}

Ovviamente a questo punto la fantasia si può sbizzarrire per le mille soluzioni che si possono trovare, l’importante è sapere dove e come ripescare il nostro oggetto JSON!

Conclusioni

A che serve tutto ciò? Ultimamente a lavoro stiamo esplorando altre strade più client-oriented rispetto a RichFaces e PrimeFaces. Senza rinunciare alla potenza lato server di JSF, con queste poche classi scritte una volte per tutte, siamo in grado di controllare le informazioni inviate al client e siamo liberi allo stesso tempo di scegliere qualsiasi framework web (come Bootstrap per esempio, che consiglio vivamente, o jQuery Mobile), senza legarsi alle funzionalità e agli stili che RichFaces o PrimeFaces si portano inesorabilmente dietro. Inoltre, adesso avete sul client l’esito della validazione in una semplice variabile javascript… come facevate sennò fino adesso? 😉

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+