Category Archives: JavaServerPages

Kończymy rejestrację w ProgramBash

No to po kilku tygodniach wracamy do programowania w JSF. Dziś spróbujemy dokończyć proces rejestracji w naszej aplikacji. Do tego potrzeba nam jeszcze dwóch rzeczy – uploadu plików będącego naszym avatarem w profilu (plus jego walidacja i skalowanie) oraz walidacji występowania adresu email w bazie. Planowałem też pierwotnie by dorzucić tu jeszcze wysyłkę mejli aktywacyjnych, ale to w przyszłości, kiedy ogólnie zajmiemy się wysyłaniem mejli z poziomu Javy i wykorzystamy do tego stosowne narzędzia. Do dzieła.

Upload plików na serwer zasadniczo jest rzeczą prostą jak konstrukcja gwoździa. Wystarczy dorzucić w formularz element input z typem file, następnie przechwycić plik po stronie serwera i po prostu go gdzieś zapisać. Proces jest bardzo łatwy – problemem są ewentualne zdarzenia, które mogą mieć miejsce po drodze. Czyli należy sprawdzić czy plik dotarł na serwer w całości, następnie czy na pewno taki typ pliku chcemy na serwerze zapisać. Proces ten również może zakończyć się niepowodzeniem – nazwa może być nieodpowiednia, możemy mieć niewystarczającą ilość miejsca na dysku, lub też mieć problem z prawami dostępu. W przypadku PHP, z którym na co dzień pracuję samo zapisanie pliku we wskazanej lokalizacji to 1 linijka kodu. Obsługa wyrzucanych po drodze wyjątków to linijek kilkadziesiąt.

Zatem by nie tracić czasu na tworzenie własnych niskopoziomowych rozwiązań postanowiłem zaprząć starego znajomego Richfaces. Dostępny komponent dla uploadu plików jest bardzo fajnie napisany, konfigurujemy go, a następnie przechwytujemy wysłany plik po stronie serwera. Nasz komponent to <rich:fileUpload>, do którego możemy przekazać szereg ciekawych rzeczy. Kod na wstępie wygląda tak:

  <rich:fileUpload fileUploadListener="#{uploaderBean.listener}"
                            maxFilesQuantity="1"
                            id="upload"
                            immediateUpload="true"
                            addButtonClass="rich-fileupload-addbuttonstyle"
                            addButtonClassDisabled="rich-fileupload-addbuttonstyle"
                            cleanButtonClass="rich-fileupload-clearallstyle"
                            cleanButtonClassDisabled="rich-fileupload-clearallstyle"
                            clearAllControlLabel="Wyczyść"
                            cancelEntryControlLabel="Wyczyść"
                            clearControlLabel="Wyczyść"
                            addControlLabel="Dodaj"
                            uploadButtonClassDisabled="rich-fileupload-uploadstyle"
                            uploadButtonClass="rich-fileupload-uploadstyle"
                            uploadControlLabel="Wyślij"
                            listHeight="110px"
                            styleClass="rich-fileupload-mainblockstyle"
                            doneLabel="Zrobione!"
                            sizeErrorLabel="Plik jest zbyt duży!"
                            transferErrorLabel="Wystąpił błąd podczas uploadu pliku!"
                            acceptedTypes="jpg"
                            allowFlash="false"
                            rendered="#{uploaderBean.uploaded == false}">
    </rich:fileUpload>

Rzecz jest dość rozbudowana (bądź też na takową wygląda). Jednakże nie ma w owym komponencie nic skomplikowanego – większość kodu to wskazania odpowiednich komunikatów o błędach w języku polskim (normalnie możnaby podpiąć tutaj komunikaty z message-bundle w zależności od wybranego Locale), a także klas CSS, którymi ładnie ostylowałem kontrolkę. Z najistotniejszych rzeczy mamy atrybut acceptedTypes, który pozwala ustwić akceptację tylko dla plików JPG. Obok tego ustalamy maksymalnie 1 plik do wysłania ( maxFilesQuantity ), a także wskazujemy bezpośrednio, że uploadowany plik zostaje automatycznie wysłany na serwer po wybraniu ( immediateUpload ). Jak widać wskazałem też stosownego beana do wykonania akcji uploadowania pliku ( uploaderBean wraz z akcją listener ). Skoro bowiem chcemy wysłać nasz plik na serwer, na tymże właśnie serwerze musi śmigać kod, który coś z tym plikiem zrobi. Na razie skupię się tylko na jego odebraniu i zapisaniu na dysku. Do tego zapiszemy pełną ścieżkę do pliku w sesji, coby można było popracować na pliku po poprawnej walidacji formularza. Kod ten jest po części zmodyfikowanym kodem ze strony RichFaces:

package com.wordpress.chlebik.util;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Calendar;
import org.richfaces.event.UploadEvent;
import org.richfaces.model.UploadItem;

