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

Wokół użytkowników kręci się cały ten internetowy biznes. Oczywistą zatem rzeczą jest obecnie implementacja procesu rejestracji użytkowników, a także ich logowania do aplikacji, co daje rzecz jasna o wiele większe możliwości niż bierne przeglądanie serwisu. Do dzieła.

Oczywiście jak każą dobre praktyki zaczniemy od modelu. Podobny proces już przerabiałem w przypadku HowToJava, a zgodnie z zasadą DRY wypadałoby cofnąć się do doświadczeń z poprzedniego projektu. To pierwsze założenie. Drugie jednakoż mówi o tym, aby w każdym kolejnym projekcie posuwać się dalej niż w poprzednim, zrobić coś bardziej ciekawego i zaawansowanego. I tak będzie tym razem. Poza standardowymi danymi użytymi w HowToJava planuję zaimplementować jeszcze następujące funkcje:

  • avatary użytkowników – rzecz jasna wraz z wgrywaniem plików. Sarkałem na obsługę uploadu plików w JSF, teraz będę miał okazję rzecz sprawdzić w praktyce.
  • określanie płci – to nie jest żart! Konkretnie możnaby w mapowaniu tabeli bazy danych zrobić rzecz na zwykłym SMALLINT i zgodzić się, że 1 to panie, a 2 panowie. Ale to nie bardzo profesjonalne. Poruszę temat typów wyliczeniowych w mapowaniu Hibernate i zobaczymy co z tego wyniknie.
  • kody aktywacyjne – w przypadku HowToJava rzecz była bardzo prosta, gdyż ograniczała się do wpisania adresu poczty elektronicznej, dwa razy hasła no i rzecz jasna CAPTCHY. Niektórych jednakże ta ostatnia niemożliwie denerwuje, zatem tym razem pobawię się w wysyłanie mejli wraz z kodami aktywacyjnymi.

Założenia dość interesujące, zatem trzeba zabrać się do dzieła. Postanowiłem zacząć od rozeznania tematu związanego z typami wyliczeniowymi (ENUM), zarówno jako konstrukcji języka, jak i typu w bazie danych MySQL.

Dla przypomnienia – typ wyliczeniowy w MySQL to specjalny typ kolumny przeznaczony do przechowywania ściśle określonych typów. Warto wspomnieć, iż MySQL konwertuje sobie zawartość takiej kolumny na odwzorowania typu INT. Co za tym idzie znacząco przyspiesza wyszukiwanie po tejże kolumnie, pomimo pozorów używania wartości tekstowych. Jeśli zaś chodzi o ENUM w Javie, jest to rzecz wprowadzona wraz z pojawieniem się wersji 1.5 języka. Temat jest dość obszerny, zatem zainteresowanych odsyłam na strony tutoriala firmy SUN. Użycie tego typu kolumny nasuwa się od razu, kiedy będziemy operować na ledwie kilku potencjalnych wartościach. Czyli określenie płci ( ‘m’,’f’ ), statusu konta ( ‘banned’,’active’,’deleted’,’awaiting’ ) czy choćby poziomu uprawnień ( ‘admin’, ‘user’, ‘guest’ ) to podręcznikowe wręcz przyłady sensowności implementacji typu wyliczeniowego. W przypadku użytkowników ProgramBash skupimy się na dwóch wymienionych powyżej typach – płci oraz statusie konta.

Konwersacja z wujkiem G zaowocowała dwoma linkami – blogiem dangertree, a także znajdującym się tam odnośnikiem do oficjalnej dokumentacji Hibernate.
Rzecz jest dość prosta – musimy stworzyć własny typ wyliczeniowy, aby dało się go zastosować jako typ składowej obiektu odwzorowania. Kiedy będziemy już mieli typ wyliczeniowy, wówczas stworzymy plik odwzorowań dla Hibernate, zaś na końcu stworzymy gotowy obiekt.

Oto kod dla naszego typu wyliczeniowego:

public enum SexEnum {
    MALE(0), FEMALE(1);

    private int id;

