Archiwum

Posts Tagged ‘FaceletViewHandler’

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

9 listopada 2009 186 Komentarzy

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.

Reklamy