/**
 * @author Ilya Shaikovsky
 *
 */
public class FileUploaderBean{
   
    private int uploadsAvailable = 1;
    private boolean autoUpload = true;
    private boolean useFlash = false;
    private String avatarPath = null;
    private boolean uploaded = false;
  
    
    public FileUploaderBean() {
    }

    /**
     * Metoda listenera, ktora przechwytuje nasz plik avatara, zapisuje go do tymczasowego pliku
     * na dysku i zapisuje sciezke do niego w sesji.
     *
     * @param event
     * @throws Exception
     */
    public void listener( UploadEvent event ) throws Exception {

        UploadItem item = event.getUploadItem();

        String newFilename = System.getProperty("java.io.tmpdir") + Calendar.getInstance().getTimeInMillis()/1000 + ".jpg";
        FileOutputStream newAvatarFileStream = new FileOutputStream( newFilename );

        try {
            newAvatarFileStream.write( item.getData() );
            uploaded = true;
            avatarPath = newFilename;
        }
        catch( Exception e ) {
           throw new IOException("Zapis pliku nie powiódł się!");
        }
        finally {
            newAvatarFileStream.close();
        }

        ProgramBashUtil.setSessionVariable( "avatar_path", newFilename );
            
    }

  
    public String clearUploadData() {
        uploaded = false;
        avatarPath = "";
        ProgramBashUtil.setSessionVariable( "avatar_path", null );
        return null;
    }

// POMINALEM GETTERY I SETTERY

}

Kod jest dość przejrzysty – podstawowym rzecz jasna elementem jest metoda listener. Otrzymuje ona obiekt wydarzenia (wgrania pliku). Z niego to możemy wyciągnąć sobie informacje o przesłanym pliku – nazwie, rozmiarze, czy w końcu jego zawartość. Nazwę pliku póki co ustawiam na bieżący timestamp (nie ma problemu z dziwnymi nazwami, krótsze i zasadniczo unikalne). Nadaję plikowi rozszerzenie JPG, gdyż tylko do takowych ograniczony jest upload z formularza, a także umożliwia to zapisanie ścieżki do pliku w sesji i ew. wyświetlenie go po walidacji formularza. Zapis odbywa się w domyślnym systemowym katalogu przeznaczonym na TEMP – wyczytujemy sobie odpowiednią ścieżkę używając statycznej metody obiektu System. Potem zwykły zapis do pliku, po czym zapisujemy jego nazwę do sesji. W razie wystąpienia jakiś problemów wyrzucony wyjątek (jakiegokolwiek typu) zostanie zwrócony do komponentu uploadera i zostanie wyświetlony w nim komunikat o fiasku uploadu.

Widać też statyczną metodę z klasy ProgramBashUtil! Jest to klasa ze statycznymi metodami (dzięki niej wyciągam również SessionFactory dla Hibernate), która ma ograniczyć ilość wpisywanego kodu – dopisałem tam zatem metody do odczytu oraz zapisu zmiennych do sesji.

Nie zapomnijmy też o małej przeróbce pliku web.xml, gdzie w sekcji konfiguracji poświęconej RichFaces Filter musimy dorzucić takie oto parametry inicjujące:

 <init-param>
	<param-name>createTempFiles</param-name>
	<param-value>false</param-value>
</init-param>
<init-param>
	<param-name>maxRequestSize</param-name>
	<param-value>2000000</param-value>
</init-param>

Cóż one znaczą? Ano tyle, że przy uploadzie nie jest tworzony tymczasowy plik tylko to my bierzemy przekazane dane i sami je zapisujemy we wskazanym katalogu. Maksymalny rozmiar przesyłanego pliku ustawia się w bajtach, zatem trzeba to sobie umiejętnie przeliczyć (w tym przypadku prawie 2 MB). Wgrywanie pliku działało bez zarzutu – zapis szedł do wskazanego katalogu. Problemem stał się z kolei cały formularz. Domyślnie kiedy dodałem komponent do uploadu dopisałem do znacznika form typ:

enctype="multipart/form-data"

i to był mój błąd. Otóż przy próbie wysłania całego formularza szedł request do serwera, ale nie był on obsługiwany przez moją klasę kontrolera. Pisałem o tym w poprzednim wpisie, całe szczęście teraz już wiem co powodowało ten błąd (ale co się nakląłem to moje). Dla uważnych – wskazanie takiego typu w elemencie form nie jest konieczne, gdyż nasz plik jest wysyłany AJAXem do serwera – nie zaś zwykłym requestem po przeładowaniu strony.

