SeamFramework.orgCommunity Documentation
Seam fornisce un ampio numero di applicazioni d'esempio per mostrare l'uso delle varie funzionalità di Seam. Questo tutorial ti guiderà attraverso alcuni di questi esempi per aiutarti nell'apprendimento di Seam. Gli esempi di Seam sono posizionati nella sottodirectory examples
della distribuzione Seam. L'esempio di registrazione, che è il primo esempio che vediamo, si trova nella directory examples/registration
.
Ciascun esempio ha la medesima struttura di directory:
La directory view
contiene i file relativi alla vista come template di pagine web, immagini e fogli di stile.
La directory resources
contiene i descrittori per il deploy ed altri file di configurazione.
La directory src
contiene il codice sorgente dell'applicazione.
Le applicazioni d'esempio girano sia su JBoss AS sia su Tomcat senza configurazioni aggiuntive. Le sezioni seguenti spiegano la procedura in entrambi i casi. Nota che tutti gli esempi sono costruiti ed eseguiti da build.xml
di Ant, e quindi ti servirà installata una versione recente di Ant prima di iniziare.
Gli esempi sono configurati per usare JBoss 4.2 o 5.0. Dovrai impostare la variabile jboss.home
affinché punti alla locazione dell'installazione di JBoss AS. Questa variabile si trova nel file condiviso build.properties
nella cartella radice dell'installazione di Seam.
Una volta impostata la locazione di JBoss AS ed avviato il server, si può eseguire il build ed il deploy degli esempi semplicemente scrivendo ant explode
nella directory dell'esempio. Ogni esempio che viene impacchettato come EAR viene messo in un URL del tipo /seam-
, dove example
example
è il nome della cartella dell'esempio, con una eccezione. Se la cartella d'esempio inizia con seam, il prefisso "seam" viene omesso. Per esempio, se JBoss AS gira sulla porta 8080, l'URL per l'esempio registrazione è http://localhost:8080/seam-registration/
, mentre l'URL per l'esempio seamspace è http://localhost:8080/seam-space/
.
Se, dall'altro lato, l'esempio viene impacchettato come WAR, allora viene messo in un URL del tipo /jboss-seam-
. La maggior parte degli esempi può essere messa come WAR in Tomcat con JBoss Embedded digitando example
ant tomcat.deploy
. Diversi esempi possono venire deployati solo come WAR. Questi esempi sono groovybooking, hibernate, jpa, and spring.
Questi esempi sono configurati anche per essere usati in Tomcat 6.0. Occorreràseguire le istruzioni in Sezione 30.6.1, «Installare JBoss Embedded» per installare JBoss Embedded in Tomcat 6.0. JBoss Embedded è richiesto per eseguire le demo di Seam che usano componenti EJB3 in Tomcat. Ci sono anche esempio di applicazioni non-EJB3 che possono funzionare in Tomcat senza JBoss Embedded.
Occorrerà impostare al percorso di Tomcat la variabile tomcat.home
, la quale si trova nel file condiviso build.properties
della cartella padre nell'installazione di Seam.
Dovrai usare un diverso target Ant per utilizzare Tomcat. Usa ant tomcat.deploy
nella sotto-directory d'esempio per il build ed il deploy in Tomcat.
Con Tomcat gli esempi vengono deployati con URL del tipo /jboss-seam-
, così per l'esempio di registrazione, l'URL sarebbe example
http://localhost:8080/jboss-seam-registration/
. Lo stesso vale per gli esempi che vengono deployati come WAR, come già detto nella precedente sezione.
La maggior parte degli esempi è fornita di una suite di test d'integrazione TestNG. Il modo più semplice per eseguire i test è ant test
. E' anche possibile eseguire i test all'interno del proprio IDE usandi il plugin di TestNG. Per ulteriori informazioni consultare il file readme.txt nella directory degli esempi nella distribuzione di Seam.
L'esempio di registrazione è una semplice applicazione per consentire all'utente di memorizzare nel database il proprio username, il nome vero e la password. L'esempio non vuole mostrare tutte le funzionalità di Seam. Comunque mostra l'uso di un EJB3 session bean come JSF action listener e la configurazione base di Seam.
Andiamo piano, poiché ci rendiamo conto che EJB 3.0 potrebbe non essere familiare.
La pagina iniziale mostra una form molto semplice con tre campi d'input. Si provi a riempirli e ad inviare la form. Verrà salvato nel database un oggetto user.
Questo esempio è implementato con due template Facelets, un entity bean e un session bean stateless. Si guardi ora il codice, partendo dal "basso".
Occorre un entity bean EJB per i dati utente. Questa classe definisce persistenza e validazione in modo dichiarativo tramite le annotazioni. Ha bisogno anche di altre annotazioni per definire la classe come componente Seam.
Esempio 1.1. User.java
@Entity
@Name("user")
@Scope(SESSION)
@Table(name="users")
public class User implements Serializable
{
private static final long serialVersionUID = 1881413500711441951L;
private String username;
private String password;
private String name;
public User(String name, String password, String username)
{
this.name = name;
this.password = password;
this.username = username;
}
public User() {}
@NotNull @Length(min=5, max=15)
public String getPassword()
{
return password;
}
public void setPassword(String password)
{
this.password = password;
}
@NotNull
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
@Id @NotNull @Length(min=5, max=15)
public String getUsername()
{
return username;
}
public void setUsername(String username)
{
this.username = username;
}
}
![]() | L'annotazione EJB3 standard |
![]() | Un componente Seam ha bisogno di un nome componente specificato dall'annotazione |
![]() | Quando Seam istanzia un componente, associa la nuova istanza alla variabile di contesto nel contesto di default del componente. Il contesto di default viene specificato usando l'annotazione |
![]() | L'annotazione standard EJB |
![]() |
|
![]() | Un costruttore vuoto è richiesto sia dalla specifica EJB sia da Seam. |
![]() | Le annotazioni |
![]() | L'annotazione standard EJB |
Le cose più importanti da notare in quest'esempio sono le annotazioni @Name
e @Scope
. Queste annotazioni stabiliscono che questa classe è un componente Seam.
Si vedrà sotto che le proprietà della classe User
sono legate direttamente ai componenti JSF e sono popolati da JSF durante la fase di aggiornamento dei valori del modello ("update model values"). Non occorre nessun codice colla per copiare i dati avanti ed indietro tra le pagine JSP ed il modello di dominio degli entity bean.
Comunque, gli entity bean non dovrebbero occuparsi della gestione delle transazioni o dell'accesso al database. Quindi non si può usare questo componente come action listener JSF. Per questo occorre un session bean.
La maggior parte delle applicazioni Seam utilizza i session bean come action listener JSF (si possono utilizzare JavaBean se si vuole).
C'è esattamente una azione JSF nell'applicazione ed un metodo di session bean attaccato ad essa. In questo caso si utilizzerà un bean di sessione stateless, poiché tutto lo stato associato all'azione è mantenuto dal bean User
.
Questo è l'unico codice veramente interessante nell'esempio!
Esempio 1.2. RegisterAction.java
@Stateless@Name("register") public class RegisterAction implements Register { @In private Use
r user; @PersistenceContext private Ent
ityManager em; @Logger private Log
log; public String register() {
List existing = em.createQuery( "select username from User where username = #{user.username}") .getR
esultList(); if (existing.size()==0) { em.persist(user); log.info("Registered new user #{user.username}"); retur
n "/registered.xhtml"; }
else { FacesMessages.instance().add("User #{user.username} already exists"); retur
n null; } } }
![]() | L'annotazione EJB |
![]() | L'annotazione |
![]() | L'annotazione EJB standard |
![]() | L'annotazione |
![]() | Il metodo action listener utilizza l'API EJB3 standard |
![]() | Si noti che Seam consente di utilizzare espressioni JSF EL dentro EJB-QL. Sotto il coperchio, questo proviene da un'ordinaria chiamata JPA |
![]() | L'API |
![]() | I metodi JSF action listener restituiscono un esito di tipo stringa, che determina quale pagina verrà mostrata come successiva. Un estio null (o un metodo action listener di tipo void) regenera la pagina precedente. Nel semplice JSF, è normale impiegare sempre una regola di navigazione JSF per determinare l'id della vista JSF dall'esito. Per applicazioni complesse quest'azione indiretta (indirection) è sia utile sia una buona pratica. Comunque, per ogni esempio semplice come questo, Seam consente di usare l'id della vista JSF come esito, eliminando l'uso della regola di navigazione. Si noti che quando viene usato l'id della vista come esito, Seam esegue sempre un redirect del browser. |
![]() | Seam fornisce un numero di componenti predefiniti per aiutare a risolvere problemi comuni. Il componente |
Si noti che questa volta non si è esplicitamente specificato uno @Scope
. Ciascun tipo di componente Seam ha uno scope di default se non esplicitamente specificato. Per bean di sessione stateless, lo scope di default è nel contesto stateless, che è l'unico valore sensato.
L'action listenere del bean di sessioni esegue la logica di persistenza e di business per quest'applicazione. In applicazioni più complesse, può essere opportuno separare il layer di servizio. Questo è facile da farsi in Seam, ma è critico per la maggior parte delle applicazioni web. Seam non forza nell'impiego di una particolare strategia per il layering dell'applicazione, consentendo di rimanere semplici o complessi a proprio piacimento.
Si noti che in questa semplice applicazione, abbiamo reso le cose di gran lunga più complicate di quanto necessario. Se si fossero impiegati i controllori di Seam, si sarebbe eliminato molto codice dell'applicazione. Comunque non avremmo avuto molto da spiegare.
Certamente il nostro session bean richiede un'interfaccia locale.
Questa è la fine del codice Java. Vediamo ora la vista.
Le pagine di vista di per un'applicazione Seam possono essere implementate usando qualsiasi tecnologia supporti JSF. In quest'esempio si usa Facelets, poiché noi pensiamo sia migliore di JSP.
Esempio 1.4. register.xhtml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:s="http://jboss.com/products/seam/taglib"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core">
<head>
<title
>Register New User</title>
</head>
<body>
<f:view>
<h:form>
<s:validateAll>
<h:panelGrid columns="2">
Username: <h:inputText value="#{user.username}" required="true"/>
Real Name: <h:inputText value="#{user.name}" required="true"/>
Password: <h:inputSecret value="#{user.password}" required="true"/>
</h:panelGrid>
</s:validateAll>
<h:messages/>
<h:commandButton value="Register" action="#{register.register}"/>
</h:form>
</f:view>
</body>
</html
>
L'unica cosa che qua è specifica di Seam è il tag <s:validateAll>
. Questo componente JSF dice a JSFdi validare tutti i campi d'input contenuti con le annotazioni di Hibernate Validator specificate nell'entity bean.
Esempio 1.5. registered.xhtml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core">
<head>
<title
>Successfully Registered New User</title>
</head>
<body>
<f:view>
Welcome, #{user.name}, you are successfully registered as #{user.username}.
</f:view>
</body>
</html>
Questa è una semplice pagina JSF che utilizza EL. Qua non c'è niente di specifico di Seam.
"Poiché questa è la prima applicazione vista, si prenderanno in esame i descrittori di deploy. Ma prima di iniziare, vale la pena di notare che Seam apprezza molto una configurazione minimale. Questi file di configurazione verranno creati al momento della creazione di un'applicazione Seam. Non sarà mai necessario metter mano alla maggior parte di questi file. Qua vengono presentati solo per aiutare a capire tutti pezzi dell'esempio preso in considerazione.
Se in precedenza si sono utilizzati altri framework Java, si è abituati a dichiarare le classi componenti in un qualche file XML che gradualmente cresce sempre più e diventa sempre più ingestibile man mano che il progetto evolve. Si resterà sollevati dal sapere che Seam non richiede che i componenti dell'applicazione siano accompagnati da file XML. La maggior parte delle applicazioni Seam richiede una quantità molto piccola di XML che non aumenta man mano che il progetto cresce.
Tuttavia è spesso utile fornire una qualche configurazione esterna per qualche componente (particolarmente per i componenti predefiniti di Seam). Ci sono due opzioni, ma l'opzione più flessibile è fornire questa configurazione in un file chiamato components.xml
, collocato nella directory WEB-INF
. Si userà il file components.xml
per dire a Seam dove trovare i componenti EJB in JNDI:
Esempio 1.6. components.xml
<?xml version="1.0" encoding="UTF-8"?>
<components xmlns="http://jboss.com/products/seam/components"
xmlns:core="http://jboss.com/products/seam/core"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://jboss.com/products/seam/core
http://jboss.com/products/seam/core-2.2.xsd
http://jboss.com/products/seam/components
http://jboss.com/products/seam/components-2.2.xsd">
<core:init jndi-pattern="@jndiPattern@"/>
</components
>
Questo codice configura una proprietà chiamata jndiPattern
di un componente Seam predefinito chiamato org.jboss.seam.core.init
. Il divertente simbolo @
viene impiegato poiché lo script di build Ant vi mette al suo posto il corretto JDNI pattern al momento del deploy dell'applicazione, ricavato dal file components.properties. Maggiori informazioni su questo processo in Sezione 5.2, «Configurazione dei componenti tramite components.xml
».
Il layer di presentazione dell'applicazione verrà deployato in un WAR. Quindi sarà necessario un descrittore di deploy web.
Esempio 1.7. web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<listener>
<listener-class
>org.jboss.seam.servlet.SeamListener</listener-class>
</listener>
<context-param>
<param-name
>javax.faces.DEFAULT_SUFFIX</param-name>
<param-value
>.xhtml</param-value>
</context-param>
<servlet>
<servlet-name
>Faces Servlet</servlet-name>
<servlet-class
>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup
>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name
>Faces Servlet</servlet-name>
<url-pattern
>*.seam</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout
>10</session-timeout>
</session-config>
</web-app
>
Il file web.xml
configura Seam e JSF. La configurazione vista qua è più o meno la stessa in tutte le applicazioni Seam.
La maggior parte delle applicazioni Seam utilizza le viste JSF come layer di presentazione. Così solitamente si avrà bisogno di faces-config.xml
. In ogni caso noi utilizzeremo Facelets per definire le nostre viste, così avremo bisogno di dire a JSF di usare Facelets come suo motore di template
Esempio 1.8. faces-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<faces-config xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd"
version="1.2">
<application>
<view-handler
>com.sun.facelets.FaceletViewHandler</view-handler>
</application>
</faces-config
>
Si noti che non occorre alcuna dichiarazione di managed bean JSF! I managed bean sono componenti Seam annotati. Nelle applicazioni Seam, faces-config.xml
è usato meno spesso che nel semplice JSF. Qua, viene usato per abilitare Faceltes come gestore di viste al posto di JSP.
Infatti una volta configurati tutti i descrittori base, l'unico XML necessario da scrivere per aggiungere nuove funzionalità ad un'applicazione Seam è quello per l'orchestrazione (orchestration): regole di navigazione o definizione di processi jBPM. Un punto fermo di Seam è che flusso di processo e configurazione dei dati siano le uniche cose che veramente appartengano alla sfera dell'XML.
Questo semplice esempio non è neppure stato necessario usare una regola di navigazione, poiché si è deciso di incorporare l'id della vista nel codice dell'azione.
Il file ejb-jar.xml
integra Seam con EJB3, attaccando SeamInterceptor
a tutti i bean di sessione nell'archivio.
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd"
version="3.0">
<interceptors>
<interceptor>
<interceptor-class
>org.jboss.seam.ejb.SeamInterceptor</interceptor-class>
</interceptor>
</interceptors>
<assembly-descriptor>
<interceptor-binding>
<ejb-name
>*</ejb-name>
<interceptor-class
>org.jboss.seam.ejb.SeamInterceptor</interceptor-class>
</interceptor-binding>
</assembly-descriptor>
</ejb-jar
>
Il file persistence.xml
dice all'EJB persistence provider dove trovare il datasource, e contiene alcune impostazioni vendor-specific. In questo caso abilita automaticamente l'esportazione dello schema all'avvio.
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
version="1.0">
<persistence-unit name="userDatabase">
<provider
>org.hibernate.ejb.HibernatePersistence</provider>
<jta-data-source
>java:/DefaultDS</jta-data-source>
<properties>
<property name="hibernate.hbm2ddl.auto" value="create-drop"/>
</properties>
</persistence-unit>
</persistence
>
Infine poiché l'applicazione viene deployata come EAR, occorre anche un descrittore di deploy.
Esempio 1.9. applicazione di registrazione
<?xml version="1.0" encoding="UTF-8"?>
<application xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/application_5.xsd"
version="5">
<display-name
>Seam Registration</display-name>
<module>
<web>
<web-uri
>jboss-seam-registration.war</web-uri>
<context-root
>/seam-registration</context-root>
</web>
</module>
<module>
<ejb
>jboss-seam-registration.jar</ejb>
</module>
<module>
<ejb
>jboss-seam.jar</ejb>
</module>
<module>
<java
>jboss-el.jar</java>
</module>
</application
>
Questo descrittore di deploy punta a moduli nell'archivio enterprise ed associa l'applicazione web al contesto radice /seam-registration
.
Adesso sono stati analizzati tutti i file dell'intera applicazione!
Quando la form viene inviata, JSF chiede a Seam di risolvere la variabile chiamata user
. Poiché non c'è alcun valore associato a questo nome (in un qualsiasi contesto Seam), Seam istanzia il componente user
e restituisce a JSF un'istanza dell'entity bean User
dopo averla memorizzata nel contesto Seam di sessione.
I valori di input della form vengono ora validati dai vincoli di Hibernate Validator specificati nell'entity User
. Se i vincoli vengono violati, JSF rivisualizza la pagina, Altrimenti, JSF associa i valori di input alle proprietà dell'entity bean User
.
Successivamente JSF chiede a Seam di risolvere la variabile chiamata register
. Seam utilizza il pattern JNDI menzionato in precedenza per localizzare il session bean stateless, lo impiega come componente Seam tramite il wrap e lo restituisce. Seam quindi presenta questo componente a JSF e JSF invoca il metodo action listener register()
.
Ma Seam non ha ancora terminato. Seam intercetta la chiamata al metodo e inietta l'entity User
dal contestosessione di Seam, prima di consentire all'invocazione di continuare.
Il metodo register()
controlla se esiste già un utente lo username inserito. Se è così, viene accodato un errore al componente FacesMessages
, e viene restituito un esito null, causando la rivisualizzazione della pagina. Il componente FacesMessages
interpola l'espressione JSF incorporata nella stringadi messaggio e aggiunge un FacesMessage
JSF alla vista.
Se non esiste nessun utente con tale username, l'esito di "/registered.xhtml"
causa un redirect del browser verso la pagina registered.xhtml
. Quando JSF arriva a generare la pagina, chiede a Seam di risolvere la variabile chiamata user
ed utilizza il valori di proprietà dell'entity User
restituito dallo scope di sessione di Seam.
Le liste cliccabili dei risultati di ricerca del database sono una parte così importante di qualsiasi applicazione online che Seam fornisce una funzionalità speciale in cima a JSF per rendere più facile l'interrogazione dei dati usando EJB-QL o HQL e la mostra comelista cliccabile usando il JSF <h:dataTable>
. I messaggi d'esempio mostrano questa funzionalità.
L'esempio di lista messaggi ha un entity bean, Message
, un session bean, MessageListBean
ed una JSP.
L'entity Message
definisce il titolo, il testo, la data e l'orario del messaggio e un flag indica se il messaggio è stato letto:
Esempio 1.10. Message.java
@Entity
@Name("message")
@Scope(EVENT)
public class Message implements Serializable
{
private Long id;
private String title;
private String text;
private boolean read;
private Date datetime;
@Id @GeneratedValue
public Long getId()
{
return id;
}
public void setId(Long id)
{
this.id = id;
}
@NotNull @Length(max=100)
public String getTitle()
{
return title;
}
public void setTitle(String title)
{
this.title = title;
}
@NotNull @Lob
public String getText()
{
return text;
}
public void setText(String text)
{
this.text = text;
}
@NotNull
public boolean isRead()
{
return read;
}
public void setRead(boolean read)
{
this.read = read;
}
@NotNull
@Basic @Temporal(TemporalType.TIMESTAMP)
public Date getDatetime()
{
return datetime;
}
public void setDatetime(Date datetime)
{
this.datetime = datetime;
}
}
Come nel precedente esempio, esiste un session bean, MessageManagerBean
, che definisce i metodi di action listener per i due bottoni della form. Uno di questi seleziona un messaggio dalla lista, e mostra tale messaggio. L'altro cancella il messaggio. Finora non è molto diverso dal precedente esempio.
Ma MessageManagerBean
è anche responsabile per il recupero della lista dei messaggi la prima volta che si naviga nella pagina della lista messaggi. Ci sono vari modi in cui l'utente può navigare nella pagina, e non tutti sono preceduti da un'azione JSF — l'utente può avere un memorizzato la pagina, per esempio. Quindi il compito di recuperare la lista messaggi avviene in un metodo factory di Seam, invece che in un metodo action listener.
Si vuole memorizzare la lista dei messaggi tra le varie richieste server, e quindi questo session bean diventerà stateful.
Esempio 1.11. MessageManagerBean.java
@Stateful @Scope(SESSION) @Name("messageManager") public class MessageManagerBean implements Serializable, MessageManager { @DataModel private List<Message > messageList; @DataModelS
election @Out(requir
ed=false) private Message message; @Persistenc
eContext(type=EXTENDED) private EntityManager em; @Factory("m
essageList") public void findMessages() { messageList = em.createQuery("select msg from Message msg order by msg.datetime desc") .getResultList(); } public void
select() { message.setRead(true); } public void
delete() { messageList.remove(message); em.remove(message); message=null; } @Remove
public void destroy() {} }
![]() | L'annotazione |
![]() | L'annotazione |
![]() | L'annotazione |
![]() | Questo bean stateful ha un contesto di persistenza EJB3 esteso. I messaggi recuperati nella query rimangono nello stato gestito finché esiste il bean, quindi ogni chiamata di metodo conseguente al bean può aggiornarli senza il bisogno di chiamare esplicitamente l' |
![]() | La prima volta che si naviga in un pagina JSP, non c'è alcun valore nella variabile di contesto |
![]() | Il metodo action listener |
![]() | Il metodo action listener |
![]() | Tutti i componenti Seam bean di sessione stateful devono avere un metodo senza parametri marcato |
Si noti che questo è un componente Seam di sessione. E' associato alla sessione di login dell'utente e tutte le richieste da una login di sessione condividono la stessa istanza del componente. (Nelle applicazioni Seam, solitamente si usano componenti con scope di sessione in maniera contenuta.)
Tutti i session bean hanno un'interfaccia di business, naturalmente.
Esempio 1.12. MessageManager.java
@Local
public interface MessageManager
{
public void findMessages();
public void select();
public void delete();
public void destroy();
}
D'ora in poi non verranno mostrate interfacce locali nei codici d'esempio.
Saltiamo i file components.xml
, persistence.xml
, web.xml
, ejb-jar.xml
, faces-config.xml
e application.xml
poiché sono praticamente uguali all'esempio precedente e andiamo dritti alla pagina JSP.
La pagina JSP è un semplice utilizzo del componente JSF <h:dataTable>
. Ancora nulla di specifico di Seam.
Esempio 1.13. messages.jsp
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<html>
<head>
<title
>Messages</title>
</head>
<body>
<f:view>
<h:form>
<h2
>Message List</h2>
<h:outputText value="No messages to display"
rendered="#{messageList.rowCount==0}"/>
<h:dataTable var="msg" value="#{messageList}"
rendered="#{messageList.rowCount
>0}">
<h:column>
<f:facet name="header">
<h:outputText value="Read"/>
</f:facet>
<h:selectBooleanCheckbox value="#{msg.read}" disabled="true"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Title"/>
</f:facet>
<h:commandLink value="#{msg.title}" action="#{messageManager.select}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Date/Time"/>
</f:facet>
<h:outputText value="#{msg.datetime}">
<f:convertDateTime type="both" dateStyle="medium" timeStyle="short"/>
</h:outputText>
</h:column>
<h:column>
<h:commandButton value="Delete" action="#{messageManager.delete}"/>
</h:column>
</h:dataTable>
<h3
><h:outputText value="#{message.title}"/></h3>
<div
><h:outputText value="#{message.text}"/></div>
</h:form>
</f:view>
</body>
</html
>
La prima volta che si naviga nella pagina messages.jsp
, la pagina proverà a risolvere la variabile di contesto messageList
. Poiché questa variabile non è inizializzata, Seam chiamerà il metodo factory findMessages()
, che esegue la query del database e mette i risultati in un DataModel
di cui verrà fatta l'outjection. Questo DataModel
fornisce i dati di riga necessari per generare la <h:dataTable>
.
Quando l'utente clicca il <h:commandLink>
, JSF chiama l'action listener select()
. Seam intercetta questa chiamata ed inietta i dati di riga selezionati nell'attributo del componente messageManager
. L'action listener viene eseguito, marcando come letto il Message
selezionato. Alla fine della chiamata, Seam esegue l'outjection del Message
selezionato nella variabile di contesto chiamata message
. Poi il container EJB committa la transazione ed i cambiamenti a message
vengono comunicati al database. Infine la pagina vienere rigenerata, rimostrando la lista dei messaggi e mostrando sotto il messaggio selezionato.
Se l'utente clicca <h:commandButton>
, JSF chiama l'action listener delete()
. Seam intercetta questa chiamata ed inietta i dati selezionati nell'attributo message
del componente messageList
. L'action listener viene eseguito, rimuovendo dalla lista il Message
, e chiamando anche il metodo remove()
dell'EntityManager
. Alla fine della chiamata, Seam aggiorna la variabile di contesto messageList
e pulisce la variabile di contesto chiamata message
. Il container EJB committa la transazione e cancella Message
dal database. Infine la pagina viene rigenerata, rimostrando la lista dei messaggi.
jBPM fornisce una funzionalità sofisticata per il workflow e la gestione dei task. Per provare come jBPM si integra con Seam, viene mostrata l'applicazione "todo list". Poiché gestire liste di task è la funzione base di jBPM, non c'è praticamente alcun codice Java in quest'esempio.
La parte centrale dell'esempio è la definizione del processo jBPM. Ci sono anche due pagine JSP e due banalissimi JavaBean (Non c'è alcuna ragione per usare session bean, poiché questi non accedono al database, e non hanno un comportamento transazionale). Cominciamo con la definizione del processo:
Esempio 1.14. todo.jpdl.xml
<process-definition name="todo"> <start-state name="start"> <transition to="todo"/> </start-state> <task-node
name="todo"> <task na
me="todo" description="#{todoList.description}"> <assi
gnment actor-id="#{actor.id}"/> </task> <transition to="done"/> </task-node> <end-state
name="done"/> </process-definition >
![]() | Il nodo |
![]() | Il nodo |
![]() | L'elemento |
![]() | I task devono essere assegnati ad un utente od un gruppo di utenti quando vengono creati. In questo caso il task viene assegnato all'utente corrente che viene recuperato dal componente Seam predefinito chiamato |
![]() | Il nodo |
Se viene impiegato l'editor per le definizioni di processo fornito da JBossIDE, questa apparirà così:
Questo documento definisce il processo di business come un grafo di nodi. Questo è un processo di business molto banale: c'è un task da eseguire e quando questo viene completato, il processo termina.
Il primo javaBean gestisce la pagina login.jsp
. Il suo compito è quello di inizializzare l'id actor jBPM usando il componente actor
. Nelle applicazioni occorrerà autenticare l'utente.
Esempio 1.15. Login.java
@Name("login")
public class Login
{
@In
private Actor actor;
private String user;
public String getUser()
{
return user;
}
public void setUser(String user)
{
this.user = user;
}
public String login()
{
actor.setId(user);
return "/todo.jsp";
}
}
Qua si vede l'uso di @In
per iniettare il componente predefinito Actor
.
Lo stesso JSP è banale:
Esempio 1.16. login.jsp
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>
<html>
<head>
<title
>Login</title>
</head>
<body>
<h1
>Login</h1>
<f:view>
<h:form>
<div>
<h:inputText value="#{login.user}"/>
<h:commandButton value="Login" action="#{login.login}"/>
</div>
</h:form>
</f:view>
</body>
</html
>
Il secondo JavaBean è responsabile per l'avvio delle istanze del processo di business e della fine dei task.
Esempio 1.17. TodoList.java
@Name("todoList") public class TodoList { private String description; public String getDescription() { return description; } public void setDescription(String description) { this.description = description; }
@CreateProcess(definition="todo") public void createTodo() {}
@StartTask @EndTask public void done() {} }
![]() | La proprietà descrizione accetta l'input utente dalla pagina JSP e lo espone alla definizione del processo, consentendo che venga impostata la descrizione del task. |
![]() | L'annotazione Seam |
![]() | L'annotazione Seam |
In un esempio più realistico @StartTask
e @EndTask
non apparirebbero nello stesso metodo, poiché solitamente c'è del lavoro da fare in un'applicazione prima che il task venga terminato.
Infine, il cuore dell'applicazione è in todo.jsp
:
Esempio 1.18. todo.jsp
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://jboss.com/products/seam/taglib" prefix="s" %>
<html>
<head>
<title
>Todo List</title>
</head>
<body>
<h1
>Todo List</h1>
<f:view>
<h:form id="list">
<div>
<h:outputText value="There are no todo items."
rendered="#{empty taskInstanceList}"/>
<h:dataTable value="#{taskInstanceList}" var="task"
rendered="#{not empty taskInstanceList}">
<h:column>
<f:facet name="header">
<h:outputText value="Description"/>
</f:facet>
<h:inputText value="#{task.description}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Created"/>
</f:facet>
<h:outputText value="#{task.taskMgmtInstance.processInstance.start}">
<f:convertDateTime type="date"/>
</h:outputText>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Priority"/>
</f:facet>
<h:inputText value="#{task.priority}" style="width: 30"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Due Date"/>
</f:facet>
<h:inputText value="#{task.dueDate}" style="width: 100">
<f:convertDateTime type="date" dateStyle="short"/>
</h:inputText>
</h:column>
<h:column>
<s:button value="Done" action="#{todoList.done}" taskInstance="#{task}"/>
</h:column>
</h:dataTable>
</div>
<div>
<h:messages/>
</div>
<div>
<h:commandButton value="Update Items" action="update"/>
</div>
</h:form>
<h:form id="new">
<div>
<h:inputText value="#{todoList.description}"/>
<h:commandButton value="Create New Item" action="#{todoList.createTodo}"/>
</div>
</h:form>
</f:view>
</body>
</html
>
Si prenda un pezzo alla volta.
La pagina renderizza una lista di task prelevati da un componente di Seam chiamato taskInstanceList
. La lista è definita dentro una form JSF.
Esempio 1.19. todo.jsp
<h:form id="list">
<div>
<h:outputText value="There are no todo items." rendered="#{empty taskInstanceList}"/>
<h:dataTable value="#{taskInstanceList}" var="task"
rendered="#{not empty taskInstanceList}">
...
</h:dataTable>
</div>
</h:form
>
Ciascun elemento della lista è un'istanza della classe jBPM TaskInstance
. Il codice seguente mostra semplicemente le proprietà di interesse per ogni task della lista. Per consentire all'utente di aggiornare i valori di descrizione, priorità e data di ultimazione, si usano i controlli d'input.
<h:column>
<f:facet name="header">
<h:outputText value="Description"/>
</f:facet>
<h:inputText value="#{task.description}"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Created"/>
</f:facet>
<h:outputText value="#{task.taskMgmtInstance.processInstance.start}">
<f:convertDateTime type="date"/>
</h:outputText>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Priority"/>
</f:facet>
<h:inputText value="#{task.priority}" style="width: 30"/>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Due Date"/>
</f:facet>
<h:inputText value="#{task.dueDate}" style="width: 100">
<f:convertDateTime type="date" dateStyle="short"/>
</h:inputText>
</h:column
>
#{task.dueDate}
.Questo pulsante termina il task chiamando il metodo d'azione annotato con @StartTask @EndTask
. Inoltre passa l'id del task come parametro di richiesta a Seam.
<h:column>
<s:button value="Done" action="#{todoList.done}" taskInstance="#{task}"/>
</h:column
>
Si noti che questo sta usando un controllo JSF Seam <s:button>
del pacchetto seam-ui.jar
. Questo pulsante è usato per aggiornare le proprietà dei task. Quando la form viene aggiornata, Seam e jBPM renderanno persistenti i cambiamenti ai task. Non c'è bisogno di alcun metodo action listener:
<h:commandButton value="Update Items" action="update"/>
Viene usata una seconda form per creare nuovi item, chiamando il metodo d'azione annotato con @CreateProcess
.
<h:form id="new">
<div>
<h:inputText value="#{todoList.description}"/>
<h:commandButton value="Create New Item" action="#{todoList.createTodo}"/>
</div>
</h:form
>
Dopo la login, todo.jsp utilizza il componente taskInstanceList
per mostrare un tabella con i compiti da eseguire da parte dell'utente corrente. Inizialmente non ce ne sono. Viene presentata anche una form per l'inserimento di una nuova voce. Quando l'utente digita il compito da eseguire e preme il pulsante "Create New Item", viene chiamato #{todoList.createTodo}
. Questo inizia il processo todo, così come definito in todo.jpdl.xml
.
L'istanza di processo viene creata a partire dallo stato di start ed immediatamente viene eseguita una transizione allo stato todo
, dove viene creato un nuovo task. La descrizione del task viene impostata in base all'input dell'utente, che è stato memorizzato in #{todoList.description}
. Poi il task viene assegnato all'utente corrente, memorizzato nel componente Seam chiamato actor. Si noti che in quest'esempio il processo non ha ulteriori stati di processo. Tutti gli stati sono memorizzati nella definizione del task. Il processo e le informazioni sul task sono memorizzati nel database alla fine della richiesta.
Quando todo.jsp
viene rivisualizzata, taskInstanceList
trova il task appena creato. Il task viene mostrato in un h:dataTable
. Lo stato interno del task è mostrato in ciascuna colonna: #{task.description}
, #{task.priority}
, #{task.dueDate}
, ecc... Questi campi possono essere tutti editati e salvati nel database.
Ogni elemento todo ha anche un pulsante "Done", che chiama #{todoList.done}
. Il componente todoList
sa a quale task si riferisce il pulsante, poiché ogni s:button specifica taskInstance="#{task}"
, che si riferisce al task per quella particolare linea della tabella. Le annotazioni @StartTast
e @EndTask
obbligano seam a rendere attivo il task e a completarlo. Il processo originale quindi transita verso lo stato done
, secondo la definizione del processo, dove poi termina. Lo stato del task e del processo sono entrambi aggiornati nel database.
Quando todo.jsp
viene di nuovo visualizzata, il task adesso completato non viene più mostrato in taskInstanceList
, poiché questo componente mostra solo i task attivi per l'utente.
Per le applicazioni Seam con una navigazione relativamente libera, le regole di navigazione JSF/Seam sono un modo perfetto per definire il flusso di pagine. Per applicazioni con uno stile di navigazione più vincolato, specialmente per interfacce utente più stateful, le regole di navigazione rendono difficile capire il flusso del sistema. Per capire il flusso occorre mettere assieme le pagine, le azioni e le regole di navigazione.
Seam consente di usare la definizione di processo con jPDL per definire il flusso di pagine. L'esempio indovina-numero mostra come fare.
Quest'esempio è implementato usando un JavaBean, tre pagine JSP ed una definizione di pageflow jPDL. Iniziamo con il pageflow:
Esempio 1.20. pageflow.jpdl.xml
<pageflow-definition xmlns="http://jboss.com/products/seam/pageflow" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jboss.com/products/seam/pageflow http://jboss.com/products/seam/pageflow-2.2.xsd" name="numberGuess"> <start-pagename="displayGuess" view-id="/numberGuess.jspx"> <redirect/> <transit
ion name="guess" to="evaluateGuess"> <acti
on expression="#{numberGuess.guess}"/> </transition> <transition name="giveup" to="giveup"/> <transition name="cheat" to="cheat"/> </start-page>
<decision name="evaluateGuess" expression="#{numberGuess.correctGuess}"> <transition name="true" to="win"/> <transition name="false" to="evaluateRemainingGuesses"/> </decision> <decision name="evaluateRemainingGuesses" expression="#{numberGuess.lastGuess}"> <transition name="true" to="lose"/> <transition name="false" to="displayGuess"/> </decision> <page name="giveup" view-id="/giveup.jspx"> <redirect/> <transition name="yes" to="lose"/> <transition name="no" to="displayGuess"/> </page> <process-state name="cheat"> <sub-process name="cheat"/> <transition to="displayGuess"/> </process-state> <page name="win" view-id="/win.jspx"> <redirect/> <end-conversation/> </page> <page name="lose" view-id="/lose.jspx"> <redirect/> <end-conversation/> </page> </pageflow-definition >
![]() | L'elemento |
![]() | L'elemento |
![]() | Una transizione |
![]() | Un nodo |
Ecco come appare il pageflow nell'editor di pageflow di JBoss Developer Studio:
Ora che abbiamo visto il pageflow, è molto facile capire il resto dell'applicazione.
Ecco la pagina principale dell'applicazione, numberGuess.jspx
:
Esempio 1.21. numberGuess.jspx
<<?xml version="1.0"?>
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:s="http://jboss.com/products/seam/taglib"
xmlns="http://www.w3.org/1999/xhtml"
version="2.0">
<jsp:output doctype-root-element="html"
doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN"
doctype-system="http://www.w3c.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"/>
<jsp:directive.page contentType="text/html"/>
<html>
<head>
<title
>Guess a number...</title>
<link href="niceforms.css" rel="stylesheet" type="text/css" />
<script language="javascript" type="text/javascript" src="niceforms.js" />
</head>
<body>
<h1
>Guess a number...</h1>
<f:view>
<h:form styleClass="niceform">
<div>
<h:messages globalOnly="true"/>
<h:outputText value="Higher!"
rendered="#{numberGuess.randomNumber gt numberGuess.currentGuess}"/>
<h:outputText value="Lower!"
rendered="#{numberGuess.randomNumber lt numberGuess.currentGuess}"/>
</div>
<div>
I'm thinking of a number between
<h:outputText value="#{numberGuess.smallest}"/> and
<h:outputText value="#{numberGuess.biggest}"/>. You have
<h:outputText value="#{numberGuess.remainingGuesses}"/> guesses.
</div>
<div>
Your guess:
<h:inputText value="#{numberGuess.currentGuess}" id="inputGuess"
required="true" size="3"
rendered="#{(numberGuess.biggest-numberGuess.smallest) gt 20}">
<f:validateLongRange maximum="#{numberGuess.biggest}"
minimum="#{numberGuess.smallest}"/>
</h:inputText>
<h:selectOneMenu value="#{numberGuess.currentGuess}"
id="selectGuessMenu" required="true"
rendered="#{(numberGuess.biggest-numberGuess.smallest) le 20 and
(numberGuess.biggest-numberGuess.smallest) gt 4}">
<s:selectItems value="#{numberGuess.possibilities}" var="i" label="#{i}"/>
</h:selectOneMenu>
<h:selectOneRadio value="#{numberGuess.currentGuess}" id="selectGuessRadio"
required="true"
rendered="#{(numberGuess.biggest-numberGuess.smallest) le 4}">
<s:selectItems value="#{numberGuess.possibilities}" var="i" label="#{i}"/>
</h:selectOneRadio>
<h:commandButton value="Guess" action="guess"/>
<s:button value="Cheat" view="/confirm.jspx"/>
<s:button value="Give up" action="giveup"/>
</div>
<div>
<h:message for="inputGuess" style="color: red"/>
</div>
</h:form>
</f:view>
</body>
</html>
</jsp:root
>
Si noti come il pulsante di comando chiama la transizione guess
invece di chiamare direttamente un'azione.
La pagina win.jspx
è prevedibile:
Esempio 1.22. win.jspx
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns="http://www.w3.org/1999/xhtml" version="2.0"> <jsp:output doctype-root-element="html" doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN" doctype-system="http://www.w3c.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"/> <jsp:directive.page contentType="text/html"/> <html> <head> <title >You won!</title> <link href="niceforms.css" rel="stylesheet" type="text/css" /> </head> <body> <h1 >You won!</h1> <f:view> Yes, the answer was <h:outputText value="#{numberGuess.currentGuess}" />. It took you <h:outputText value="#{numberGuess.guessCount}" /> guesses. <h:outputText value="But you cheated, so it doesn't count!" rendered="#{numberGuess.cheat}"/> Would you like to <a href="numberGuess.seam" >play again</a >? </f:view> </body> </html> </jsp:root>
lose.jspx
è più o meno uguale, quindi si passa oltre.
Infine diamo un'occhiata al codice dell'applicazione:
Esempio 1.23. NumberGuess.java
@Name("numberGuess")
@Scope(ScopeType.CONVERSATION)
public class NumberGuess implements Serializable {
private int randomNumber;
private Integer currentGuess;
private int biggest;
private int smallest;
private int guessCount;
private int maxGuesses;
private boolean cheated;
@Create
public void begin()
{
randomNumber = new Random().nextInt(100);
guessCount = 0;
biggest = 100;
smallest = 1;
}
public void setCurrentGuess(Integer guess)
{
this.currentGuess = guess;
}
public Integer getCurrentGuess()
{
return currentGuess;
}
public void guess()
{
if (currentGuess
>randomNumber)
{
biggest = currentGuess - 1;
}
if (currentGuess<randomNumber)
{
smallest = currentGuess + 1;
}
guessCount ++;
}
public boolean isCorrectGuess()
{
return currentGuess==randomNumber;
}
public int getBiggest()
{
return biggest;
}
public int getSmallest()
{
return smallest;
}
public int getGuessCount()
{
return guessCount;
}
public boolean isLastGuess()
{
return guessCount==maxGuesses;
}
public int getRemainingGuesses() {
return maxGuesses-guessCount;
}
public void setMaxGuesses(int maxGuesses) {
this.maxGuesses = maxGuesses;
}
public int getMaxGuesses() {
return maxGuesses;
}
public int getRandomNumber() {
return randomNumber;
}
public void cheated()
{
cheated = true;
}
public boolean isCheat() {
return cheated;
}
public List<Integer
> getPossibilities()
{
List<Integer
> result = new ArrayList<Integer
>();
for(int i=smallest; i<=biggest; i++) result.add(i);
return result;
}
}
![]() | La prima volta che una pagina JSP richiede un componente |
Il file pages.xml
inizia una conversazione Seam (maggiori informazioni più avanti), e specifica la definizione pageflow da usare per il flusso delle pagine della conversazione.
Esempio 1.24. pages.xml
<?xml version="1.0" encoding="UTF-8"?>
<pages xmlns="http://jboss.com/products/seam/pages"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jboss.com/products/seam/pages http://jboss.com/products/seam/pages-2.2.xsd">
<page view-id="/numberGuess.jspx">
<begin-conversation join="true" pageflow="numberGuess"/>
</page>
</pages
>
Come si può vedere, questo componente Seam è pura logica di business! Non ha bisogno di sapere niente riguardo il flusso delle interazioni utente. Questo rende il componente potenzialmente più riutilizzabile.
Si analizzerà ora il flusso base dell'applicazione. Il gioco comincia con la vista numberGuess.jspx
. Quando la pagina viene mostrata la prima volta, la configurazione pages.xml
porta ad iniziare la conversazione ed associa il pageflow numberGuess
a tale conversazione. Il pageflow inizia con un tag start-page
che è uno stato d'attesa, e poi viene visualizzata la pagina numberGuess.xhtml
.
La vista fa riferimento al componente numberGuess
, provocando la creazione di una nuova istanza e la sua memorizzazione all'interno della conversazione. Viene chiamato il metodo @Create
che inizializza lo stato del gioco. La vista mostra un h:form
per consentire all'utente di editare #{numberGuess.currentGuess}
.
Il pulsante "Guess" lancia l'azione guess
. Seam usa il pageflow per gestire l'azione, la quale impone che il pageflow transiti allo stato evaluateGuess
, innanzitutto invocando #{numberGuess.guess}
che aggiorna il contatore ed i suggerimenti più alto/più basso nel componente numberGuess
.
Lo stato evaluateGuess
controlla il valore di #{numberGuess.correctGuess}
e le transizioni agli stati win
o evaluatingRemainingGuesses
. Si assumache ilnumero sia sbagliato, nel qual caso il pageflow transita verso evaluatingRemainingGuesses
. Questo è anche uno stato di decisione, che testa lo stato #{numberGuess.lastGuess}
per determinare se l'utente ha ulteriori tentativi oppure no. Se ne ha (lastGuess
è falso
), si torna allo stato originale displayGuess
. Infine si raggiunge lo stato page, e quindi viene mostrata la pagina associata /numberGuess.jspx
. Poiché la pagina ha un elemento redirect, Seam invia un redirect al browser dell'utente, ricominciando il processo.
Non si analizzerà ulteriormente lo stato, tranne per notare che se in una richiesta futura venisse presa la transizione win
oppure lose
, l'utente verrebbe portato a /win.jspx
oppure /lose.jspx
. Entrambi gli stati specificano che Seam debba terminare la conversazione, liberandosi dello stato del gioco e di quello del pageflow, prima di reindirizzare l'utente alla pagina finale.
L'esempio indovina-numero contiene anche i pulsanti Giveup (abbandona) e Cheat (imbroglia). Si dovrebbe essere facilmente in grado di tracciare lo stato pageflow per le relative azioni. Si presti attenzione alla transizione cheat
, che carica un sotto-processo per gestire tale flusso. Sebbene sia superfluo per quest'applicazione, questo dimostra come pageflow complessi possano venire spezzati in parti più piccole per renderle più facili da capire.
L'applicazione booking (prenotazione) è un sistema completo per la prenotazione di camere d'hotel, ed incorpora le seguenti funzionalità:
Registrazione utente
Login
Logout
Impostazione password
Ricerca hotel
Scelta hotel
Prenotazione stanza
Conferma prenotazione
Lista di prenotazioni esistenti
L'applicazione booking utilizza JSF, EJB 3.0 e Seam, assieme a Facelets per la vista. C'è anche un port di quest'applicazione con JSF, Facelets, Seam, JavaBeans e Hibernate3.
Una delle cose che si noteranno utilizzando quest'applicazione per qualche tempo è che questa risulta essere estremamente robusta. Si può giocare con il pulsante indietro ed aggiornare le pagine, aprire più finestre ed inserire dati senza senso quanto si vuole, ma si vedrà che è molto difficile mettere in difficoltà l'applicazione. Si può pensare che siano occorse settimane per testare e risolvere bug prima di raggiungere questo risultato. In verità non è così. Seam è stato progettato per rendere semplice la costruzione di applicazioni web robuste, e molta della robustezza che si è soliti doversi codificare da soli, con Seam viene naturale e automatica.
Quando si sfoglia il codice sorgente delle applicazioni e si impara come questa funziona, si osservi come sono stati usati la gestione dichiarativa dello stato e la validazione integrata per ottenere questa robustezza.
La struttura del progetto è identica al precedente, per installare e deployare quest'applicazione, si faccia riferimento a Sezione 1.1, «Utilizzo degli esempi di Seam». Una volta avviata l'applicazione, si può accedere a questa puntando il browser all'indirizzo http://localhost:8080/seam-booking/
L'applicazione utilizza sei bean di sessione per implementare la logica di business per le funzionalità nella lista.
AuthenticatorAction
fornisce la logica per l'autenticazione della login.
BookingListAction
recupera le prenotazioni esistenti per l'utente attualmente loggato.
ChangePasswordAction
aggiorna la password per l'utente attualmente loggato.
HotelBookingAction
implementa le funzionalità di prenotazione e conferma. Questa funzionalità è implementata come conversazione, e quindi è una delle classi più interessanti dell'applicazione.
HotelSearchingAction
implementa la funzionalità di ricerca hotel.
RegisterAction
registra un nuovo utente di sistema.
Tre entity bean implementano il modello di dominio di persistenza dell'applicazione.
Hotel
è un entity bean che rappresenta un hotel
Booking
è l'entity bean che rappresenta una prenotazione esistente
User
è un entity bean che rappresenta un utente che può fare una prenotazione
Si incoraggia a guardare il codice sorgente a piacimento. In questo tutorial ci concentreremo su alcune particolari funzionalità: ricerca hotel, selezione, prenotazione e conferma. Dal punto di vista dell'utente, tutto - dalla selezione dell'hotel alla conferma dellaprenotazione - è un'unica continua unità di lavoro, una conversazione. La ricerca, comunque, non è una parte della conversazione. L'utente può selezionare più hotel dalla stessa pagina dei risultati, in diversi tab del browser.
La maggior parte delle architetture delle applicazioni non ha alcun costrutto per rappresentare una conversazione. Questo causa enormi problemi nella gestione dello stato conversazionale. Solitamente le applicazioni web Java usano una combinazione di diverse tecniche. Alcuni stati possono essere trasferiti nell'URL. Ciò che non può è messo o in HttpSession
o mandato a database dopo ogni richiesta, e ricostruito dal database all'inizio di ogni nuova richiesta.
Poiché il database è il livello meno scalabile, questo risulta essere spesso ad un livello inaccettabile di scalabilità. La latenza è un ulteriore problema, dovuto al traffico extra verso e dal database ad ogni richiesta. Per ridurre questo traffico ridondante, le applicazioni Java spesso introducono una cache di dati (di secondo livello) che mantiene i dati comunemente acceduti tra le varie richieste. Questa cache è necessariamente inefficiente, poiché l'invalidazione è basato su una policy LRU invece di essere basata su quando l'utente termina di lavorare con i dati. Inoltre, poiché la cache è condivisa da diverse transazioni concorrenti, si è introdotta una schiera di problemi associati al fatto di mantenere lo stato della cache consistente con il database.
Ora si consideri lo stato mantenuto nella HttpSession
. HttpSession è un ottimo posto per i veri dati di sessione, cioè dati che sono comuni a tutte le richieste che l'utente fa con l'applicazione. Comunque, non è un posto dove vanno memorizzati i dati riguardanti serie individuali di richieste. L'uso della sessione si complica velocemente quando si ha a che fare con il pulsante indietro e con le finestre multiple. In cima a questo, senza una programmazione attenta, i dati nella sessione HTTP posso crescere parecchio, rendendo la sessione HTTP difficile da tenere assieme. Lo sviluppo di meccanismi per isolare lo stato della session associata a differenti conversazioni concorrenti, e l'aggiunta di meccanismi di sicurezza per assicurare che lo stato della conversazione venga distrutto quando l'utente interrompe una delle conversazioni chiudendo una finestra del browser non è una questione per gente poco coraggiosa. Fortunatamente con Seam non occorre preoccuparsi di queste problematiche.
Seam introduce il contesto conversazionale come first class construct. Si può mantenere in modo sicuro lo stato conversazionale in questo contesto ed essere certi che avrà un ciclo di vita ben definito. Ancor meglio non servirà mandare continuamente avanti ed indietro i dati tra server e database, poiché il contesto di conversazione è una cache naturale di dati su cui l'utente sta lavorando.
In quest'applicazione si userà il contesto di conversazione per memorizzare i session bean stateful. C'è un'antica credenza nella comunità Java che ritiene che i session bean stateful siano nocivi alla scalabilità. Questo poteva essere vero nei primissimi giorni di Java Enterprise, ma oggi non è più vero. I moderni application server hanno meccanismi estremamente sofisticati per la replicazione dello stato dei session bean stateful. JBoss AS, per esempio, esegue una replicazione a grana fine, replicando solo quei valori degli attributi bean che sono cambiati. Si noti che tutti gli argomenti tecnici tradizionali per cui i bean stateful sono inefficienti si applicano allo stesso modo alla HttpSession
, e quindi risulta fuorviante la pratica di cambiare stato dai componenti (session bean stateful) del business tier alla sessione web per cercare di migliorare le performance. E' certamente possibile scrivere applicazioni non scalabili usando session bean stateful non in modo corretto, o usandoli per la cosa sbagliata. Ma questo non significa che non si debba mai. Se non si è convinti, Seam consente di usare POJO invece dei session bean statefull. Con Seam la scelta è vostra.
L'applicazione di esempio prenotazione mostra come i componenti stateful con differenti scope possano collaborare assieme per ottenere comportamenti complessi. La pagina principale dell'applicazione consente all'utente di cercare gli hotel. I risultati di ricerca vengono mantenuti nello scope di sessione di Seam. Quando l'utente naviga in uno di questi hotel, inizia una conversazione ed il componente con scope conversazione chiama il componente con scope sessione per recuperare l'hotel selezionato.
L'esempio di prenotazione mostra anche l'uso di RichFaces Ajax per implementare un comportamento rich client senza usare Javascript scritto a mano.
La funzionalità di ricerca è implementata usando un session bean statefull con scope di sessione, simile a quello usato nell'esempio di lista messaggi.
Esempio 1.25. HotelSearchingAction.java
@Stateful@Name("hotelSearch") @Scope(ScopeType.SESSION) @Restrict("#{i
dentity.loggedIn}") public class HotelSearchingAction implements HotelSearching { @PersistenceContext private EntityManager em; private String searchString; private int pageSize = 10; private int page; @DataModel
private List<Hotel > hotels; public void find() { page = 0; queryHotels(); } public void nextPage() { page++; queryHotels(); } private void queryHotels() { hotels = em.createQuery("select h from Hotel h where lower(h.name) like #{pattern} " + "or lower(h.city) like #{pattern} " + "or lower(h.zip) like #{pattern} " + "or lower(h.address) like #{pattern}") .setMaxResults(pageSize) .setFirstResult( page * pageSize ) .getResultList(); } public boolean isNextPageAvailable() { return hotels!=null && hotels.size()==pageSize; } public int getPageSize() { return pageSize; } public void setPageSize(int pageSize) { this.pageSize = pageSize; } @Factory(value="pattern", scope=ScopeType.EVENT) public String getSearchPattern() { return searchString==null ? "%" : '%' + searchString.toLowerCase().replace('*', '%') + '%'; } public String getSearchString() { return searchString; } public void setSearchString(String searchString) { this.searchString = searchString; }
@Remove public void destroy() {} }
![]() | L'annotazione EJB standard |
![]() | L'annotazione |
![]() | L'annotazione |
![]() | L'annotazione standard EJB |
La pagina principale dell'applicazione è una pagina Facelets. Guardiamo al frammento relativo alla ricerca hotel:
Esempio 1.26. main.xhtml
<div class="section"> <span class="errors"> <h:messages globalOnly="true"/> </span> <h1 >Search Hotels</h1> <h:form id="searchCriteria"> <fieldset > <h:inputText id="searchString" value="#{hotelSearch.searchString}"style="width: 165px;"> <a:support event="onkeyup" actionListener="#{hotelSearch.find}" reRender="searchResults" /> </h:inputText>   <a:commandButton id="findHotels" value="Find Hotels" action="#{hotelSearch.find}"
reRender="searchResults"/>   <a:status> <f:facet name="start"> <h:graphicImage value="/img/spinner.gif"/> </f:facet> </a:status> <br/> <h:outputLabel for="pageSize" >Maximum results:</h:outputLabel >  <h:selectOneMenu value="#{hotelSearch.pageSize}" id="pageSize"> <f:selectItem itemLabel="5" itemValue="5"/> <f:selectItem itemLabel="10" itemValue="10"/> <f:selectItem itemLabel="20" itemValue="20"/> </h:selectOneMenu> </fieldset> </h:form>
</div> <a:outputPanel id="searchResults"> <div class="section"> <h:outputText value="No Hotels Found" rendered="#{hotels != null and hotels.rowCount==0}"/> <h:dataTable id="hotels" value="#{hotels}" var="hot" rendered="#{hotels.rowCount >0}"> <h:column> <f:facet name="header" >Name</f:facet> #{hot.name} </h:column> <h:column> <f:facet name="header" >Address</f:facet> #{hot.address} </h:column> <h:column> <f:facet name="header" >City, State</f:facet> #{
hot.city}, #{hot.state}, #{hot.country} </h:column > <h:column> <f:facet name="header" >Zip</f:facet> #{hot.zip} </h:column> <h:column> <f:facet name="header" >Action</f:facet> <s:link id="viewHotel" value="View Hotel" action="#{hotelBooking.selectHotel(hot)}"/> </h:column> </h:dataTable> <s:link value="More results" action="#{hotelSearch.nextPage}" rendered="#{hotelSearch.nextPageAvailable}"/> </div> </a:outputPanel >
![]() | Il tag RichFaces Ajax |
![]() | Il tag RichFaces Ajax |
![]() | Il tag RichFaces Ajax |
![]() | Il tag Seam Se ci si chiede come avvenga la navigazione, si possono trovare tutte le regole in |
Questa pagina mostra i risultati di ricerca in modo dinamico man mano si digita, e consente di scegliere un hotel e passarlo al metodo selectHotel()
di HotelBookingAction
, che è il posto in cui veramente succede qualsosa di interessante.
Vediamo ora come l'applicazione d'esempio usa un bean di sessione stateful con scope di conversazione per ottenere una naturale cache di dati persistenti relativi alla conversazione. Il seguente codice d'esempio è abbastanza lungo. Ma se si pensa a questo come una lista di azioni che implementano vari passi della conversazione, risulta comprensibile. Si legga la classe dalla cima verso il fondo, come se fosse un racconto.
Esempio 1.27. HotelBookingAction.java
@Stateful @Name("hotelBooking") @Restrict("#{identity.loggedIn}") public class HotelBookingAction implements HotelBooking { @PersistenceContext(type=EXTENDED) private EntityManager em; @In private User user; @In(required=false) @Out private Hotel hotel; @In(required=false) @Out(requir
ed=false) private Booking booking; @In private FacesMessages facesMessages; @In private Events events; @Logger private Log log; private boolean bookingValid; @Begin
public void selectHotel(Hotel selectedHotel) { hotel = em.merge(selectedHotel); } public void bookHotel() { booking = new Booking(hotel, user); Calendar calendar = Calendar.getInstance(); booking.setCheckinDate( calendar.getTime() ); calendar.add(Calendar.DAY_OF_MONTH, 1); booking.setCheckoutDate( calendar.getTime() ); } public void setBookingDetails() { Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_MONTH, -1); if ( booking.getCheckinDate().before( calendar.getTime() ) ) { facesMessages.addToControl("checkinDate", "Check in date must be a future date"); bookingValid=false; } else if ( !booking.getCheckinDate().before( booking.getCheckoutDate() ) ) { facesMessages.addToControl("checkoutDate", "Check out date must be later than check in date"); bookingValid=false; } else { bookingValid=true; } } public boolean isBookingValid() { return bookingValid; } @End
public void confirm() { em.persist(booking); facesMessages.add("Thank you, #{user.name}, your confimation number " + " for #{hotel.name} is #{booki g.id}"); log.info("New booking: #{booking.id} for #{user.username}"); events.raiseTransactionSuccessEvent("bookingConfirmed"); } @End public void cancel() {} @Remove
public void destroy() {}
![]() | Questo bean utilizza un contesto di persistenza esteso EJB3, e quindi ogni istanza di entity rimane gestita per l'intero ciclo di vita del session bean stateful. |
![]() | L'annotazione |
![]() | L'annotazione |
![]() | L'annotazione |
![]() | Questo metoto EJB di rimozione verrà chiamato quando Seam distruggerà il contesto della conversazione. Non si dimentichi di definire questo metodo! |
HotelBookingAction
contiene tutti i metodi action listenet che implementano, selezione, prenotazione e conferma, e mantiene lo stato relativo a questo lavoro nelle variabili di istanza. Pensiamo che questo codice sia molto più pulito e semplice degli attributi get e set in HttpSession
.
Ancor meglio, un utente può avere conversazioni multiple isolate per ogni sessione di login. Si provi! Loggarsi, eseguire una ricerca e navigare in diverse pagine d'hotel in diverse schede del browser. Si sarà in grado di lavorare e creare due differenti prenotazioni contemporaneamente. Se una conversazione viene lasciata a lungo inattiva, Seam andrà in timeout e distruggerà lo stato di quella conversazione. Se, dopo la chiusura di una conversazione, si premerà il pulsante indietro per tornare alla pagina precedente e si eseguirà un'azione, Seam si accorgerà che la conversazione è già terminata, e rimanderà l'utente alla pagina di ricerca.
Il WAR include anche seam-debug.jar
. La pagina di debug di Seam sarà disponibile se questo jar è deployato in WEB-INF/lib
, assieme a Facelets e se è stata impostata la proprietà di debug nel componente init
:
<core:init jndi-pattern="@jndiPattern@" debug="true"/>
Questa pagina consentirà di sfogliare ed ispezionare i componenti Seam in ogni contesto Seam associato alla sessione di login corrente. Si punti il browser su http://localhost:8080/seam-booking/debug.seam
.
Le conversazioni long-running rendono semplice mantenere la consistenza dello stato in un'applicazione anche in presenza di operazioni con finestre multiple o con il pulsante indietro. Sfrotunatamente, iniziare e finire una conversazione long-running non è sempre sufficiente. A seconda dei requisiti dell'applicazione, le inconsistenze tra le aspettative dell'utente ed il reale stato dell'applicazione possono comunque sussistere.
L'applicazione prenotazione annidata estende le caratteristiche dell'applicazione prenotazione hotel aggiungendo la selezione della stanza. Ogni hotel ha camere disponibili con delle descrizioni che l'utente può scegliere. Questo richiede l'aggiunta di una pagina di selezione camera nel flusso di prenotazione hotel.
L'utente adesso ha l'opzione di selezionare una camera disponibile da aggiungere alla prenotazione. Come per l'applicazione precedentemente vista, questo porta a problemi di consistenza dello stato. Come per la memorizzazione dello stato in HTTPSession
, se una variabile di conversazione cambia, questo influenza tutte le finestre che operano dentro lo stesso contesto di conversazione.
Per dimostrare questo si supponga che l'utente cloni la schermata di selezione delle camera in una nuova finestra. L'utente quindi seleziona la Wonderful Room e procede alla schermata di conferma. Per vedere solamente quando costa vivere alla grande, l'utente ritorna alla finestra originale, seleziona la Fantastic Suite ed procede quindi alla conferma. Dopo aver visto il costo totale, l'utente decide che la praticità vince e ritorna alla finestra della Wonderful Room per procedere alla conferma.
In questo scenario, se semplicemente si memorizza lo stato nella conversazione non si è protetti da operazioni a finestre multiple all'interno della stessa conversazione. Le conversazioni innestate consentono di ottenere un comportamento corretto quando il contesto può variare all'interno della stessa conversazione.
Si veda ora come l'esempio di prenotazione innestata estenda il comportamento dell'applicazione di prenotazione hotel tramite l'utilizzo di conversazioni innestate. Ancora, si può leggere la classe dalla cima verso il fondo, come un racconto.
Esempio 1.28. RoomPreferenceAction.java
@Stateful @Name("roomPreference") @Restrict("#{identity.loggedIn}") public class RoomPreferenceAction implements RoomPreference { @Logger private Log log; @In private Hotel hotel; @In private Booking booking; @DataModel(value="availableRooms") private List<Room > availableRooms; @DataModelSelection(value="availableRooms") private Room roomSelection; @In(required=false, value="roomSelection") @Out(required=false, value="roomSelection") private Room room; @Factory("availableRooms") public void loadAvailableRooms() { availableRooms = hotel.getAvailableRooms(booking.getCheckinDate(), booking.getCheckoutDate()); log.info("Retrieved #0 available rooms", availableRooms.size()); } public BigDecimal getExpectedPrice() { log.info("Retrieving price for room #0", roomSelection.getName()); return booking.getTotal(roomSelection); }
@Begin(nested=true) public String selectPreference() { log.info("Room selected");
this.room = this.roomSelection; return "payment"; } public String requestConfirmation() { // all validations are performed through the s:validateAll, so checks are already // performed log.info("Request confirmation from user"); return "confirm"; } @End(before
Redirect=true) public String cancel() { log.info("ending conversation"); return "cancel"; } @Destroy @Remove public void destroy() {} }
![]() | L'istanza |
![]() | Quando si incontra |
![]() |
|
![]() | L'annotazione |
Quando si inizia una conversazione innestata, questa viene messa nello stack delle conversazioni. Nell'esempio nestedbooking
, lo stack consiste in una conversazione long-running più esterna (la prenotazione) e ciascuna delle conversazioni innestate (selezione camere).
Esempio 1.29. rooms.xhtml
<div class="section"> <h1 >Room Preference</h1> </div> <div class="section"> <h:form id="room_selections_form"> <div class="section"> <h:outputText styleClass="output" value="No rooms available for the dates selected: " rendered="#{availableRooms != null and availableRooms.rowCount == 0}"/> <h:outputText styleClass="output" value="Rooms available for the dates selected: " rendered="#{availableRooms != null and availableRooms.rowCount > 0}"/> <h:outputText styleClass="output" value="#{booking.checkinDate}"/> - <h:outputText styleClass="output" value="#{booking.checkoutDate}"/><br/><br/> <h:dataTable value="#{availableRooms}" var="room" rendered="#{availableRooms.rowCount > 0}"> <h:column> <f:facet name="header" >Name</f:facet> #{room.name} </h:column> <h:column> <f:facet name="header" >Description</f:facet> #{room.description} </h:column> <h:column>
<f:facet name="header" >Per Night</f:facet> <h:outputText value="#{room.price}"> <f:convertNumber type="currency" currencySymbol="$"/> </h:outputText> </h:column> <h:column> <f:facet name="header" >Action</f:facet>
<h:commandLink id="selectRoomPreference" action="#{roomPreference.selectPreference}" >Select</h:commandLink> </h:column> </h:dataTable> </div> <div class="entry"> <div class="label" > </div> <div class="input"> <s:button id="cancel" value="Revise Dates" view="/book.xhtml"/> </div> </div > </h:form> </div>
![]() | Quando richiesto da EL, |
![]() | L'invocazione dell'azione |
![]() | Un cambiamento alle date semplicemente riporta a |
Ora che si è visto come innestare una conversazione, vediamo come si può confermare la prenotazione una volta selezionata la camera. Questo può essere ottenuto semplicemente estendendo il comportamento di HotelBookingAction
.
Esempio 1.30. HotelBookingAction.java
@Stateful @Name("hotelBooking") @Restrict("#{identity.loggedIn}") public class HotelBookingAction implements HotelBooking { @PersistenceContext(type=EXTENDED) private EntityManager em; @In private User user; @In(required=false) @Out private Hotel hotel; @In(required=false) @Out(required=false) private Booking booking; @In(required=false) private Room roomSelection; @In private FacesMessages facesMessages; @In private Events events; @Logger private Log log; @Begin public void selectHotel(Hotel selectedHotel) { log.info("Selected hotel #0", selectedHotel.getName()); hotel = em.merge(selectedHotel); } public String setBookingDates() { // the result will indicate whether or not to begin the nested conversation // as well as the navigation. if a null result is returned, the nested // conversation will not begin, and the user will be returned to the current // page to fix validation issues String result = null; Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_MONTH, -1); // validate what we have received from the user so far if ( booking.getCheckinDate().before( calendar.getTime() ) ) { facesMessages.addToControl("checkinDate", "Check in date must be a future date"); } else if ( !booking.getCheckinDate().before( booking.getCheckoutDate() ) ) { facesMessages.addToControl("checkoutDate", "Check out date must be later than check in date"); } else { result = "rooms"; } return result; } public void bookHotel() { booking = new Booking(hotel, user); Calendar calendar = Calendar.getInstance(); booking.setCheckinDate( calendar.getTime() ); calendar.add(Calendar.DAY_OF_MONTH, 1); booking.setCheckoutDate( calendar.getTime() ); } @End(root=true) public voidconfirm() { // on confirmation we set the room preference in the booking. the room preference // will be injected based on the nested conversation we are in. booking.setRoomPreference(roomSelection);
em.persist(booking); facesMessages.add("Thank you, #{user.name}, your confimation number for #{hotel.name} is #{booking.id}"); log.info("New booking: #{booking.id} for #{user.username}"); events.raiseTransactionSuccessEvent("bookingConfirmed"); } @End(root=t
rue, beforeRedirect=true) public void cancel() {} @Destroy @Remove public void destroy() {} }
![]() | Annotare un'azione con |
![]() |
|
![]() | Annotando semplicemente l'azione di cancellazione con |
Prova il deploy dell'applicazione, apri più finestre o tab e prova combinzioni di vari hotel con varie opzioni di camera. La conferma risulterà sempre nel giusto hotel e con la corretta opzione grazie al modello di conversazioni innestate.
L'applicazione demo Negozio DVD mostra un utilizzo pratico di jBPM sia per la gestione task sia per il pageflow.
Le schermate utente sfruttano il pageflow jPDL per implementare la ricerca e la funzionalità di carrello della spesa.
Le schermate di amministrazione utilizzano jBPM per gestire il ciclo di approvazione e di spedizione degli ordini. Il processo di business può anche essere cambiato dinamicamente, selezionando una diversa definizione di processo!
La demo Negozio DVD può essere eseguita dalla directory dvdstore
, così come le altre applicazioni.
Seam facilita l'implementazione di applicazioni che mantengano lo stato lato server. Comunque lo stato lato server non è sempre appropriato, specialmente per funzionalità che lavorano per il contenuto. Per questo genere di problemi spesso si vuole mantenere lo stato dell'applicazione nell'URL affiché ogni pagina possa essere acceduta in qualsiasi momento attraverso un segnalibro. L'esempio Blog mostra come implementare un'applicazione che supporti i segnalibri, anche nel caso di pagine con risultati di ricerca. Questo esempio mostra come Seam può gestire nell'URL lo stato di un'applicazione, così come Seam può riscrivire questi URL.
L'esempio Blog mostra l'uso di MVC di tipo "pull", dove invece di usare metodi action listener per recuperare i dati e preparare i dati per la vista, la vista preleva (pull) i dati dai componenti quando viene generata.
Questo frammento della pagina facelets index.xhtml
mostra una lista di messaggi recenti al blog:
Esempio 1.31.
<h:dataTable value="#{blog.recentBlogEntries}" var="blogEntry" rows="3">
<h:column>
<div class="blogEntry">
<h3
>#{blogEntry.title}</h3>
<div>
<s:formattedText value="#{blogEntry.excerpt==null ? blogEntry.body : blogEntry.excerpt}"/>
</div>
<p>
<s:link view="/entry.xhtml" rendered="#{blogEntry.excerpt!=null}" propagation="none"
value="Read more...">
<f:param name="blogEntryId" value="#{blogEntry.id}"/>
</s:link>
</p>
<p>
[Posted on 
<h:outputText value="#{blogEntry.date}">
<f:convertDateTime timeZone="#{blog.timeZone}" locale="#{blog.locale}" type="both"/>
</h:outputText
>]
 
