Archiwum
JSF + Facelets tutorial – ciąg dalszy [część 2]
Miało być dwa wpisy odnośnie JSF i Facelets, ale założenia się zmieniły i będzie trzy 🙂
Dzisiaj w sumie nieco mniej o Facelets a trochę więcej o JSF + i18n (internationalization).
JSF ułatwia stworzenie wielojęzycznej aplikacji – wspomniałem o tym w poprzednim wpisie. Ale to za mało, przecież to użytkownik ma decydować o języku, a nie konfiguracja aplikacji. Dlatego trzeba dać mu wybór i zachować go na dłużej, np w sesji. Jednak sesja dość szybko wygasa i co wtedy? Użytkownik znowu wjedzie na stronę i musi ponownie wybrać język. Dobrym miejscem na przechowywanie tej informacji jest cookie.
Zaczynamy 🙂
Tworzymy kontroler który obsłuży nam wybór języka [czyli nowy JSF Managed Bean]
package eu.ryznar.controller; import java.util.Locale; import javax.faces.context.ExternalContext; import javax.faces.context.FacesContext; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; public class Language { public Language() { } public String changeLanguage() { FacesContext context = FacesContext.getCurrentInstance(); ExternalContext externalContext= context.getExternalContext(); Map requestParameterMap = (Map) context.getExternalContext().getRequestParameterMap(); String language = requestParameterMap.get("language").toString(); HttpServletResponse httpServletResponse = (HttpServletResponse) externalContext.getResponse(); String cookieName = externalContext.getInitParameter("i18n.languageCookieName"); Cookie cookie = new Cookie(cookieName, language); cookie.setMaxAge(365); httpServletResponse.addCookie(cookie); context.getViewRoot().setLocale(new Locale(language)); return null; } }
W kodzie jest użyty parametr i18n.languageCookieName definiujący nazwę ciastka w którym przechowamy wybór użytkownika
Parametr należy dodać w pliku web.xml
<context-param> <param-name>i18n.languageCookieName</param-name> <param-value>culture</param-value> </context-param>
Teraz w szablonie, np w template.xml w pasku bocznym można dodać 2 przyciski do zmiany języka
<h:commandLink action="#{language.changeLanguage}" value="#{msgs.langPL}"> <f:param name="language" value="pl" /> </h:commandLink> <h:commandLink action="#{language.changeLanguage}" value="#{msgs.langEN}"> <f:param name="language" value="en" /> </h:commandLink>
w pikach z tłumaczeniami trzeba nadać odpowiednie wartości dla langPL i langEN Jeśli to zrobimy można uruchomić aplikację, i wybrać język – w tym momencie powinno zostać utworzone ciasteczko.
Wybór języka już mamy, ale trzeba jeszcze zmienić język interfejsu. Ogólnie to miałem dylemat JSF + i18n – jak ugryźć? – gdzie umieścić kod odpowiedzialny za język interfejsu [Filter, View Handler czy Phase Listener]?
W PHP [w frameworku symfony] taki kod znajdował się w filtrze, tutaj w filtrze nie chciało działać 😛
Stanęło na Phase Listener – powiecie „Leo why?”. Żeby rozszerzając klasę FaceletViewHandler nie uzależniać się od Facelets.
Wyglądało by to tak:
import com.sun.facelets.FaceletViewHandler; import eu.ryznar.utils.Cookies; import eu.ryznar.utils.I18N; import java.util.Locale; import javax.faces.application.ViewHandler; import javax.faces.context.FacesContext; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; public class CustomViewHandler extends FaceletViewHandler { public CustomViewHandler(ViewHandler parent) { super(parent); } @Override public Locale calculateLocale(FacesContext context) { return I18N.getLocaleFromCookie(context); } }
i faces-config.xml:
<view-handler>eu.ryznar.view.CustomViewHandler</view-handler>
Natomiast aby rozwiązać to wykorzystując PhaseListener tworzymy package eu.ryznar.listener i klasę LocalePhaseListener implementującą interfejs PhaseListener
package eu.ryznar.listener; import eu.ryznar.utils.I18N; import java.util.Locale; import javax.faces.context.FacesContext; import javax.faces.event.PhaseEvent; import javax.faces.event.PhaseId; import javax.faces.event.PhaseListener; public class LocalePhaseListener implements PhaseListener { public void afterPhase(PhaseEvent event) { FacesContext context = event.getFacesContext(); Locale currentLocale = I18N.getLocaleFromCookie(context); context.getViewRoot().setLocale(currentLocale); return; } public void beforePhase(PhaseEvent event) { return; } public PhaseId getPhaseId() { return PhaseId.RENDER_RESPONSE; } }
a w pliku faces-config.xml
<lifecycle> <phase-listener>eu.ryznar.listener.LocalePhaseListener</phase-listener> </lifecycle>
Jeszcze tylko klasy do internacjonalizacji i obsługi cookie:
package eu.ryznar.utils; import java.text.MessageFormat; import java.util.Locale; import java.util.MissingResourceException; import java.util.ResourceBundle; import javax.faces.application.FacesMessage; import javax.faces.context.ExternalContext; import javax.faces.context.FacesContext; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; public class I18N { private static final String LANGUAGE_COOKIE = "i18n.languageCookieName"; public static Locale getLocale(String culture) { if (culture != null && culture.length() == 2) { return new Locale(culture); } return Locale.getDefault(); } public static Locale getLocaleFromCookie(FacesContext context) { ExternalContext externalContext = context.getExternalContext(); Cookie cookie[] = ((HttpServletRequest) externalContext.getRequest()).getCookies(); String cookieName = externalContext.getInitParameter(LANGUAGE_COOKIE); String cultureCookie = Cookies.getCookie(cookie, cookieName); return getLocale(cultureCookie); } }
eu.ryznar.utils Cookies.java package eu.ryznar.utils; import javax.servlet.http.Cookie; public class Cookies { public static String getCookie(Cookie[] cookies, String cookieName) { String value = null; if (cookies != null) { int cookieCounter = cookies.length; for (int i = 0; i < cookieCounter; i++) { if (cookies[i].getName().equalsIgnoreCase(cookieName)) { value = cookies[i].getValue(); } } } return value; } }
Po odświeżeniu strony [jeśli poprzednio wybraliśmy język angielski], widzimy ze cały interfejs jest w języku angielskim.
Ok, ale jeszcze coś nie tak. Jeśli wybierzemy inny język to nie ma zmiany, trzeba odświeżyć stronę. Rozwiązanie jest bardzo proste. W klasie Language, w funkcji setLanguageCookie(), przed return wpisujemy poniższą linijkę
context.getViewRoot().setLocale(new Locale(language));
Od teraz, każdy wybór języka skutkuje natychmiastową zmianą.
Czyli JSF i 18n mamy załatwione. Można oczywiście pomyśleć o tym żeby tłumaczenia przechowywać w bazie danych a nie w plikach, żeby bez potrzeby nie wykonywać akcji re-deploy, ale o tym może innym razem.
Kolejny krok w uzupełnianiu aplikacji: edycja i usuwanie
w kontrolerze UserController dodajemy kod
public String deleteUser() { String r = "user_deleted"; FacesContext context = FacesContext.getCurrentInstance(); Map requestParameterMap = (Map) context.getExternalContext().getRequestParameterMap(); try { Integer userId = Integer.parseInt(requestParameterMap.get("userId").toString()); User u = userDao.getUser(userId); String userName = u.getUsername(); userDao.deleteUser(u); addSuccessMessage("User " + userName + " successfully deleted."); } catch (NumberFormatException ne) { addErrorMessage(ne.getLocalizedMessage()); r = "failed"; } catch (Exception e) { addErrorMessage(e.getLocalizedMessage()); r = "failed"; } return r; } public String editUser() { FacesContext context = FacesContext.getCurrentInstance(); Map requestParameterMap = (Map) context.getExternalContext().getRequestParameterMap(); Integer userId = Integer.parseInt(requestParameterMap.get("userId").toString()); this.user = userDao.getUser(userId); return "edit_user"; } private void addErrorMessage(String msg) { FacesMessage facesMsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg); FacesContext fc = FacesContext.getCurrentInstance(); fc.addMessage(null, facesMsg); } private void addSuccessMessage(String msg) { FacesMessage facesMsg = new FacesMessage(FacesMessage.SEVERITY_INFO, msg, msg); FacesContext fc = FacesContext.getCurrentInstance(); fc.addMessage("successInfo", facesMsg); }
w templejtce list.xhtml dodajemy 2 kolumny
<h:column> <f:facet name="header"> <h:outputText value="#{msgs.actionEdit}"/> </f:facet> <h:commandLink action="#{user.editUser}"> <h:outputText value="#{msgs.actionEdit}"/> <f:param name="userId" value="#{dataTableItem.userId}" /> </h:commandLink> </h:column> <h:column> <f:facet name="header"> <h:outputText value="#{msgs.actionDelete}"/> </f:facet> <h:commandLink action="#{user.deleteUser}"> <h:outputText value="#{msgs.actionDelete}"/> <f:param name="userId" value="#{dataTableItem.userId}" /> </h:commandLink> </h:column>
tworzymy plik edituser.xhtml
<ui:define name="header"> #{msgs.userEdit} </ui:define> <ui:define name="content"> <h:form> <h:messages/> <custom:inputTextLabeled label="#{msgs.userName}" value="#{user.user.username}" required="true" labelStyle="addUserInput" /> <br /> <custom:inputTextLabeled label="#{msgs.userFirstName}" value="#{user.user.firstName}" labelStyle="addUserInput" /> <br /> <custom:inputTextLabeled label="#{msgs.userLastName}" value="#{user.user.lastName}" labelStyle="addUserInput" /> <br /> <custom:inputTextLabeled label="#{msgs.userEmail}" value="#{user.user.email}" required="true" labelStyle="addUserInput" /> <br /><br /> <h:commandButton action="#{user.saveUser}" value="#{msgs.btnUpdate}" styleClass="saveButton" /> </h:form> </ui:define> </ui:composition>
w faces-config.xml
<navigation-rule> <from-view-id>/list.xhtml</from-view-id> <navigation-case> <from-action>#{user.editUser}</from-action> <from-outcome>edit_user</from-outcome> <to-view-id>/edituser.xhtml</to-view-id> </navigation-case> </navigation-rule> <navigation-rule> <from-view-id>/edituser.xhtml</from-view-id> <navigation-case> <from-action>#{user.saveUser}</from-action> <from-outcome>success</from-outcome> <to-view-id>/list.xhtml</to-view-id> </navigation-case> <navigation-case> <from-action>#{user.saveUser}</from-action> <from-outcome>fail</from-outcome> <to-view-id>/list.xhtml</to-view-id> </navigation-case> </navigation-rule>
i do plików z tłumaczeniami dodajemy
actionEdit=Edit actionDelete=Delete userEdit=Edit user btnUpdate=Update
oraz
actionEdit=Edytuj actionDelete=Usuń userEdit=Edytuj użytkownika btnUpdate=Aktualizuj
w list.xhtml dodajemy linię
<h:messages errorClass="msgFail" infoClass="msgSuccess"></h:messages>
tu będą wyświetlane komunikaty zależnie od wykonanej akcji [żeby użytkownik wiedział co się stało].
msgFail i msgSuccess to nazwy klas z pliku CSS [plik dostępny w archiwum dołaczonym do wpisu]
Po usunięciu użytkownika powinniśmy zobaczyć coś takiego:
Jednak coś tu nie gra 😉 intefejs mamy polski, a komunikat jest w języku angielskim. Trzeba jeszcze dodać tłumaczenie treści pochodzących z kontrolera.
na początek prznieśmy metody addErrorMessage(String msg) i addSuccessMessage(String msg) z kontrolera do klasy I18N.
Metody są podobne więc można uprościć sprawę i zapisać je w takiej postaci
public static void addErrorMessage(String msg, String clientId) { addMessage(msg, FacesMessage.SEVERITY_ERROR, clientId); } public static void addSuccessMessage(String msg, String clientId) { addMessage(msg, FacesMessage.SEVERITY_INFO, clientId); } private static void addMessage(String messsage, FacesMessage.Severity messageType, String clientId) { FacesContext fc = FacesContext.getCurrentInstance(); FacesMessage facesMsg = new FacesMessage(messageType, message, message); fc.addMessage(clientId, facesMsg); }
a w klasie User kod
addSuccessMessage("User " + userName + " successfully deleted.");
zamieniamy na
I18N.addSuccessMessage("User " + userName + " successfully deleted.", null);
Dzięki temu mamy możliwość wielokrotnego używania komunikatów w innych kontrolerach. Możemy przetsesować aplikację. Jeśli działa przechodzimy dalej.
Ale chwila? Jak przetłumaczyć string którego fragment jest zmienny?
Bardzo prosto, w plikach z tłumaczeniamy dodajemy linie:
userDeleted=User {0} has been deleted userDeleted=Usunięto użytkownika {0}
W nawiasach klamrowych mamy parametry które podczas tłumaczenia zostaną zamienione na odpowiednie wartości.
Teraz trochę kodu – w klasie I18N dodajemy jeszcze trzy funkcje [pasowałoby jeszcze dopisać obsługę wyjątków].
private static String getMessageFromResourceBundle(String key, Locale locale, Object[] params) { ResourceBundle bundle = getBundle(locale); String message = ""; if (bundle == null) { return null; } try { message = formatMessage(bundle.getString(key), params, locale); } catch (Exception e) { } return message; } private static final String BUNDLE_NAME = "eu.ryznar.bundle.messages"; private static ResourceBundle getBundle(Locale locale) { ResourceBundle bundle = null; try { bundle = ResourceBundle.getBundle(BUNDLE_NAME, locale); } catch (MissingResourceException e) { // plik nie odnaleziony } return bundle; } private static String formatMessage(String message, Object[] params, Locale locale) { if (params != null) { MessageFormat mf = new MessageFormat(message, locale); return mf.format(params, new StringBuffer(), null).toString(); } return message; }
i w tej samej klasie modyfikujemy wcześniej dodane funkcje [rozsdzerzamy funkcje o możliwość przetworzenia stringów ze parametrami]:
public static void addErrorMessage(String msg, String clientId, Object[] params) { addMessage(msg, FacesMessage.SEVERITY_ERROR, clientId, params); } public static void addSuccessMessage(String msg, String clientId, Object[] params) { addMessage(msg, FacesMessage.SEVERITY_INFO, clientId, params); } private static void addMessage(String msg, FacesMessage.Severity messageType, String clientId, Object[] params) { FacesContext fc = FacesContext.getCurrentInstance(); String message = I18N.getMessageFromResourceBundle(msg, fc.getViewRoot().getLocale(), params); FacesMessage facesMsg = new FacesMessage(messageType, message, message); fc.addMessage(clientId, facesMsg); }
To wszystko. Archiwum z projektem jest dostępne tutaj.
Do przedstawionego kodu mam jeszcze jedną uwagę. Nie podoba mi się. Czy nie można jakoś norlanie pobrać wartości ciasteczka? Albo funkcja haszująca hasło [w poprzednim artykule]. Czy tego nie da się załątwić jedną lub dwoma linijkami kodu?
I jak zwykle będę wdzięczny za wskazanie wszelkich uchybień jakich dopuściłem się w kodzie bądź w treści.
Facelets tutorial – wprowadzenie – część 1
Tym razem wpis nie będzie o problemach ale o czymś działającym 😉 Poprzednio odniosłem się do przykładowej aplikacji wykorzystującej JSF i JPA, teraz pokażę na przykładzie podobnej aplikacji, jak wykorzystać silnik szablonów dla JSF – Facelets. Tutorial będzie się składał z dwóch części, żeby nie było zbyt długo 😉 w pierwszej wprowadzenie a w drugiej dokończenie aplikacji która nic konkretnego nie robi 😉
Aby w miarę łagodnie zapoznać się z tym co oferują Facelets polecam pozycję wydawnictwa Apress: Facelets Essentials: Guide to JavaServer Faces View Definition Framework – nie ma tego dużo, około 90 stron.
Do wykonania przykładowej aplikacji potrzebujemy
– IDE – ja używam NetBeans 6.7
– baza danych – może być MySQL
– plugin dla Netbeans nbfaceletssupport
Jeśli mamy zainstalowaną bazę to tworzymy potrzebną tabelę
CREATE TABLE IF NOT EXISTS `users` ( `user_id` int(11) NOT NULL auto_increment, `username` varchar(255) NOT NULL, `first_name` varchar(255) NOT NULL, `last_name` varchar(255) NOT NULL, `password` char(64) NOT NULL, `email` varchar(255) default NULL, `active` tinyint(1) NOT NULL default '1', `date_created` timestamp NOT NULL default CURRENT_TIMESTAMP, PRIMARY KEY (`user_id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=3; INSERT INTO `users` (`user_id`, `username`, `first_name`, `last_name`, `password`, `email`, `active`, `date_created`) VALUES (1, 'pablo', 'pawel', 'ryznar', 'a7e8363bba55ff817479fe761befa5169e9bec4b', NULL, 1, '2009-10-29 00:00:00'), (2, 'adamo', 'adam', 'nowak', 'c0ce63151c40b9ed8d93b8a4029633dc07211a84', NULL, 1, '2009-10-29 00:00:00');
Jeśli mamy wszystko to zaczynamy 🙂
Tworzymy aplikację JavaEE -> Enterprise Application
otrzymujemy taką strukturę [trochę bałagan mam na liście 😉 aplikacja którą teraz robię to UserStore]
Kolejny krok to podpięcie frameworków JSF i Facelets [w menu kontekstowym części webowej wybieramy Properties i zakładkę Frameworks]:
konfigurujemy je tak jak na screenach
W części enterpise dodajemy packages: eu.ryznar.facade i eu.ryznar.domain a w części webowej eu.ryznar.controller
Teraz z pomocą Netbeans’a wygenerujemy klasy encji
Stworzymy jeszcze ziarna do operowania na wygenerowanej klasie encji
Zmieniamy nazwę interfejsu na UserDao
I mniej więcej mamy gotowy szkielet 🙂
Teraz czas na co nieco kodu 😛
w klasie User [eu.ryzna.domain] zmieniamy ciało metody toString():
public String toString() { return this.firstName + " " + this.lastName; }
w pliku UserDaoBean.java wpisujemy poniższy kod
@Stateless public class UserDaoBean implements UserDao { @PersistenceContext private EntityManager em; public User getUser(int UserId) { User u = new User(); u = em.find(User.class, UserId); return u; } public List<User> getAllUsers() { Query q = em.createQuery("SELECT u FROM User u"); List<User> users = q.getResultList(); return users; } public void createUser(User u) { String hashedPw = hashPassword(u.getPassword()); u.setPassword(hashedPw); em.persist(u); } public void updateUser(User u) { String hashedPw = hashPassword(u.getPassword()); u.setPassword(hashedPw); em.merge(u); } public void deleteUser(User u) { User mgdUser = em.merge(u); em.remove(mgdUser); } private String hashPassword(String password) { StringBuilder sb = new StringBuilder(); try { MessageDigest messageDigest = MessageDigest.getInstance("SHA"); byte[] bs; bs = messageDigest.digest(password.getBytes()); for (int i = 0; i < bs.length; i++) { String hexVal = Integer.toHexString(0xFF & bs[i]); if (hexVal.length() == 1) { sb.append("0"); } sb.append(hexVal); } } catch (NoSuchAlgorithmException ex) { Logger.getLogger(UserDaoBean.class.getName()).log(Level.SEVERE, null, ex); } return sb.toString(); } }
oczywiście mamy masę błędów, ponieważ brakuje bibliotek. Możemy je szybko zaimportować wciskając klawisze Ctrl + Shift + I
brakuje nam jeszcze deklaracji metod w interfejsie NetBeans sugeruje nam to żółtymi lampkami
wybieramy „Expose method in local business interface …” i odpowiedni kod pojawia się w pliku UserDao.java
z katalogu WebPages usuwamy pliki z rozszerzeniem jsp, nie będzie nam potrzebny.
W części webowej potrzebujemy plik kontrolera
i kod dla kontrolera
package eu.ryznar.controller; import eu.ryznar.domain.User; import eu.ryznar.facade.UserDao; import javax.ejb.EJB; import javax.faces.model.DataModel; import javax.faces.model.ListDataModel; public class UserController { @EJB UserDao userDao; private User user; private DataModel model; public String createUser() { this.user = new User(); return "create_new_user"; } public String saveUser() { String r = "success"; try { userDao.createUser(user); } catch (Exception e) { e.printStackTrace(); r = "failed"; } return r; } public DataModel getUsers() { model = new ListDataModel(userDao.getAllUsers()); return model; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } }
Netbeans automatycznie dodaje odpowiedni wpis o ziarnie zarządzanym do pliku faces-config.xml
Zanim przystąpimy do tworzenia widoku warto pomyśleć o internacjonalizacji naszej aplikacji. Dlatego w częsci webowej tworzymy nowy package o nazwie eu.ryznar.bundle i w nim dwa pliku messages_pl.properties i messages_en.properties
takie pliki można w bardzo prosty sposób edytować, klikamy na jednym z nich prawym przyciskiem i wybieramy „Otwórz” – dostajemy fajną tabelę w której szybko dodamy bądź edyujemy nowe wartośći
potrzebne tłumaczenia są w archiwum do którego link jest na końcu wpisu
Trzeba jeszcze dodać odpowiednią konfigurację, aby aplikacja automatycznie wczytywałą tłumaczenia [można to robić ręcznie w szablonach używając f:loadBundle]
<application> <locale-config> <default-locale>pl</default-locale> <supported-locale>en</supported-locale> </locale-config> <resource-bundle> <base-name>eu.ryznar.bundle.messages</base-name> <var>msgs</var> </resource-bundle> </application>
Czyli mamy już obsługę danych, mamy kontroler czas na widok. Na początku stworzymy ogólny szablon z którego będą korzystały wszystkie podstrony.
Nie będę tu wgłębiał się w tłumaczenie elementów Facelets [odsyłam do wskazanej na początku pozycji bądź innych tutoriali, kilka można znaleźć w moich „delicjach”]
Tworzymy ogólny szablon –
<?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:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <link href="./css/default.css" rel="stylesheet" type="text/css" /> <title>Facelets -Szablon</title> </head> <body> <div id="outerWrapper"> <div id="header"> <ui:insert name="header"/> </div> <div id="contentWrapper"> <div id="leftColumn"> <ui:insert name="leftColumn"> <h:form> <p><h:commandLink action="#{user.createUser}" value="#{msgs.userCreate}"/></p> </h:form> </ui:insert> </div> <div id="content"> <ui:insert name="content"/> </div> <br /> </div> <br class="clear" /> <div id="footer"> <ui:insert name="footer"> #{msgs.pageCopyright} </ui:insert> </div> </div> </body> </html>
Kolejny krok to rzeczywisty szablon – plik list.xhtml i wpisujemy kod
<?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:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <link href="./css/default.css" rel="stylesheet" type="text/css" /> <title>Facelets - Szablon</title> </head> <body> <ui:composition template="/template.xhtml"> <ui:define name="header"> #{msgs.userList} </ui:define> <ui:define name="content"> <h:form> <h:dataTable value="#{user.users}" var="dataTableItem" border="0" cellpadding="0" cellspacing="0" styleClass="tableList"> <h:column> <f:facet name="header"> <h:outputText value="#{msgs.userName}"/> </f:facet> <h:outputText value="#{dataTableItem.username}" /> </h:column> <h:column> <f:facet name="header"> <h:outputText value="#{msgs.userFirstName} i #{msgs.userLastName}"/> </f:facet> <h:outputText value="#{dataTableItem}" /> </h:column> <h:column> <f:facet name="header"> <h:outputText value="#{msgs.userEmail}"/> </f:facet> <h:outputText value="#{dataTableItem.email}" /> </h:column> </h:dataTable> </h:form> </ui:define> </ui:composition> </body> </html>
kod CSS również zamieściłem w dołaczonym archiwum. Mając pierwszy szablon możemy testowo uruchomić aplikację.
Pod adresem http://localhost:8080/UserStore-war/list.jsf powinniśmy zobaczyć coś takiego:
Teraz tworzymy kolejne szablony adduser.xhtml i failed.xhtml
Netbeans może nam pomóc [wygeneruje podstawową strukturę dokumentu na podstawie pliku bazowego template.xhtml]
W pliku failed.xhtml wpisujemy kod
<?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:ui="http://java.sun.com/jsf/facelets"> <body> <ui:composition template="./template.xhtml"> <ui:define name="header"> #{msgs.error} </ui:define> <ui:define name="content"> #{msgs.userAddError} </ui:define> </ui:composition> </body> </html>
Natomiast z adduser.xhtml jeszcze poczekamy 😉 żeby wykorzystać jeszcze jeden element silnika Facelets, mianowicie komponenty, które możemy wielokrotnie wykorzystywać. Dlaczego akurat w adduser.xhtml? bo często pola na dane są poprzedzone etykietą, więcj dlaczego nie uprościć sprawy i zamiast dwóch tagów [h:outputLabel i h:inputText] nie użyć jednego? Jest pomysł więc do dzieła 😉
W katalogu WEB-INF tworzymy katalog facelets a w nim plik mycustom.taglib.xml i katalog components. W katalogu components tworzymy 2 pliki InputTextLabeled.xhtml i SecretTextLabeled.xhtml
W utworzynych plikach komponentów odpowiednio wpisujemy kod
<!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:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html"> <ui:component> <h:outputLabel value="#{label}: " styleClass="#{labelStyle}"> <h:inputText value="#{value}" required="#{required}" label="#{label}"/> </h:outputLabel> </ui:component> </html>
i
<!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:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html"> <ui:component> <h:outputLabel value="#{label}: " styleClass="#{labelStyle}"> <h:inputSecret value="#{value}" required="#{required}" label="#{label}"/> </h:outputLabel> </ui:component> </html>
Teraz konfiguracja komponentów:
w pliku mycustom.taglib.xml
<!DOCTYPE facelet-taglib PUBLIC "-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN" "http://java.sun.com/dtd/ facelet-taglib_1_0.dtd"> <facelet-taglib> <namespace>http://ryznar.eu/facelets</namespace> <tag> <tag-name>inputTextLabeled</tag-name> <source>components/InputTextLabeled.xhtml</source> </tag> <tag> <tag-name>secretTextLabeled</tag-name> <source>components/SecretTextLabeled.xhtml</source> </tag> </facelet-taglib>
w web.xml wskazainie na powyższy plik
<context-param> <param-name>facelets.LIBRARIES</param-name> <param-value>/WEB-INF/facelets/mycustom.taglib.xml</param-value> </context-param>
Teraz czas na szablon adduser.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:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:custom="http://ryznar.eu/facelets"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <link href="./css/default.css" rel="stylesheet" type="text/css" /> <title>Facelets - Szablon</title> </head> <body> <ui:composition template="./template.xhtml"> <ui:define name="header"> #{msgs.userCreate} </ui:define> <ui:define name="content"> <h:form> <h:messages/> <custom:inputTextLabeled label="#{msgs.userName}" value="#{user.user.username}" required="true" labelStyle="addUserInput" /> <br /> <custom:inputTextLabeled label="#{msgs.userFirstName}" value="#{user.user.firstName}" labelStyle="addUserInput" /> <br /> <custom:inputTextLabeled label="#{msgs.userLastName}" value="#{user.user.lastName}" labelStyle="addUserInput" /> <br /> <custom:inputTextLabeled label="#{msgs.userEmail}" value="#{user.user.email}" required="true" labelStyle="addUserInput" /> <br /> <custom:secretTextLabeled label="#{msgs.userPwd}" value="#{user.user.password}" required="true" labelStyle="addUserInput" /> <br /><br /> <h:commandButton action="#{user.saveUser}" value="#{msgs.btnSave}" styleClass="saveButton" /> </h:form> </ui:define> </ui:composition> </body> </html>
import własnych komponentów wykonujemy poprzez
linię xmlns:custom="http://ryznar.eu/facelets"
i ostatnia rzecz, nawigacja między stronami, w pliku faces-config.xml dodajemy reguły:
<navigation-rule> <from-view-id>/list.xhtml</from-view-id> <navigation-case> <from-outcome>create_new_user</from-outcome> <to-view-id>/adduser.xhtml</to-view-id> </navigation-case> </navigation-rule> <navigation-rule> <from-view-id>/adduser.xhtml</from-view-id> <navigation-case> <from-outcome>failed</from-outcome> <to-view-id>/failed.xhtml</to-view-id> </navigation-case> <navigation-case> <from-outcome>success</from-outcome> <to-view-id>/list.xhtml</to-view-id> </navigation-case> </navigation-rule>
Uruchamiamy aplikację ponownie, lub wywołujemy deploy. Lista użytkowników nie uległa zmianie, więc czas zobaczyć jak działa dodawanie.
Klikamy „Dodaj użytkownika”, powinniśmy zobaczyć taki formularz
Wypełniamy pola, klikamy zapisz i dostajemy taki błąd:
javax.el.PropertyNotFoundException: /WEB-INF/facelets/components/InputTextLabeled.xhtml @7,84 value="#{value}": /adduser.xhtml @25,149 value="#{user.user.username}": Target Unreachable, 'user' returned null
Skąd się to wzięło? Ano zagapiłem się przy tworzeniu ziarna encji zakres ustawiłem na request zamiast session. Czyli przy wysłaniu formularza nie ma żadnego ziarna i stąd błąd. Zmieniamy więc zakres w pliku faces-config.xml i teraz działa, po zapisaniu przechodzimy automatycznie na listę użytkowników.
W tej części to wszystko. Archiwum jest dostępne tutaj. Jeśli ktoś przebrnął przez ten wpis 😉 i dopatrzył się jakiegoś błędu to proszę o informację 🙂 W następnej części dodam jeszcze edycję i walidację.
JSF i EJB 3 – prosta aplikacja CRUD
Tutorial do którego dzisiaj się odniosę to prosta aplikacja webowa wykonana w środowisku Netbeans z wykorzystaniem JSF, EJB 3 oraz bazy danych (autor wykorzystuje PostgreSQL, ja miałem pod ręką MySQL – ale w tym przypadku baza nie robi różnicy).
Tutorial składa się z trzech części: pierwsza druga i trzecia
U mnie problemy zaczęły się w trzeciej części, w drugim kroku „Add fields to the User Entity Bean”
Po dodaniu nowych pól do bazy i klasy aplikacja przestała działać – pojawił sie wyjątek, już nie pamiętam dokładnie co tam pisało 😛 ale jakiś problem z PU i odnalezieniem klasy UserDAOBean.
Jak sobie z tym poradziłem? W części webowej aplikacji, z gałęzi „Libraries” usunąłem JumpStartEjbJsf-ejb.jar i dodałem go jeszcze raz. Czy można inaczej, nie wiem. Jeśli ktoś ma pomysł proszę o podpowiedź.
Ale to nie koniec. Kolejny problem to kroki 3-7. Po dodaniu do gałęzi „Libraries” podanych bibliotek Apache Commons Lang, Apache Commons Validator oraz Jakarta Oro, i próbie dodania użytkownika z błędnym mejlem dostałem taki wyjątek
codjava.lang.NoClassDefFoundError: org/apache/commons/validator/EmailValidator
to samo z lang/StringUtils. Oczywiście wujek Google prawie zawsze pomaga, no i tutaj podpowiada żeby te pliki *jar wrzucić do katalogu /WEB-INF/lib – pomogło.
Jednak walidacja mejla nadal sypie wyjątkami, tym razem
javax.servlet.ServletException: Uncompilable source code.
Problem leży w klasie EmailValidator, nie ma takiej metody jak
htmlInputText.getLabel()
Oczywiście znowu google, przeglądanie API i taki przebłysk, skąd w gałęzi „Libraries” mam MyFaces? Okazuje się że podczas dodawania JSF do projektu z menu „Properties”, w zakładce „Libraries” opcja „Registered Libraries” domyślnie wybrane jest „Facelets 1.1.14 MyFaces”. W związku z tym usunąłem te pliki jar, i z menu kontekstowego „Add Library…” wybrałem JSF 1.2.
Co się okazuje, tu metoda getLabel() jest, i poprzedni problem również znika, tzn można usunąć katalog /lib z WEB-INF i wszystko ładnie działa.
Źródeł nie zamieszczam, w przytoczonym tutorialu źródła są pełne, więc przerabiajac go nic nam nie umknie 🙂
Mam nadzieję że nie ma jakiś strasznych herezji w tym artykule 😉 jeśli są proszę o zwrócenie mi uwagi.
Pozdrawiam
Najnowsze komentarze