Kiedy już wpiszemy wszystkie dane do formularza (nawet poprawne), dorzucimy plik graficzny (nie jest potrzebny) jako avatar operację przejmuje klasa UserController. Pobieramy dane z obiektu żądania, sprawdzamy czy zgadza się CAPTCHA (mój nick w wydaniu: chleb lub chlebik, wielkość liter nie ma znaczenia) oraz hasła, aby jeśli wszystko jest OK zapytać bazę o tak podstawową rzecz jak fakt, iż podany przy rejestracji email nie występuje już w bazie. Jak wspominałem wcześniej o właśnie email będzie identyfikatorem w bazie (unikalnym, poza ID rzecz jasna), w związku z czym takie zabezpieczenie. Kiedy adres email nie zostanie znaleziony w bazie, wówczas bierzemy nasz plik graficzny (ścieżkę do niego wyciągamy z sesji, jeśli jest pusta to nie robimy nic) i przerabiamy go na miniaturkę 110x100px. Kod, który to robi został skopiowany spod tego linku i jest raczej prosty i klarowny. Oczywiście możnaby podpiąć śliczny plugin do jQuery i pozwolić użytkownikom na wybór obszaru resizowanego obrazka, ale w przypadku zwykłego avatarka w takim serwisie to sztuka dla sztuki (przynajmniej moim zdaniem). Oto kod kontrolerka:

package com.wordpress.chlebik.controllers;

import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGEncodeParam;
import com.sun.image.codec.jpeg.JPEGImageEncoder;
import com.wordpress.chlebik.User;
import com.wordpress.chlebik.mappings.enums.SexEnum;
import com.wordpress.chlebik.util.MD5;
import com.wordpress.chlebik.util.ProgramBashUtil;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Date;
import java.util.Map;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.imageio.ImageIO;
import org.hibernate.NonUniqueResultException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import sun.awt.image.ImageAccessException;

/**
 * Klasa, ktora ma za zadanie zajmowac sie uzytkownikiem - rejestracja, logowanie i wylogowanie plus 
 * szereg innych rzeczy, ktore pewnie przyjda mi do glowy. 
 *
 * @author Michal 'Chlebik' Piotrowski
 */
public class UserController {

    private User user;
    String avatarPath   = null;

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

       
        FacesContext context = FacesContext.getCurrentInstance();
        Map params = context.getExternalContext().getRequestParameterMap();
        avatarPath = (String) ProgramBashUtil.getSessionVariable("avatar_path");

        String pass       =    (String)  params.get("pass");
        String passconf   =    (String)  params.get("passconfirmation");
        String nick       =    (String)  params.get("nick");
        String email      =    (String)  params.get("email");
        String captcha    =    (String)  params.get("captcha");
        Date addDate = new Date();
        String pathToAvatar = "";
       
        if( captcha.toLowerCase().equals( "chlebik" ) == false && captcha.toLowerCase().equals( "chleb" ) == false ) {
             FacesMessage msg = com.wordpress.chlebik.util.Messages.getMessage( "com.wordpress.chlebik.util.msg" , "captchaNotValid", null);
             msg.setSeverity( FacesMessage.SEVERITY_ERROR );
             context.addMessage("captcha", msg );
             return "register-failure";
        }

        if( pass.equals( passconf ) )  {

            try {

                SessionFactory sessionFactory = ProgramBashUtil.getSessionFactory();
                Session session = sessionFactory.openSession();
                Query zapytanie = session.createQuery( "FROM User WHERE email = '" + email + "'" );
                String emailRegistered = null; 

                try { 
                    User existingUser = (User) zapytanie.uniqueResult();

                    if( existingUser != null ) {
                        emailRegistered = existingUser.getEmail();

                        if( emailRegistered.equals( email) ) {
                            throw new NonUniqueResultException(1);
                        }
                    }

                } catch( NonUniqueResultException e ) {
                    context.addMessage( null, new FacesMessage( FacesMessage.SEVERITY_ERROR, "Ten adres e-mail już jest zarejestrowany!!", "") );
                    return "register-failure";
                }
              
                try {
                    if( avatarPath != null ) {
                        pathToAvatar = createAvatarThumb();
                    }
                }
                 catch( ImageAccessException e ) {
                     context.addMessage( null, new FacesMessage( FacesMessage.SEVERITY_ERROR,"Coś poszło nie tak przy operacji na avatarze. Spróbuj za chwilkę!", "") );
                     return "register-failure";
                 }
                catch (Exception e ) {
                     context.addMessage( null, new FacesMessage( FacesMessage.SEVERITY_ERROR, "Coś poszło nie tak przy operacji na avatarze. Spróbuj za chwilkę!", "") );
                     return "register-failure";
                }
                         

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

                session.save( newUser );
                session.close();
                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";
        }
             
    }

