Monthly Archives: January 2010

Nowe rozdanie

Ostatnio zacząłem zastanawiać się nad nową formą aktywności. Oczywiście rzecz wciąż ma dotyczyć programowania w Javie i okolicach – chodzi o pewną zmianę formuły. Otóż jak zauważyłem w statystykach odsłon największą popularnością cieszą się wpisy, które niekoniecznie od razu mają coś wspólnego z realizowanymi projektami.

Rzecz jest całkiem zrozumiała – jeśli ktoś np. nie potrzebuje JSF w ogóle, wówczas nie będzie czytał wpisów dotyczących tego frameworka. No i tak ze wszystkim – czytamy to, co nas interesuje. Przykład z poprzednich dni – krótki wpis o tablecie od Appla spowodował, że pobity został dzienny rekord ilości odsłon (dla ciekawych – obecnie wynosi od 230 wyświetleń). Stąd też naszła mnie myśl, aby ten trend wykorzystać. Po raz kolejny zainspirował mnie Jacek Laskowski (oj zbiera się tych inspiracji) – postanowiłem wydzielić część merytoryczną mojego bloga i przenieść ją na wiki, które uruchomiłem pod adresem http://wiki.chlebik.pl. Na razie przeniosłem tam tylko pierwszy artykuł o stawianiu serwera dla Javy. Cykle o HowToJava oraz ProgramBash znajdą się tam również, jednakże wymaga to o wiele więcej pracy redakcyjnej (dodanie formatowania typowego dla Wiki).

Co zaś z blogiem? Rzecz jasna dalej będą pojawiały się wpisy stricte merytoryczne (czyli omawiające tworzenie aplikacji w Javie krok po kroku) – przynajmniej do czasu zakończenia prac nad ProgramBash, aby wyszukiwarki nie prowadziły w 2 różne miejsca. Myślę, że formuła bloga sprawdza się przy moim stylu tworzeniu wpisów oraz poznawania Javy. Czyli wraz z tworzeniem kodu powstaje wpis. Niestety często przez to jego ostateczny kształt nie jest taki, jakim chciałbym go widzieć. Czasem zdarzy się literówka, coś niedopatrzyłem w kodzie lub też dowiedziałem się, iż pewne rzeczy można zrobić inaczej. I wiki świetnie się do tego nada – pojawiające się tam artykuły (tudzież cykle) powstaną po jakimś czasie od zakończenia wpisów na blogu. Dzięki temu z perspektywy czasu (a i pewnie zwiększanego doświadczenia i wiedzy) artykuły będą bardziej dopracowane. Na blogu zaś pojawi się więcej wpisów “okołotematowych” – z pewnością znajdą się artykuły o bazach danych, systemach linuxowych, a i może innych językach programowania (ciągnie mnie do C i C++, a także do Flexa, choć jego użyję pewnie razem z Javą). Pozwoli to na rozszerzenie kręgu potencjalnych czytelników, a i wprowadzi też pewien porządek we wszystkich wpisach, których nota bene zebrało się już 106.

Przy okazji fundamentalnych zmian koncepcyjnych zmieniłem też skórkę bloga (choć w sumie jej regularne zmienianie to u mnie tradycja). Podobnie jak poprzednia jest bardzo rozciągliwa, dzięki czemu szerokie listingi nie są łamane i czytelność kodu jest bardzo duża. A kolor? Mój pokój z komputerem ma groszkowe ściany, niedługo przyjdzie wiosna, będzie lepiej 🙂

Advertisements

Zatabletowanie

Od mniej więcej dwóch tygodni odnoszę wrażenie, że do końca nie jestem “pro” w dzisiejszym świecie. Można machnąć ręką na fakt, iż nie bawię się Twitterem, nie posiadam konta na Facebooku, albo że nie mam najnowszej wersji GG. Jednakże nowy produkt firmy Apple wzbudził we mnie tyle odczuć, że naprawdę aż żal nie popełnić przy tej okazji wpisu.

Z produktami wspomnianej firmy kontakt mam pośredni. Wokół mnie ludzie używają zarówno iPhona jak i makówek do codziennej pracy. Jestem skłonny rozumieć gadżeciarzy – dla nich (teraz już nie, ale kilka miesięcy temu na pewno) posiadanie lanserskiego telefonu z jabłuszkiem było szczytem marzeń. Z tego zresztą co wiem telefon jest całkiem znośny, a dodatkowe możliwości są OK. Powstała też zresztą przy tej okazji nowa jakość, która nazywa się Android (nota bede zamierzam kupić sobie niedługo HTC Hero). Zgoda jest.

