27 czerwca 2008

Obsługa formularza w JBoss Seam

Już miałem do czynienia z lekturą rozdziału 4. The contextual component model z dokumentacji JBoss Seam 2.0.3.CR1, ale jedynie w niewielkiej jego części. Dzisiaj za zadanie wybrałem obsługę formularza z wyświetleniem danych w strukturze drzewiastej i baczniejsze przyjrzenie się treści rozdziału wydało mi się jak najbardziej na miejscu. Do tej struktury drzewiastej przymierzam się od dobrych kilku dni, więc skoro już temat czeka kilka dni, to i tak jest zagrożony niedotrzymaniem terminu, a nowy - obsługa formularza - jest nowy, więc zagrożony dopiero może być i szkoda byłoby skończyć z dwoma zagrożonymi tematami (coś mi to przypomina komedię Barei, której tytułu nie mogę sobie przypomnieć, gdzie w jednej ze scen facet przychodzi odebrać samochód z naprawy, a on wciąż w naprawie, a nowe obsługiwane są od ręki).

W sekcji 4.2.4. JavaBeans można przeczytać:

By default, JavaBeans are bound to the event context.

I faktycznie, podczas uruchomienia mojej skromnej aplikacji rejestracja komponentu seamowego pozdrow związana jest z kontekstem EVENT (z książki Beginning JBoss Seam: From Novice to Professional dowiedziałem się, że kontekst EVENT to tak na prawdę stary dobry zasięg zlecenia - ang. request scope).
 [Component] Component: pozdrow, scope: EVENT, type: JAVA_BEAN, class: pl.jaceklaskowski.seam.PozdrowAction
Seam definiuje dwa podstawowe pojęcia: kontekst oraz komponent. Kontekst definiuje przestrzeń aktywności komponentów o ustalonej nazwie określanej adnotacją @Name lub stosowną konfiguracją w components.xml. Za pomocą mechanizmu bijekcji Seam potrafi przypisać zmiennym egzemplarza komponenty (adnotacja @In), których zmiana może być propagowana spowrotem do środowiska (adnotacja @Out). Żąglując nimi możemy automatycznie uzyskiwać dostęp do komponentów zewnętrznych i modyfikować je wraz z zapisem zmian "na zewnątrz". Można to przyrównać do dwukierunkowego IoC, gdzie przy @In odbiorcą referencji do obiektu jest nasza klasa, podczas gdy przy @Out będzie to przestrzeń całej aplikacji.

I jeszcze jedna ciekawa uwaga w kontekście użycia komponentów seamowych, które są POJO:

Seam JavaBean components may be instantiated using Component.getInstance() or @In(create=true). They should not be directly instantiated using the new operator.

Zatem, pozwalamy je tworzyć wyłącznie Seamowi, np. za pomocą @In(create=true).

I kolejna ciekawostka związana z POJO w roli komponentów seamowych:

In order to perform its magic (bijection, context demarcation, validation, etc), Seam must intercept component invocations. For JavaBeans, Seam is in full control of instantiation of the component, and no special configuration is needed.

Zaleca się, aby nazwa komponentów seamowych odpowiadała pakietowi, w jakiej klasa reprezentująca komponent się znajduje, aby ustrzec się przed konfliktem nazw. Istnieje możliwość skorzystania z aliasowania nazw, aby z pełnych stworzyć krótsze, jednowyrazowe. Będzie o tym jeszcze w kolejnych wpisach o Seamie.

Jeszcze kilka słów o konstrukcjach komponentów seamowych i lektura rozdziału 4-tego za mną.

Z tą wiedzą podchodzę do tematu stworzenia obsługi formularza w Seamie. Tworzę zwykłą klasę javową odpowiadającą polom na formularzu - pl.jaceklaskowski.seam.Kategoria.
 package pl.jaceklaskowski.seam;

import org.hibernate.validator.Length;
import org.hibernate.validator.NotNull;
import org.jboss.seam.annotations.Name;