    /**
     * Metoda zmieniajaca rozmiar naszego obrazka by pasowal jako avatar
     * Kod sciagniety ze strony: http://www.webmaster-talk.com/coding-forum/63227-image-resizing-in-java.html
     * i obkrojony tylko do twardego resize na 110x110.
     */
    private String createAvatarThumb() throws ImageAccessException, IOException {

        if( avatarPath == null ) {
            throw new ImageAccessException("Nie znaleziono ścieżki do pliku!");
        }

        File   avatarBaseFile = new File( avatarPath );
        Image image = (Image) ImageIO.read(avatarBaseFile );

        // Draw the scaled image
        BufferedImage thumbImage = new BufferedImage(110, 110, BufferedImage.TYPE_INT_RGB);
        Graphics2D graphics2D = thumbImage.createGraphics();
        graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
        RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        graphics2D.drawImage(image, 0, 0, 110, 110, null);

        // Write the scaled image to the outputstream
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);
        JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(thumbImage);
        int quality = 100; // Use between 1 and 100, with 100 being highest quality
        quality = Math.max(0, Math.min(quality, 100));

        param.setQuality((float)quality / 100.0f, false);
        encoder.setJPEGEncodeParam(param);
        encoder.encode(thumbImage);
        ImageIO.write(thumbImage, "jpg" , out);

        // Zapisujemy przekonwertowany plik do oddzielnego pliku
        ByteArrayInputStream bis = new ByteArrayInputStream( out.toByteArray() );
      
        File newFile = new File( ProgramBashUtil.getContextVariable( "avatarUploadDirectory" ) + avatarBaseFile.getName() );
        FileOutputStream fos = new FileOutputStream( newFile );
        int data;
        while( (data=bis.read()) != -1 )
        {
            char ch = (char)data;
            fos.write( ch );
        }

        fos.flush();
        fos.close();
        out.close();
        avatarBaseFile.delete();
        ProgramBashUtil.setSessionVariable( "avatar_path", null );
        return avatarBaseFile.getName();
    }
  
}

 

I to by było prawie na tyle. Prawie. Do omówienia zostały jeszcze dwie rzeczy. Pierwszą z nich jest nawigacja. Docelowo kiedy kontroler zwróci wartość “registered” request zostanie skierowany na główną stronę naszego serwisu. Podpięciem takich rzeczy jak ew. zmiana layoutu zajmiemy się w następnym wpisie. Natomiast problemem jest to, iż po pomyślnej rejestracji pasek adresu nie zmienia się. Widok mamy ze strony głównej, zaś adres nie ten. By po pomyślnej rejestracji zmienił się również pasek adresu trzeba dorzucić do reguł nawigacji znacznik redirect. Wygląda to tak w pliku faces-config.xml:

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

Tutaj jednakże kolejna zagwozka – nie wyświetla się nam komunikat informujący o pomyślnej rejestracji. Dzieje się to dlatego, iż flesh-message w przypadku JSF trzymają się tylko w obrębie jednego requestu. By zatem po ew. przekierowaniu requestu (a to zdarza się najczęściej w nawigacji) nasze wiadomości były wciąż widoczne potrzebujemy małego filterka. Oto jego kod, który zapożyczyłem spod adresu http://blog.kaiec.org/2009/03/01/message-handling-with-jsf-and-redirects/ :

package com.wordpress.chlebik.filters;

import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.Iterator;
import javax.faces.event.PhaseListener;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseEvent;
import javax.faces.context.FacesContext;
import javax.faces.application.FacesMessage;

/**
* Enables messages to be rendered on different pages from which they were set.
* To produce this behaviour, this class acts as a <code>PhaseListener</code>.
*
* This is performed by moving the FacesMessage objects:
* <li>After each phase where messages may be added, this moves the messages from
* the page-scoped FacesContext to the session-scoped session map.
* <li>Before messages are rendered, this moves the messages from the session-scoped
* session map back to the page-scoped FacesContext.
*
* Only messages that are not associated with a particular component are ever
* moved. These are the only messages that can be rendered on a page that is different
* from where they originated.
*
* To enable this behaviour, add a <code>lifecycle</code> block to your
* faces-config.xml file. That block should contain a single <code>phase-listener</code>
* block containing the fully-qualified classname of this file.
*
* @author <a href="mailto:jesse@odel.on.ca">Jesse Wilson</a>
*/

