14 października 2007

Konwertery w JavaServer Faces część 6

Podczas mojej prezentacji mechanizmu konwerterów w JSF korzystałem z pomocy tymczasowej klasy pl.jaceklaskowski.konwerter.TymczasowaBazaDanych, której czas się właśnie kończy. Klasę TymczasowaBazaDanych zastąpię bardziej wartościową usługą dostarczaną przez serwer aplikacji Java EE 5 - Java Persistence (JPA). Wprowadzenie JPA służy usprawnieniu działania konwertera stworzonego w poprzednich częściach z serii Konwertery w JavaServer Faces.

Zanim zacznę warto zauważyć, że do chwili obecnej przykłady można było uruchomić na kontenerze servletów, np. Apache Tomcat, Jetty, czy Resin. Kolejne zmiany będą wymagały pełnego serwera aplikacji Java EE (chociaż znawcy tematu JPA mogliby wskazać na taką konfigurację JPA, któraby tego nie wymagałaby). Poprzednie przykłady uruchamiałem na Glassfish, który jest serwerem aplikacji Java EE 5 i tak pozostanie i tym razem, więc dla mnie nie będzie żadnej zmiany środowiska (wybór podyktowany był wyłącznie wsparciem serwera przez NetBeans IDE 6.0).

W Konwertery w JavaServer Faces część 4 przedstawiłem sposób związania konwertera z kontrolką za pomocą atrybutu converter w h:selectOneMenu. Później, w Konwertery w JavaServer Faces część 5 wykorzystałem możliwość konfiguracji konwertera w pliku faces-config.xml. Wykorzystanie JPA zarządzanego przez serwer aplikacji wymaga, aby powrócić do wykorzystania możliwości atrybutu converter. Powód zostanie wyjaśniony za moment.

Rozpocznę od modyfikacji konwertera pl.jaceklaskowski.konwerter.PracownikConverter.

package pl.jaceklaskowski.konwerter;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.persistence.EntityManager;

public class PracownikConverter implements Converter {

private EntityManager em;

public PracownikConverter(EntityManager em) {
this.em = em;
}

public Object getAsObject(FacesContext context, UIComponent component, String value) {
return em.find(Pracownik.class, Long.valueOf(value));
}

public String getAsString(FacesContext context, UIComponent component, Object value) {
return Long.toString(((Pracownik) value).getNumer());
}
}
Zmiana polega na użyciu zarządcy trwałego JPA i tym samym usunięciu wykorzystania klasy pl.jaceklaskowski.konwerter.TymczasowaBazaDanych jako dostawcy informacji o pracownikach z bazy danych. Możnaby zapytać o cel wprowadzenia konstruktora konwertera z parametrem będącym zarządcą trwałym, skoro w środowisku serwera Java EE 5 można użyć adnotacji @PersistenceContext? Właśnie tu tkwi problem i to, co za chwilę zaprezentuję będzie możliwym obejściem braku obsługi konwerterów przez mechanizm wstrzeliwania zależności (ang. DI - dependency injection). Z niezrozumiałych dla mnie powodów, konwertery zostały wyłączone z obsługi mechanizmu DI, tj. nie są komponentami zarządzanymi z punktu widzenia specyfikacji Java EE 5, a tylko w nich można oczekiwać poprawnego działania adnotacji. Brak wsparcia dla DI dla konwerterów rekompensuję przez wprowadzenie konstruktora, który akceptuje parametr wejściowy będący referencją do egzemplarza typu javax.persistence.EntityManager (który, gdyby serwer Java EE 5 wspierał DI dla konwerterów byłby wstrzelowny automatycznie). Rozwiązanie będzie polegało na przekazaniu zarządcy encji z komponentu, który z funkcjonalności DI może korzystać. Warto więc odpowiedzieć na pytanie, który z elementów aplikacji JSF ma taką możliwość? Oczywiście jest to ziarno zarządzane (zgodnie z jego nazwą). Jeśli do tego dodać, że ziarno może udostępniać właściwość, której wartość może być wykorzystana w atrybutach kontrolek, np. converter, to dalsze kroki powinny być już jasne. Pora na zmiany w aplikacji.

