Stronnice Chlebika – Java Blog for Newbies

Luty 6, 2010

Uniwersytet też dla ludzi

Zaszufladkowany do: Life — chlebik @ 9:39 am

Uniwersytet kończyłem, nawet studia dzienne, problem polega na tym, iż nie kończony kierunek nie był informatyką. Odbije się to pewnie jeszcze nie raz przy procesach rekrutacyjnych, ale cóż począć – tak wyszło. Dlatego cieszy dość ciekawa inicjatywa, jaką podjęły nasze uniwerki.

Konkretnie chodzi o zamieszczanie w sieci materiałów dydaktycznych, których autorami są pracownicy naukowi tychże uczelni. Nie mówię zatem o skryptach pisanych przez studentów, notatkach z wykładów i laborek, ale w pełni opracowanych materiałach dydaktycznych. Od kilku lat w sieci można znaleźć tego typu materiały prezentowane przez ludzi z informatyki UW, zaś w ostatnich dniach doszedł też AGH. Co więcej – w przypadku tej ostatniej uczelni mamy nie tylko dane z informatyki, ale również szeregu innych wydziałów. Bardzo miłe inicjatywy i dla takich osób jak ja – bezcenne.

Luty 3, 2010

Formatowanie kodu w aplikacjach webowych w najprostszy mozliwy sposób

Zaszufladkowany do: JavaScript, JavaServer Faces, Narzędzia, ProgramBash — chlebik @ 2:36 pm

Przymierzając się wprowadzenia funkcjonalności dodawania wpisów w ProgramBash natknąłem się na problem wprowadzenia formatowania kodu. No i mały problem.

Domyślnie do aplikacji będzie można wklejać dowolny niemalże język programowania. Oczywiście pojawia się problem formatowania kodu źródłowego – zgodzimy się, iż czyste wskaźniki <pre> to nie jest dobre rozwiązanie. Oczywiście istnieje masa skryptów, które zajmują się tego typu konwersją, jednakże najcześciej są one napisane w PHP, zaś przepisywanie tego typu skryptów to benedyktyńska praca i niezbyt do tego rozwojowa. Java pod tym względem (dostępności takich skryptów) jest o wiele bardziej uboga. Jednakże okazuje się, iż istnieje inny sposób – można do tego typu zadań zaprząc JavaScript!. Jest to najzwyklejszy skrypt w tymże języku, dodane pliki stylowania kodu i viola – po wrzuceniu do skryptu łańcucha z kodem powinniśmy mieć pięknie sformatowany kod.

Narzędzie to nazywa się niezbyt oryginalnie SyntaxHighlighter, zaś jego strona domowa to wiki, gdzie znajdziemy wszystkie możliwe informacje. Na razie wygląda to bardzo obiecująco – jak tylko wezmę się za kolorowanie składni to na pewno skorzystam z tego skryptu.

Luty 1, 2010

[NAPRAWIONY] Błąd sesji przy starcie ProgramBash

Zaszufladkowany do: JavaServer Faces, ProgramBash — chlebik @ 12:59 pm

Zdziwiłem się kiedy przy okazji restartu serwera odwiedziłem stronkę ProgramBash. Ni stąd ni z owąd dostałem ekran błędu (rzuconego wyjątku). Hmmm, coś nie bardzo.

Szybkie spojrzenie do sysloga i co się okazuje? To wina mojego phaseListenera, który jest wywoływany po raz pierwszy i sprawdza czy mamy widok do odtworzenia. Kod odpowiedzialny za rzucanie wyjątku jest związany z sesją – śliczny NullPointerException. Pomaga ponowne odświeżenie okna przeglądarki. Bug niemożliwy i to z mojej winy. Poprawię go pewnie jak dotrę do domu.

No i poprawiłem. Zmieniony kod można znależć w ostatnim wpisie poświęconym ProgramBash

Debian jako serwer Javy – podstawy systemu

Zaszufladkowany do: Linux — chlebik @ 12:29 am

Poprzedni wpis (będący jednocześnie pierwszym) na temat wykorzystania Debiana jako serwera dla aplikacji Java skupiał się wyłącznie na uruchomieniu usług, które miały po prostu działać. Dziś skupimy się bardziej na samym systemie.