    private SexEnum(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public static SexEnum valueOf(int id) {
        switch (id) {
            case 0: return MALE;
            case 1: return FEMALE;
            default: return MALE;
        }
    }
}

Okazuje się jednakże, że to nie wszystko. Potrzeba jeszcze stworzyć klasę, która będzie imlpementowała interfejs UserType oraz ParameterizedType, które są potrzebne naszemu ORMowi. Kod dość długi:

public class SexEnumUserType implements UserType, ParameterizedType {

    private Class<? extends Enum> enumClass;
    private Class<?> identifierType;

    private Method identifierMethod;
    private Method valueOfMethod;

    private static final String defaultIdentifierMethodName = "getId";
    private static final String defaultValueOfMethodName = "valueOf";

    private NullableType type;
    private int [] sqlTypes;

    public void setParameterValues(Properties parameters) {
        String enumClassName = parameters.getProperty("enumClass");
        try {
            enumClass = Class.forName(enumClassName).asSubclass(Enum.class);
        }
        catch (ClassNotFoundException exception) {
            throw new HibernateException("Enum class not found", exception);
        }

        String identifierMethodName =
            parameters.getProperty("identifierMethod", defaultIdentifierMethodName);

        try {
            identifierMethod = enumClass.getMethod(identifierMethodName, new Class[0]);
            identifierType = identifierMethod.getReturnType();
        }
        catch(Exception exception) {
            throw new HibernateException("Failed to optain identifier method", exception);
        }

        type = (NullableType)TypeFactory.basic(identifierType.getName());

        if(type == null)
            throw new HibernateException("Unsupported identifier type " + identifierType.getName());

        sqlTypes = new int [] {type.sqlType()};

        String valueOfMethodName =
            parameters.getProperty("valueOfMethod", defaultValueOfMethodName);

        try {
            valueOfMethod = enumClass.getMethod(
                    valueOfMethodName, new Class[] { identifierType });
        }
        catch(Exception exception) {
            throw new HibernateException("Failed to optain valueOf method", exception);
        }
    }

    public Class returnedClass() {
        return enumClass;
    }

    public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
                        throws HibernateException, SQLException {
        Object identifier=type.get(rs, names[0]);
        try {
            return valueOfMethod.invoke(enumClass, new Object [] {identifier});
        }
        catch(Exception exception) {
            throw new HibernateException(
                    "Exception while invoking valueOfMethod of enumeration class: ", exception);
        }
    }

    public void nullSafeSet(PreparedStatement st, Object value, int index)
            throws HibernateException, SQLException {
        try {
            Object identifier = value != null ? identifierMethod.invoke(value, new Object[0]) : null;
            st.setObject(index, identifier);
        }
        catch(Exception exception) {
            throw new HibernateException(
                    "Exception while invoking identifierMethod of enumeration class: ", exception);

        }
    }
    public int[] sqlTypes() {
        return sqlTypes;
        //There was a logical bug within the set-up phase of any user type
        //I reported the issue and it got instantly solved (Thanks again Garvin!)
        //But it might still exist in your Hibernate version. So if you are
        //facing any null-pointer exceptions, use the return statement below.
        //Note: INTEGER works even for String based mappings...
        //return new int [] {Types.INTEGER};
    }

        public Object assemble(Serializable cached, Object owner) throws HibernateException {
        return cached;
    }

    public Object deepCopy(Object value) throws HibernateException {
        return value;
    }

    public Serializable disassemble(Object value) throws HibernateException {
        return (Serializable)value;
    }

    public boolean equals(Object x, Object y) throws HibernateException {
        return x==y;
    }

    public int hashCode(Object x) throws HibernateException {
        return x.hashCode();
    }

    public boolean isMutable() {
        return false;
    }

    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return original;
    }
}

