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

4 thoughts on “Kończymy rejestrację w ProgramBash

  1. Bartek Majsak

    Patrzac na pierwszy przyklad z rich:fileUpload naszla mnie taka refleksja, ze ktos tu zdrowo przekombinowal z atrybutami 😀 Nie dalo sie tego zaprojektowac w jakis przejrzysty sposob? Naprzyklad zagniedzone tagi etc?

    Pozdrawiam,
    Bartek

  2. chlebik Post author

    Jak widac wiekszosc rzeczy dotyczy ostylowania i komunikatow. Pod tym wzgledem Richfaces jest tragiczne.

  3. Tomek N.

    Kilka uwag co do przedstawionego kodu:

    1. Używasz na potęgę statycznych metod z jakiegoś utila. Domyślam się, że nie próbowałeś testować takiego kontrolera jednostkowo? W praktyce static i testy jednostkowe wzajemnie się wykluczają.

    2. UploadItem#getData() – to wywołanie powoduje załadowanie całego obrazka do pamięci (do 2 MiB). Nie znam JSF, ale to chyba niezbyt dobrze?

    3. Unikalność plików tymczasowych z obrazkami jest wątpliwa – wystarczy dwóch użytkowników uploadujących plik w tej samej sekundzie, co nie jest niczym niezwykłym.

    4. Jeszcze co do samego pliku: polecam raczej File#createTempFile(). Zresztą sam pomysł na taki “cache” jest wątpliwy, nie zadziała chociażby przy migracji sesji na inny serwer.

    5. captcha.toLowerCase().equals( “chlebik” ) == false && captcha.toLowerCase().equals( “chleb” ) == false można zapisać prościej:
    !captcha.toLowerCase().equals( “chlebik” ) && !captcha.toLowerCase().equals( “chleb” )

    6. Nie zamykasz sesji Hibernate -> połączenia JDBC

    7. Ręczne użycie context.getExternalContext().getRequestParameterMap() – czy JSF nie powinien jakoś sam mapować parametrów żądania?

    8. Trzymanie jakichkolwiek danych na dysku (z reguły w kontekście multimediów) zamiast w bazie jest przedmiotem zaciekłych dyskusji, także tylko wspominam ;-).

    Mam nadzieję, że uwagi się przydadzą. I pamiętaj o testach jednostkowych!

  4. chlebik Post author

    1. Testow jako takich do JSF nie podpinam, gdyz na razie zasadniczo podstawa jest ogarniecie sie w konstrukcji aplikacji co z JSFem jakos mi wybitnie trudno przychodzi. Akurat TDD zaklada z gory pisanie testow przed kodowaniem, ale ja nawet do konca nie widzialbym co testowac 🙂

    2. W mysl tego co wyczytalem jak widac w metodzie “listener” jako parametr do metody przekazywany jest obiekt eventu. Tym samym wywolanie “getData” to po prostu skorzystanie z tego co zostalo przeslane w requescie (AJAXowym poprzez RichFaces). Zatem ten obiekt/zdjecie juz i tak wisi w pamieci bo gdzie mialby wisiec? Zaraz potem jest zapisywany na dysku w postaci pliku.

    3. Nie jest watpliwa. Pracuje w poszkole.pl – serwisie, ktory notuje olbrzymia ogladalnosc i taka metoda jest uzywana w zasadniczo wszelakich prawie uploadach plikow. Prawdopodobienstwo, ze naraz 2 ludzi pusci do serwera request z avatarem jest baaardzo maly. Moze facebook czy NK ma tego typu problemy. Ja sie poki co nie spotkalem.

    4. Taki kodzik zaimportowalem ze strony RichFaces. To aplikacja edukacyjna – no i tez od razu obrazek leci do pliku wiec problemu nie ma.

    5. Mozna, tak jest czytelniej w tych mikroskopijnych listingach jakie oferuje WordPress.

    6. Fakt, moj blad.

    7. Wyciaganie w ten sposob jest pokazane w ksiazce “Core JavaServerFaces” zasadniczo w kazdym przykladzie, ktory tam jest zaprezentowany. Zakladam zatem, ze tak to wyglada.

    8. Jak juz mowilem jestem przyzwyczajony do trzymania wszystkiego w zewnetrznych plikach. Zreszta wygodniej jest miec plik na dysku – latwiej toto podpiac pod subdomene i wyobraz sobie taka sytuacje (i pewnie ona bedzie miala miejsce), ze renderujemy np. 20 najbardziej aktywnych ludzi umieszczjajacych logi. Wyciagniecie 20 obrazkow z bazy to koniecznosc przeslania miedzy baza i serwerem, a potem do usera ilustam KB/MB. W przypadku plikow przesylamy juz gotowy plik, ktory gdzies tam sobie lezy.

    Dzieki bardzo za komentarz – oby takich wiecej od czytelnikow moich wypocin.

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