Użytkownicy systemów Windows są przyzwyczajeni do konieczności poinstalacyjnych operacji związanych ze ściąganiem olbrzymiej liczby poprawek połączonych z restartami systemu. W przypadku linuxa jest lepiej – cała operacja aktualizacji systemu przebiega bardzo sprawnie i nie wymaga spędzenia przy komputerze kilku godzin.

Zanim jednakże weźmiemy się do pracy kilka słów wyjaśnienia. Zakładam, iż czytelnik ma rozeznanie w podstawowych komendach w systemach linuxowych. Jeśli takowego brak (w co wątpię) wówczas można zajrzeć pod ten adres, gdzie można znaleźć szybką ściągawkę z podstawowych komend oraz ich parametrów. Zakładam również, że posiadamy na danym serwerze prawa ROOTa – mowa o konfiguracji serwera zatem oczywistym powino być, że mamy pełną kontrolę nad systemem.

Aktualizacje

Podstawową rzeczą w systemie jest jego ciągła aktualizacja. Na pewno zaś przynajmniej aktualizacje bezpieczeństwa, które niemożliwiają skorzystanie w naszym systemie z odkrytych w oprogramowaniu luk. Rzecz jest zatem bardzo istotna, ale tym samym diablo nudna i rutynowa. Potencjalna ilość aktualizacji codziennie dołączanych do dystrybucji jest bardzo duża. Dlatego też rzecz jasna cały proces trzeba zautomatyzować. Od wydania wersji Debian Woody sugerowanym rozwiązaniem jeśli chodzi o zarządzanie oprogramowaniem w systemie jest program apt. Po części to właśnie on stał się powodem olbrzymiej popularności Ubuntu. Klepiemy linijkę kodu i oto mamy zainstalowane oprogramowanie. To właśnie wzięło się z Debiana i jest jego olbrzymią siłą. Pełną dokumentację narzędzia apt warto przeczytać, zaś znaleźć można ją pod tym adresem.

Zaraz po instalacji najlepiej w konsoli wydać takie oto polecenia:

apt-get update <-- Co zaktualizuje liste pakietow
apt-get upgrade <-- Wprowadzi zmiany w systemie

Co sprawi, że wszystkie pakiety zainstalowane wraz z systemem zostaną zastąpione ich nowszym wersjami (jeśli takowe w ogóle istnieją). W przypadku zwykłego systemu serwerowego, który bynajmniej nie posiada serwera Xów, ani dużej ilości oprogramowania wchodzącego w skład KDE czy Gnome procesy akutalizacji są szybkie i nie wymagają ściągania olbrzymich ilości danych. Zasadniczo nie ma opcji by system automatycznie ściągał poprawki i je instalował – mogłoby to spowodować prawdziwą katastrofę w przypadku zwłaszcza serwera produkcyjnego. Dlatego praktyką jest automatyczne pobieranie aktualizacji, ale decyzja czy je zainstalować jest pozostawiana administratorowi. Można to osiągnąć instalując narzędzie cron-apt, które zgodnie z nazwą cyklicznie dokonuje aktualizacji pakietów. Mamy też możliwość dość solidnej konfiguracji tego narzędzia – w sumie dość profesjonalnym jest otrzymywanie mejli za każdym razem kiedy coś pójdzie nie tak. Wstęp do instalacji tego narzędzia i jego używania można znaleźć pod tym adresem. W związku z tym, iż posiadam tylko jeden serwer pozwoliłem sobie zostawić na razie ręczne akutalizowanie systemu. Na razie :)

Ekran logowania

Kolejna rzecz jest kosmetyką, ale dość istotną jeśli mamy raptem 1 serwer. Otóż istnieje możliwość własnego zdefiniowania bannera, który pojawi się przy zalogowaniu się do systemu przez SSH. By osiągnąć taki efekt musimy poddać edycji plik /etc/ssh/sshd_config i w nim odkomentować stosowną linijkę:

Banner /etc/issue.net

Dzięki temu w pliku wskazanym (tutaj /etc/issue.net) możemy wkleić swój komunikat. Restartujemy demona SSH i oto mamy efekt – mała rzecz, a jak cieszy.

Co w procesach piszczy