Oto jak wygląda kod klasy odwzorowującej dla Hibernate:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="hibernate.mappings">

    <typedef class="com.wordpress.chlebik.mappings.enums" name="sexEnum">
        <param name="enumClassName">com.wordpress.chlebik.mappings.enums.SexEnum</param>
        <param name="identifierMethod">getId</param>
    </typedef>

  <class name="com.wordpress.chlebik.User" table="users">
    <id column="id" name="id" type="long">
      <generator class="native"/>
    </id>
    <property column="adddate" name="adddate" type="integer"/>
    <property column="registerdate" name="registerdate" type="integer"/>
    <property column="avatarpath" name="avatarpath" type="string"/>
    <property column="email" name="email" type="string"/>
    <property column="nick" name="nick" type="string"/>
    <property column="pass" name="pass" type="string"/>
    <property column="sex" name="sex" type="sexEnum"/>
  </class>
</hibernate-mapping>

Kilka słów wyjaśnienia. addDate różni się od registerDate tym, iż ta pierwsza jest datą pojawienia się konkretnego rekordu w bazie danych, zaś registerDate jest zapisaną datą aktywacji konta. Jak widać na początku pliku odwzorowania trzeba było poinformować Hibernate o tym, iż w użyciu będzie nasz specjalny typ (deklaracja typedef).

Na sam koniec kod klasy reprezentującej naszego użytkownika:

@Entity
@Table(name = "users")
public class User implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String nick;
    private String email;
    private String avatarpath;
    private Integer adddate;
    private Integer registerdate;
    private SexEnum sex;
    private String pass;
    private String passconfirmation;

    public User() {
    }

    public User(String nick, String email, String pass, String passconfirmation, String avatarpath, Integer adddate, Integer registerdate, SexEnum sex) {
        this.nick = nick;
        this.pass = pass;
        this.passconfirmation = passconfirmation;
        this.email = email;
        this.avatarpath = avatarpath;
        this.adddate = adddate;
        this.registerdate = registerdate;
        this.sex = sex;
    }

    // Gettery
    public String getNick() {
        return nick;
    }

    public String getPass() {
        return pass;
    }

    public String getPassconfirmation() {
        return passconfirmation;
    }

    public String getEmail() {
        return email;
    }

    public String getAvatarPath() {
        return avatarpath;
    }

    public Date getAdddate() {
        Date dateDateObject = new Date();
        long dateInMilis = ((long) adddate) * 1000;
        dateDateObject.setTime(dateInMilis);
        return dateDateObject;
    }

    public Date getRegisterdate() {
        Date dateDateObject = new Date();
        long dateInMilis = ((long) registerdate) * 1000;
        dateDateObject.setTime(dateInMilis);
        return dateDateObject;
    }

    public SexEnum getSex() {
        return sex;
    }

    protected Long getId() {
        return id;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public void setSex(SexEnum sex) {
        this.sex = sex;
    }

    public void setNick(String nick) {
        this.nick = nick;
    }

    public void setPass(String pass) {
        this.pass = pass;
    }

    public void setPassconfirmation(String passconfirmation) {
        this.passconfirmation = passconfirmation;
    }

    public void setRegisterdate(Integer registerdate) {
        this.registerdate = registerdate;
    }

    public void setAdddate(Integer adddate) {
        this.adddate = adddate;
    }

    public void setAvatarpath(String avatarpath) {
        this.avatarpath = avatarpath;
    }
}

Tak to wygląda póki co. Teraz jednakże wypada stworzyć trochę kodu, który umożliwi nam wprowadzenie stosownych danych do aplikacji. Po małych zabawach z CSSem i HTMLem do odpowiedniego pliku widoku dorzuciłem taki oto kod:

<%@taglib uri="http://richfaces.org/a4j" prefix="a4j"%>
<%@taglib uri="http://richfaces.org/rich" prefix="rich"%>

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

<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>

<div class="form-container">

	<h:form>

	<fieldset>
		<legend>Podaj swoje dane</legend>
			<div>
                            <label for="nick">Nick</label>
                            <h:inputText value="#{user.nick}" id="nick" />
                        </div>

			<div>
                            <label for="email">E-mail</label>
                            <h:inputText value="#{user.email}" id="email" />
                        </div>

                        <div>
                            <label for="pass">Hasło</label>
                            <h:inputSecret value="#{user.pass}" id="pass" />
                        </div>

                        <div>
                            <label for="pass_confirmation">Powtórz hasło</label>
                            <h:inputSecret value="#{user.pass_confirmation}" id="pass_confirmation" />
                        </div>

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