Użytkowników makówek też jestem w stanie zrozumieć. Do zabaw z grafiką i filmami są jak znalazł, znajdzie się też wystarczająco w nich mocy i programów by móc programować i to w różnych językach i platformach. Do tego niby zgodność sprzętu z systemem operacyjnym, dopracowane rzekomo aplikacje, klasa i styl. Rozumiem to. Nawet fakt, iż za taką przyjemność trzeba zapłacić i po 5 tysięcy złotych, a w takiej cenie można dostać naprawdę dobrego laptopa.

Natomiast dlaczego nowy wynalazek Appla czyli iPad postrzega się jak lekarstwo na raka i AIDS w jednym – nie wiem i nie rozumiem. Zasadniczo urządzeniem, które do iPada jest podobne to netbooki. Jak pisałem niedawno kupiłem sobie takowy by móc pracować/czytać w drodze do pracy. I tutaj faktycznie tablecik byłby niezły – ma trochę mniejszy ekran, ale za to w najgorszym razie waży coś koło 0,7kg. I do tego ma trzymać nawet 10h na baterii. Fajnie. Problem tylko w tym, że:

  • nie ma multitaskingu!!!
  • nie obsługuje flasha
  • nie ma USB
  • nie ma kamery (choć to można przeżyć)

To rzeczy chyba najbardziej kłujące w oczy od samego początku. I zasadniczo jedynym frontem, na którym broni się iPad to właśnie mobilność, ale niestety tylko tyle. Czyli zasadniczo jest to powiększony iPhone z jednym dużym wyjątkiem – ceny. 600$ to w Polsce będzie pewnie ok. 2,5-3k (uwzględniając strasznie dziwne przeliczniki i marże ). W związku z tym mam pytanie – czy tylko ja nie widzę dla tego typu urządzeń miejsca, czy po prostu jestem ślepy i potrzebuję pomocy?

Sesja i zmiana layoutu w ProgramBash

Po rejestracji/zalogowaniu w jakimkolwiek serwisie internetowym w zdecydowanej większości przypadków zmianie ulega layout strony. Czasem prawie wcale, jednakże najczęściej pojawia się całkiem sporo nowych rzeczy – pasek z linkami, informacje o sesji i różne takie. Nie ma w tym nic zdrożnego zatem i w przypadku ProgramBash wypadałoby zrobić coś podobnego.

Prace zakończyliśmy w momencie, w którym rejestrujemy nowego użytkownika i automatycznie jest on logowany do serwisu. Jak również pisałem poprzednio po fakcie rejestracji jesteśmy przerzucani (póki co) na stronę główną z radosnym komunikatem o powodzeniu operacji. Jednak poza tym komunikatem, absolutnie nic nie pokazuje nam, że jesteśmy zalogowani, zaś o takich detalach jak login choćby nie wspomnę. Czyli musimy pobawić się w logikę widoku.

Dla przypomnienia – w przypadku ProgramBash warstwą widoku rządzi biblioteka Apache Tiles. Przypomnieć również wypada, iż obecnie istniejący layout został zdefiniowany w taki oto sposób (plik faces-config.xml):

<tiles-definitions>
    <definition name="guestLayout" template="/WEB-INF/layout/guestLayout.jsp">
   
         <put-attribute name="upperMenu" value="/WEB-INF/layout/upperMenuTemplate.jsp" />
         <put-attribute name="content" />
         <put-attribute name="footer" value="/WEB-INF/layout/footerTemplate.jsp" />

    </definition>
</tiles-definitions>

