08 lutego 2008

Nawigacja w Wicket

6 komentarzy
Pora na kwestię nawigacji w Wicket. Waldi już Wam wszystko zdradził w Wicket po godzinnej przygodzie - moje trzy grosze..., ale ja lubię po malutku.

Porównując Wicketa do najbliższego znanego mi partnera, który odpowiadałby spojrzeniu na kwestię strona vs akcja w podobny sposób, czyli JSF, w Wicket próżno by szukać pliku z nawigacją typu faces-config.xml, gdzie definiujemy nawigację między stronami deklaratywnie. W przypadku Wicketa mamy do dyspozycji programistyczną nawigację.

Do aktualnej aplikacji, którą rozwijam od kilku dni rozpoznając Wicketa (Wicket i jego podejście zorientowane na widok, Wicket prościej z mavenowym archetypem wicket-archetype-quickstart czy Pierwsze kroki z Apache Wicket 1.3) dodaję nową stronę Powitanie.html
<html>
<head>
<title>Powitanie</title>
</head>
<body>
Witaj <span wicket:id="login"></span>!
</body>
</html>
oraz odpowiadającą jej klasę pl.jaceklaskowski.wicket.Powitanie.
package pl.jaceklaskowski.wicket;

import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;

public class Powitanie extends WebPage {

private static final long serialVersionUID = 1L;

public Powitanie(String login) {
add(new Label("login", login));
}
}
Wiadomo, dlaczego konieczna jest para html-java do reprezentacji pojedyńczej strony w Wicket i klasa Powitanie nie powinna nastręczać problemów w zrozumieniu, co autor miał na myśli. Novum aplikacji polega na skorzystaniu z metody org.apache.wicket.Component.setResponsePage(Page page), która instruuje Wicketa o widoku do wyświetlenia. Podobnie jak ma to miejsce w JSF brak strony (dokładniej: wskazanie na nieistniejący identyfikator bez związanej z nim strony) lub zwrócenie null powoduje powrót do strony inicjującej żądanie, w Wicket brak wskazania kolejnej strony poprzez metodę Component.setResponsePage skutkuje powrotem do strony inicjującej żądanie.

Dla kompletnej prezentacji zmian klasa pl.jaceklaskowski.wicket.HomePage reprezentująca stronę HomePage.html. Należy zwrócić uwagę na użycie setResponsePage() w metodzie onSubmit(), która wywoływana jest jako wynik zatwierdzenia formularza loginForm.
package pl.jaceklaskowski.wicket;

import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.model.PropertyModel;

public class HomePage extends WebPage {

private static final long serialVersionUID = 1L;

private String login;

public HomePage(final PageParameters parameters) {

add(new Label("message", "If you see this message wicket is properly configured and running"));
Form loginForm = new Form("loginForm") {
public void onSubmit() {
setResponsePage(new Powitanie(getLogin()));
}
};
loginForm.add(new TextField("login", new PropertyModel(this, "login")));
add(loginForm);
}

public String getLogin() {
return login;
}

public void setLogin(String login) {
this.login = login;
}
}
Jedną z istotnych cech Wicketa jest dowolność w konstruowaniu stron reprezentowanych przez klasy Java. W przypadku klasy Powitanie konstruktor wymagał podania parametru typu String, który następnie wykorzystałem do zainicjowania parametru login, podczas gdy klasa HomePage udostępniała konstruktor public HomePage(final PageParameters parameters). W jaki sposób i kiedy tworzymy stronę decydujemy my. Z tym, że jeśli chcielibyśmy wywołać stronę z przeglądarki konieczne jest udostępnienie bezparametrowego konstruktora lub konstruktora z pojedyńczym parametrem typu org.apache.wicket.PageParameters, jak to ma miejsce w przypadku strony HomePage.

Wywołanie wybranej strony polega na podaniu jej w adresie URL - http://localhost:8080/wicket-demo/?wicket:bookmarkablePage=:pl.jaceklaskowski.wicket.HomePage, gdzie wartością wicket:bookmarkablePage jest strona do wyświetlenia. Próba wywołania strony Powitanie zakończy się błędem.

p.s. Kontynuując moje p.s.-owanie odnośnie konkursu Blog Roku 2007 muszę przyznać, że ostatnimi czasy bardzo jesteście pracowici. Z 9. miejsca z 49 głosami Notatnik przesunął się na 7. miejsce z 57 głosami! Należą się podziękowania wszystkim, którzy zechcieli brać udział w konkursie. Dziękuję! Gdyby jednak porównać to ze statystykami poczytności Notatnika, gdzie do ponad 100 skrzynek trafia streszczenie brakuje mi kolejnej 50-tki bez wysłanego SMSa (treść B00248 pod numer 71222). Gdyby do tego dodać, że każdy ma dziewczynę, chłopaka, dzieci, znajomych i wszyscy mają komórki to możnaby tak spróbować przesunąć Notatnik ponad robale i ipoda. Niewiele brakuje, ale i czasu coraz mniej.

07 lutego 2008

Book review: Java EE 5 Development using GlassFish Application Server

0 komentarzy
Kolejną książkę, którą przyszło mi przeczytać podczas niekończącej się podróży do Stanów była Java EE 5 Development using GlassFish Application Server autorstwa Davida Heffelfinger'a wydana przez Packt w październiku 2007 roku. Książka zawiera 400 stron, jednakże wiele z nich to zrzuty ekranu, przykłady, a jeśli do tego dodać prezentację technologii Java EE 5 to okaże się, że samej wiedzy o GlassFishu może nie być wiele. Książka zaskoczyła mnie kilkakrotnie (głównie pozytywnie), więc ostateczna ocena jest wysoka, chociaż osoby szukające informacji nt. GlassFisha mogą się zawieść. Z tego punktu widzenia książka nie odpowiada tytułowi, albo inaczej - jego uwypukleniu GlassFisha, co może sugerować, że książka jest rzeczywiście o nim. Nic bardziej mylnego. Jest tam trochę o GlassFishu, ale większość informacji to dobre wprowadzenie i późniejsze rozwinięcie tematu prezentacji Java EE 5. Książkę czyta się lekko i przyjemnie, więc warto po nią sięgnąć. Już jest na półce w Bibliotece Warszawskiego JUGa.

