Rejestrowanie użytkowników w ProgramBash – część 2

Wracając do poprzedniego wpisu – rejestracja to rzecz dość istotna. Dla przypomnienia – na razie zasadniczo poza wygenerowaniem formularza i napisaniem od cholery kodu , który nie za wiele jest w stanie zrobić, przyszedł czas na coś bardziej spektakularnego.

Zaczniemy od walidacji. Zawsze uważałem, że bawienie się w bajery w stylu walidacji po stronie klienta (JavaScript) to tak naprawdę strata czasu – i tak ostatecznie walidacja po stronie serwera będzie miała miejsce, zaś ilość potencjalnych danych, które można źle wpisać nie jest aż tak wielka, aby uznać ewentualny request do serwera za jakoś specjalnie drogi. Dlatego też skupię się tylko na mechanizmach walidacyjnych wbudowanych w sam mechanizm JSF. Poki co mamy do czynienia z takim oto formularzem:

formularz

Co powinno być walidowane? Ano z całą pewnością nick i adres e-mail. Załóżmy, że to e-mail jest identyfikującą składową dla naszych użytkowników i przez to musi być unikalny. Do tego adres e-mail to adres e-mail. Musi spełniać wymogi poprawnego formatu (opisanego w którymś z RFC) – tutaj bez wyrażeń regularnych raczej się nie obejdzie. Długość nicka oraz haseł jest zależna od nas samych – dodamy jednakże ograniczenie minimum jak i maksimum dla tych wartości.

Jednakże zanim to nastąpi należy zastanowić się nad formą wyświetlania komunikatów walidacyjnych. W praktyce stosuje się dwa podejścia – albo listuje się stosowne komunikaty na górze strony, albo też wyświetla się wszystkie po kolei obok walidowanego pola/wartości. Myślę, że to drugie podejście jest o tyle bardziej odpowiednie, iż od razu wiadomo gdzie coś poszło nie tak. Przeglądanie rozbudowanej listy błędów walidacji nie jest zbyt dobrym podejściem, zwłaszcza kiedy nagle komunikaty zajmują połowę ekranu. To jedna sprawa. Druga zaś dotyczy samej treści komunikatów. Nie podchodzę nawet do zagadnienia internacjonalizacji aplikacji, gdyż byłyby to perły rzucane przed wieprze i nawet edukacyjny charakter projektu nie usprawiedliwiałby takich ozdobników. Jednakże posiadanie absolutnie unikalnych i zgromadzonych w jednym miejscu komunikatów jest w każdej aplikacji dość istotne. Dlatego też napiszę kilka słów o pakietach komunikatów.

Podobną rzecz opisywałem już wcześniej przy okazji projektu HowToJava. Jednakże w przypadku Grails istnieją już tam gotowe do wypełnienia pliki komunikatów – wystarczy pobawić się w copywritera i gotowe. W przypadku JSF wymaga to troszeczkę więcej pracy. Podstawą jest rzecz jasna stworzenie pliku komunikatów. W przypadku mojej aplikacyjki dorzuciłem takowy plik (msg.properties) do pakietu z klasami narzędziowymi. Proste. Oczywiście nasz pakiet musimy dopisać do konfiguracji w pliku faces-config.xml. Robimy to w ten oto sposób:

<application>
     <message-bundle>
        com.wordpress.chlebik.util.msg
    </message-bundle>

    <resource-bundle>
        <base-name>com.wordpress.chlebik.util.msg</base-name>
        <var>msgs</var>
    </resource-bundle>
  </application>

Dlaczego wpisujemy dwa razy ten sam plik? Albowiem przy pominięciu pierwszej definicji ( tej z message-bundle ) nie będą działały nadpisania dla standardowych komunikatów! Warto o tym wiedzieć, mi zajęło to parę minut rzucania mięchem 🙂 Samo posiadanie pliku nie jest rzecz jasna do niczego przydatne. Należy jeszcze zaimplementować klasę, która będzie potrafiła coś z naszego pliku wyciągnąć. I tutaj zaczynają się schody. Wyciągnięcie bowiem czegokolwiek z takiego pliku wymaga bowiem trochę zabawy – mianowicie należy uzyskąc referencję do bieżących ustawień regionalnych, potem mechanizm ładowania klas (by zlokalizować nasz plik z komunikatami), zaś na końcu otrzymać dostęp do właściwego pakietu dla wybranych ustawień regionalnych. Wyciągnięcie w tym momencie konkretnego komunikatu to już fraszka. Ufff. Trochę tego za dużo. Dlatego też zgodnie z zasadą DRY zamierzam użyć kodu, który autorzy mojej książki o JSF łaskawie udostępnili dla szerokiej rzeszy programistów. Oto on:

package com.wordpress.chlebik.util;

import java.text.MessageFormat;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import javax.faces.application.Application;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;

public class Messages {
   public static FacesMessage getMessage(String bundleName, String resourceId,
      Object[] params) {
      FacesContext context = FacesContext.getCurrentInstance();
      Application app = context.getApplication();
      String appBundle = app.getMessageBundle();
      Locale locale = getLocale(context);
      ClassLoader loader = getClassLoader();
      String summary = getString(appBundle, bundleName, resourceId,
         locale, loader, params);
      if (summary == null) summary = "???" + resourceId + "???";
      String detail = getString(appBundle, bundleName, resourceId + "_detail",
         locale, loader, params);
      return new FacesMessage(summary, detail);
   }

   public static String getString(String bundle, String resourceId,
         Object[] params) {
      FacesContext context = FacesContext.getCurrentInstance();
      Application app = context.getApplication();
      String appBundle = app.getMessageBundle();
      Locale locale = getLocale(context);
      ClassLoader loader = getClassLoader();
      return getString(appBundle, bundle, resourceId, locale, loader, params);
   }

   public static String getString(String bundle1, String bundle2,
         String resourceId, Locale locale, ClassLoader loader,
         Object[] params) {
      String resource = null;
      ResourceBundle bundle;

      if (bundle1 != null) {
         bundle = ResourceBundle.getBundle(bundle1, locale, loader);
         if (bundle != null)
            try {
               resource = bundle.getString(resourceId);
            } catch (MissingResourceException ex) {
            }
      }

      if (resource == null) {
         bundle = ResourceBundle.getBundle(bundle2, locale, loader);
         if (bundle != null)
            try {
               resource = bundle.getString(resourceId);
            } catch (MissingResourceException ex) {
            }
      }

      if (resource == null) return null; // brak dopasowania
      if (params == null) return resource;

      MessageFormat formatter = new MessageFormat(resource, locale);
      return formatter.format(params);
   }

   public static Locale getLocale(FacesContext context) {
      Locale locale = null;
      UIViewRoot viewRoot = context.getViewRoot();
      if (viewRoot != null) locale = viewRoot.getLocale();
      if (locale == null) locale = Locale.getDefault();
      return locale;
   }

   public static ClassLoader getClassLoader() {
      ClassLoader loader = Thread.currentThread().getContextClassLoader();
      if (loader == null) loader = ClassLoader.getSystemClassLoader();
      return loader;
   }
}

Listing nie jest specjalnie skomplikowany – to po prostu żonglerka dużymi partiami jakże często powtarzającego się kodu. Dla programisty istotne jest to, iż by wyciągnąć jakikolwiek komunikat wystarczy użyć statycznej metody getMessage przekazując jej odpowiednie parametry, aby otrzymać stosowny komunikat. Mogłoby to wyglądać na przykład tak:

FacesMessage msg = com.wordpress.chlebik.util.Messages.getMessage ( "com.wordpress.chlebik.msg", "niepoprawnyFormatAdresuEmail", null );

Podanie wartości NULL jako trzeciego parametru dla metody wskazuje na fakt, iż w komunikacie nie będą występowały symbole zastępcze (o nich później). Zaś gdybyśmy jednak chcieli by wystąpiły, wówczas należy przekazać do metody tablicę Object[]. Jak widać tę klasę również trzeba było wyszczególnić w pliku konfiguracyjnym!

Teraz zajmiemy się pisaniem własnego walidatora. Standardowe walidatory JSF nadają się do prozaicznych rzeczy – sprawdzania czy dana wartość została wpisana, walidacji długości łancucha i tym podobnych rzeczy. Same w sobie są to rzeczy całkiem potrzebne i używane zasadniczo bardzo często. Jednakże nie w tym rzecz by zadowalać się prostymi rozwiązaniami. Wszak na walidację czeka adres e-mail, chyba jeden z najbardziej “przerobionych” walidatorów na świecie. Samo wyrażenie regularne, które będzie walidowało wpisaną wartość ściągniemy od innych, natomiast ja podam przepis jak opakować porównanie w naszą klasę walidującą.