Nazwy mówią za siebie – domyślnie mamy do czynienia z guestLayout, czyli widokiem przeznaczonym dla niezalogowanych użytkowników. Zadanie jest proste – wyświetlić kilka dodatkowych informacji bazując na fakcie, iż jesteśmy zalogowani. I tutaj zasadniczo problem się rozwiązał, gdyż na dłuższą metę rzecz w 2 instrukcjach warunkowych, które umieszczone w widoku (konkretniej w kafelku z menu) będą określały czy coś pokazać, czy nie. Dlatego też ten zapis pozostanie niezmieniony, zaś przegrzebiemy kilka rzeczy w odpowiednim pliku JSP, a konkretnie upperMenuTemplate.jsp. Wyglądać on będzie po tej operacji tak:

 <div id="menu">
                <ul>
                    <li><a href="/glowna.faces" title="home">GŁÓWNA</a></li>
                    <li><a href="/top" title="top">TOP</a></li>
                    <li><a href="/szukajka" title="Szukaj">SZUKAJ</a></li>

                     <h:panelGroup rendered="#{userController.logged == true}">
                         <li><a href="/dodajwpis" title="Dodaj wpis">DODAJ WPIS</a></li>
                         <li><a href="/logout.faces" title="Wyloguj">WYLOGUJ</a></li>
                    </h:panelGroup>

                    <h:panelGroup rendered="#{userController.logged == false}">
                        <li><a href="/rejestruj.faces" title="Rejestracja">REJESTRACJA</a></li>
                        <li><a href="/login.faces" title="Zaloguj">ZALOGUJ</a></li>
                    </h:panelGroup>
                    
                    <li>
                        <a href="#" onclick="document.getElementById('mp').component.show(); return false;" tabindex="0">O AUTORZE</a>
                    </li>
                    <li><a href="mailto:michpio@gazeta.pl" title="Contact">KONTAKT</a></li>

                     <h:panelGroup rendered="#{userController.logged}">
                         <li>
                             <h:form>
                                 Zalogowany jako: <strong><h:commandLink action="#{userController.showUser}" ><h:outputText value="#{user.nick}" /></h:commandLink></strong>
                                 <h:inputHidden id="userId" value="#{user.id}" />
                             </h:form>
                         </li>
                    </h:panelGroup>
                </ul>
        </div>

Jak widać do klasy UserController dodano własność logged wraz ze stosownymi metodami dostępowymi. Rzecz jest dość prosta. Bardziej skomplikowaną rzeczą jest stworzenie czegoś na kształt ACLa, który pilnowałby użytkownika przed wejściem pod wskazane adresy. Skoro bowiem ktoś jest już zalogowany, to po co miałby wchodzić ponownie na ekran rejestracji? Takie sprawdzenie pewnych warunków (konkretniej zalogowania, przynajmniej na razie) musi odbywać się za każdym żądaniem skierowanym do aplikacji. Tutaj przydałby się nam znów jakiś phaseListener. Jednakże by ułatwić sobie pracę wpierw dodamy możliwość zalogowania się oraz wylogowania – bez konieczności rejestrowania się w serwisie za każdym razem. Dodamy zatem widok związany z logowaniem. Oto jak powinien wyglądać:

<%@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 id="registrationForm" prependId="false">

	<fieldset>
		<legend>Zaloguj się do serwisu</legend>
		
			<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}">
                                 <f:validateLength maximum="30" minimum="5" />
                            </h:inputSecret>
                            <h:message for="pass" errorClass="error"/>
                        </div>
              
	</fieldset> 

	<div class="buttonrow">
            <h:commandButton type="submit" value="Zaloguj" id="login" immediate="false" action="#{userController.login}"  />
	</div>

        </h:form>

</div>

Jak widać rzecz jest prosta jak konstrukcja snopowiązałki. 2 pola, pod które podpięto walidatory i akcja to wywołania. Akcję wrzucamy do naszej starej i znajomej klasy userController. Dzięki temu część kodu będzie można pewnikiem zrefaktoryzować i sprawić by służył zarówno rejestracji jak i logowaniu. Kod akcji prezentuje taki oto listing:

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

        FacesContext context = FacesContext.getCurrentInstance();
        Map params = context.getExternalContext().getRequestParameterMap();
       
        String pass       =    (String)  params.get("pass");
        String email      =    (String)  params.get("email");

         SessionFactory sessionFactory = ProgramBashUtil.getSessionFactory();
         Session session = sessionFactory.openSession();
         String toReturn  =   "login-failed";

        try {
               Query zapytanie = session.createQuery( "FROM User WHERE email = '" + email + "' AND pass = '" + MD5.MD5( pass ) + "'" );
               User existingUser = (User) zapytanie.uniqueResult();

               if( existingUser instanceof User ) {
                   context.getExternalContext().getSessionMap().put("user", existingUser );
                   logged = true;
                   context.addMessage( null, new FacesMessage( FacesMessage.SEVERITY_INFO, "Logowanie powiodło się", "") );
                   toReturn = "login-success";
               } else {
                   context.addMessage( null, new FacesMessage( FacesMessage.SEVERITY_ERROR, "Podano złe dane!", "") );
                   toReturn = "login-failure";
               }
    
               return toReturn;

        } catch( Exception e ) {
            context.addMessage( null, new FacesMessage( FacesMessage.SEVERITY_ERROR, "Logowanie nie powiodło się!", "") );
            return "login-failure";
        } finally {
            session.close();
        }
    }