Zainteresowanych recenzją zapraszam do lektury Book review: Java EE 5 Development using GlassFish Application Server (ze względu na wymogi wydawnictwa recenzja jest w języku angielskim).

Wicket i jego podejście zorientowane na widok

17 komentarzy
Wielu z Was pracujący z różnymi szkieletami programistycznymi do tworzenia aplikacji webowych zauważyła już zapewne wyłaniający się ich podział względem ich głównego zainteresowania - strona vs akcja. Dla mnie najbardziej interesującymi szkieletami wydają się być te, dla których w centrum zainteresowania znajduje się strona, dla której budowany jest model obiektowy (ang. page-centric approach), np. JSF czy GWT. Istnieją również takie, gdzie główny nacisk kładzie się na wykonywane akcje (ang. action-centric approach) jak np. Struts, gdzie programista tworzy zazwyczaj akcje reagujące na wybrane zdarzenia bez ich szczególnego związku ze stroną, z której są wywoływane. Nie wdając się w szczegóły napiszę, że Wicket jest zorientowany na stronę/widok, co jest zauważalne już przy pierwszym zetknięciu z nim. Istnieje bezpośrednia zależność między klasą w Javie reprezentującą stronę (dziedziczy po org.apache.wicket.markup.html.WebPage), a stroną HTML, która jej odpowiada. Po pierwsze ich nazwy muszą być identyczne z dokładnością do rozszerzenia (java dla klasy, a html dla strony). Druga niezwykle istotna cecha Wicketa to utworzenie modelu w klasie dziedziczącej po wspomnianym WebPage, który musi odpowiadać stronie HTML. Odstępstwo od tej zasady kończy się przykładowo następującym błędem:
ERROR - RequestCycle               - The component(s) below failed to render. 
A common problem is that you have added a component in code but forgot to reference it in the markup
(thus the component will never be rendered).

1. [MarkupContainer [Component id = anotherUserId, page = pl.jaceklaskowski.wicket.HomePage,
path = 0:loginForm:anotherUserId.TextField, isVisible = true, isVersioned = false]]

org.apache.wicket.WicketRuntimeException: The component(s) below failed to render.
A common problem is that you have added a component in code but forgot to reference it in the markup
(thus the component will never be rendered).

1. [MarkupContainer [Component id = anotherUserId, page = pl.jaceklaskowski.wicket.HomePage,
path = 0:loginForm:anotherUserId.TextField, isVisible = true, isVersioned = false]]

at org.apache.wicket.Page.checkRendering(Page.java:1102)
at org.apache.wicket.Page.renderPage(Page.java:899)
at org.apache.wicket.request.target.component.BookmarkablePageRequestTarget.respond(BookmarkablePageRequestTarget.java:231)
at org.apache.wicket.request.AbstractRequestCycleProcessor.respond(AbstractRequestCycleProcessor.java:103)
at org.apache.wicket.RequestCycle.processEventsAndRespond(RequestCycle.java:1100)
at org.apache.wicket.RequestCycle.step(RequestCycle.java:1169)
at org.apache.wicket.RequestCycle.steps(RequestCycle.java:1245)
at org.apache.wicket.RequestCycle.request(RequestCycle.java:535)
at org.apache.wicket.protocol.http.MockWebApplication.processRequestCycle(MockWebApplication.java:356)
at org.apache.wicket.protocol.http.MockWebApplication.processRequestCycle(MockWebApplication.java:341)
at org.apache.wicket.util.tester.BaseWicketTester.startPage(BaseWicketTester.java:289)
at pl.jaceklaskowski.wicket.TestHomePage.testRenderMyPage(TestHomePage.java:21)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at junit.framework.TestCase.runTest(TestCase.java:164)
at junit.framework.TestCase.runBare(TestCase.java:130)
at junit.framework.TestResult$1.protect(TestResult.java:106)
at junit.framework.TestResult.runProtected(TestResult.java:124)
at junit.framework.TestResult.run(TestResult.java:109)
at junit.framework.TestCase.run(TestCase.java:120)
at junit.framework.TestSuite.runTest(TestSuite.java:230)
at junit.framework.TestSuite.run(TestSuite.java:225)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at org.apache.maven.surefire.junit.JUnitTestSet.execute(JUnitTestSet.java:213)
at org.apache.maven.surefire.suite.AbstractDirectoryTestSuite.executeTestSet(AbstractDirectoryTestSuite.java:138)
at org.apache.maven.surefire.suite.AbstractDirectoryTestSuite.execute(AbstractDirectoryTestSuite.java:125)
at org.apache.maven.surefire.Surefire.run(Surefire.java:132)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at org.apache.maven.surefire.booter.SurefireBooter.runSuitesInProcess(SurefireBooter.java:290)
at org.apache.maven.surefire.booter.SurefireBooter.main(SurefireBooter.java:818)
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.593 sec <<< FAILURE!

Results :

Tests in error:
testRenderMyPage(pl.jaceklaskowski.wicket.TestHomePage)

Tests run: 1, Failures: 0, Errors: 1, Skipped: 0

[INFO] ------------------------------------------------------------------------
[ERROR] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
Błąd wystąpił z powodu zadeklarowania
 new TextField("anotherUserId", new Model(""))
, podczas gdy odpowiadająca strona HTML nie zawierała elementu oznaczonego przez wicket:id="anotherUserId".

W stronie HTML, dla każdego elementu zawierającego atrybut wicket:id musi istnieć jego odpowiednik w reprezentacji obiektowej w Javie (z pewnymi wyjątkami).

Model, który jest jednym z najważniejszych pojęć Wicketa, jest źródłem danych dla komponentu. Komponentem nazywa się obiektowy odpowiednik elementu na stronie HTML z atrybutem wicket:id. W naszym przypadku następująca deklaracja odpowiada zasileniu pola tekstowego przez ciąg pusty.
 new TextField("userId", new Model(""))