@Name("kategoria")
public class Kategoria {
private String nazwa;
private String opis;
private Kategoria rodzic;

@NotNull @Length(min=5, max=15)
public String getNazwa() {
return nazwa;
}

public void setNazwa(String nazwa) {
this.nazwa = nazwa;
}

@NotNull @Length(min=5, max=15)
public String getOpis() {
return opis;
}

public void setOpis(String opis) {
this.opis = opis;
}

public Kategoria getRodzic() {
return rodzic;
}

public void setRodzic(Kategoria rodzic) {
this.rodzic = rodzic;
}

}
Warto zwrócić uwagę na adnotacje pochodzące z pakietu org.hibernate.validator - @NotNull oraz @Length, które gwarantują odpowiednią "jakość" danych w ramach komponentu kategoria. Skorzystanie z adnotacji @NotNull oraz @Length wymaga dodania zależności hibernate-commons-annotations.jar do projektu (podglądam root-2.0.3.CR1.pom, ale tam nie ma tej zależności wymienionej!):
 <dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-commons-annotations</artifactId>
<version>3.3.0.ga</version>
</dependency>
W przeciwnym przypadku - brak biblioteki hibernate-commons-annotations.jar - wykonanie aplikacji skutkuje zgłoszeniem wyjątku:
 java.lang.NoClassDefFoundError: org/hibernate/annotations/common/reflection/XMember
at org.jboss.seam.core.Validators.createValidator(Validators.java:122)
at org.jboss.seam.core.Validators.getValidator(Validators.java:105)
at org.jboss.seam.core.Validators.getValidator(Validators.java:88)
at org.jboss.seam.core.Validators$ValidatingResolver.setValue(Validators.java:199)
at org.jboss.el.parser.AstPropertySuffix.setValue(AstPropertySuffix.java:73)
at org.jboss.el.parser.AstValue.setValue(AstValue.java:84)
at org.jboss.el.ValueExpressionImpl.setValue(ValueExpressionImpl.java:249)
at com.sun.facelets.el.TagValueExpression.setValue(TagValueExpression.java:93)
at org.jboss.seam.core.Validators.validate(Validators.java:140)
at org.jboss.seam.ui.validator.ModelValidator.validate(ModelValidator.java:35)
at javax.faces.component._ComponentUtils.callValidators(_ComponentUtils.java:156)
at javax.faces.component.UIInput.validateValue(UIInput.java:288)
at javax.faces.component.UIInput.validate(UIInput.java:332)
at javax.faces.component.UIInput.processValidators(UIInput.java:144)
at javax.faces.component.UIComponentBase.processValidators(UIComponentBase.java:658)
at javax.faces.component.UIComponentBase.processValidators(UIComponentBase.java:658)
at javax.faces.component.UIForm.processValidators(UIForm.java:74)
at javax.faces.component.UIComponentBase.processValidators(UIComponentBase.java:658)
at javax.faces.component.UIViewRoot.access$101(UIViewRoot.java:43)
at javax.faces.component.UIViewRoot$2.process(UIViewRoot.java:97)
at javax.faces.component.UIViewRoot.process(UIViewRoot.java:205)
at javax.faces.component.UIViewRoot.processValidators(UIViewRoot.java:93)
at org.apache.myfaces.lifecycle.ProcessValidationsExecutor.execute(ProcessValidationsExecutor.java:32)
at org.apache.myfaces.lifecycle.LifecycleImpl.executePhase(LifecycleImpl.java:103)
at org.apache.myfaces.lifecycle.LifecycleImpl.execute(LifecycleImpl.java:76)
at javax.faces.webapp.FacesServlet.service(FacesServlet.java:148)
Tworzę stronę kategoria.xhtml, która zawiera formularz do wprowadzania nowych kategorii.
 <?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}" />
</h:form>
</f:view>
</body>
</html>
Na uwagę zasługuje skorzystanie z JSF EL w atrybutach value oraz action, które korzystają z komponentów seamowych kategoria oraz kategoriaAgent, jakby były zdefiniowane w faces-config.xml zgodnie z zasadami JavaServer Faces. Właśnie tutaj tkwi siła Seama, który znosi obowiązek utrzymywania pliku faces-config.xml.