Po pierwsze, aby skorzystać z JPA musimy ustanowić klasę pl.jaceklaskowski.konwerter.Pracownik encją JPA, co pociąga za sobą dodanie kilku adnotacji JPA (bądź ustawieniem odpowiedniej konfiguracji w pliku orm.xml). Wybieram podejście pierwsze - dodanie adnotacji. Zmienił się również konstruktor, który niepotrzebuje już parametru wejściowego numer, który od tej pory jest generowany automatycznie przez JPA.

package pl.jaceklaskowski.konwerter;

import java.io.Serializable;
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.NamedQuery;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@Entity
@NamedQuery(name = "Pracownik.wszyscyPracownicy", query = "SELECT p FROM Pracownik p")
public class Pracownik implements Serializable {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long numer;

private String imie;

@Temporal(TemporalType.DATE)
private Date zatrudnionyOd;

public Pracownik() {
}

public Pracownik(String imie, Date zatrudnionyOd) {
this.imie = imie;
this.zatrudnionyOd = zatrudnionyOd;
}

public long getNumer() {
return numer;
}

public void setNumer(long numer) {
this.numer = numer;
}

public String getImie() {
return imie;
}

public void setImie(String imie) {
System.out.println("setImie(" + imie + ")");
this.imie = imie;
}

public Date getZatrudnionyOd() {
return zatrudnionyOd;
}

public void setZatrudnionyOd(Date zatrudnionyOd) {
System.out.println("setZatrudnionyOd(" + zatrudnionyOd + ")");
this.zatrudnionyOd = zatrudnionyOd;
}

@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Pracownik other = (Pracownik) obj;
if (this.numer != other.numer) {
return false;
}
if (this.imie.equals(other.imie) && (this.imie == null || !this.imie.equals(other.imie))) {
return false;
}
if (this.zatrudnionyOd != other.zatrudnionyOd && (this.zatrudnionyOd == null || !this.zatrudnionyOd.equals(other.zatrudnionyOd))) {
return false;
}
return true;
}

@Override
public int hashCode() {
int hash = 5;
hash = 47 * hash + (int) (this.numer ^ (this.numer >>> 32));
hash = 47 * hash + (this.imie != null ? this.imie.hashCode() : 0);
hash = 47 * hash + (this.zatrudnionyOd != null ? this.zatrudnionyOd.hashCode() : 0);
return hash;
}
}
Metody equals() oraz hashCode(),które zazwyczaj są pomijane, w tym przypadku są kluczowe. Podczas kontroli poprawności wyboru (w etapie przetwarzania zlecenia JSF - uruchomienie kontrolerów - ang. process validations) tworzone są nowe egzemplarze Pracowników, które bez użycia wspomnianych metod, powodowałyby błąd kontroli. Gdyby, więc polegać na domyślnej implementacji metod, pojawiałby się błąd kontroli, gdyż sprawdzenie, czy wybrana opcja była na liście opcji odbywa się między innymi na podstawie porównania egzemplarzy przy pomocy metody equals(). Nieufnym polecam uruchomienie przykładu bez wspomnianych metod - equals() oraz hashCode(), a dla dociekliwych taką modyfikację aplikacji, aby equals() straciło na znaczeniu (podpowiedź: aktualna żywotność ziarna to pojedyńcze zlecenie - request).

Po zmianie konwertera modyfikuję ziarno zarządzane pracownikAgent, które jest reprezentowane przez klasę pl.jaceklaskowski.konwerter.PracownikAgent.

package pl.jaceklaskowski.konwerter;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.faces.event.ActionEvent;
import javax.faces.model.SelectItem;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

public class PracownikAgent {

private Pracownik pracownik;

@PersistenceContext
private EntityManager em;

public PracownikConverter getKonwerterDlaPracownika() {
return new PracownikConverter(em);
}

public void wykonajAkcje(ActionEvent event) {
System.out.println("Wykonano akcję z pracownikiem: " + pracownik);
}

public Collection<SelectItem> getPracownicySelectItems() {
Collection<SelectItem> pracownicy = new ArrayList<SelectItem>();
for (Pracownik p : getPracownicy()) {
pracownicy.add(new SelectItem(p, p.getImie()));
}
return pracownicy;
}

public List<Pracownik> getPracownicy() {
return em.createNamedQuery("Pracownik.wszyscyPracownicy").getResultList();
}

public Pracownik getPracownik() {
return pracownik;
}

public void setPracownik(Pracownik pracownik) {
this.pracownik = pracownik;
}
}
Zmiana polega na dodaniu właściwości konwerterDlaPracownika (metoda odczytująca - getKonwerterDlaPracownika()) oraz pola egzemplarza em, w której serwer aplikacji umieści zarządcę encji korzystając z adnotacji @PersistenceContext. Zmodyfikowana została również metoda getPracownicy(), która opiera swoje działanie na JPA poprzez zapytanie nazwane (mianowane) - Pracownik.wszyscyPracownicy - do pobrania listy wszystkich pracowników.