Niektóre z komponentów Wicketa (uwaga na duże podobieństwo do JSF!) potrafią reagować na zdarzenia po stronie klienta (strona HTML). W naszym przypadku formularz na stronie (element form) ma odpowiednik w postaci klasy org.apache.wicket.markup.html.form.Form w modelu obiektowym (w Javie) i zatwierdzenie formularza skutkuje wywołaniem metody Form.onSubmit(). Istotna uwaga w JavaDoc dla klasy Form:

However, it is not necessary to use Wicket's button class, just putting e.g. <input type="submit" value="go"> suffices.

co powoduje, że jest to ten właśnie wspomniany wyjątek, gdzie element na stronie HTML nie musi mieć reprezentacji obiektowej w klasie dziedziczącej po WebPage.

Ciekawe jest pobieranie wartości z widoku (strona HTML) poprzez org.apache.wicket.model.PropertyModel. Wystarczy zarejestrować obiekt, którego wartość chcemy pobierać/ustawiać w danym elemencie na stronie i umieszczamy go w WebPage.
 public class HomePage extends WebPage {

private String login;

public String getLogin() {
return login;
}

public void setLogin(String login) {
this.login = login;
}

public HomePage(final PageParameters parameters) {

Form loginForm = new Form("loginForm") {
public void onSubmit() {
System.out.println("onSubmit wywolany z login: " + getLogin());
}
};
loginForm.add(new TextField("login", new PropertyModel(this, "login")));
add(loginForm);
}
}
Ostatecznie moja pierwsza aplikacja prezentuje się następująco.
package pl.jaceklaskowski.wicket;

import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.model.PropertyModel;

public class HomePage extends WebPage {

private static final long serialVersionUID = 1L;

private String login;
private String password;

public HomePage(final PageParameters parameters) {

add(new Label("message", "If you see this message wicket is properly configured and running"));
Form loginForm = new Form("loginForm") {
public void onSubmit() {
System.out.println("onSubmit wywolany z login: " + getLogin());
}
};
loginForm.add(new TextField("login", new PropertyModel(this, "login")));
add(loginForm);
}

public String getLogin() {
return login;
}

public void setLogin(String login) {
this.login = login;
}
}
, gdzie odpowiadająca strona HTML jest jak następuje.
 <html>
<head>
<title>Wicket Quickstart Archetype Homepage</title>
</head>
<body>
<strong>Wicket Quickstart Archetype Homepage</strong>
<br/><br/>
<span wicket:id="message">message will be here</span>
<form wicket:id="loginForm" action="something">
Login: <input type="text" wicket:id="login"/>
<br>
<input type="submit" value="Wcisnij mnie"/>
</form>
</body>
</html>
Nad widokiem możnaby popracować, ale jak wspominałem daleko mi do HTMLowego mistrza sztuk walki, więc zostawiam to innym. Jak widać jest to o wiele bardziej możliwe niż w przypadku JSF, gdzie liczba nieznajomych znaczników może przerazić nawet najbardziej gorliwych praktyków technologii klienckich jak HTML, CSS, JavaScript i DHTML. Zauważam wiele podobieństw JSF do Wicketa i coraz bardziej zastanawiam się, który był inspiracją dla drugiego. Podobno rozpoczęły się prace nad wprowadzeniem Wicketa jako technologii "wizualizacji" alternatywnej do JSF w JBoss Seam. Zobaczymy, co z tego wyniknie. Jak na razie jestem pod dużym wrażeniem łatwości programowania z Wicket, chociaż nie ukrywam, że łączenie strony HTML z klasą WebPage za pomocą identyfikatorów wicket:id jest irytujące. Chciałoby się coś mniej "błędotwórczego" (chociaż na pytanie Co miałoby to być? nie znam odpowiedzi).

p.s. Kontynuując mój zachwyt dla Waszego zawzięcia z wysyłaniem SMSów w konkursie Blog Roku 2007 spieszę donieść, że Notatnik przesunął się na pozycję 9 z 49 głosami. Udało się więc przekonać kolejnych 6 czytelników Notatnika do zagłosowania wysyłając SMSa o treści B00248 pod numer 71222. Jeszcze brakuje do pierwszej trójki, ale idzie ku dobremu. Wierzę, że nie opuści Was nadzieja na 1. miejsce.

06 lutego 2008

Book review: Learning Ruby