<!--   Inne pola póki co pominąłem, chodzi o pokazanie tematu -->

	</fieldset>

	<div class="buttonrow">
            <h:commandButton type="submit" value="Zarejestruj" id="register" action="#{userController.register}"  />
	</div>

        </h:form>
</div>

Rzecz jest wstępnie skrócona do podstawowych własności by zobaczyć czy nasz przykład działa. Jak widać w znaczniku <h:commandButton> podajemy akcję zaimplementowaną w klasie userController. Albowiem gdzieś te nasze requesty muszą zostać obsłużone. Bean o nazwie user będzie nam służył do reprezentacji obiektu obecnie zalogowanego użytkownika.  Plik faces-config.xml dla aplikacji wygląda póki co tak:

<managed-bean>
  <managed-bean-name>newsBean</managed-bean-name>
  <managed-bean-class>com.wordpress.chlebik.beans.NewsBean</managed-bean-class>
  <managed-bean-scope>request</managed-bean-scope>
 </managed-bean>

 <managed-bean>
  <managed-bean-name>userController</managed-bean-name>
  <managed-bean-class>com.wordpress.chlebik.controllers.UserController</managed-bean-class>
  <managed-bean-scope>session</managed-bean-scope>
 </managed-bean>

  <managed-bean>
  <managed-bean-name>user</managed-bean-name>
  <managed-bean-class>com.wordpress.chlebik.User</managed-bean-class>
  <managed-bean-scope>session</managed-bean-scope>
 </managed-bean>

 <managed-bean>
  <managed-bean-name>sexBean</managed-bean-name>
  <managed-bean-class>com.wordpress.chlebik.beans.SexBean</managed-bean-class>
  <managed-bean-scope>session</managed-bean-scope>
 </managed-bean>


<navigation-rule>
    <from-view-id>/rejestruj.jsp</from-view-id>
    <navigation-case>
        <from-outcome>registered</from-outcome>
        <to-view-id>/glowna.jsp</to-view-id>
        <redirect />
    </navigation-case>
    <navigation-case>
        <from-outcome>register-failure</from-outcome>
        <to-view-id>/rejestruj.jsp</to-view-id>
    </navigation-case>
</navigation-rule>

Jak widać poza klasami zajmującymi się logiką, w pliku konfiguracyjnym umieściłem reguły nawigacji. Mówią one jedno – z widoku o adresie /rejestruj.jsp (w regułach podajemy nazwy pliku widoku, a nie mapowania np. rejestruj.faces będzie błędne) należy w razie powodzenia przejść do widoku strony głównej (poprzez REDIRECT), zaś w przypadku pomyłki należy ponownie wyświetlić formularz rejestracji. Kod naszego kontrolera na razie będzie przykładowy – akcja register będzie po prostu zwracała wartość ‘registered’.

public class UserController {
   
   /**
    * Akcja obslugujaca rejestracje w systemie
    *
    * @return String
    * @author Michał Piotrowski
    */
    public String register()   {
       return "registered";
    }
}

Uruchamiamy aplikację, przechodzimy pod adres rejetruj.faces i naszym oczom pokazuje się śliczny formularz rejestracyjny. Niezależnie od tego czy wpiszemy cokolwiek w pola formularza czy też nie, po naciśnięciu przycisku ‘REJESTRUJ’ zostaniemy przekierowani na główną stronę aplikacji. Póki co rzecz jest posta jak konstrukcja gwoździa, jednakże wpis stał się cokolwiek za długi, zatem kolejna część następnym razem. Spróbujemy zwalidować nasz formularz oraz zapisać coś do bazy.

Advertisements

One thought on “Rejestrowanie użytkowników w ProgramBash – część 1

  1. copernic777

    Możesz również zastosować adnotację @Enumerated(EnumType.ORDINAL) z JPA i dodać hibernate-annotations do projektu – wtedy nie trzeba kompletnie tworzyć własnego typu hibernate’owego.

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