30 czerwca 2008

@DataModel, @DataModelSelection oraz @Factory w JBoss Seam

Jakiś czas temu podczas rozpoznawania JavaServer Faces (JSF) i źródeł danych dla kontrolki h:dataTable pisałem o strukturze danych javax.faces.model.DataModel - Tabele w JavaServer Faces - znacznik <h:dataTable>. Za pomocą tej struktury istnieje możliwość dostarczania danych tabelarycznych i w prosty sposób otrzymywać informację zwrotną o wybranym przez użytkownika wierszu. Nie potrzebne jest przechwytywanie identyfikatora wiersza czy danych w nim zawartych, a wystarczy jedynie związać h:dataTable ze strukturą DataModel i temat obsługi tabeli mamy załatwiony. Skoro JBoss Seam opiera swoje działanie na JSF możnaby oczekiwać podobnej funkcjonalności, potencjalnie uproszczonej w świetle wielu uproszczeń dostarczanych przez Seama. I nie mylił się ten, kto tego oczekiwał (wprowadzenie jak z okładki Seama - taka radość płynie z tego wstępu ;-)).

JBoss Seam dostarcza adnotację @DataModel, która oznacza kolekcję danych (listę, mapę, zbiór, tablicę) jako typ odpowiadający javax.faces.model.DataModel. Dodatkową adnotacją @DataModelSelection określamy pole egzemplarza jako przechowujące wybór użytkownika w h:dataTable i ponownie temat obsłużony. Niewiele uproszczeń dostarczają obie adnotacje, ale w dobie adnotacji programowanie bez nich stało się jakieś takie niemodne.

Przykład opisany w dokumentacji JBoss Seam 2.0.3.CR1 dotyczący tematu @DataModel to 1.3. Clickable lists in Seam: the messages example i większość z mojego przykładu była na nim wzorowana z akompaniamentem książki Beginning JBoss® Seam: From Novice to Professional wydawnictwa Apress.

Zacznijmy od prezentacji komponentu kategoriaAgent reprezentowanego klasą pl.jaceklaskowski.seam.KategoriaAgent:
 package pl.jaceklaskowski.seam;

import java.util.ArrayList;
import java.util.List;

import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Factory;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Out;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.datamodel.DataModel;
import org.jboss.seam.annotations.datamodel.DataModelSelection;
import org.jboss.seam.log.Log;

@Name("kategoriaAgent")
@Scope(ScopeType.SESSION)
public class KategoriaAgent {

@In
private Kategoria kategoria;

@DataModel
private List<Kategoria> kategorie = new ArrayList<Kategoria>();

@DataModelSelection
@Out(required = false)
private Kategoria kategoriaWybrana;

@Logger
private Log log;

public void dodaj() {
log.info("Zapis kategorii \"#{kategoria.nazwa}\"");
kategorie.add(kategoria);
}

public void wybierz() {
log.info("Wybrano kategorię \"#{kategoriaWybrana.nazwa}\"");
}

public void skasuj() {
log.info("Kasowanie kategorii \"#{kategoriaWybrana.nazwa}\"");
kategorie.remove(kategoriaWybrana);
kategoriaWybrana = null;
}

@Factory("kategorie")
public void pobierzKategorie() {

log.info("Wykonanie metody pobierzKategorie()");

Kategoria rodzic = new Kategoria("Rodzic", "Kategoria nadrzędna", null);
Kategoria potomek = new Kategoria("Potomek", "Kategoria podrzędna", rodzic);
kategorie.add(rodzic);
kategorie.add(potomek);
}

}
Na uwagę zasługuje miejsce "przyłożenia" adnotacji @DataModel oraz @DataModelSelection. W zasadzie nie ma co opisywać, po co i dlaczego, mając na uwadze ich działanie (warto zapoznać się z ich dokumentacją javadoc dla pełnego obrazu).

Jako adnotację wspierającą należy wskazać @Factory, która wskazuje metodę będącą fabryką danych dla struktury o nazwie wskazanej jako jej wartość (w moim przykładzie będzie to pole egzemplarza o nazwie kategorie).

Najważniejszą zmianą, która powoduje, że pobranie danych nie następuje przy każdym żądaniu jest @Scope(ScopeType.SESSION). Dzięki tej adnotacji określamy, że zakres dostępności komponentu kategoriaAgent to czas sesji, więc wykonanie metody pobierzKategorie() będzie następowało podczas pierwszego pobrania danych do tabeli w sesji i tak długo, jak sesja będzie aktywna tak długo nie nastąpi kolejne wywołanie tej metody.

Dodając do tego użycie adnotacji @Out przy polu kategoriaWybrana określa udostępnienie wyboru klienta "światu zewnętrznemu", tj. nastąpi utworzenie zmiennej kategoriaWybrana o zasięgu EVENT (równoznaczne z zasięgiem żądania).

Pozostaje zaprezentować stronę XHTML, w której skorzystam z utworzonych struktur danych - kategoria.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>Administracja kategoriami</title>
</head>
<body>
<f:view>
<h:form>
<s:validateAll>
<h:panelGrid columns="2">
Nazwa: <h:inputText value="#{kategoria.nazwa}" required="true" />
Opis: <h:inputText value="#{kategoria.opis}" required="true" />
</h:panelGrid>
</s:validateAll>
<h:messages />
<h:commandButton value="Dodaj" action="#{kategoriaAgent.dodaj}" />
<br />
<h:outputText value="Brak kategorii" rendered="#{kategorie.rowCount==0}" />
<h:dataTable var="ktgria" value="#{kategorie}" rendered="#{kategorie.rowCount>0}">
<h:column>
<f:facet name="header">
<h:outputText value="Nazwa" />
</f:facet>
<h:commandLink value="#{ktgria.nazwa}" action="#{kategoriaAgent.wybierz}" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Opis" />
</f:facet>
<h:outputText value="#{ktgria.opis}" />
</h:column>
<h:column>
<h:commandButton value="Delete" action="#{kategoriaAgent.skasuj}" />
</h:column>
</h:dataTable>
<h3><h:outputText value="#{kategoriaWybrana.nazwa}" /></h3>
<div><h:outputText value="#{kategoriaWybrana.opis}" /></div>
</h:form>
</f:view>
</body>
</html>
Wdrożenie aplikacji na serwer aplikacyjny Apache Geronimo i uruchomienie strony http://localhost:8080/seam-richfaces-tree/seam/kategoria.xhtml:


a na konsoli Geronimo:
 23:43:30,187 INFO  [KategoriaAgent] Wykonanie metody findMessages()
Zatwierdzenie formularza z danymi nowej kategorii i kolejne komunikaty aplikacji na konsoli Geronimo:
 23:45:34,546 INFO  [Version] Hibernate Validator 3.0.0.GA
23:45:34,671 INFO [KategoriaAgent] Zapis kategorii "Nowa kategoria"
Nazwa jest aktywnym odnośnikiem do akcji na serwerze i wybranie dowolnej skutkuje kolejnymi komunikatami:
 23:46:52,781 INFO  [KategoriaAgent] Wybrano kategorię ""
Właśnie! Dlaczego zawsze w komunikacie mam poprzednią wartość #{kategoriaWybrana.nazwa}? Przez "poprzednią" dla pierwszego wywołania będzie to wartość pusta, a dla kolejnych...hmmm...właśnie poprzednia.


Pytanie konkursowe: Jakie 2 adnotacje upraszczają pracę z h:dataTable w komponentach seamowych?