public class FlashMessageRedirectHandler implements PhaseListener {
/**
* a name to save messages in the session under
*/
private static final String sessionToken = "MULTI_PAGE_MESSAGES_SUPPORT";

/**
* Return the identifier of the request processing phase during which this
* listener is interested in processing PhaseEvent events.
 * @return
 */
public PhaseId getPhaseId() {
return PhaseId.ANY_PHASE;
}

/**
* Handle a notification that the processing for a particular phase of the
* request processing lifecycle is about to begin.
*/
public void beforePhase(PhaseEvent event) {

if(event.getPhaseId() == PhaseId.RENDER_RESPONSE) {
FacesContext facesContext = event.getFacesContext();
restoreMessages(facesContext);
}
}

/**
* Handle a notification that the processing for a particular phase has just
* been completed.
*/
public void afterPhase(PhaseEvent event) {

if(event.getPhaseId() == PhaseId.APPLY_REQUEST_VALUES ||
event.getPhaseId() == PhaseId.PROCESS_VALIDATIONS ||
event.getPhaseId() == PhaseId.INVOKE_APPLICATION) {

FacesContext facesContext = event.getFacesContext();
saveMessages(facesContext);
}

}

/**
* Remove the messages that are not associated with any particular component
* from the faces context and store them to the user's session.
*
* @return the number of removed messages.
*/
private int saveMessages(FacesContext facesContext) {
// remove messages from the context
List messages = new ArrayList();
for(Iterator i = facesContext.getMessages(null); i.hasNext(); ) {
messages.add(i.next());
i.remove();
}
// store them in the session
if(messages.size() == 0) {
return 0;
}
Map sessionMap = facesContext.getExternalContext().getSessionMap();
// if there already are messages
List existingMessages = (List)sessionMap.get(sessionToken);
if(existingMessages != null) {
existingMessages.addAll(messages);
}
else {
sessionMap.put(sessionToken, messages); // if these are the first messages
}


return messages.size();
}

/**
* Remove the messages that are not associated with any particular component
* from the user's session and add them to the faces context.
*
* @return the number of removed messages.
*/
private int restoreMessages(FacesContext facesContext) {
// remove messages from the session
Map sessionMap = facesContext.getExternalContext().getSessionMap();
List messages = (List)sessionMap.remove(sessionToken);
// store them in the context
if(messages == null) {
return 0;
}
int restoredCount = messages.size();
for(Iterator i = messages.iterator(); i.hasNext(); ) {
facesContext.addMessage(null, (FacesMessage)i.next());
}

return restoredCount;
}

}

By ów filtr działał trzeba go rzecz jasna dorzucić do naszych plików konfiguracyjnych. Czyli do faces-config.xml (poza elementem application) dorzucamy taki kawałek kodu:

  <lifecycle>
        <phase-listener> com.wordpress.chlebik.filters.FlashMessageRedirectHandler</phase-listener>
    </lifecycle> 

Rzecz jasna nazwy pakietów czy klas to już zależą od programisty. Tym samym każdy request z redirectem będzie również miał dostęp do komunikatów zapisanych przez użytkownika. Bardzo przydatny kawałek kodu. Druga rzecz to nasze dane do ściągania i uploadu pliku. Wiadomo, że ostatecznie gdzieś nasz plik będzie musiał na dysku leżeć, podobnie jego widoczność w serwisie będzie wymagała użycia zewnętrznej subdomeny do tego. Obie te dane zaszyć należy w pliku web.xml jako zmienne wgrywane wraz z aplikacją:

 <context-param>
        <param-name>avatarUploadDirectory</param-name>
        <param-value>/sciezka/do/avatarow/na/dysku</param-value> 
  </context-param>

      <context-param>
        <param-name>avatarDownloadDomain</param-name>
        <param-value>http://static.programbash.chlebik.pl/avatars/</param-value>
      </context-param>

Subdomena jest skonfigurowana w ngixie by wskazywała na katalog z avatarami. I to by było na tyle. Obiekt użytkownika wisi sobie w sesji póki co i czeka na wykorzystanie. Zajmiemy się tym w następnym wpisie poświęconym ProgramBash. Na razie można obejrzeć działającą aplikację pod znanym wszystkim adresem. Na zakończenie dorzucę jeszcze informacje, iż wpis wygląda na krótki, zaś mechanizmy na proste. Wygląda. Jednakże moje obiekcje wobec JSF jako takie bynajmniej nie zmalały, a nawet się powiększyły. Nie jest to rzecz dla ludzi na dłuższą metę. Cóż jednak począć skoro tyle firm go używa.

Advertisements

Coś się chyba Helionowi pomyliło

Dziś o czymś troszeczkę innym niż Grails. Odwiedziłem stronę wydawnictwa Helion, coby przepatrzyć jakieś promocje czy nowości. Jakie było moje zdziwienie, kiedy na stronie głównej pośród nowości zobaczyłem tę oto książkę.

Jasno i wyraźnie stoi napisane, iż jest to TOM 2. Hmmm, a gdzie tom pierwszy? W swojej naiwności liczyłem na to, że może chodzi o książkę o JSF, którą miałem okazję zakupić i leży u mnie na półce. Rzeczywistość okazała się jednakże inna – nasze cudowne wydawnictwo przetłumaczyło i wydrukowało tylko drugi tom! Z ciekawości zajrzałem do googla i co się okazuje? Że tom pierwszy tejże książki można sobie spokojnie ściągnąć z internetu spod tego adresu. Wiadomo, jest też jakieś “ale” – w treści książki mamy materiały reklamowe (niezbyt dokuczliwe), a także nie da się ściągnąć wersji all-in-one. Jednakże i to jest świetna gratka dla każdego, kto chciałby poznać tę technologię bliżej. Do tej pory pozostawało tylko “Head First”, a niekoniecznie każdemu forma tej serii wydawniczej pasuje. Jak widać i z pomyłek mogą narodzić się konkretne korzyści 🙂