Wszystkie niestandardowe walidatory muszą implementować interfejs javax.faces.validator.Validator, a także zostać dodane do pliku faces-config.xml. Implementacja interfejsu polega na nadpisaniu tylko jednej metody – validate(), która w razie poprawnej walidacji nie zwraca nic, natomiast w przeciwnym wypadku zgłasza wyjątek ValidatorException z komunikatem błędu wyciągniętym z naszego pakietu komunikatów. Oto klasa walidująca:

public class EmailValidator implements Validator {

    /**
     * Metoda walidujaca
     *
     */
    public void validate(FacesContext context, UIComponent component, Object value) {

        if( value == null ) return;
        if( !(value instanceof String) ) return;

        if( !checkEmail( (String) value) )  {
            FacesMessage msg = com.wordpress.chlebik.util.Messages.getMessage( "com.wordpress.chlebik.util.msg" , "emailNotValid", null);
            msg.setSeverity( FacesMessage.SEVERITY_ERROR );
            throw new ValidatorException( msg );
        }
    }

    /**
     * Prywatna metodka gdzie mielimy temat
     *
     * @param  String   email
     * @return boolean
     */
    private boolean checkEmail( String email ) {

      Pattern p = Pattern.compile(".+@.+\\.[a-z]+");
      Matcher m = p.matcher(email);

      boolean result = m.matches();

      if ( result )
        return true;
      else
        return false;
    }

}

By zaś nie przedłużać – oto kod formularza rejestracyjnego z dodanymi wbudowanymi walidatorami JSF.

<fieldset>
		<legend>Podaj swoje dane</legend>
			<div>
                            <label for="nick">Nick</label>
                            <h:inputText value="#{user.nick}" id="nick" required="true" requiredMessage="#{msgs.nickRequired}">
                                <f:validateLength maximum="60" minimum="3" />
                            </h:inputText>
                            <h:message for="nick" errorClass="error" />
                        </div>

			<div>
                            <label for="email">E-mail</label>
                            <h:inputText value="#{user.email}" id="email" required="true"  requiredMessage="#{msgs.emailRequired}">
                                <f:validateLength maximum="256" />
                                <f:validator validatorId="com.wordpress.chlebik.Email" />
                            </h:inputText>
                            <h:message for="email" errorClass="error"/>
                        </div>

                        <div>
                            <label for="pass">Hasło</label>
                            <h:inputSecret value="#{user.pass}" id="pass"  required="true"  requiredMessage="#{msgs.passRequired}"/>
                            <h:message for="pass" errorClass="error"/>
                        </div>

                        <div>
                            <label for="passconfirmation">Powtórz hasło</label>
                            <h:inputSecret value="#{user.passconfirmation}" id="passconfirmation"  required="true"  requiredMessage="#{msgs.passconfRequired}"/>
                            <h:message for="passconfirmation" errorClass="error"/>
                        </div>

                        <div>
                            <label for="sex">Płeć</label>
                            <h:selectOneMenu value="#{user.sex}" id="sex" >
                                <f:selectItems value="#{sexBean.sexes}" />
                            </h:selectOneMenu>
                        </div>

	</fieldset>

Wysłanie źle wypełnionego formularza powoduje ponowne wyświetlenie strony wraz z komunikatami o błędach. Komunikaty te są składowane w pliku msg.properties, który wygląda w ten sposób (podano komunikaty walidacji, a także nadpisane standardowe powiadomienia).

javax.faces.component.UIInput.REQUIRED=Wypełnienie tego pola jest obowiązkowe!
javax.faces.validator.LengthValidator.MAXIMUM=Podana wartość jest dłuższa niż {0} znaków
javax.faces.validator.LengthValidator.MINIMUM=Podana wartość jest krótsza niż {0} znaki
nickRequired=Podanie nicka jest obowiązkowe!
emailRequired=Podanie adresu e-mail jest obowiązkowe!
passRequired=Podanie hasła jest obowiązkowe!
passconfRequired=Potwierdzenie hasła jest obowiązkowe!
emailNotValid=Podany adres e-mail jest niepoprawny!

I tak mniej więcej może wyglądać nasz formularz po wypełnieniu go złymi danymi:

walidacja

Jednakże kiedy nasz formularz przejdzie już walidację – dotrze on ostatecznie do klasy UserController. Ona z kolei bieże na siebie kwestię jeszcze jednej walidacji (czy oba hasła wpisane w formularzu są zgodne), po czym w zależności od wyniku zapisuje nowy rekord w bazie danych, albo też ponownie wyświetla formularz z błędem walidacji. Oto kod:

public class UserController {

    private User user;

   /**
    * Akcja obslugujaca rejestracje w systemie
    *
    * @return User
    * @author Michał Piotrowski
    */
    public String register() {

        FacesContext context = FacesContext.getCurrentInstance();
        Map params = context.getExternalContext().getRequestParameterMap();

        String pass       =    (String) params.get("pass");
        String passconf   =    (String) params.get("passconfirmation");
        String nick       =    (String) params.get("nick");
        String email      =    (String) params.get("email");
        Date addDate = new Date();

        if( pass.equals( passconf ) )  {

            try {

                pass = MD5.MD5(pass);
                User newUser = new User( null, nick, email, pass, "/bedzie/path/do/pliku", Math.round(addDate.getTime()/1000), 0, SexEnum.valueOf( (String) params.get("sex") ) );

                SessionFactory sessionFactory = com.wordpress.chlebik.util.ProgramBashUtil.getSessionFactory();
                Session session = sessionFactory.openSession();
                session.save( newUser );

                context.addMessage( null, new FacesMessage( FacesMessage.SEVERITY_INFO, "Rejestracja zakończyła się sukcesem!", "") );
             }
             catch( Exception e ) {
                 context.addMessage( null, new FacesMessage( FacesMessage.SEVERITY_ERROR, "Rejestracja nie powiodła się!", "") );
                 return "register-failure";
             }

             return "registered";
        }
        else {
             FacesMessage msg = com.wordpress.chlebik.util.Messages.getMessage( "com.wordpress.chlebik.util.msg" , "passconfFailed", null);
             msg.setSeverity( FacesMessage.SEVERITY_ERROR );
             context.addMessage("passconfirmation", msg );
             return "register-failure";
        }

    }
}

Kod jest dość oczywisty. Wpierw odczytujemy z mapy parametrów przekazanych w tabeli POST potrzebne nam wartości. Porównujemy hasła, a na końcu tworzymy obiekt i zapisujemy go w bazie. Dorzucamy także stosowny komunikat w zależności od tego, czy rejestracja zakończyła się sukcesem czy nie. W pliku layoutu dorzuciłem taki oto mały HTML:

 <div id="messagesWrapper">
            <h:messages globalOnly="true" infoClass="flashMessageInfo" errorClass="flashMessageError" />
 </div>

Parę zmian w CSSie i pięknie wszystko śmiga. Ah byłbym zapomniał. W regule nawigacji w pliku faces-config w przypadku powodzenia operacji by wyświetlił się nam komunikat, należy wyrzucić znacznik <redirect />. Jeśli tego nie zrobimy nasz komunikat nie zostanie wyświetlony, gdyż po fazie wizualizacji odpowiedzi zostanie wysłany kolejny request. Rozwiązanie to nastręcza jednakże pewien problem, a mianowicie ponownego wysłania tych samych danych. Tym jednakże zajmiemy się następnym razem. Póki co cieszę się z istniejącego (w bardzo skromnym wymiarze) rejestrowania użytkowników.

 

walidacjaSukces

Advertisements

4 thoughts on “Rejestrowanie użytkowników w ProgramBash – część 2

  1. Darek Ludera

    Hej!

    Bardzo rzeczowy artykuł. Dla pełni obrazu, dopisałbym przykład użycia validatorów z poziomu kontrolerów (managed beanów).

    Czyli np w h:input deklarujemy validator, zdefiniowany w managed beanie:

    <h:inputText … validator="#{user.validateLogin}

    i definiujemy w managerze odpowiednią metodę:

    public void validateLogin(FacesContext context, UIComponent toValidate, Object value) {
    String login = (String) value;

    Pattern p = Pattern.compile("[^A-Za-z0-9_]");
    Matcher m = p.matcher(login);

    if (m.find()) {
    ((UIInput)toValidate).setValid(false);

    FacesMessage message = new FacesMessage("*niepoprawny login");
    context.addMessage(toValidate.getClientId(context), message);
    }
    }

    Na przykład taką jak powyżej.

  2. chlebik Post author

    A pewnie. Ale na to przyjdzie jeszcze czas bo przeciez to wstepna wersja – mialo byc jeszcze wysylanie mejli aktywujacych oraz wgrywanie avatarow. Zatem sporo jeszcze przed nami.

  3. chlebik Post author

    Z RichFaces zamierzam podprowadzić upload plików. Troche tego JSF czystego trzeba tez poznac 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s