I takie oto działanie podpięte zostaje pod wywołanie widoku /login.faces. Możemy się zatem spokojnie zalogować. Efektem będzie taki oto choćby widok:

Wylogowanie z kolei podpięliśmy pod stosowny link w górnym menu. Kod w kontrolerze użytkownika jest prościutki:

 /**
     * Metoda sluzaca do wylogowania z serwisu
     *
     * @return String
     */
    public String logout() {

         FacesContext context = FacesContext.getCurrentInstance();
         context.getExternalContext().getSessionMap().put( "user", null );
         logged = false;
         context.addMessage( null, new FacesMessage( FacesMessage.SEVERITY_INFO, "Poprawnie wylogowano Cię z serwisu!", "") );
         return null;
    }

Teraz kiedy już mamy możliwość zalogowania się i wylogowania kiedy tylko chcemy możemy przystąpić do implementacji czegoś na kształt ACLa. Jak już wspomniałem wyżej potrzebny nam będzie do tego kolejny phaseListener, który będzie sprawdzał czy user jest zalogowany czy nie, a tym samym czy pewne akcje są dla niego dostępne, a które nie są.

package com.wordpress.chlebik.filters;

import com.wordpress.chlebik.controllers.UserController;
import java.util.Arrays;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.servlet.http.HttpSession;

/**
 * Klasa obslugujaca ACL
 *
 * @author Michał Piotrowski
 */
public class ACLHandler implements PhaseListener {

    String[] loggedNotAllowed    =   new String[]{ "/login.jsp", "/rejestruj.jsp" };
    String[] loggedNeeded        =   new String[]{};

    public PhaseId getPhaseId() {
         return PhaseId.ANY_PHASE;
    }

    @Override
     public void beforePhase(PhaseEvent e) {

        if( e == null ) {
            return;
        }

        if( e.getPhaseId()== PhaseId.RENDER_RESPONSE )
        {
            FacesContext facesContext = e.getFacesContext();

            HttpSession session = (HttpSession) facesContext.getExternalContext().getSession(true);
            UserController controller = (UserController) session.getAttribute( "userController" );
            String viewId = facesContext.getViewRoot().getViewId();
            Arrays.sort( loggedNotAllowed );
            Arrays.sort( loggedNeeded );

            if( controller != null && controller.isLogged() ) {

                int viewIndex = Arrays.binarySearch( loggedNotAllowed, viewId );

                if( viewIndex > 0 ) {
                    facesContext.addMessage( null, new FacesMessage( FacesMessage.SEVERITY_ERROR, "Musisz się wylogować by móc skorzystać z tej opcji!", "") );
                    facesContext.getApplication().getNavigationHandler().handleNavigation(facesContext, "", "logoutNeeded" );
                }

            }  else {

                int viewIndex = Arrays.binarySearch( loggedNeeded, viewId );

                if( viewIndex > 0 ) {
                    facesContext.addMessage( null, new FacesMessage( FacesMessage.SEVERITY_ERROR, "Musisz się zalogować by móc skorzystać z tej opcji!", "") );
                    facesContext.getApplication().getNavigationHandler().handleNavigation(facesContext, "", "loginNeeded" );
                }
            }
        }
    }

    @Override
    public void afterPhase(PhaseEvent event) { }

}

Rzecz jasna klasę należy dorzucić do konfiguracji. Modyfikujemy zatem plik faces-config.xml:

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

I tym samym mamy śliczny pseudo-ACL. Z premedytacją użyłem przedrostka “pseudo”, gdyż w duzych i skalowalnych aplikacjach ACL opiera się najczęściej na rolach/grupach przywilejów, często z możliwością ich modyfikacji, zaś to wszystko wyciągane z bazy danych i równie często trzymane w cache. Jednakże na potrzeby naszej aplikacji powyższe rozwiązanie działa bardzo dobrze – zasadniczo niewiele rzeczy (tak planuję) będzie wymagało zalogowania/wylogowania. Grunt, iż podane rozwiązanie działa.

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.

[NAPRAWIONE] ProgramBash – nie wiem co się zepsuło

Pisałem ostatnio, że po przygodach z nowym NetBeans miałem problemy z uruchomieniem ProgramBash. Jednakże jakoś to poszło – lokalnie zaczęło działać i zabrałem się za kończenie rejestracji. Upload pliku, jego obróbka, validacja istnienia emaila w bazie i takie tam. Działało. Do wczoraj.