2 komentarzy
Skończyłem czytanie kilku książek, które grupa Warszawa JUG otrzymała do recenzji od różnych wydawnictw, więc pora na give back. Na pierwszy ogień poszła książka o Ruby - Learning Ruby by Michael Fitzgerald (O'Reilly May 2007). Książkę można potraktować jako wprowadzenie do języka Ruby, po którym koniecznie należy uzupełnić wiedzę o praktyczne jego wykorzystanie. Bez tego pozostaje niedosyt i ciągłe drapanie się po głowie, dlaczego Ruby to taki ciekawy język i jak tworzyć z jego pomocą aplikacje.

Zainteresowanych moją recenzją zapraszam do lektury Book review: Learning Ruby. Recenzja jest po angielsku, gdyż tego oczekiwał wydawca. Ostatnio będąc w Traffic w Warszawie widziałem jej polskie tłumaczenie od Helionu, więc można do niej zajrzeć bez znajomości angielskiego.

Książka wraca na półkę biblioteki Warszawskiego JUGa i jest do wypożyczenia przez grupowiczów.

p.s. Nieustannie mnie zaskakujecie, kiedy codziennie widzę jak liczba wysłanych SMSów na Notatnik wzrasta w konkursie Blog Roku 2007. Z miejsca 9-tego z 37 głosami Notatnik znalazł się na 11-tym z 43 głosami (!) Nie jest to dokładnie pnięcie się w górę pod względem zajmowanego miejsca, ale liczby wysłanych SMSów jak najbardziej. Do Mikrobów (aktualny blog na 1. miejscu) pozostaje jeszcze 71 głosów.

05 lutego 2008

Wicket prościej z mavenowym archetypem wicket-archetype-quickstart

5 komentarzy
2. dzień w Stanach nie mniej obfitujący w ciekawostki niż pierwszy. Co mnie zaskoczyło ogromnie to 3 sprawy. Śniadanie prawie w całości oparte o przetwory mleczne plus płatki. Owoce też są, ale trochę mniej. Dużo różnistego pieczywa, do tego przetwory mleczne i niewiele owoców (sporo bananów). Wystarczy, aby wyobrazić sobie jak po tej wyprawie może człowiek wypaść wagowo. Dodając do tego siedzący tryb pracy i waga skacze w górę z każdym dniem. Szczęśliwie, apartament, w którym stacjonuję, ma do dyspozycji korty i, co najbardziej istotne, basen z placami do kosza. Właśnie - do kosza. I tutaj pojawia się kolejna ciekawostka. Nie brałem ze sobą piłki wiedząc, że tutaj będzie można zakupić Spaldinga za grosze, co dzisiaj potwierdziłem w Walmarcie. Właśnie z niego wróciłem, gdzie znalazłem Spalding NBA "TACK SOFT" Composite leather basketball za 16 USD (!) Brakuje jeszcze butów, stroju i można ruszać. Temperatura w dzień wystarczająca na grę. Podobno jednak najtaniej wychodzi kupowanie na Amazonie lub temu podobnych sklepach i kierowanie przesyłek do hotelu. Wtedy "schodzi" dodatkowa marża sklepowa, więc cena może jeszcze spaść.
Ostatnią rzeczą, która mnie dzisiaj zaskoczyła była liczba innych nacji niż tą, którą ja nazwałbym amerykańską (teraz dopiero załapię się na miano rasisty, czy podobnie). Nie wdając się w szczegóły (a mając potwierdzenie u osób z Włoch, Indii oraz Szwajcarii) liczba innych nacji w San Jose waha się w granicy 99% (!) Jest ich na prawdę wiele, więc często słyszy się hiszpański, czy jego amerykańską odmianę (spenglish) i Ci, którzy przyjechali poszlifować angielski mogą się zawieść. Może pora zabrać się za hiszpański? Jestem w Stanach z Włochem, który przekonuje mnie do włoskiego, którego, jeśli dobrze zrozumiałem, ciekawostką jest zakończenie wszystkich słów samogłoską. Kiedy czytałem na głos książkę tego Włocha o historii Trzeciej Rzeszy, której druga część zaczynała się od rozdziału o Polsce, okazało się, że wymowa jest identyczna do naszej z dokładnością do litery 'z', która staje się naszym 'c' oraz ch => k i c => k. Jeśliby wspomnieć o bliskości włoskiego do hiszpańskiego, to warto rozważyć włoski jako potencjalny, trzeci język europejczyka, za angielskim i niemieckim (później hiszpański i ostatecznie francuski).

Koniec z tymi dywagacjami na tematy inne niż javowe. Pora na właściwe danie w postaci tworzenia aplikacji opartych o Apache Wicket z pomocą specjalnie w tym celu przygotowanemu archetypowi mavenowemu - wicket-archetype-quickstart. Rola archetypów w Apache Maven 2 polega na uproszczeniu zestawiania projektów, tj. ułożeniu struktury katalogowej z niezbędnymi plikami konfiguracyjnymi (więcej o nich w dokumencie Introduction to Archetypes). Problemów z zestawianiem projektu z Wicketem doświadczyłem kilka dni temu (Pierwsze kroki z Apache Wicket 1.3) i nie długo trwało zanim dowiedziałem się o istnieniu dedykowanego archetypu, który zdejmie ze mnie niemiły obowiązek dbania o wymagane zależności do poprawnego ustawienia projektu aplikacji z Wicket. A trzeba było czytać dokumentację, gdzie w pliku README dostarczanym w pakiecie Apache Wicket 1.3 napisano:

The easiest way of getting the dependencies of your Wicket based projects right is to use Maven with your projects and include the wicket dependencies you want like is outlined in the quickstart. Maven will then take care of including the appropriate dependencies.

To odpowiada dokładnie mojemu wyobrażeniu tworzenia oprogramowania bazując na Wicket, który niefortunnie (dla osób nie posługujących się Maven) nie dostarcza wszystkich wymaganych bibliotek w katalogu lib. Takie podejście sprowadza problem do czasów średniowiecza w temacie zestawiania projektu, gdzie dbanie o zależności zrzuca się obecnie na narzędzie wspomagające jak Maven, Ivy czy potencjalnie Buildr. Po raz kolejny przekonuję się, żę rozpoznanie nowego oprogramowania należy rozpocząć od lektury dostępnej dokumentacji, artykułów czy książek zanim zacznie się faktyczne konstruowanie aplikacji. Nie ma nic "przyjemniejszego" niż po kilku godzinach naprawiania błędu dowiadujemy się o jego sposobie rozwiązania w pliku, który znajdował się pod ręką (jak np. plik README w dystrybucji Wicket). Jeszcze tak niedawno doświadczyłem zawirowań, kiedy do mojej pierwszej aplikacji webowej z Wicketem dodałem wyłącznie plik wicket-1.3.jar, a jej uruchomienie zakończyło się...wyjątkiem CNFE. Znalezienie rozwiązania nie sprawiło problemu, jednakże sam fakt takiego zapisu w README:

- wicket and wicket-extensions:

You only need to include the Servlet API (2.3, just for compiling) and the SLF4J logging implementation you want. You cannot use Wicket without adding a SLF4J logging implementation to your classpath. Most people use log4j. If you do, just include slf4j-log4j12.jar on your classpath to get Wicket to use log4j too. If you want to use commons-logging or JDK14 logging or something else, please see the SLF4J site (http://www.slf4j.org/) for more information.


jest potwierdzeniem, że każdorazowo należy przynajmniej pobieżnie przejrzeć dostępną dokumentację. Początkowo może to odłożyć w czasie faktyczny moment programowania, ale zdobyta wiedza suma sumarum pozwoli uniknąć nieprzyjemnych niespodzianek, które wpływają na nasz zapał do dalszego studiowania wybranego rozwiązania.

W katalogu src/archetypes znajduje się plik README, którego lektura dostarczyła mi informacji dotyczących tworzenia aplikacji opartych o Wicket z pomocą Apache Maven 2. W ten sposób mam obsłużone dwie rzeczy, które musiałem do tej pory wykonywać samodzielnie: 1) tworzenie struktury projektu (to mogłem jeszcze w zasadzie zrzucić na barki narzędzia IDE, np. NetBeans, Eclipse czy IDEA) oraz 2) zarządzanie zależnościami (które wymagało ode mnie umiejętności rozpoznania przyczyny błędu - analiza wyjątku, bądź zapoznania się z dokumentacją i odszukiwania zależności na podstawie wykorzystywanych funkcjonalności). Jeśli mogę coś zrzucić na barki dostępnego oprogramowania, które w dodatku jest darmowe, to z reguły skłaniam się ku takiemu podejściu. Jest zbyt wiele ciekawego oprogramowania i pomysłów do zrealizowania, aby tracić czas na zarządzanie projektem ręcznie (oczywiście nie znaczy to, że Maven jest najwłaściwszym z możliwych rozwiązań, ale w tym momencie w zupełności wystarczy). Okazuje się, że istnieje nawet asystent w postaci formularza na stronach Wicketa do skonstruowania odpowiedniego polecenia do utworzenia projektu - Getting Started: QuickStart. Wystarczy, więc wprowadzić dane o pakiecie i nazwie projektu, i mamy podane na tacy polecenie do wykonania.