Swinguj z nami czyli piszemy aplikację cz.2

Po kilku miesiącach od pierwszego wydania przyszła kolej na krok dalej – oto druga wersja klienta do gry w Arkadię, opartego na Swingu i mojej niedostatecznej znajomości Javy 🙂

Przyznaję, że wydanie to mogłoby pojawić się wcześniej. Jednakże wpierw mały romans z JEE, a potem wypadki losowe odwlekły ten fakt w czasie. Przez kilkanaście ostatnich dni przysiadłem fałdów i oto jest – druga wersja ChlebikClient. Wiele jeszcze jej brakuje do optymalnego funkcjonowania jako wymarzony klient, jednakże widać już pewne zarysy jak mogłaby wyglądać taka aplikacja, kiedy bierze się za nią newbies. Jednakże postanowiłem, że wydanie drugie będzie również i ostatnim – gdyż kolejne wydania byłyby po prostu dokładaniem bardzo podobnych do obecnych cegiełek. Czyli kolejny JInternalFrame, kolejny plik XMLa i tak dalej – ani to wiele już nie rozwija, a i sensowności dalszej w tym nie ma zbyt wiele.

Popracowałem z NetBeansem używając jego pomocy do tworzenia GUI, mam mniej więcej pojęcie o wątkach aplikacji pisanej w Swingu (choć i tak niewiele jak znam życie), operowanie podstawowe na plikach XML, poszczególne komponenty też obejrzałem sobie z kilku stron. Na dzień dzisiejszy dalsze prace zostają wstrzymane i odłożone na czasy obfitujące w wolny czas (może kiedyś nadejdą). Póki co zapraszam na podstronę projektu, gdzie można przeczytać listę zmian oraz ściągnąć najnowszą wersję.

Co zaś do przyszłości – zmierzam w kierunku JEE. Dla nauki rozpoczynam pisanie aplikacji internetowej. Wstępnie technologie użyte to JSP, Hibernate/JDBC, wszystko zapakowane w Tomcata. Być może tym razem spróbuję użyć do pracy Eclipse’a wraz z jego rozszerzeniem WTP.

JSF i dlaczego jestem zdumiony

Jak wspomniałem w jednym z poprzednich wpisów nabyłem trochę ciekawych książek o wiadomym temacie. No i po tym jak ‘prześlizgnąłem się’ po Head-First JSP i Servlety zabrałem się od razu za Core JSF. I ZONK! Po raz kolejny podejście wzięte z PHP sprawiło, że uderzyłem o ścianę. Ale tym razem cholera miało być inaczej.

Tok mego rozumowania był prosty – mamy JSP i serwlety. Służy to do pisania aplikacji webowych dla platformy JEE (nie całych, ale na pewno widoku i kontrolera). Znaczy się może służyć, bo wiadomym jest, że dzisiaj raczej w czystych językach nikt nie pisze – używa się różnych bibliotek i komponentów, których połączenie często okazuje się na tyle fajne, że powstaje framework. I takie coś mamy w PHP – język językiem, no ale w okolicy wersji 5.1 kiedy to OOP przestało być czymś na papierze, a stało się rzeczywistością pojawiły się pierwsze frameworki. Kiedy rozpoczynałem pracę jako koder PHP dostałem się w szpony Zend Frameworka i wciąż w nich tkwię. Po drodze bawiłem się trochę Symphony, a także RORem (choć to inny język). Dla mnie zatem framework to zestaw przynajmniej gotowych komponentów, które rutynowe czynności w budowie aplikacji sprowadzają do inicjalizacji kilku obiektów i wywołania ich metod. To co potrafi momentami Symphony czy ROR to już w ogóle kosmos i nic dziwnego, że ludzie tego używają.

Wracając do Javy – myślałem, że JSP to takie PHP, no a JSF to framework, czyli liznę tego pierwszego na razie byle się tylko orientować, przepatrzę książkę o JSFie i wezmę się za pisanie projektu łączącego tę wiedzę w jednym. Może to wina książki, albo i samego narzędzia. Jednakże załamało mnie to, iż po lekturze 100 stron to wiem na pewno, że wszystko o czym czytam zostanie omówione w następnych rozdziałach. No nic, przepatruję z ciekawości spis treści. Przerobiłem na chwilę obecną wstęp, coś o beansach, rozdzialik o nawigacji (XML rządzi) i zacząłem znaczniki standardowe. Zostaje mi rozdział o znacznikach niestandardowych, weryfikacji danych, obsłudze zdarzeń i… w sumie nic. Bo następne rozdziały to niestandardowe elementy, potem JDBC/LDAP, AJAX no i omówienie bardzo pokrótkie inych frameworków!!! Na koniec mam rozdział zatytułowany ‘Jak to zrobić?’. Mam pomysł by na tym pytaniu oprzeć ćwiczeniowy projekt pisany w JSP (aplikacji internetowej) zatem przerzuciłem te 400 stron i wziąłem się za szybkie przeglądanie rozdziału. ZONK!

