SeamFramework.orgCommunity Documentation
Seam semplifica la creazione di applicazioni tramite la scrittura di classi Java semplici con annotazioni, che non hanno bisogno di estendere speciali interfacce o superclassi. Ma è possibile semplificare ulteriormente alcuni comuni compiti di programmazione, fornendo un set di componenti predefiniti che possono essere riutilizzati o tramite configurazione in components.xml
(per casi molto semplici) o tramite estensione.
Seam Application Framework può ridurre la quantità di codice da scrivere nel fornire l'accesso ai database nelle applicazioni web, usando Hibernate o JPA.
Sottolineiamo che il framework è estremamente semplice, solamente una manciata di classi molto semplici, facili da capire e da estendere. La "magia" è in Seam stesso — la stessa magia che si usa nel creare un'applicazione Seam anche senza usare questo framework.
I componenti forniti dal framework Seam possono essere usati secondo due differenti approcci. Il primo modo è installare e configurare un'istanza del componente in components.xml
, come si è fatto con altri tipi di componenti Seam predefiniti. Per esempio, il seguente frammento da components.xml
installa un componente che esegue semplici operazioni CRUD per un'entità Person
:
<framework:entity-home name="personHome"
entity-class="eg.Person"
entity-manager="#{personDatabase}">
<framework:id
>#{param.personId}</framework:id>
</framework:entity-home
>
Se per i propri gusti tutto questo sembra troppo "programmare in XML", è possibile altrimenti usare l'estensione:
@Name("personHome")
public class PersonHome extends EntityHome<Person
> {
@In EntityManager personDatabase;
public EntityManager getEntityManager() {
return personDatabase;
}
}
Il secondo approccio ha un vantaggio enorme: si possono facilmente aggiungere funzionalità extra ed eseguire l'override di funzionalità predefinite (le classi del framework sono state attentamente progettate per l'estensione e la personalizzazione).
Un secondo vantaggio è che le classi possono essere bean di sessione stateful EJB, se si vuole. (Non devono per forza esserlo, se si vuole possono essere componenti JavaBean semplici.) Se si sta usando JBoss AS, serve la versione 4.2.2.GA o successive:
@Stateful
@Name("personHome")
public class PersonHome extends EntityHome<Person
> implements LocalPersonHome {
}
Si possono rendere le proprie classi bean di sessione stateless. In questo caso occorre usare l'injection per fornire il contesto di persistenza, anche se viene chiamato l'entityManager
:
@Stateless
@Name("personHome")
public class PersonHome extends EntityHome<Person
> implements LocalPersonHome {
@In EntityManager entityManager;
public EntityManager getPersistenceContext() {
entityManager;
}
}
Attualmente Seam fornisce quattro componenti predefiniti: EntityHome
e HibernateEntityHome
per le operazioni CRUD, assieme a EntityQuery
e HibernateEntityQuery
per le query.
I componenti Home e Query sono scritti per funzionare con scope di sessione, evento o conversazione. Quale scope usare dipende dal modello di stato che si desidera usare nella propria applicazione.
Seam Application Framework funziona solo con contesti di persistenza gestiti da Seam. Di default i componenti cercano un contesto di persistenza chiamato entityManager
.
Un oggetto Home fornisce operazioni per la persistenza per una particolare classe entity. Si supponga di avere una classe Person
:
@Entity
public class Person {
@Id private Long id;
private String firstName;
private String lastName;
private Country nationality;
//getters and setters...
}
E' possibile definire un componente personHome
o via configurazione:
<framework:entity-home name="personHome" entity-class="eg.Person" />
O tramite estensione:
@Name("personHome")
public class PersonHome extends EntityHome<Person
> {}
Un oggetto Home fornisce le seguenti operazioni: persist()
, remove()
, update()
e getInstance()
. Prima di chiamare le operazioni remove()
, o update()
, occorre impostare l'identificatore dell'oggetto interessato, usando il metodo setId()
.
Si può usare un Home direttamente da una pagina JSF, per esempio:
<h1
>Create Person</h1>
<h:form>
<div
>First name: <h:inputText value="#{personHome.instance.firstName}"/></div>
<div
>Last name: <h:inputText value="#{personHome.instance.lastName}"/></div>
<div>
<h:commandButton value="Create Person" action="#{personHome.persist}"/>
</div>
</h:form
>
Di solito è più comodo poter fare riferimento a Person
semplicemente come person
, e quindi si aggiunga una linea a components.xml
:
<factory name="person"
value="#{personHome.instance}"/>
<framework:entity-home name="personHome"
entity-class="eg.Person" />
(Se si usa la configurazione.) O si aggiunga un metodo @Factory
a PersonHome
:
@Name("personHome")
public class PersonHome extends EntityHome<Person
> {
@Factory("person")
public Person initPerson() { return getInstance(); }
}
(Se si usa l'estensione.) Questo cambiamento semplifica le pagine JSF come segue:
<h1
>Create Person</h1>
<h:form>
<div
>First name: <h:inputText value="#{person.firstName}"/></div>
<div
>Last name: <h:inputText value="#{person.lastName}"/></div>
<div>
<h:commandButton value="Create Person" action="#{personHome.persist}"/>
</div>
</h:form
>
Bene, questo crea nuove entry per Person
. Esatto, questo è tutto il codice che serve! Ora se si vuole mostrare, aggiornare e cancellare le entry di Person
già esistenti nel database, occorre passare l'identificatore delle entry a PersonHome
. I parametri di pagina sono un eccezionale modo per farlo:
<pages>
<page view-id="/editPerson.jsp">
<param name="personId" value="#{personHome.id}"/>
</page>
</pages
>
Ora possiamo aggiungere operazioni extra alle pagine JSF:
<h1>
<h:outputText rendered="#{!personHome.managed}" value="Create Person"/>
<h:outputText rendered="#{personHome.managed}" value="Edit Person"/>
</h1>
<h:form>
<div
>First name: <h:inputText value="#{person.firstName}"/></div>
<div
>Last name: <h:inputText value="#{person.lastName}"/></div>
<div>
<h:commandButton value="Create Person" action="#{personHome.persist}" rendered="#{!personHome.managed}"/>
<h:commandButton value="Update Person" action="#{personHome.update}" rendered="#{personHome.managed}"/>
<h:commandButton value="Delete Person" action="#{personHome.remove}" rendered="#{personHome.managed}"/>
</div>
</h:form
>
Quando ci si collega alla pagina senza parametri di richiesta, la pagina verrà mostrata come una pagina "Create Person". Quando si fornisce un valore per il parametro di richiesta personId
, sarà una pagina "Edit Person".
Si supponga di dover creare entry di Person
con la nazionalità inizializzata. E' possibile farlo semplicemente via configurazione:
<factory name="person"
value="#{personHome.instance}"/>
<framework:entity-home name="personHome"
entity-class="eg.Person"
new-instance="#{newPerson}"/>
<component name="newPerson"
class="eg.Person">
<property name="nationality"
>#{country}</property>
</component
>
O tramite estensione:
@Name("personHome")
public class PersonHome extends EntityHome<Person
> {
@In Country country;
@Factory("person")
public Person initPerson() { return getInstance(); }
protected Person createInstance() {
return new Person(country);
}
}
Certamente Country
può essere un oggetto gestito da un altro oggetto Home, per esempio, CountryHome
.
Per aggiungere altre operazioni sofisticate (gestione dell'associazione, ecc.) si possono aggiungere dei metodi a PersonHome
.
@Name("personHome")
public class PersonHome extends EntityHome<Person
> {
@In Country country;
@Factory("person")
public Person initPerson() { return getInstance(); }
protected Person createInstance() {
return new Person(country);
}
public void migrate()
{
getInstance().setCountry(country);
update();
}
}
L'oggetto Home solleva un'evento org.jboss.seam.afterTransactionSuccess
quando una transazione ha successo (una chiamata a persist()
, update()
o remove()
ha successo). Osservando questo evento si può fare il refresh delle query quando cambiano le entità sottostanti. Se si vuole solo eseguire il refresh quando una particolare entità viene persistita, aggiornata o rimossa, si può osservare l'evento org.jboss.seam.afterTransactionSuccess.<name>
(dove <name>
è il nome semplice dell'entity, es. un entity chiamata "org.foo.myEntity" ha nome semplice "myEntity").
L'oggetto Home mostra automaticamente i messaggi faces quando un'operazione ha successo. Per personalizzare questi messaggi si può ancora usare la configurazione:
<factory name="person"
value="#{personHome.instance}"/>
<framework:entity-home name="personHome"
entity-class="eg.Person"
new-instance="#{newPerson}">
<framework:created-message
>New person #{person.firstName} #{person.lastName} created</framework:created-message>
<framework:deleted-message
>Person #{person.firstName} #{person.lastName} deleted</framework:deleted-message>
<framework:updated-message
>Person #{person.firstName} #{person.lastName} updated</framework:updated-message>
</framework:entity-home>
<component name="newPerson"
class="eg.Person">
<property name="nationality"
>#{country}</property>
</component
>
O estensione:
@Name("personHome")
public class PersonHome extends EntityHome<Person
> {
@In Country country;
@Factory("person")
public Person initPerson() { return getInstance(); }
protected Person createInstance() {
return new Person(country);
}
protected String getCreatedMessage() { return createValueExpression("New person #{person.firstName} #{person.lastName} created"); }
protected String getUpdatedMessage() { return createValueExpression("Person #{person.firstName} #{person.lastName} updated"); }
protected String getDeletedMessage() { return createValueExpression("Person #{person.firstName} #{person.lastName} deleted"); }
}
Ma il modo migliore per specificare i messaggi è metterli in un resource bundle noto a Seam (di default, il nome del bundle è messages
).
Person_created=New person #{person.firstName} #{person.lastName} created Person_deleted=Person #{person.firstName} #{person.lastName} deleted Person_updated=Person #{person.firstName} #{person.lastName} updated
Questo abilita l'internazionalizzazione e mantiene il codice e la configurazione puliti dagli elementi di presentazione.
Il passo finale è aggiungere alla pagina la funzionalità di validazione, usando <s:validateAll>
e <s:decorate>
, ma verrà lasciato al lettore come esercizio.
Se occorre una lista di tutte le istanze Person
nel database, si può usare un oggetto Query. Per esempio:
<framework:entity-query name="people"
ejbql="select p from Person p"/>
E' possibile usarlo da una pagina JSF:
<h1
>List of people</h1>
<h:dataTable value="#{people.resultList}" var="person">
<h:column>
<s:link view="/editPerson.jsp" value="#{person.firstName} #{person.lastName}">
<f:param name="personId" value="#{person.id}"/>
</s:link>
</h:column>
</h:dataTable
>
Probabilmente occorre un supporto per la paginazione:
<framework:entity-query name="people"
ejbql="select p from Person p"
order="lastName"
max-results="20"/>
Si userà un parametro di pagina per determinare la pagina da mostrare:
<pages>
<page view-id="/searchPerson.jsp">
<param name="firstResult" value="#{people.firstResult}"/>
</page>
</pages
>
Il codice JSF per il controllo della paginazione è un pò verboso, ma gestibile:
<h1
>Search for people</h1>
<h:dataTable value="#{people.resultList}" var="person">
<h:column>
<s:link view="/editPerson.jsp" value="#{person.firstName} #{person.lastName}">
<f:param name="personId" value="#{person.id}"/>
</s:link>
</h:column>
</h:dataTable>
<s:link view="/search.xhtml" rendered="#{people.previousExists}" value="First Page">
<f:param name="firstResult" value="0"/>
</s:link>
<s:link view="/search.xhtml" rendered="#{people.previousExists}" value="Previous Page">
<f:param name="firstResult" value="#{people.previousFirstResult}"/>
</s:link>
<s:link view="/search.xhtml" rendered="#{people.nextExists}" value="Next Page">
<f:param name="firstResult" value="#{people.nextFirstResult}"/>
</s:link>
<s:link view="/search.xhtml" rendered="#{people.nextExists}" value="Last Page">
<f:param name="firstResult" value="#{people.lastFirstResult}"/>
</s:link
>
Le schermate di ricerca consentono all'utente di inserire una serie di criteri di ricerca per restringere la lista dei risultati restituiti. L'oggetto Query consente di specificare delle "restrizioni" opzionali per supportare quest'importante caso d'uso:
<component name="examplePerson" class="Person"/>
<framework:entity-query name="people"
ejbql="select p from Person p"
order="lastName"
max-results="20">
<framework:restrictions>
<value
>lower(firstName) like lower( concat(#{examplePerson.firstName},'%') )</value>
<value
>lower(lastName) like lower( concat(#{examplePerson.lastName},'%') )</value>
</framework:restrictions>
</framework:entity-query
>
Si noti l'uso di un oggetto "esempio".
<h1
>Search for people</h1>
<h:form>
<div
>First name: <h:inputText value="#{examplePerson.firstName}"/></div>
<div
>Last name: <h:inputText value="#{examplePerson.lastName}"/></div>
<div
><h:commandButton value="Search" action="/search.jsp"/></div>
</h:form>
<h:dataTable value="#{people.resultList}" var="person">
<h:column>
<s:link view="/editPerson.jsp" value="#{person.firstName} #{person.lastName}">
<f:param name="personId" value="#{person.id}"/>
</s:link>
</h:column>
</h:dataTable
>
Per fare il refresh della query qualora cambino le entità sottostanti, si può osservare l'evento org.jboss.seam.afterTransactionSuccess
:
<event type="org.jboss.seam.afterTransactionSuccess">
<action execute="#{people.refresh}" />
</event
>
O semplicemente per fare il refresh della query quando l'entity person viene persistita, aggiornata o rimossa attraverso PersonHome
:
<event type="org.jboss.seam.afterTransactionSuccess.Person">
<action execute="#{people.refresh}" />
</event
>
Sfortunatamente gli oggetti Query non funzionano bene con query join fetch - non è consigliato l'uso della paginazione con queste query, ed occorrerà implementare un proprio metodo di calcolo del numero totale di risultati (con l'override di getCountEjbql()
).
Gli esempi in questa sezione hanno mostrato tutti il riuso tramite configurazione. Comunque per gli oggetti Query il riuso tramite estensione è ugualmente possibile.
Una parte totalmente opzionale del framework Seam è la classe Controller
e le sue sottoclassi EntityController
, HibernateEntityController
e BusinessProcessController
. Queste classi forniscono nient'altro che alcuni metodi di convenienza per l'accesso a componenti predefiniti comunemente usati e a metodi di componenti predefiniti. Essi aiutano a risparmiare alcuni colpi di tastiera ed a fornire un trampolino di lancio ai nuovi utenti per esplorare le ricche funzionalità definite in Seam.
Per esempio, qua è come appare RegisterAction
dell'esempio Registrazione:
@Stateless
@Name("register")
public class RegisterAction extends EntityController implements Register
{
@In private User user;
public String register()
{
List existing = createQuery("select u.username from User u where u.username=:username")
.setParameter("username", user.getUsername())
.getResultList();
if ( existing.size()==0 )
{
persist(user);
info("Registered new user #{user.username}");
return "/registered.jspx";
}
else
{
addFacesMessage("User #{user.username} already exists");
return null;
}
}
}
Come si può vedere, non è un miglioramento sconvolgente...