Przy starcie każdego sytemu operacyjnego po załadowaniu podstawowych rzeczy składających się na sam system, rozpoczyna się sekwencyjne uruchamianie procesów. Jeśli uruchamiamy linuxa choćby na komputerze lokalnym wówczas mamy możliwość zobaczenia komunikatów o startowaniu kolejnych programów/demonów. Tym samym uruchamiane są kolejne procesy. Proces to nic innego jak pewne działanie, które jest wykonywane na serwerze. Każdy z procesów jest identyfikowany jednoznaczenie przez swój PID, czyli ID procesu. By zobaczyć działające w systemie procesy wydajemy w konsoli polecenie top lub htop (to troche ladniejsze i bardziej rozbudowane narzędzie). U mnie wygląda to tak:

Zasadniczo nie ma tam nic podejrzanego. Mamy i procesy Javy, mamy serwer WWW, działa też PHP i kilka podstawowych dla systemu rzeczy. Najistotniejszą rzeczą jest proces o nazwie init:

1 root 20 0 1988 528 496 S 0 0.1 0:00.56 init

Jest to pierwszy proces odpalany w systemie. Powoduje on zainicjalizowanie systemu, a następnie tenże system przechodzi po kolei po wszystkich skryptach znajdujących się w katalogach rcX.d, gdzie X to kolejne cyfry oznaczające tzw. runlevel. Dla nas interesujące są te z przedziału 2-5, gdyż oznaczają one normalne działanie systemu (w sensie uruchamiania). Level 0 to zamykanie systemu, zaś 6 to jego restart. Warto pochodzić po tychże katalogach i zobaczyć co z czym. U mnie dla przykładu katalog rc2.d wygląda tak:

drwxr-xr-x 2 root root 4096 Jan 31 20:48 .
drwxr-xr-x 74 root root 4096 Jan 31 21:18 ..
-rw-r--r-- 1 root root 556 Aug 12 2008 README
lrwxrwxrwx 1 root root 17 Dec 13 10:08 S10rsyslog -> ../init.d/rsyslog
lrwxrwxrwx 1 root root 14 Dec 13 10:17 S12dbus -> ../init.d/dbus
lrwxrwxrwx 1 root root 22 Dec 13 10:18 S14avahi-daemon -> ../init.d/avahi-daemon
lrwxrwxrwx 1 root root 13 Dec 13 10:08 S16ssh -> ../init.d/ssh
lrwxrwxrwx 1 root root 23 Dec 13 11:46 S17mysql-ndb-mgm -> ../init.d/mysql-ndb-mgm
lrwxrwxrwx 1 root root 19 Dec 13 11:46 S18mysql-ndb -> ../init.d/mysql-ndb
lrwxrwxrwx 1 root root 15 Dec 13 11:46 S19mysql -> ../init.d/mysql
lrwxrwxrwx 1 root root 18 Dec 21 12:04 S20lighttpd -> ../init.d/lighttpd
lrwxrwxrwx 1 root root 15 Dec 13 10:45 S20nginx -> ../init.d/nginx
lrwxrwxrwx 1 root root 23 Dec 13 10:08 S20openbsd-inetd -> ../init.d/openbsd-inetd
lrwxrwxrwx 1 root root 17 Dec 13 10:08 S20postfix -> ../init.d/postfix
lrwxrwxrwx 1 root root 18 Jan 31 20:48 S20startphp -> ../init.d/startphp
lrwxrwxrwx 1 root root 13 Dec 19 09:57 S21fam -> ../init.d/fam
lrwxrwxrwx 1 root root 13 Dec 13 10:08 S89atd -> ../init.d/atd
lrwxrwxrwx 1 root root 14 Dec 13 10:08 S89cron -> ../init.d/cron
lrwxrwxrwx 1 root root 17 Dec 13 11:08 S91apache2 -> ../init.d/apache2
lrwxrwxrwx 1 root root 17 Dec 13 10:26 S92tomcat6 -> ../init.d/tomcat6
lrwxrwxrwx 1 root root 18 Dec 13 10:08 S99rc.local -> ../init.d/rc.local
lrwxrwxrwx 1 root root 19 Dec 13 10:08 S99rmnologin -> ../init.d/rmnologin
lrwxrwxrwx 1 root root 23 Dec 13 10:08 S99stop-bootlogd -> ../init.d/stop-bootlogd