Rozbiło mnie już pytanie trzecie (sic!) na łącznie około 35. Jak zaimplementować obsługę wysyłania plików na serwer?. O nareszcie coś z bliskich mi klimatów. Dla niezorientowanych w temacie PHP wyjaśniam – w tym języku jest to kwestia sprawdzenia czy przesłano element “file” z formularza, po czym sprawdza się czy plik doszedł i zapisuje się go w docelowym katalogu. W wersji spartańskiej robi się to może 5 linijkami kodu (wliczając po nawiasach klamrowych na linię). W Zend Frameworku mam do tego jego element w postaci Zend_File, a w nim cuda niewidy – walidatory, filtry, plus adaptery dla wysyłania plików. Można to łatwo zobaczyć – kilka linii kodu i działamy.

A co mamy w JSF? Chwilka, już liczę… Serwlet filtrujący (109 linii kodu) plus klasa renderująca (89 linii) plus klasa dla znacznika ( 28 linii ) plus sam plik JSP ( 18 linii ), ale to można pominąć. Szybkie dodawanie i okazuje się, że by wysłać plik na serwer i wiedzieć, że dotarł cały i bezpieczny muszę sklepać ponad 200 linii kodu!!! Cholera no to jest framework czy jakiś #$@#@%^^%? Jak można nazywać taką technologię frameworkiem, skoro zamiast usprawniać tworzenie aplikacji niewiele ona w sobie zawiera? No niby co poza jasno określonym MVC (ale przecież JSP też to ma, a tu jeszcze od cholery XMLa na ścieżki nawigacji), pomocą z renderowaniem widoku (View) czy w końcu znacznikami JSF technologia ta może zaoferować?

Ufff, przepraszam, uniosłem się, to był przedostatni raz. Naprawdę zaczynam rozumieć, dlaczego w ogłoszeniach o pracę dla javowców wymienia się po 4 elementy z każdej dziedziny (Struts, Spring, JSF, Ant, Maven, Hiberante, TopLink, iBATIS, JBoss, WebSphere, Tomcat). Nie wiem, ja generalnie młody jestem, zaczynam bawić się w te klocki, ale błagam, niech ktoś mi powie: “potem będzie lepiej”. Framework będzie frameworkiem z zapleczem, a nie tylko ulepszonymi grabiami czy łopatą. Proszę niech ktoś powie: “młody jesteś, ucz się dalej”. Jak już poznasz to zrozumiesz.

Proszę 🙂

Christmasing-Holidaying

Te słowotwórcze potworki w tytule tego wpisu doskonale oddają mój stan od kilku dni, a co więcej, będą dalej go uosabiać do 7 stycznia kiedy to po dłuuuuugiej przerwie pojawię się w pracy. Okazuje się, że nawał pracy przed świętami jest dobry, bo wyrobione wówczas nadgodziny można spożytkować w bardzo przyjemny sposób – na przykład mając możliwość pójścia na długi urlop. Na blogu nie piszę zasadniczo o niczym innym jak Java i pokrewne technologie, ale święta i urlopy to wyjątek.

Zatem zacznijmy od najważniejszego – życzę wszystkim czytelnikom mojego bloga (oj a naprawdę jest ich całkiem sporo, co mnie cieszy niepomiernie) spokojnych świąt i szczęśliwego nowego roku. Wiem, że podobne słowa krzyczą obecnie z każdego miejsca (nawet w lodówce :), ale pragnę zapewnić, że są to życzenia szczere i płynące z dobrego serca. Obyście mogli w spokoju wypocząć i nacieszyć się życiem, niezależnie czy uznajecie te święta czy nie. To raz.

Dwa, że znowu znikam na jakiś czas. Konkretnie – na czas urlopu. Wizyta w domu rodzinnym, a potem zapakunek do samochodu i wyprawa w kierunku Świeradowa-Zdrój, gdzie wraz z przyjaciółmi wybieram się w okresie sylwestrowo-noworocznym na narty. Czyli znowu góry, podobnie zresztą jak latem, ale kilka kilometrów na zachód od Szklarskiej Poręby. Na nartach ostatni raz jeździłem w liceum (tzw. zielone szkoły), czas zatem sobie po tych 6 latach przypomnieć o co w tym sporcie chodzi. Mam nadzieję, że nic się nie stanie, tym bardziej, że biorę żonę, która na nartach jeszcze nigdy nie jeździła 🙂 Do Warszawy wrócę 6 stycznia – i w okolicach tego dnia należy spodziewać się kolejnych wpisów.