<s:link view="/entry.xhtml" propagation="none" value="[Link]">
<f:param name="blogEntryId" value="#{blogEntry.id}"/>
</s:link>
</p>
</div>
</h:column>
</h:dataTable
>
Se si arriva in quaste pagina da un segnalibro, come viene inizializzato #{blog.recentBlogEntries}
usato da <h:dataTable>
? Blog
viene recuperato in modo lazy — "tirato" — quando serve, da un componente Seam chiamato blog
. Questo è il flusso di controllo opposto a quello usato nei tradizionali framework web basati sull'azione, come ad esempio Struts.
Esempio 1.32.
@Name("blog") @Scope(ScopeType.STATELESS) @AutoCreate public class BlogService { @In EntityManager entityManager; @Unwrap
public Blog getBlog() { return (Blog) entityManager.createQuery("select distinct b from Blog b left join fetch b.blogEntries") .setHint("org.hibernate.cacheable", true) .getSingleResult(); } }
![]() | Questo componente utilizza un contesto di persistenza gestito da Seam. A differenza deglialtri esempi visti, questo contesto di persistenza è gestito da Seam, invece che dal container EJB3. Il contesto di persistenza estende l'intera richiesta web, consentendo di evitare le eccezioni che avvengono quando nella vista si accede ad associazioni non recuperate (unfetched). |
![]() | L'annotazione |
Finora va bene, ma cosa succede se si memorizza il risultato di un invio di form, come ad esempio una pagina di risultati di ricerca?
L'esempio Blog ha una piccola form in alto a destra di ogni pagina, che consente all'utente di cercare le entry del blog. E' definito in un file, menu.xhtml
, incluso nel template facelets, template.xhtml
:
Esempio 1.33.
<div id="search">
<h:form>
<h:inputText value="#{searchAction.searchPattern}"/>
<h:commandButton value="Search" action="/search.xhtml"/>
</h:form>
</div
>
Per implementare una pagina di risultati di ricerca memorizzabili come segnalibro, occorre eseguire un redirect del browser dopo aver elaborato la form di ricerca inviata. Poiché si è impiegato come esito d'azione l'id della vista JSF, Seam redirige automaticamente all'id vista quando la form viene inviata. In alternativa, si può definire una regola di navigazione come questa:
<navigation-rule>
<navigation-case>
<from-outcome
>searchResults</from-outcome>
<to-view-id
>/search.xhtml</to-view-id>
<redirect/>
</navigation-case>
</navigation-rule
>
Quindi la form avrebbe dovuto essere così:
<div id="search">
<h:form>
<h:inputText value="#{searchAction.searchPattern}"/>
<h:commandButton value="Search" action="searchResults"/>
</h:form>
</div
>
Ma quando viene fatto il redirect, occorre includere i valori sottomessi con la form dentro l'URL per ottenere un URL memorizzabile come ad esempio http://localhost:8080/seam-blog/search/
. JSF non fornisce un modo semplice per farlo, ma Seam sì. Per ottenere questo si usano due funzionalità di Seam: i parametri di pagina e la riscrittura dell'URL. Entrambi sono definiti in WEB-INF/pages.xml
:
Esempio 1.34.
<pages>
<page view-id="/search.xhtml">
<rewrite pattern="/search/{searchPattern}"/>
<rewrite pattern="/search"/>
<param name="searchPattern" value="#{searchService.searchPattern}"/>
</page>
...
</pages
>
Il parametro di pagina istruisce Seam a fare collegare il parametro di richiesta chiamato searchPattern
al valore di #{searchService.searchPattern}
, sia quando arriava una richiesta per la pagina di ricerca, sia quando viene generato un link alla pagina di ricerca. Seam si prende la responsabilità di mantenere il link tra lo stato dell'URL e lo stato dell'applicazione, mentre voi, come sviluppatori, non dovete preoccuparvene.
Senza riscrittura, l'URL di una ricerca di un termine book
sarebbe http://localhost:8080/seam-blog/seam/search.xhtml?searchPattern=book
. Questo può andare bene, ma Seam può semplificare l'URL usando una regola di riscrittura. La prima regola, per il pattern /search/{searchPattern}
, dice che in ogni volta che si ha un URL per search.xhtml con un parametro di richiesta searchPattern, si può semplificare quest'URL. E quindi l'URL visto prima, http://localhost:8080/seam-blog/seam/search.xhtml?searchPattern=book
viene riscritto come http://localhost:8080/seam-blog/search/book
.
Come per i parametri di pagina, la riscrittura dell'URL è bidirezionale. Questo significa che Sean inoltra le richieste di URL più semplici alla giusta vista e genera automaticamente la vista più semplice per voi. Non serve preoccuparsi della costruzione dell'URL. Viene tutto gestito in modo trasparente dietro. L'unico requisito è che per usare la riscrittura dell'URL, occorre abilitare il filtro di riscrittura in components.xml
.
<web:rewrite-filter view-mapping="/seam/*" />
Il redirect di porta alla pagina search.xhtml
:
<h:dataTable value="#{searchResults}" var="blogEntry">
<h:column>
<div>
<s:link view="/entry.xhtml" propagation="none" value="#{blogEntry.title}">
<f:param name="blogEntryId" value="#{blogEntry.id}"/>
</s:link>
posted on
<h:outputText value="#{blogEntry.date}">
<f:convertDateTime timeZone="#{blog.timeZone}" locale="#{blog.locale}" type="both"/>
</h:outputText>
</div>
</h:column>
</h:dataTable
>
Il quale usa ancora MVC di tipo "pull" per recuperare i risultati di ricerca usando hibernate Search.
@Name("searchService")
public class SearchService
{
@In
private FullTextEntityManager entityManager;
private String searchPattern;
@Factory("searchResults")
public List<BlogEntry
> getSearchResults()
{
if (searchPattern==null || "".equals(searchPattern) ) {
searchPattern = null;
return entityManager.createQuery("select be from BlogEntry be order by date desc").getResultList();
}
else
{
Map<String,Float
> boostPerField = new HashMap<String,Float
>();
boostPerField.put( "title", 4f );
boostPerField.put( "body", 1f );
String[] productFields = {"title", "body"};
QueryParser parser = new MultiFieldQueryParser(productFields, new StandardAnalyzer(), boostPerField);
parser.setAllowLeadingWildcard(true);
org.apache.lucene.search.Query luceneQuery;
try
{
luceneQuery = parser.parse(searchPattern);
}
catch (ParseException e)
{
return null;
}
return entityManager.createFullTextQuery(luceneQuery, BlogEntry.class)
.setMaxResults(100)
.getResultList();
}
}
public String getSearchPattern()
{
return searchPattern;
}
public void setSearchPattern(String searchPattern)
{
this.searchPattern = searchPattern;
}
}
Alcune volte ha più senso usare MVC push-style per processare pagine RESTful, e quindi Seam fornisce la nozione di azione di pagina. L'esempio di Blog utilizza l'azione di pagina per pagina di entry del blog, entry.xhtml
. Notare che questo è un pò forzato, sarebbe stato più facile usare anche qua lo stile MVC pull-style.
Il componente entryAction
funziona come una action class in un framework tradizionale orientato alle azioni e push-MVC come Struts:
@Name("entryAction")
@Scope(STATELESS)
public class EntryAction
{
@In Blog blog;
@Out BlogEntry blogEntry;
public void loadBlogEntry(String id) throws EntryNotFoundException
{
blogEntry = blog.getBlogEntry(id);
if (blogEntry==null) throw new EntryNotFoundException(id);
}
}
Le azione nella pagina vengono anche dichiarate in pages.xml
:
<pages>
...
<page view-id="/entry.xhtml"
>
<rewrite pattern="/entry/{blogEntryId}" />
<rewrite pattern="/entry" />
<param name="blogEntryId"
value="#{blogEntry.id}"/>
<action execute="#{entryAction.loadBlogEntry(blogEntry.id)}"/>
</page>
<page view-id="/post.xhtml" login-required="true">
<rewrite pattern="/post" />
<action execute="#{postAction.post}"
if="#{validation.succeeded}"/>
<action execute="#{postAction.invalid}"
if="#{validation.failed}"/>
<navigation from-action="#{postAction.post}">
<redirect view-id="/index.xhtml"/>
</navigation>
</page>
<page view-id="*">
<action execute="#{blog.hitCount.hit}"/>
</page>
</pages
>
Notare che l'esempio utilizza azioni di pagina per la validazione e per il conteggio delle pagine visitate. Si noti anche l'uso di un parametro nel binding di metodo all'interno della azione di pagina. Questa non è una caratteristiva standard di JSF EL, ma Seam consente di usarla, non solo per le azioni di pagina, ma anche nei binding di metodo JSF.
Quando la pagina entry.xhtml
viene richiesta, Seam innanzitutto lega il parametro della pagina blogEntryId
al modello. Si tenga presente che a causa della riscrittura dell'URL, il nome del parametro blogEntryId non verrà mostrato nell'URL. Seam quindi esegue l'azione, che recupera i dati necessari — blogEntry
— e li colloca nel contesto di evento di Seam. Infine, viene generato il seguente:
<div class="blogEntry">
<h3
>#{blogEntry.title}</h3>
<div>
<s:formattedText value="#{blogEntry.body}"/>
</div>
<p>
[Posted on 
<h:outputText value="#{blogEntry.date}">
<f:convertDateTime timeZone="#{blog.timeZone}" locale="#{blog.locale}" type="both"/>
</h:outputText
>]
</p>
</div
>
Se l'entry del blog non viene trovata nel database, viene lanciata l'eccezione EntryNotFoundException
. Si vuole che quest'eccezione venga evidenziata come errore 404, non 505, e quindi viene annotata la classe dell'eccezione:
@ApplicationException(rollback=true)
@HttpError(errorCode=HttpServletResponse.SC_NOT_FOUND)
public class EntryNotFoundException extends Exception
{
EntryNotFoundException(String id)
{
super("entry not found: " + id);
}
}
Un'implementazione alternativa dell'esempio non utilizza il parametro nel method binding:
@Name("entryAction")
@Scope(STATELESS)
public class EntryAction
{
@In(create=true)
private Blog blog;
@In @Out
private BlogEntry blogEntry;
public void loadBlogEntry() throws EntryNotFoundException
{
blogEntry = blog.getBlogEntry( blogEntry.getId() );
if (blogEntry==null) throw new EntryNotFoundException(id);
}
}
<pages>
...
<page view-id="/entry.xhtml" action="#{entryAction.loadBlogEntry}">
<param name="blogEntryId" value="#{blogEntry.id}"/>
</page>
...
</pages
>
E' una questione di gusti su quale implementazione tu preferisca.
La demo del blog mostra anche una semplice autenticazione di password, un invio di un post al blog, un esempio di caching frammentato della pagina e la generazione di atom feed.