Jak zatem widać wszystko co się znajduje w tym katalogu (i w pozostałych też) to zasadniczo dowiązania symboliczne do skryptów z katalogu /etc/init.d. W ten sposób startują wszystkie nasze usługi i demony – poprzez dowiązanie symboliczne do skryptu. Istnieje też plik/skrypt o nazwie rc.local, w którym powinny znaleźć się nasze skrypty, które mają być uruchamiane po każdorazowym starcie systemu. Jednakże u mnie to rozwiązanie nie zadziałało (użyłem tego pliku do wrzucenia startu PHP ). I potem dziwiłem się czemu za każdym restartem (w sumie raptem 2 do tej pory, no ale zawsze) nie działały mi aplikacje PHP. Odpowiedź na ten problem znajduje się nawet w oficjalnej dokumentacji Debiana (punkt 10.6). By zatem wystartować PHP podczas startu systemu trzeba zrobić następujące czynności:

  • Tworzymy plik w katalogu /etc/init.d. Ja nadałem mu nazwę startphp.
  • Jego zawartość jest krótka
    /usr/bin/spawn-fcgi -a 127.0.0.1 -p 9000 -u www-data -g www-data -f /usr/bin/php5-cgi -P /var/run/fastcgi-php.pid -C 12
  • Informujemy system o fakcie istnienia takiego skryptu. W tym celu użyjemy narzędzia update-rc.d, które to trzeba wywołać z odpowiednimi parametrami. Ja zrobiłem to dość skromnie:
    update-rc.d /etc/init.d/startphp defaults
    I teraz skrypt ten będzie uruchamiał się podczas startu systemu (w runlevelach 2-5). Więcej na temat tego narzędzia oraz jego parametrów można znależć na stronach dokumentacji systemowej.

Rzecz jasna w ten sam sposób moglibyśmy podpiąć do wykonywania plik rc.local, ale po namyśle doszedłem do wniosku, że rozdzielenie skryptów będzie bardziej czytelne. Choć tutaj już każdy musi podjąć decyzję, co mu bardziej pasuje. Podobną decyzję trzeba też podjąć jeśli chodzi o procesy już uruchamiane. Przykładem jest choćby demon avahi, który jest dostarczany wraz z instalacją systemu i automatycznie uruchamiany. Co ten konkretny demon robi można przeczytać tutaj, zaś dyskusję na temat jego pozostawienia i używania tutaj. Mi jakoś specjalnie do niczego potrzebne to nie będzie, zatem trzeba temat wyrzucić. Komenda:

update-rc.d -f /etc/init.d/avahi-daemon remove

I po restarcie mamy nadzieję, że demon już nie powstaje. Niestety jest to nadzieja płonna – albowiem taki trick w Debianie bynajmniej nie wystarcza. Należy jeszcze edycji poddać plik /etc/default/avahi-daemon i tam dopisać linijkę z dyrektywą:

AVAHI_DAEMON_START

I ustawić jej wartość na 0. Reboot i w procesach już Avahi nie ma.

Co słychać w portach

Poza procesami warto też przyjrzeć się otwartym portom i usługom na nich działającym. Pomoże nam w tym taka instrukcja:

netstat -nlp

Pokaże ona interesujące nas rzeczy.

Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:869 0.0.0.0:* LISTEN 693/famd
tcp 0 0 127.0.0.1:9000 0.0.0.0:* LISTEN 663/php5-cgi
tcp 0 0 94.23.178.117:3306 0.0.0.0:* LISTEN 792/mysqld
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN 308/portmap
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 557/nginx
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 414/sshd
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 653/master
tcp6 0 0 :::8080 :::* LISTEN 740/jsvc
tcp6 0 0 :::22 :::* LISTEN 414/sshd
udp 0 0 0.0.0.0:111 0.0.0.0:* 308/portmap

Portmapper to demon, który jest potrzebny do działania RPC, zatem zostawiamy go póki co sobie. Ciekawym z kolei jest pierwszy wiesz, czyli port 693 i działający tam demon famd. Szybka wycieczka do wujka googla i dowiadujemy się, iż jest to demon, którego celem jest monitorowanie zmian w systemie plików. Więcej na ten temat pod tym adresem no i co istotne – zalecana jest jego zamiana na demona gamin. Lecimy zaptem:

apt-get install gamin

I dostajemy w odpowiedzi:

chlebik@chlebik:~$ sudo apt-get install gamin
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following extra packages will be installed:
libgamin0
The following packages will be REMOVED:
fam libfam0
The following NEW packages will be installed:
gamin libgamin0
0 upgraded, 2 newly installed, 2 to remove and 54 not upgraded.
Need to get 102kB of archives.
After this operation, 65.5kB disk space will be freed.
Do you want to continue [Y/n]? Y
Get:1 http://ftp.debian.org lenny/main libgamin0 0.1.9-2 [37.8kB]
Get:2 http://ftp.debian.org lenny/main gamin 0.1.9-2 [64.0kB]
Fetched 102kB in 0s (558kB/s)
(Reading database ... 24203 files and directories currently installed.)
Removing fam ...
Stopping file alteration monitor: FAM.

dpkg: libfam0: dependency problems, but removing anyway as you request:
lighttpd depends on libfam0.
Removing libfam0 ...
Processing triggers for man-db ...
Selecting previously deselected package libgamin0.
(Reading database ... 24181 files and directories currently installed.)
Unpacking libgamin0 (from .../libgamin0_0.1.9-2_i386.deb) ...
Selecting previously deselected package gamin.
Unpacking gamin (from .../gamin_0.1.9-2_i386.deb) ...
Setting up gamin (0.1.9-2) ...
Setting up libgamin0 (0.1.9-2) ...

I po reboocie jest już lepiej:

tcp 0 0 127.0.0.1:9000 0.0.0.0:* LISTEN 636/php5-cgi
tcp 0 0 94.23.178.117:3306 0.0.0.0:* LISTEN 784/mysqld
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN 308/portmap
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 561/nginx
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 414/sshd
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 628/master
tcp6 0 0 :::8080 :::* LISTEN 694/jsvc
tcp6 0 0 :::22 :::* LISTEN 414/sshd
udp 0 0 0.0.0.0:111 0.0.0.0:* 308/portmap

Ciekawe też rzeczy może nam pokazać znany pogram nmap, ale tutaj zainteresowanych odsyłam na stronę domową nmapa.

Chcę oglądać twoje logi

Zasadniczo wszystkie aplikacje pozostawiają po sobie jakieś logi. Jest to dość naturalne – jak inaczej sprawdzić czy z naszymi programami wszystko porządku jeśli nie podsłuchując co do nas mówią. Problemem jednakże stają się przestarzałe artefakty – kiedy nagle kilkaset MB dysku jest zajętych przez stare pliki z logami. Możnaby rzecz jasna napisać własny skrypt powłoki, który usuwałby wskazane pliki (i najlepiej podpiąć go do crontaba i byłby spokój). Jednakże jest to rozwiązanie dość toporne. Rzecz jasna problem nie pojawił się wczoraj i powstały stosowne narzędzia dla uporania się z tym problemem. Jednym z nich jest logrotate. U mnie ta aplikacyjka była już zainstalowana i działała całkiem sprawnie. W katalogu /etc/logrotate.d/ znalazły się informacje o logach ngixa, lightiego, mysqla i kilku innych. Nie grozi mi zatem wyczerpanie miejsca na dysku :)
Pomijając usuwanie starych logów, warto też czasem zajrzeć do /var/log/syslog by wiedzieć co pod obudową się dzieje.

Zakończenie

Przedstawiłem dość podstawowe rzeczy dotyczące administracji serwerem opartym na Debianie. Rzecz jasna codzienna praktyka z pewnością będzie bardziej skomplikowana, coś pewnie nie raz się zepsuje i nie będzie działać tak jak powinno. Jeśli znalazłeś jakiś błąd w tekście, albo też masz pomysł, co by tu jeszcze dopisać proszę pisz śmiało.

Styczeń 31, 2010

Nowe rozdanie

Zaszufladkowany do: Blog — chlebik @ 10:04 am

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

Styczeń 29, 2010

Zatabletowanie

Zaszufladkowany do: Life — chlebik @ 6:27 am

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?

Styczeń 27, 2010

Sesja i zmiana layoutu w ProgramBash

Zaszufladkowany do: JavaServer Faces, ProgramBash — chlebik @ 11:10 pm

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.

Styczeń 24, 2010

Kończymy rejestrację w ProgramBash

Zaszufladkowany do: JavaServerPages, ProgramBash — chlebik @ 11:00 pm

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ę 110×100px. 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.

Styczeń 16, 2010

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

Zaszufladkowany do: JavaServer Faces, ProgramBash — chlebik @ 11:56 am

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

Styczeń 14, 2010

Kupiłem netbooka

Zaszufladkowany do: Life — chlebik @ 10:56 pm

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.

Starsze wpisy »

Blog na WordPress.com.