Miałem trochę przerwy w kodowaniu (nowy netbook i parę innych rzeczy), zatem kodu zasadniczo nie ruszałem. Przy okazji dodawania wysyłki plików na serwer (poprzez komponent do uploadu z Richfaces) za każdym wysłaniem walidował się cały formularz (ale tylko “w tle”, nie były wyświetlane komunikaty o tym we froncie). Miodzio. Wczoraj wróciłem od tego tematu. I co? ZONK. Kliknięcie przycisku commandButton w ogóle nie uruchamia wskazanej w atrybucie action metody!!! No jak to możliwe – wcześniej działał (nawet mam przecież screeny z tegoż faktu), a teraz nagle przestał? Może to ta migracja na JSF 2? Jednakże dokumentacja nic nie mówi o zmianie atrybutów czy w ogóle struktury tagów. Co więcej – zarzut, że być może nie ładują się konfiguracje (np. z faces-config.xml) jest nietrafiony – bo np. lista newsów na stronie głównej się wyświetla.

Przyznam szczerze, że zaczynam mieć powoli dość JSF. Ilość rzeczy, które ni cholery nie chcą działać tak jak to czytam w książkach/tutorialach/przykładach jest wręcz zatrważająca. Chyba czas przerzucić się na Springa i usługi sieciowe. Autentycznie jestem wkurzony. Ktoś może ma jakiś pomysł?

UPDATE: ostatecznie wyjaśniło się co i z czym. Okazało się, że winne było dopisanie do tagu FORM typu w postaci enctype=”multipart/form-data”. Przez to JSF nie potrafił rozpoznać requestu i nie stosował wszystkich przewidzianych walidatorów i reszty przepływu aplikacji. Banał, ale jak wkurzający

Kupiłem netbooka

Ostatnio cicho i sza na blogu, gdyż skutecznie czas wykorzystuję codziennie dojeżdżając do pracy. A to za sprawą mojego najnowszego nabytku – netbookowi Samsung NC10.

Cała rzecz odbyła się dość szybko. Policzyłem sobie ile spędzam mniej więcej czasu w środkach komunikacji publicznej (codziennie) i wyszło mi z tego jakieś 1,5h. Hmmm, 90minut * 5 dni pracy = 450 minut / 60 = 7,5h tygodniowo. No to jak siedzę po nocach średnio od godziny 23 do 2-3 nad ranem wyrabiam tygodniowo jakieś 12-17h. Czyli dorzucenie do tychże godzin dodatkowych 7,5 to potencjalny wzrost “wydajności” o około 50%. Sporo. Decyzja została podjęta.

Szybki research uświadomił mi jedną rzecz – wszystkie netbooki są do siebie niemożliwie wręcz podobne. 99% sprzętu ma Intel Atom na pokładzie (1,6 GHz), 1GB RAMu, wbudowaną grafikę i dźwięk, ekran 10,2 cala i podstawowe w sumie wejścia (VGA, USB, liniówki do dźwięku). Jakiekolwiek odchyły z definicji przekładają się na konieczność dopłacenia kilkuset złotych. Przeglądając Allegro w poszukiwaniu odpowiadającego mi sprzętu znalazłem właśnie mojego Samsunga NC10. Co jest w nim tak istotnego? Ano kilka rzeczy:

  • jest bardzo lekki. Ledwo 1,33kg co wśród netbooków jest dobrym wynikiem.
  • czas pracy baterii jest baaaardzo długi (tak wyszło z testów przeprowadzonych przez Komputer Świat). Nie było to dla mnie aż tak istotne (czas podróży do pracy w jedną stronę to max godzina i 15 minut, zatem nie potrzebowałem jakiś rewelacyjnych osiągów), ale skoro jest to tym lepiej.
  • najistotniejsza rzecz to 2 GB RAMu. I to przeważyło. System na pokładzie to Windows XP – preinstalowany rzecz jasna – jednakże 2 GB RAMu spokojnie dają radę.

