Questo articolo mostrerà come costruire passo passo un piccolo sito web utilizzando le tecnologie JSF, Facelets e ICEFaces assieme. Inoltre procederemo a disegnare prima i widget delle pagine web (la parte View), e poi separatamente i backing bean (cioe‘ la parte Controller); in questa maniera porremo l‘accento sui benefici che otteniamo dall‘uso del pattern MVC (grazie alle JSF) per il disegno e scrittura di un sito web.
In questo articolo scriveremo un sito Web 2.0 basato su ICEFaces che ci permetterà di utilizzare alcune caratteristiche proprie dei siti Web 2.0, dove l’interattività con l’utente è ben maggiore rispetto ai siti più vecchi e dove il server non è solo una componente passiva che riceve richieste e invia risposte al client, ma è anche parte attiva: può inviare dati e cambiare delle parti di pagine web autonomamente senza alcuna interazione dell’utente (ajax server push). Questa caratteristica dei siti basati su ajax è molto importante: basti pensare alle tante chat “HTML” che sono state implementate recentemente grazie al server push.
Il sito che andremo a costruire è molto semplice: è composto da due pagine, la prima è una pagina di login dove l’utente si autentica, la seconda è una pagina in cui l’utente autenticato inserisce un messaggio al server, e il server risponde in “broadcast” elaborando il messaggio e rimandando indietro un messaggio di saluto contemporaneamente a tutti i browser collegati.
Vediamo ora i requisiti del sito e le soluzioni proposte per implementarlo.
Requisiti
Ecco una prima parte dei requisiti del progetto “HelloWorld ICEFaces!” (il sito lo arricchiremo nel prossimo articolo della serie dove aggiungeremo nuove funzionalità):
- Sito Web interattivo: Il sito fornisce con l’utente un front-end web-based ed interattivo.
- Pagina di login: Al sito è possibile accedere solo dopo una autenticazione tramite utenza/password.
- Validazione per la login e per la password: Sia il login che la password devono essere presenti e di almeno 3 caratteri.
- Ogni pagina ha un header e un footer, e nell’header si deve mostrare la data e l’ora: Il sito deve mostrare sempre un header ed un footer dove vengono mostrati: l’ora corrente e il nome dell’utente registrato.
- Ogni istanza del browser deve mostrare in tempo reale i dati aggiornati: Dopo essersi autenticato, l’utente accede ad una pagina in cui immette il proprio messaggio di saluto in una text field: l’ultimo messaggio di saluto immesso deve essere visibile in tempo reale a tutti gli altri utenti che sono connessi in quel momento.
Soluzione
Vediamo che soluzioni adottare per implementare ogni punto descritto nel paragrafo precedente.
Sito Web interattivo
Questo primo punto si implementa facilmente utilizzando le JSF, ed in particolare utilizzando la libreria Mojarra 1.2 (Reference Implementation della Sun) con ICEFaces 1.8.2. Per implementare questo sito faremo uso anche delle Facelets 1.1.14 (il progetto è ovviamente gestito con Maven, quindi fate riferimento al pom.xml che troverete nel pacchetto dei sorgenti per analizzare quali librerie sono state incluse).
Pagina di login
Questo requisito lo implementeremo in maniera molto semplice tramite una form di login/password (un pannello di tipo ice:panelCollapsible) senza aggiungere alcuna tecnologia di cifratura.
Validazione standard per la password
Questo requisito lo implementeremo in due passi:
- Utilizeremo uno standard validator delle JSF (f:validateLength) in cui valideremo i dati immessi dall’utente e mostreremo messaggi di errore.
- Utilizzeremo il partial submit dei campi della form in modo tale da validare subito uno dei due dati (password o login) senza aspettare ne’ l’immissione dell’altro dato ne’ la pressione del tasto di “submit”.
Ogni pagina ha un header e un footer, e nell’header si deve mostrare la data e l’ora
Questo requisito verrà implementato in due passi:
- Come primo passo utilizziamo un template di pagina in cui includeremo un headerr, un body ed un footer. L’header ed il footer sono definiti una volta per tutte, mentre il contenuto del body cambia a seconda della pagina richiesta. Questo meccanismo verrà implementato tramite l’ausilio delle facelets.
- Come secondo passo, per la visualizzazione dell’ora corrente, è necessario definire un backing bean di tipo Renderable con un InternalRenderer di 1000 millisecondi: in sostanza stiamo usando la tecnologia ajax server push.
Ogni istanza del browser deve mostrare in tempo reale i dati aggiornati
L’essenza di “Ajax Push” è l’abilità di notificare aggiornamenti o cambiamenti al client che partono direttamente dal server. Questo rende il Server Push utile per applicazioni “collaborative” dove un utente fa qualcosa che cambia lo stato corrente dell’applicazione e gli altri utenti che sono interessati devono essere notificati del cambiamento in tempo reale.
Quindi la soluzione che adottiamo è sicuramente quella di utilizzare un renderer Ajax per aggiornare tutte le viste in tempo reale e solo quando serve. Basterà, infatti, utilizzare il SessionRenderer di ICEFaces ogniqualvolta l’utente cambia il messaggio di benvenuto (per verificare questo requisito faremo vedere che il messaggio viene aggiornato in due browser differenti contemporaneamente (Safari e Firefox), in modo tale da garantire che i due browser non utilizzino la stessa cache). Questa pagina la realizzeremo con una text field dove viene immesso il saluto ed un bottone di submit per sottomettere il dato.
Struttura delle pagine con le Facelets
Prima di tutto definiamo la struttura del progetto: si tratta di un progetto web, quindi rispettiamo le specifiche delle servlet 2.5 (usiamo Tomcat 6) utilizzando la directory WEB-INF come contenitore di tutte le pagine web.
Creiamo il file web.xml per la definizione delle servlet e listener che servono per far funzione le JSF con ICEFaces ed il file faces-config.xml dove vengono definiti sia i backing bean che le pagine con le regole di navigazione.
Ecco in figura 1 come compare l’alberatura dei sorgenti.
Il sito sarà composto da due pagine: la pagina iniziale di “login” e la pagina “hello” che conterrà il messaggio di saluto: la navigazione da login a menu avverrà dopo essersi loggato con successo.
Siccome ogni pagina del sito deve contenere sempre lo stesso header e lo stesso footer, utilizziamo le facelets per scrivere un template che contenga una vista (f:view) con le seguenti inclusioni:
- un header.xhtml dove compariranno l’ora e il nome dell’utente loggato;
- un body dove compariranno i contenuti delle pagine;
- un footer.xhtml dove comparirà solamente il Copyright del sito.
Per convenzione creiamo una cartella template dentro WEB-INF dove creiamo il nostro template.xhtml mentre i frammenti di pagine da includere (header, footer e le parti centrali) dentro la cartella includes
Configurazione di ICEFaces in web.xml
Il file web.xml contiene le definizioni di tutte le servlet e listener che permettono ad icefaces di funzionare. Vediamo ora le definizioni da includere.
Parametri di contesto ()
- com.icesoft.faces.concurrentDOMViews: Specifica se ICEFaces deve supportare viste multiple di una singola applicazione dallo stesso browser: mettendo a true questo parametro permettiamo ad ICEFaces di aggiornare il valore dei dati in tempo reale ad ogni vista della stessa pagina (requisito 5).
- com.icesoft.faces.synchronousUpdate: Specifica se ICEFaces deve supportare l’update sincrono o asincrono: mettiamo questo parametro a false in modo tale da attivare gli aggiornamenti parziali.
- com.icesoft.faces.blockUIOnSubmit: Definiamo a true questo parametro: in questo modo in ogni pagina in cui ci sia un submit, ICEFaces non permette all’utente di cliccare sulla pagina facendo comparire una clessidra (per esempio in questo modo l’utente non può cliccare velocemente più volte sul bottone di submit della pagina di login).
Listeners ( )
- com.icesoft.faces.util.event.servlet.ContextEventRepeater: Serve per gestire il contesto di ICEFaces effettuando il forward di eventi delle servlet verso ICEFaces.
Servlets ():
- javax.faces.webapp.FacesServlet: Faces servlet.
- com.icesoft.faces.webapp.xmlhttp.PersistentFacesServlet: Faces Servlet di ICEFaces.
- com.icesoft.faces.webapp.xmlhttp.BlockingServlet: Servlet per gestire gli eventi asincroni.
Descrizione dei componenti del sito web
Procediamo adesso a descrivere le singole parti del sito web utilizzando la seguente strategia: descriveremo separatamente prima la parte Vista e poi la parte Controller per dividere concettualmente le due parti del sito: la parte descrittiva e la parte algoritmica.
Questo modo di procedere è utile quando si scrive un sito web basato su JSF (pattern MVC): l’eleganza di questo web framework risiede proprio nella possibilità di dividere in modo netto la parte che descrive i pannelli grafici (fatta in xhtml) dalla parte che “fa funzionare” il sito, cioè la parte algoritmica scritta in java.
Header e Footer
Descriviamo ora i componenti grafici dell’header. I componenti che dovrà avere l’header sono:
- la scritta “Not Logged” (se l’utente non è loggato);
- il nome dell’utente loggato (se presente);
- la data e l’ora corrente;
- una icona che indica lo stato di connessione verso il server (connesso, idle, non connesso).
Il footer invece sarà molto semplice: al suo interno conterrà solamente un messaggio di copyright.
Ecco in figura 4 come dovranno comparire header e footer associati alla pagina di login.
Per costruire il pannello di header possiamo fare riferimento allo showcase di ICEFaces: è un sito web (è possibile scaricarne il WAR ed installarlo su Tomcat 6) in cui vengono mostrati gran parte dei componenti grafici che abbiamo a disposizione con la descrizione del loro funzionamento.
Dallo showcase possiamo apprendere la definizione dei pannelli, dell’outputConnectionStatus e degli outputText che saranno utili al nostro scopo. L’header sarà quindi composto da:
- ice:panelGroup: è il contenitore di tutti i widget grafici dell’header; in questo caso viene utilizzato per definire lo stile grafico: icePnlClpsbl (ICE Panel Collapsible style);
- ice:panelGroup: definisce lo stile del contenuto del pannello: icePnlClpsblCnt (ICE Panel Collapsible Content style);
- ice:outputText: contiene la scritta “Not Logged” se l’utente non è loggato;
- ice:outputText: contiene il nome dell’utente loggato;
- ice:outputText: contiene la data e l’ora: utilizziamo un backing bean chiamato timeZoneBean che analizzeremo in seguito;
- ice:outputConnectionStatus: permette di visualizzare una icona diversa per stato di connessione, nell’immagine riportata di sopra l’icona è di colore grigio che sta ad indicare che lo stato di connessione è idle.
Ecco di seguito un estratto del sorgente di header.xhtml:
value="Not Logged" rendered="#{!userController.logged}"/> value="Welcome #{userController.loggedUser} !" rendered="#{userController.logged}"/> value=" - "/> value="#{timeZoneBean.serverTime}"/>
Il footer deve essere sempre agganciato al bordo del browser, inoltre il messaggio da mostrare deve essere preso da un file di proprietà.
Ecco di seguito un estratto del file footer.xhtml:
<ice:panelGroup styleClass="icePnlClpsbl" style="position:absolute;bottom:0;left:4px;right:4px"> style="font-weight:800" />
Notiamo l’uso di ice:loadBundle che definisce il file messages.properties utilizzato per i messaggi testuali all’interno del pannello.
Lo stile dell’ice:panelGroup è definito con posizione assoluta sul fondo della pagina, lasciando 4 pixel di spazio al bordo sinistro e destro: in questa maniera avremo il pannello allineato con quello dell’header.
Il backing bean TimeZoneBean
Analizziamo ora nel dettaglio il bean TimeZoneBean. Ecco la definizione della classe:
public class TimeZoneBean implements Renderable, DisposableBean { public PersistentFacesState getState() { ... } public void renderingException(RenderingException renderingException) { ... } public void dispose() throws Exception { ... } }
Il bean implementa l’interfaccia Renderable che indica che il bean è “disegnabile” sulla pagina html tramite l’unico RenderManager che registra tutti i renderer definiti dall’utente.
L’interfaccia DisposableBean serve per pulire lo stato del bean all’atto della distruzione.
Vediamo ora passo passo come implementare il TimeZoneBean.
Prima di tutto inizializziamo correttamente il persistent state gestito dal RenderManager, e creiamoci l’oggetto java.util.TimeZone da cui prendere l’ora di sistema del server:
private void init() { serverTimeZone = TimeZone.getDefault(); serverFormat = buildDateFormatForTimeZone(serverTimeZone); selectedTimeZone = TimeZone.getTimeZone("Etc/GMT+0"); ... state = PersistentFacesState.getInstance(); }
Il metodo che costruisce l’ora da visualizzare è buildDateFormatForTimeZone() che restituisce un java.text.DateFormat: il metodo è molto semplice, legge l’ora da sistema tramite time zone e formatta l’ora in base a un pattern pre-fissato.
Dovrebbe esserci sempre un solo RenderManager per applicazione. Per assicurarci di questo occorre definire, nel file faces-config.xml, lo scope di TimeZoneBean come “application”, ed usare sempre il RenderManager che viene fornito dal framework tramite il metodo di callback setRenderManager().
Vediamo ora come è stato implementato il metodo setRenderManager():
public void setRenderManager(RenderManager renderManager) { clock = renderManager.getIntervalRenderer("clock"); clock.setInterval(renderInterval); clock.add(this); clock.requestRender(); }
Come si vede, non abbiamo definito una proprietà di tipo RenderManager nel TimeZoneBean perche’ non ci serve direttamente e perche’ deve essere creato sempre dal framework.
Tramite il RenderManager ci creiamo il nostro Render (IntervalRenderer) che chiamiamo “clock”, e assegniamolo alla proprietà clock del TimeZoneBean. Definiamo l’intervallo di tempo in cui disegnarsi (fornito in millisecondi). Aggiungiamo alla lista di renderer associati al clock il nostro backing bean e registriamolo tramite requestRender().
A questo punto il nostro TimeZoneBean è stato registrato come Renderer al RendererManager (tramite clock) e verrà quindi “disegnato” su tutti i client ad ogni intervallo renderInterval. Adesso basterà includere nel componente header.xhtml la seguente definizione di outputText:
per disegnare non solo il valore serverTime (che è una stringa che contiene l’ora corrente), ma per assicurarsi anche che il suo valore verrà sempre ridisegnato ad ogni intervallo di tempo.
Infine il metodo di dispose del bean (ereditato dall’interfaccia DisposableBean) non fa altro che pulire l’instanza di clock nel seguente modo:
if (clock != null) { clock.remove(this); if (clock.isEmpty()) { clock.dispose(); } clock = null; }
La pagina di login
Dopo aver completato la descrizione dell’header e del footer facendo vedere prima la parte “vista” e poi la parte “controller”, passiamo ora al pannello di login con il solito procedimento.
Il pannello del Login
Il pannello del login è definito come un ui:composition agganciato al template decritto precedentemente. Il pannello deve inviare due dati al server: la stringa di login (il nome dell’utenza) e la stringa della password.
Costruiamo il login come una ice:form (con partialSubmit settato a true per poter validare i dati immediatamente e singolarmente) che contiene un ice:panelCollapsible, che è un pannello di ICEFaces, contenente un header ice:outputText con la scritta “User Login”, e un corpo formato da un ice:panelGrid che contiene (in griglia) i campi da immettere.
In ultimo abbiamo utilizzato un bottone ice:commandButton per inviare i dati della form al server.
Ecco di seguito un estratto del file login.xhtml:
<ice:panelCollapsible style="width:300px;position:absolute;top:30%;left:30%" id="loginPanel" toggleOnClick="false" expanded="true"> required="true"> value="#{userController.user.password}" required="true"/> action="#{userController.login}" />
A questo punto basterà definire il backing bean userController nel file faces-config.xml e, per evitare di prendere un errore dal D2DFaceletViewHandler, basterà solamente definire tutti i metodi che servono senza implementarli completamente: in questo modo il pannello sarà già visibile nel browser per essere valutato (ed eventualmente accettato) in fase di analisi del lavoro svolto.
Adesso che il pannello è pronto, aggiungiamo i validatori alla login e alla password.
Per validare la lunghezza dei dati immessi dall’utente basterà aggiungere il tag f:validateLength all’interno dei tag ice:inputText ed ice:inputSecret ed aggiungere i messaggi di errore alla fine della form (ice:message).
Ecco la versione definitiva del pannello di login con i validatori:
<ice:panelCollapsible style="width:300px;position:absolute;top:30%;left:30%" id="loginPanel" toggleOnClick="false" expanded="true"> required="true"> value="#{userController.user.password}" required="true"> action="#{userController.login}" />
In figura 5 un esempio di messaggi di errore notificati all’utente.
Il backing bean UserController
Il backing bean UserController è veramente semplice: definisce solamente il metodo login() per autenticare l’utente.
Il metodo di login() è stato implementato in modo tale da autenticare sempre l’utente, qualunque siano i dati immessi (è solo un esempio!); e di far attendere 3 secondi prima di restituire la risposta al client.
In questa maniera possiamo analizzare due comportamenti diversi di ICEFaces:
- l’icona di stato: cambia colore per 3 secondi per indicare che la connessione sta in stato di “active”;
- compare per 3 secondi la clessidra che impedisce all’utente di premere nuovamente il tasto di submit (com.icesoft.faces.blockUIOnSubmit) evitando al server di autenticare più volte lo stesso utente con gli stessi dati.
I dati dell’utente vengono così registrati nella SessionMap delle JSF per poter essere letti dall’header.
La pagina del messaggio di saluto
La pagina in cui l’utente inserisce il proprio messaggio di saluto è costituita da un pannello che contiene una text field dove viene inserito il testo e un output text dove viene scritto il messaggio elaborato dal server.
In figura 6 ecco come deve comparire il pannello.
La scritta di saluto compare in basso a destra di colore rosso (l’applicazione aggiunge la parola “Mondo” al saluto inserito dall’utente). Il pannello usato è simile a quello di login, non ci sono molte descrizioni interessanti da fare.
Ecco come sono definiti l’ice:commandButton per invocare il metodo che genera il nuovo messaggio e l’outputLabel del messaggio:
partialSubmit="true" action="#{applicationHello.changeMessage}" />
Il backing bean ApplicationHelloService
Il backing bean ApplicationHelloService gestisce l’elaborazione del messaggio di saluto. Estende la classe HelloService che definisce solamente due proprietà: hello ed helloMsg usati rispettivamente come input dell’utente ed output del server:
public class HelloService { protected String hello; protected String helloMsg; // ... get & set delle due proprietà public void changeMessage() { helloMsg = hello + " Mondo!"; } }
Se avessimo utilizzato solamente questa classe come backing bean della pagina di saluto, non avremmo propagato le modifiche dei dati (helloMsg ed hello) a tutte le viste aperte. Per forzare ICEFaces a ridisegnare tutto a seguito di un aggiornamento dobbiamo derivare questa classe e chiamare in causa il SessionRenderer ogniqualvolta viene lanciato l’evento changeMessage:
public class ApplicationHelloService extends HelloService { @Override public synchronized void changeMessage() { super.changeMessage(); SessionRenderer.render("all"); } }
Abbiamo ridefinito il metodo changeMessage aggiungendo la chiamata al metodo SessionRenderer.render(“all”) per permettere la propagazione del messaggio; inoltre il metodo è sincronizzato per evitare accessi concorrenti ai dati condivisi del bean (è di application).
Nella figura 7 possiamo vedere come si comporta il backing bean a seguito di un aggiornamento fatto su un browser (Safari) e propagato su un altro browser (Firefox):
Figura 7 – Aggiornamento del backing bean su due browser diversi.
Conclusioni
In questo articolo abbiamo visto un semplice esempio di sito web che utilizza alcune delle peculiarità di ICEFaces. Abbiamo infatti visto il Submit parziale (per ottenere una validazione immediata e parziale di una form); poi l’Ajax server push (utilizzato due volte: una volta tramite l’IntervalRenderer per forzare un ridisegno ogni 1000 millisecondi, e un’altra volta con il SessionRenderer per forzare un ridisegno ogniqualvolta cambia un dato di una pagina); successivamente abbiamo affrontato il Blocco dell’interfaccia grafica dopo il submit dell’utente (è molto utile per evitare che i dati vengano immessi più volte); e dopo l’Icona sullo stato della comunicazione con il server, abbiamo infine concluso con i pannelli grafici di ICEFaces (panelCollapsible) per costruire interfacce grafiche.
Il prossimo articolo sulle ICEFaces vedrà un ulteriore sviluppo di questo sito aggiungendo una pagina in cui in cui viene mostrato come effettuare il drag’n’drop; una pagina di visualizzazione di un grafico (OutputChart component); una serie di effetti grafici generati dinamicamente.