Co do przyszłości – rysuje się ona w dość ciekawych kolorach. Jak tylko znajdę chwilkę czasu by wydać wersję 0.2 ChlebikClient zaczną się wpisy na temat J2EE, póki co głównie o JSP i servletach, z czasem pójdziemy dalej. Przy poprzednim urlopie zapowiadałem, że zacznę cykl o pisaniu systemu blogowego w JSP, ale czas i okoliczności zrewidowały te zamierzenia na rzecz Swinga i klienta do Arkadia MUD. Jednakże może to i dobrze? Zobaczymy co czas przyniesie, póki co kiełkuje u mnie powoli ciekawy pomysł na cykl artykułów, a także będący ich rezultatem portal.

Trzymajcie się ciepło.

Wierzchołek góry lodowej czyli Java i XML

O tym, że XML w świecie internetu odgrywa rolę fundamentalną nie muszę chyba nikogo przekonywać. O tym, że podobna sytuacja ma miejsce w przypadku Javy wiedziałem już wcześniej, choć dopiero lektura opisywanej ostatnio książki pokazała, że XML jest de facto standardem przyjętym w świecie Javy jeśli chodzi o konfigurację oraz przesyłanie danych. W kolejnej wersji ChlebikClient konfiguracja będzie w związku z tym oparta na plikach XML. Są one o tyle fajne, że ich edycja jest banalna, zaś jeśli chodzi o obsługę programową to mnogość narzędzi jest przytłaczająca.

W swojej aplikacji postanowiłem użyć powszechnie znanej biblioteki Dom4j. Umożliwia ona operowanie na plikach XML oferując API, które jest cokolwiek bardziej użyteczne niż te oferowane przez SAX, który jest niskopoziomowym API dla XMLa w Javie opartym na sterowaniu zdarzeniami. Odczyt pliku XMLa w przypadku dom4j jest banalny.

Podstawowym obiektem, na którym przyjdzie nam operować to org.dom4j.Element – jest to interfejs, który implementuje sporo klas z tego pakietu.

Załóżmy, że mamy plik XML z 2 elementami, w ktorych do tego mamy 2 potomków. Czyli np. element “wyszukiwarki” z dwoma potomkami (onet i google), a do tego też element “portale” z znów dwoma potomkami (interia i gazeta). Odczytanie tego pliku i zapisanie węzłów do LinkedHashSet można zrealizować tak (obiekt document jest instancją klasy org.dom4j.document, która reprezentuje cały wczytany plik XML):

org.dom4j.Element root = document.getRootElement();
LinkedHashSet listaWartosci = new LinkedHashSet();

for ( Iterator i = root.elementIterator(); i.hasNext(); ) {
DefaultElement element = (DefaultElement) i.next();
listaWartosci.add(element);
}

Proste? A pewnie, że proste. Choć to oczywiście wierzchołek góry lodowej jak mówi tytuł wpisu – o XMLu i Javie z pewnością jeszcze napiszę.

W końcu dobra książka

Ano to prawda. Nareszcie w Polsce ukazała się pozycja, która nie jest głębokim opisem języka, ale też nie jest szczegółowym opisem jednej z dostępnych technologii. Mowa o “Eclipse Web Tools Platform”, którą to dziś przez zupełny przypadek znalazłem w Empiku. Zdziwienie moje było tym większe, iż z ofertą wydawnictwa Helion staram się być w miarę na bieżąco, a o tej pozycji jakoś nic nie wiedziałem. Powód mej niewiedzy był prozaiczny – ktoś tę cenną pozycję wrzucił do działu ‘JavaScript’… Pozostawię bez komentarza.

No ale do rzeczy. Książka kosztuje jak typowa pozycja dotycząca Javy, czyli sporo (okrągłe 99 zł). Ale już po pobieżnym przejrzeniu wiedziałem, że na pewno nie będzie to chybiona inwestycja. O czym traktuje ta książeczka? Ano kilka osób tworzących WTP, czyli rozszerzenie Eclipse’a wspomagające budowanie aplikacji webowych i EE, wzięło się za napisanie na ten temat książki. I trafili w dziesiątkę (przynajmniej według mnie), gdyż do tej pory w naszym kraju nie było równie fajnej pozycji, która pokazywałaby proces tworzenia aplikacji tego typu od podstaw i z użyciem konkretnego narzędzia. Co więcej – poruszone są wszystkie zagadnienia szeroko pojętego J2EE – servlety, bazy, XML, webservices. Cud, miód i orzeszki. Wracam do lektury – mam nadzieję, że zaowocuje ona kilkoma wpisami na blogu.