Wspomniany komponent seamowy kategoriaAgent reprezentowany jest przez klasę pl.jaceklaskowski.seam.KategoriaAgent:
 package pl.jaceklaskowski.seam;

import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.log.Log;

@Name("kategoriaAgent")
public class KategoriaAgent {

@In
private Kategoria kategoria;

@Logger
private Log logger;

public void dodaj() {
logger.info("Tu nastąpi zapis kategorii \"#{kategoria.nazwa}\" do bazy danych");
}
}
Pozostaje sprawdzić działanie aplikacji uruchamiając stronę http://localhost:8080/seam-richfaces-tree/seam/kategoria.xhtml. Formularz wyświetla się poprawnie, a podanie poprawnych danych do formularza spowoduje wyświetlenie komunikatu na konsoli Geronimo:
 11:59:36,984 INFO  [Version] Hibernate Validator 3.0.0.GA
11:59:37,134 INFO [KategoriaAgent] Tu nastąpi zapis kategorii "Kategoria #1" do bazy danych
Dla kompletu informacji prezentuję deskryptor wdrożenia aplikacji webowej - /WEB-INF/web.xml:
 <?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<context-param>
<param-name>javax.faces.DEFAULT_SUFFIX</param-name>
<param-value>.jsp</param-value>
</context-param>
<context-param>
<param-name>contextFactory</param-name>
<param-value>com.tonbeller.wcf.controller.RequestContextFactoryImpl</param-value>
</context-param>
<context-param>
<param-name>com.tonbeller.wcf.controller.RequestContextFactory</param-name>
<param-value>com.tonbeller.wcf.controller.RequestContextFactoryImpl</param-value>
</context-param>
<filter>
<filter-name>JPivotController</filter-name>
<filter-class>com.tonbeller.wcf.controller.RequestFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>JPivotController</filter-name>
<url-pattern>*.seam</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>JPivotController</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<listener>
<listener-class>org.jboss.seam.servlet.SeamListener</listener-class>
</listener>
<listener>
<listener-class>org.apache.myfaces.webapp.StartupServletContextListener</listener-class>
</listener>
<listener>
<listener-class>com.tonbeller.tbutils.res.ResourcesFactoryContextListener</listener-class>
</listener>
<context-param>
<param-name>facelets.VIEW_MAPPINGS</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>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/seam/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>10</session-timeout>
</session-config>
<jsp-config>
<taglib>
<taglib-uri>http://www.tonbeller.com/wcf</taglib-uri>
<taglib-location>/WEB-INF/wcf/wcf-tags.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>http://www.tonbeller.com/jpivot</taglib-uri>
<taglib-location>/WEB-INF/jpivot/jpivot-tags.tld</taglib-location>
</taglib>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<scripting-invalid>true</scripting-invalid>
<is-xml>false</is-xml>
</jsp-property-group>
</jsp-config>
<resource-ref>
<res-ref-name>jdbc/MondrianFoodmart</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
<res-sharing-scope>Shareable</res-sharing-scope>
</resource-ref>
</web-app>
Nie mogę poradzić sobie z konfiguracją Seam z facelets i uruchomieniem znaczników JSP projektu jpivot. Facelets nie rozpoznaje znaczników JSP jpivota i są one po prostu umieszczane w stronie wynikowej. Dlatego też zdecydowałem się na podwójne mapowanie /seam/* oraz *.seam, tak abym miał możliwość tworzenia stron korzystających ze znaczników jpivota bez użycia facelets. Ma ktoś receptę na to, aby przekonać facelets do rozpoznania znaczników jpivota? Czy jest konieczne stworzenie własnej biblioteki znaczników dla facelets odpowiadających znacznikom JSP z jpivota?

Pytanie konkursowe (z tych bardziej wymagających): Dlaczego uruchomienie strony /seam-richfaces-tree/kategoria.seam zakończy się błędem HTTP ERROR: 404 - /seam-richfaces-tree/kategoria.jsp? I kolejne z serii dla wymagających: Dlaczego uruchomienie strony /seam-richfaces-tree/seam/kategoria.xhtml zakończy się poprawnym uruchomieniem strony kategoria.xhtml z jednoczesnym wykonaniem kontrolek JSF?