Wykorzystanie zarządcy encji wymaga stworzenia pliku konfiguracji JPA - persistence.xml.

<?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="Konwerter-czesc6PU" transaction-type="JTA">
<jta-data-source>jdbc/sample</jta-data-source>
<properties>
<property name="toplink.ddl-generation" value="drop-and-create-tables"/>
</properties>
</persistence-unit>
</persistence>
Przechodzę do utworzenia strony strona6.jsp.

<%@page contentType="text/html" pageEncoding="UTF-8"%>

<%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%>
<%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Mini-aplikacja JSF - konwerter</title>
</head>
<body>
<f:view>
<h:form>
<h:selectOneMenu value="#{pracownikAgent.pracownik}"
converter="#{pracownikAgent.konwerterDlaPracownika}">
<f:selectItems value="#{pracownikAgent.pracownicySelectItems}" />
</h:selectOneMenu>
<br/>
<h:commandButton value="Zatwierdź" actionListener="#{pracownikAgent.wykonajAkcje}"/>
<br/>
<h:messages showSummary="true" errorStyle="color: red" />
</h:form>
</f:view>
</body>
</html>
Na stronie korzystam z przekazania konwertera do kontrolki h:selectOneMenu poprzez #{pracownikAgent.konwerterDlaPracownika}, czyli wywołanie metody PracownikAgent.getKonwerterDlaPracownika(), która z kolei utworzy nowy egzemplarz konwertera z przekazanym zarządcą encji.

W pliku faces-config.xml usuwamy sekcję dotyczącą związania konwertera PracownikConverter z klasą Pracownik.

<?xml version='1.0' encoding='UTF-8'?>

<faces-config version="1.2"
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">
<managed-bean>
<managed-bean-name>pracownik</managed-bean-name>
<managed-bean-class>pl.jaceklaskowski.konwerter.Pracownik</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
</managed-bean>
<managed-bean>
<managed-bean-name>pracownikAgent</managed-bean-name>
<managed-bean-class>pl.jaceklaskowski.konwerter.PracownikAgent</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
</managed-bean>
</faces-config>
I to wszystkie zmiany, aby oprzeć działanie konwertera na JPA i wykorzystać bazę danych z prawdziwego zdarzenia. Pozostało jeszcze kilka ciekawych usprawnień, które mógłbym wprowadzić do aplikacji, a które wynikają z zastosowania JPA, jednakże miałoby to niewielki związek z tematem przewodnim, więc pozostawiam je na później.

Dla pełności obrazu wprowadzonych zmian należy jeszcze wspomnieć o pewnej funkcjonalności, która służy jedynie do wypełnienia bazy danych pracownikami, aby można było w pełni zaprezentować działanie aplikacji. W obecnej konfiguracji każdorazowe uruchomienie aplikacji kasuje bazę danych i tworzy schemat ponownie. Pusta baza danych pracowników nie pozwoliłaby na wybór pracownika w liście rozwijalnej. Dla obejścia tej niedogodności, podczas uruchomienia aplikacji internetowej, deklaruję wykonywanie słuchacza pl.jaceklaskowski.konwerter.DodajPracownikowListener.

package pl.jaceklaskowski.konwerter;

import java.util.Calendar;
import java.util.Date;
import javax.annotation.Resource;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.transaction.UserTransaction;