Wykonanie polecenia tworzy projekt oparty o Wicket.
jlaskowski@dev /cygdrive/c/projs/sandbox
$ mvn archetype:create \
-DarchetypeGroupId=org.apache.wicket \
-DarchetypeArtifactId=wicket-archetype-quickstart \
-DarchetypeVersion=1.3.0 \
-DgroupId=pl.jaceklaskowski.wicket -DartifactId=wicket-demo
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'archetype'.
[INFO] ----------------------------------------------------------------------------
[INFO] Building Maven Default Project
[INFO] task-segment: [archetype:create] (aggregator-style)
[INFO] ----------------------------------------------------------------------------
...
[INFO] [archetype:create]
[INFO] Defaulting package to group ID: pl.jaceklaskowski.wicket
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating Archetype: wicket-archetype-quickstart:1.3.0
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: pl.jaceklaskowski.wicket
[INFO] Parameter: packageName, Value: pl.jaceklaskowski.wicket
[INFO] Parameter: basedir, Value: c:\projs\sandbox
[INFO] Parameter: package, Value: pl.jaceklaskowski.wicket
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: artifactId, Value: wicket-demo
[INFO] ********************* End of debug info from resources from generated POM ***********************
[INFO] Archetype created in dir: c:\projs\sandbox\wicket-demo
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3 seconds
[INFO] Finished at: Mon Feb 04 16:09:38 PST 2008
[INFO] Final Memory: 5M/254M
[INFO] ------------------------------------------------------------------------
Następnie wystarczy wykonać polecenie mvn clean install, aby zbudować projekt.
jlaskowski@dev /cygdrive/c/projs/sandbox/wicket-demo
$ mvn clean install
[INFO] Scanning for projects...
[INFO] ----------------------------------------------------------------------------
[INFO] Building quickstart
[INFO] task-segment: [clean, install]
[INFO] ----------------------------------------------------------------------------
...
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running pl.jaceklaskowski.wicket.TestHomePage
...
Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] [war:war]
[INFO] Packaging webapp
[INFO] Assembling webapp[wicket-demo] in [c:\projs\sandbox\wicket-demo\target\wicket-demo-1.0-SNAPSHOT]
[INFO] Processing war project
[INFO] Webapp assembled in[156 msecs]
[INFO] Building war: c:\projs\sandbox\wicket-demo\target\wicket-demo-1.0-SNAPSHOT.war
[INFO] [install:install]
[INFO] Installing c:\projs\sandbox\wicket-demo\target\wicket-demo-1.0-SNAPSHOT.war to
C:\.m2\pl\jaceklaskowski\wicket\wicket-demo\1.0-SNAPSHOT\wicket-demo-1.0-SNAPSHOT.war
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 44 seconds
[INFO] Finished at: Mon Feb 04 16:10:31 PST 2008
[INFO] Final Memory: 8M/254M
[INFO] ------------------------------------------------------------------------
i/lub uruchomić go za pomocą komendy mvn jetty:run.
jlaskowski@dev /cygdrive/c/projs/sandbox/wicket-demo
$ mvn jetty:run
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'jetty'.
[INFO] ----------------------------------------------------------------------------
[INFO] Building quickstart
[INFO] task-segment: [jetty:run]
[INFO] ----------------------------------------------------------------------------
...
[INFO] [jetty:run]
[INFO] Configuring Jetty for project: quickstart
[INFO] Webapp source directory = C:\projs\sandbox\wicket-demo\src\main\webapp
[INFO] web.xml file = C:\projs\sandbox\wicket-demo\src\main\webapp\WEB-INF\web.xml
[INFO] Classes = C:\projs\sandbox\wicket-demo\target\classes
2008-02-04 16:15:30.756::INFO: Logging to STDERR via org.mortbay.log.StdErrLog
[INFO] Context path = /wicket-demo
[INFO] Tmp directory = determined at runtime
[INFO] Web defaults = org/mortbay/jetty/webapp/webdefault.xml
[INFO] Web overrides = none
[INFO] Webapp directory = C:\projs\sandbox\wicket-demo\src\main\webapp
[INFO] Starting jetty 6.1.7 ...
2008-02-04 16:15:30.896::INFO: jetty-6.1.7
...
********************************************************************
*** WARNING: Wicket is running in DEVELOPMENT mode. ***
*** ^^^^^^^^^^^ ***
*** Do NOT deploy to your live server(s) without changing this. ***
*** See Application#getConfigurationType() for more information. ***
********************************************************************
2008-02-04 16:15:31.756::INFO: Started SelectChannelConnector@0.0.0.0:8080
[INFO] Started Jetty Server
Teraz wystarczy uruchomić przeglądarkę i skierować ją na adres http://localhost:8080/wicket-demo


