Archive

Archive for the ‘Facelets’ Category

JSF + Facelets tutorial – ciąg dalszy [część 2]

9 listopada 2009 183 komentarze

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

7 listopada 2009 125 komentarzy

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





Zmieniamy nazwę klasy na User


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ę.

Kategorie:Facelets, JavaEE 5, JSF Tagi: , ,