public class DodajPracownikowListener implements ServletContextListener {

@PersistenceContext
EntityManager em;

@Resource
UserTransaction ut;

public void contextInitialized(ServletContextEvent event) {
System.out.println("Dodaję pracowników do bazy danych");
Date dzisiaj = Calendar.getInstance().getTime();
Pracownik agata = new Pracownik("Agatka", dzisiaj);
Pracownik jacek = new Pracownik("Jacek", dzisiaj);
try {
ut.begin();
em.persist(em.merge(agata));
em.persist(em.merge(jacek));
ut.commit();
} catch (Exception e) {
System.out.println("Ignorowany wyjątek: " + e.getLocalizedMessage());
}
}

public void contextDestroyed(ServletContextEvent event) {
}
}
, który zarejestrowany jest w głównym pliku konfiguracyjnym aplikacji internetowej - web.xml.

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
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">
<context-param>
<param-name>com.sun.faces.verifyObjects</param-name>
<param-value>false</param-value>
</context-param>
<context-param>
<param-name>com.sun.faces.validateXml</param-name>
<param-value>true</param-value>
</context-param>
<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>client</param-value>
</context-param>
<listener>
<listener-class>pl.jaceklaskowski.konwerter.DodajPracownikowListener</listener-class>
</listener>
<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>/faces/*</url-pattern>
</servlet-mapping>
</web-app>
Ciekawostką słuchacza DodajPracownikowListener jest skorzystanie z adnotacji @PersistenceContext oraz @Resource do wykorzystania z JPA.

Uruchomienie aplikacji nie zmienia się w stosunku do poprzednich części, więc pozostawiam to jako zadanie domowe. Przedstawię jedynie fragment dziennika zdarzeń Glassfish, który wskazuje na wykorzystywanego dostawcę JPA jakim jest TopLink Essentials 2.0.

Initializing Sun's JavaServer Faces implementation (1.2_04-b20-p03) for context '/konwerter'
Dodaję pracowników do bazy danych
TopLink, version: Oracle TopLink Essentials - 2.0 (Build b58g-fcs (09/07/2007))
Server: unknown
file:/Konwerter-czesc6/build/web/WEB-INF/classes/-Konwerter-czesc6PU login successful
Wykonano akcję z pracownikiem: pl.jaceklaskowski.konwerter.Pracownik@b73d3e3
Gotowy projekt aplikacji Konwerter dostępny jest jako jsf-konwerter-czesc6.zip.

Tym samym zakończyłem prezentację konwerterów JSF wieńcząc dzieło użyciem JPA. Dalsze udogodnienia mogłyby wykorzystać kolejne możliwości EJB3, m.in. ziarno bezstanowe do interakcji z JPA w celu usunięcia użycia transakcji po stronie aplikacji internetowej, itp. Wszelkie sugestie mile widziane.

Niepokoi mnie brak komentarzy do poszczególnych części serii o konwerterach JSF. Czyżby wszystko było jasne i działało od pierwszego uruchomienia?! Z ankiety Czy odpowiada Ci sposób prezentacji tematu w postaci krótkich części jak Konwertery JSF? wydaje się, że prezentacja tematu zdaje się być odpowiednia dla 14 osób, co cieszy, jednakże aż 3 osoby nie są w pełni zadowolone. Dlaczego? Dobrze byłoby podnieść swój warsztat techniczny uwzględniając ich uwagi (a może niekoniecznie techniczy, a literacki ;-)).

Przy okazji, dzisiejsze otwarcie NetBeans IDE 6.0 Nightly prezentuje Start Page z tłumaczeniem mojego artykułu Building an EJB 3.0 application using GlassFish v2, Apache Maven 2 and NetBeans IDE 6.0 (polska wersja to Tworzenie aplikacji EJB 3.0 z GlassFish v2, Apache Maven 2 i NetBeans IDE 6.0).

2 komentarze:

  1. A mi się nadal nie podobają te sztuczki z konwerterem i JPA. (konwerter brany z ziarna itd...)
    W tej chwili jest coś takiego. Wybieramy z bazy danych 5 pracowników, użytkownik wybiera z tych 5 jednego, i my go pobieramy na nowo z BD. Po co? Jak tego uniknąć?
    Widzę ty spory problem, przy kontrolowaniu wersji modyfikowanych obiektów. Użytkownik ma listę "Agatka" i "Jacek", wybiera Agatkę (wiadomo dlaczego), klika dalej...
    Drugi użytkownik zmienia nazwę tych osób na "Jaś" i "Małgosia".
    Pierwszy ma na następnej stronie "wybrałeś Jasia" ... ale o co chodzi, wybrałem Agatkę!

    OdpowiedzUsuń
  2. heh... wystarczy do konwertera jako parametr przekazać wskaźnik na odpowiednią kolekcję.

    OdpowiedzUsuń