i można podziwiać prostotę tworzenia projektu opartego o Wicket z pomocą archetypu wicket-archetype-quickstart.

p.s. Podziękowania dla 13-tu kolejnych esemesowiczów, który zechcieli wyrazić swoją opinię o Notatniku w konkursie Blog Roku 2007. Z miejsca 13-tego wczoraj Notatnik przesunął się na 9-te z 37 głosami (!) Czyżby deklaracja "jestem Waszym dłużnikiem" robiła swoje? (teraz tylko czekać na rachunki za wysłane smsy ;-)) Pierwsze miejsce okupowane jest przez blog z 93. głosami, więc jeszcze dużo pracy przed nami.

04 lutego 2008

1. dzień w US - będzie wiele o EJB3 w wykonaniu WAS 6.1

4 komentarzy
Do końca nie byłem pewien jak sprawy się potoczą, więc cierpliwie przygotowując się nie pisałem o tym ni słowa w Notatniku. Teraz już jestem na miejscu, więc mogę w końcu oznajmić, że właśnie piszę ten wpis siedząc w Oakwood Mountain View w Kalifornii (USA). Będę sobie tutaj siedział przez najbliższe 5 tygodni i...nie tego nie zdradzę już teraz. Niech pozostanie to jako niespodzianka. "Projekt" jest bardzo ciekawy z mojego punktu widzenia i cieszyłbym się, gdyby komuś udało się również wyjechać w tym celu do US na koszt własnej firmy.

Ciekawostką tego wyjazdu jest fakt, że data wylotu ze Stanów - 6 marca 2008 - pokrywa się dokładnie z datą wygaśnięcia ważności mojego paszportu (!) Wiza jeszcze ważna przez caluteńki rok, więc obawy nie ma. Zresztą już jestem na "American soil", więc jestem tego dowodem, że mit pod tytułem "paszport musi być ważny przynajmniej przez 1/2 roku" nie ma racji bytu. Potwierdziłem to w Ambasadzie Amerykańskiej w Warszawie jeszcze przez wylotem oraz na granicy w Stanach, gdzie celnik wypytywał mnie tylko, czy biorę na siebie "if something unexpected happens and my flight gets delayed". Na końcu języka miałem, że będę się martwił jak "something unexpected happens", ale powstrzymałem się i tylko potwierdziłem, że biorę.

A wylot zaczął się ciekawiej niż się tego spodziewałem. Zaopatrzony w ciuchy na 5 tygodni przekroczyłem dopuszczalną wagę 23 kg na przelocie międzykontynentalnym o caluteńkie 3 kg, co kosztowało mnie 50 USD + 40 PLN kosztów wystawienia rachunku. Razem 161 PLN dodatkowo za 3 kg (!) Trzeba będzie wliczyć to w koszty.

Kolejną ciekawostką było spotkanie Balcerowicza, który zagadnął mnie "Do you speak English?" na co uprzejmie odpowiedziałem "A jasne!". Nie tak dawno, w Borach Tucholskich, spotkałem Buzka i nie mogłem opanować się, aby głośno nie wyrazić mojego zdumienia, że najpierw Buzek, na odludziu, a teraz Balcerowicz. Na co, p. Balcerowicz odpowiedział, że czasami tak bywa. Natknęliśmy się jeszcze we Frankfurcie, ale jako, że p. premier do Waszyngtonu jechał, a p. niepremier (=ja) dalej, więc tyle udało mi się wyrobić sobie chodów na szczeblu ministerialnym ;-)

Później kolejka przy celnikach i 11-godzinny przelot do SFO. Już dawno nie miałem tyle czasu i to bez dostępu do Sieci. Udało mi się posprzątać w komputerze, przeczytać książkę o GlassFishu i Java EE 5 oraz pobawić się Wicketem. Trochę spania, jedzenie, oglądanie filmu i w końcu wylądowałem o 3:30PM w San Francisco. Tutaj pogaduchy z celnikami i odbiór bryki.


Czekała na mnie uśmiechnięta. Na początku sądziłem, że z zadowolenia, że będzie ją prowadził taki kierowca jak ja, ale po niedługiej chwili okazało się, że to była drwina, bo prowadzenie samochodu z automatyczną skrzynią biegów przysporzyło mi nie lada problemów. Kto by pomyślał, że zanim odpalę silnik to zabiorę się za Driver's Manual. Zeszło mi bodajże godzinę na wertowaniu manuala, a później jeszcze ustawieniu GPSa i wio autostradą US-101 SOUTH do hotelu. Śmieszne, bo początkowo za gwałtownie przyciskałem hamulec, tak że stawałem "w miejscu". Można sobie tylko wyobrazić co by było, gdyby za mną ktoś jechał. A już myślałem, żem kierowca na schwał.

Siedząc tutaj 5 tygodni będę miał niezwykle duuuużo czasu na różne różności, które w natłoku codziennych obowiązków rodzinnych schodziły na plan drugi. Teraz z braku tychże obowiązków, pojawiła się "przestrzeń czasowa" do wypełnienia i jako, że głównym celem mojego pobytu w Stanach jest właśnie pisanie, zatem pisania w Notatniku również nie powinno zabraknąć. I jako, że doczytuję książkę o Wickecie będzie sporo właśnie o nim.

Auć, to już 5:30 (CET), a tutaj 8:30PM, więc pora myśleć o spaniu. Jutro zapowiada się ciekawy dzień. Zatem gaz do dechy i wio za lekturę dokumentacji, którą zawsze odkładałem na później. Do jutra!