Komputer kupiłem używany (raptem 2 miesiące bodajże) – wygląda ślicznie, ani ryski (na ekranie wciąż folia ochronna) – wszystko pięknie i ładnie. System sprawuje się na medal, zainstalowałem nawet Netbeans i na razie nie jest źle. Do czego używam owo cudo? Ano Javy jako takiej jeszcze na nim nie próbowałem (może jutro się za to wezmę). 2GB RAMu to dużo, jednakże to zawsze netbook – uruchomienie Glassfisha może być zbyt wielkim obciążeniem (wraz z Netbeans rzecz jasna). Na razie grzecznie zabawiam się Ruby’m, gdyż gdzie się nie obejrzę to język ten jest używany (albo implementowany jako wewnętrzny, że o JRuby nie wspomnę). Grunt jednakże, iż zyskałem masę dodatkowego czasu z komputerem. Może i teraz wieczorne zmęczenie nie będzie powodowało wyrzutów sumienia.

Nie samym Helionem Polska stoi

I zasadniczo całe szczęście. Za starych dawnych czasów, kiedy to szczytem innowacyjności była “żółta biblioteka” (BUM – Biblioteka Użytkownika Mikrokomputerów) oraz książki wydawnictwa ReadMe (bodajże o Visual Basicu z kolesiem będącym skrzyżowaniem Dextera z Johnym Bravo w roli głównej) nikt za bardzo nie słyszał o czymś takim jak Helion. Wszyscy byli szczęśliwi. Do czasu.

Coś się po drodze stało i powstał Helion. Nic złego w tym nie ma – im więcej podmiotów na interesującym nas podwórku tym lepiej. Jednakże z czasem zaczęło się coś psuć. Zasadniczo półki w księgarniach zawsze były pełne tylko Helionowych biblii. Może to wina tego, iż bynajmniej nie wychowywałem się w Warszawie czy innym dużym mieście, zatem i ilość księgarni była dość ograniczona. Zamawianie książek przez internet też nie od razu zyskało popularność. I tak sobie informatycznie rosłem – w objęciach Helionu. A były kiedyś inne wydawnictwa – i całkiem niedawno sobie o nich przypomniałem.

Czekam ponownie na kupon rabatowy z Helionu 🙂 Mam nadzieję, że “kryzys” nie spowoduje jakiś ograniczeń w tej dziedzinie. Rok temu trochę $$ wydałem przy okazji rabatowych zakupów, w tym roku również nie miałbym nic przeciw. Jednakże czekając na atrakcyjne zniżki i rabaty zapuściłem się głębiej w czeluście internetu, próbując namierzyć po nazwach znane mi kiedyś wydawnictwa. Jednym z nich było wydawnictwo Mikom, które wydawało fajniutkie książeczki (miałem jedną bodajże o Norton Commanderze i o Corel Draw). Wrzucamy nazwę w wujka Googla i co się okazuje? Nasze wydawnictwo działa w ramach PWNu! No to już bardzo fajnie. Przeglądam listę dostępnych pozycji, trochę tam tego jest, przyznaję. Moje najnowsze zakupy to:

Modelowanie systemów informatycznych w języku UML 2.1
Podstawy architektury i technologii usług XML sieci WEB
Oracle PL/SQL. Kieszonkowy słownik języka

Takie oto pozycje odebrałem na poczcie dzień przed świętami. Najbardziej cieszy mnie druga pozycja – całościowe opisanie usług sieciowych – co z czym i dlaczego. Takiej książki w Helionie nie ma. Podobnie pozycja o UMLu pozytywnie mnie zaskoczyła – mamy i ćwiczenia, pytanka, a nie tylko suche opisanie wszystkich potencjalnych diagramów i tyle. Książeczkę o Oraclu wziąłem głównie dlatego, aby nie płacić za przesyłkę 🙂 choć ostatnio coraz bardziej interesuję się tym RDBMSem (poczytuję sobie w drodze do pracy tę oto pozycję). Pewnie niedługo zainstaluję go na twardzielu w wersji Express, coby pobawić się nim troszeczkę bardziej.

Dlaczego powstał ten wpis? By przypomnieć, że i w Polsce wydawane są czasem książki, które są na tyle dobre, że nie trzeba szukać ich odpowiedników w Apressie, Manningu czy O’Reillym. Wystarczy troszeczkę poszukać. Ostatnio zaczyna się również coś dziać w bliskim Javowym podwórku – wspominałem już o książce dotyczącej Hibernate. Wydawnictwo PowerNet też ruszyło temat GWT, zobaczymy co wydadzą niedługo. Warto też zajrzeć na strony WNT – to o wiele bardziej poważne publikacje, niekoniecznie w pełni praktyczne, ale z pewnością ciekawe. Świetnie też radzi sobie PJWSTK – ze swoją serią wydawniczą przeznaczoną głównie dla studentów. Jest z czego wybierać.