p.s. Bardzo dziękuję wszystkim esemesowiczom, którzy zechcieli wziąść udział w konkursie Blog Roku 2007, w którym zgłosiłem swój Notatnik. Wystarczy wysłać SMSik, a Jacek szczęśliwy co nie miara widząc coraz większą liczbę głosów. Dzisiaj Notatnik jest na 13. miejscu z 24-oma głosami w III etapie. To daje 10 SMSów w ostatnich 2 dniach (!) Brawo. Jestem Waszym dłużnikiem!

01 lutego 2008

Pierwsze kroki z Apache Wicket 1.3

0 komentarzy
Wśród moich postanowień noworocznych znalazło swoje miejsce rozpoznanie Apache Wicket. Jest kilka powodów, dla których Wicket zwrócił moją uwagę, ale najmniej technicznym acz najważniejszym jest zwykła ciekawość zrozumienia, co powoduje wzrastające zainteresowanie tym rozwiązaniem wśród twórców aplikacji internetowych w Javie. Do tej pory jedynym słusznym szkieletem tworzenia aplikacji webowych dla mnie był JavaServer Faces z pewnymi przebłyskami w postaci GWT. Ich zaletą było ukrycie wszystkich tych technologii webowych jak JavaScript, (D)HTML czy CSS i zastąpienie ich javą. Tym razem będzie trochę inaczej, gdyż Wicket bezinwazyjnie wplata się w HTML (za pomocą standardowych rozszerzeń w jego elementach) powodując, że rozdzielenie prac między zespołami odpowiedzialnymi za interfejs użytkownika (w aplikacji webowej) a "wnętrzem" aplikacji (uruchomionym po stronie serwera aplikacyjnego) faktycznie staje się możliwe i to bez konieczności poznawania specjalnego języka skryptowego (JSTL i znaczniki JSP) czy korzystania ze specjalizowanych narzędzi edycyjnych do modyfikacji stron HTML. To oczywiście nakłada na mnie pewien obowiązek powrotu do HTMLa, jednakże zamierzam powracać do niego wyłącznie na niezbędną chwilę. Interesują mnie cechy Wicketa, które przyciągają programistów Javy, którzy zazwyczaj są zachwyceni siłą jego możliwości, a nie są mocni w HTMLu i temu podobnym. Mówi się, że Wicket to taka mieszanka JSF i Apache Tapestry bez "udziwnień" JSP, więc takim cechom nie sposób się oprzeć.

Poznawanie Apache Wicket czas zacząć.

Pobieżne przejrzenie dokumentacji dostępnej na jego stronie dało mi przegląd co i jak, aby utworzyć prostą aplikację webową. Wartościowym dla mnie sposobem na poznawanie danego rozwiązania jest czytanie cudzego kodu źródłowego w postaci przykładów, ale zdecydowanie najbardziej przemawiającym do mojej wyobraźni jest próba stworzenia czegoś podobnego samodzielnie. Właśnie to spowodowało, że po przeczytaniu Hello World! postanowiłem spróbować własnych sił i utworzyć podobną aplikację.

Zacząłem od utworzenia projektu aplikacji internetowej w NetBeans 6.1 M1.

Po pobraniu wersji dystrybucyjnej Apache Wicket 1.3 zdefiniowałem bibliotekę ApacheWicket w projekcie z pojedyńczym plikiem wicket-1.3.0.jar. W ten sposób miałem projekt aplikacji internetowej rozszerzony o bibliotekę Apache Wicket.

Zmieniłem index.jsp na następującą zawartość:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Wicket Demo</title>
</head>
<body>
<span wicket:id="komunikat">Obszar komunikatów</span>
</body>
</html>
Na uwagę załuguje atrybut wicket:id, który instruuje Wicketa, które elementy strony są specjalnego traktowania. W tym przypadku nazywamy element span identyfikatorem komunikat, co powoduje, że każdorazowe odwołanie się do pola o nazwie komunikat w kodzie opartym o Wicketa będzie wskazywało właśnie na nie. Pozostałe rzeczy są doskonale znane twórcom stron HTML i nie są niczym nowym i wartym omawiania.

Kolejnym krokiem było utworzenie podstawowego elementu aplikacji korzystającej z Wicketa - pl.jaceklaskowski.wicket.WicketDemoApplication, - klasy rozszerzającej org.apache.wicket.protocol.http.WebApplication, która jest "punktem wejściowym" dla jego mechanizmów.
package pl.jaceklaskowski.wicket;

import org.apache.wicket.protocol.http.WebApplication;

public class WicketDemoApplication extends WebApplication {

@Override
public Class getHomePage() {
return WicketDemo.class;
}

}
Klasa aplikacyjna w metodzie getHomePage() wskazuje na klasę realizującą pojęcie strony domowej w Wicket - pl.jaceklaskowski.wicket.WicketDemo.
package pl.jaceklaskowski.wicket;

import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;

public class WicketDemo extends WebPage {

public WicketDemo() {
add(new Label("komunikat", "Komunikat z wnętrza strony startowej"));
}

}
Pozostaje zmodyfikować deskryptor wdrożenia aplikacji webowej - web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<display-name>Wicket Demo</display-name>
<filter>
<filter-name>WicketDemoApplication</filter-name>
<filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
<init-param>
<param-name>applicationClassName</param-name>
<param-value>pl.jaceklaskowski.wicket.WicketDemoApplication</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>WicketDemoApplication</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
i uruchomić aplikację.

Niefortunnie, próba uruchomienia aplikacji zakończyła się błędem o niedostępności klas z projektu SLF4J. Pobrałem go i utworzyłem nową zależność z dwoma plikami slf4j-api-1.4.3.jar oraz slf4j-simple-1.4.3.jar jako biblioteka SLF4J.

Kolejna próba uruchomienia i na konsoli GlassFisha pojawiły się następujące komunikaty:

265531 [httpWorkerThread-4848-1] null org.apache.wicket.RequestListenerInterface -
registered listener interface [RequestListenerInterface name=INewBrowserWindowListener,
method=public abstract void org.apache.wicket.markup.html.INewBrowserWindowListener.onNewBrowserWindow()]
deployed with moduleid = wicket-demo
281 [httpWorkerThread-4848-1] INFO org.apache.wicket.Application -
[WicketDemoApplication] init: Wicket core library initializer
297 [httpWorkerThread-4848-1] INFO org.apache.wicket.RequestListenerInterface -
registered listener interface [RequestListenerInterface name=IBehaviorListener,
method=public abstract void org.apache.wicket.behavior.IBehaviorListener.onRequest()]
297 [httpWorkerThread-4848-1] INFO org.apache.wicket.RequestListenerInterface -
registered listener interface [RequestListenerInterface name=IBehaviorListener,
method=public abstract void org.apache.wicket.behavior.IBehaviorListener.onRequest()]
297 [httpWorkerThread-4848-1] INFO org.apache.wicket.RequestListenerInterface -
registered listener interface [RequestListenerInterface name=IFormSubmitListener,
method=public abstract void org.apache.wicket.markup.html.form.IFormSubmitListener.onFormSubmitted()]
297 [httpWorkerThread-4848-1] INFO org.apache.wicket.RequestListenerInterface -
registered listener interface [RequestListenerInterface name=IFormSubmitListener,
method=public abstract void org.apache.wicket.markup.html.form.IFormSubmitListener.onFormSubmitted()]
297 [httpWorkerThread-4848-1] INFO org.apache.wicket.RequestListenerInterface -
registered listener interface [RequestListenerInterface name=ILinkListener,
method=public abstract void org.apache.wicket.markup.html.link.ILinkListener.onLinkClicked()]
297 [httpWorkerThread-4848-1] INFO org.apache.wicket.RequestListenerInterface -
registered listener interface [RequestListenerInterface name=ILinkListener,
method=public abstract void org.apache.wicket.markup.html.link.ILinkListener.onLinkClicked()]
297 [httpWorkerThread-4848-1] INFO org.apache.wicket.RequestListenerInterface -
registered listener interface [RequestListenerInterface name=IOnChangeListener,
method=public abstract void org.apache.wicket.markup.html.form.IOnChangeListener.onSelectionChanged()]
297 [httpWorkerThread-4848-1] INFO org.apache.wicket.RequestListenerInterface -
registered listener interface [RequestListenerInterface name=IOnChangeListener,
method=public abstract void org.apache.wicket.markup.html.form.IOnChangeListener.onSelectionChanged()]
297 [httpWorkerThread-4848-1] INFO org.apache.wicket.RequestListenerInterface -
registered listener interface [RequestListenerInterface name=IRedirectListener,
method=public abstract void org.apache.wicket.IRedirectListener.onRedirect()]
297 [httpWorkerThread-4848-1] INFO org.apache.wicket.RequestListenerInterface -
registered listener interface [RequestListenerInterface name=IRedirectListener,
method=public abstract void org.apache.wicket.IRedirectListener.onRedirect()]
297 [httpWorkerThread-4848-1] INFO org.apache.wicket.RequestListenerInterface -
registered listener interface [RequestListenerInterface name=IResourceListener,
method=public abstract void org.apache.wicket.IResourceListener.onResourceRequested()]
297 [httpWorkerThread-4848-1] INFO org.apache.wicket.RequestListenerInterface -
registered listener interface [RequestListenerInterface name=IResourceListener,
method=public abstract void org.apache.wicket.IResourceListener.onResourceRequested()]
297 [httpWorkerThread-4848-1] INFO org.apache.wicket.protocol.http.WebApplication -
[WicketDemoApplication] Started Wicket version 1.3.0 in development mode
********************************************************************
*** WARNING: Wicket is running in DEVELOPMENT mode. ***
*** ^^^^^^^^^^^ ***
*** Do NOT deploy to your live server(s) without changing this. ***
*** See Application#getConfigurationType() for more information. ***
********************************************************************
W przeglądarce wyłącznie komunikat, który umieszczony jest w samej stronie HTML.


Gdybym czytał dokładniej dokumentację to z pewnością nie natrafiłbym na ten problem. Wyraźnie napisano w dokumentacji Wicket 1.3 - Hello World!, że:

In all the Wicket examples, you have to put all files in the same package directory. This means putting the markup files and the java files next to one another. It is possible to alter this behavior, but that is beyond the scope of this example. The only exception is the oblibatory web.xml file which should reside in the WEB-INF/ directory of your web application root folder.

Przenoszę index.jsp do katalogu z klasami (WEB-INF/classes) w pakiecie pl.jaceklaskowski.wicket i ponownie uruchamiam aplikację.


Po chwili zastanowienia i ponownej lektury dokumentacji znajduję rozwiązanie, które polega na zmianie nazwy strony index.jsp na WicketDemo.html, czyli dokładnie tak, jak nazwałem klasę reprezentującą stronę główną aplikacji - WicketDemo.java z rozszerzeniem html. Ponownie uruchamiam przykład.


Teraz już otrzymuję wynik jakiego oczekiwałem. Nie było lekko przyznam, ale ostateczny wynik zrekompensował poniesione straty ;-)

Na zakończenie widok na strukturę projektową aplikacji w NetBeans 6.


Projekt aplikacji wicket-demo dostępny jest do pobrania jako wicket-demo.zip.

p.s. Ponownie napomknę o konkursie Blog Roku 2007, w którym biorę udział. Wczoraj blog był na 13. miejscu z 12-oma głosami, a dzisiaj jest na 15. z 14-oma (!) Czyżby liczba SMSów była odwrotnie proporcjonalna do miejsca w rankingu? Nie możesz się zwieść takiemu myśleniu i jeśli jeszcze nie wysłałeś SMSa w III etapie zrób to teraz. Ukłony dla dzisiejszych 2 zacnych głosujących.