20 lutego 2008

Sesja i przekierowanie żądania w Wicket

Czytanie książek informatycznych zaczyna ponownie sprawiać mi przyjemność. Kilkakrotne czytanie tego samego rozdziału i wertowanie kartek w tą i z powrotem w poszukiwaniu odpowiedzi tylko mnie w tym utrwala. Zauważyłem, że lektura Pro Wicket wydawnictwa Manning, jakkolwiek dotyczy wcześniejszych wersji Apache Wicket, to mimo wszystko napisana jest bardzo przyzwoicie. Gdzie tam przyzwoicie?! Czytam rozdział 3 Developing a Simple Application już bodajże z 10 raz i zawsze coś ciekawego odnajduję. Autor książki - Karthik Gurumurthy - wykonał świetną robotę.

Wciąż nie mogę przestać porównywać Wicketa z JSF. Z jednej strony podoba mi się zwięzłość kontrolek JSF a z drugiej przyciąga prostota Wicketa, która objawia się możliwością tworzenia stron w Javie. Tak, nie jest to najprzyjemniejsze, biorąc pod uwagę, co możnaby jeszcze uprościć w tej kwestii patrząc na GWT, ale to, co oferuje Wicket, zaczyna mnie przyciągać ku niemu coraz silniej. JSF to standard, część Korporacyjnej Javy, więc nie sposób go nie znać, ale Wicket rulez.

Od kilku dni "trawię" obsługę sesji w Wicket. Niby nic nadzwyczajnego, a jednak żadne ze znanych mi szkieletów aplikacyjnych do tej kwestii nie podeszło w ten sposób (przypominam, że nie znam Tapestry, a o nim się pisze jako o najbliższym krewnym). Na czym polega fenomen obsługi sesji w Wicket? Na silnym typowaniu obiektów przechowywanych w sesji bez bezpośredniego dostępu do obiektu javax.servlet.http.HttpSession znanego każdemu tworzącemu aplikacje webowe oparte o JSP i serwlety. To tak, jakby dodać typy generyczne do HttpSession.
package pl.jaceklaskowski.wicket;

import org.apache.wicket.Request;
import org.apache.wicket.protocol.http.WebSession;

import pl.jaceklaskowski.wicket.entities.Osoba;

public class WicketDemoSession extends WebSession {

private static final long serialVersionUID = 1L;

private Osoba osoba;

public WicketDemoSession(Request request) {
super(request);
}

public Osoba getOsoba() {
return osoba;
}

public void setOsoba(Osoba osoba) {
this.osoba = osoba;
}

}
pl.jaceklaskowski.wicket.WicketDemoSession to typ reprezentujący sesję w mojej aplikacji opartej o Wicket. Klasa koniecznie musi rozszerzać typ org.apache.wicket.protocol.http.WebSession. Zalecane jest udostępnienie konstruktora z pojedyńczym parametrem reprezentującym bieżące żądanie (typ org.apache.wicket.Request) a pozostałe "rzeczy" są już nasze. Skoro chciałem przechowywać egzemplarz typu Osoba w sesji udostępniłem metodę zapisu i odczytu dla tego typu. I tyle! Chcemy przechowywać więcej w sesji, dodajemy kolejne atrybuty do własnej klasy reprezentującej sesję.

Aktywacja naszej sesji polega na nadpisaniu metody public Session newSession(Request request, Response response) w klasie aplikacji.
package pl.jaceklaskowski.wicket;

...

public class WicketDemoApplication extends WebApplication {

...

@Override
public Session newSession(Request request, Response response) {
return new WicketDemoSession(request);
}
}
I tyle. Użycie sesji to skorzystanie z pomocy org.apache.wicket.Component.getSession(), która sprowadza się do wywołania org.apache.wicket.Session.get(), która z kolei pobiera aktualną sesję z bieżącego wątku (robiąc jeszcze poboczne sprawy). Możliwości dostania się do sesji jest więc kilka, a zwracany obiekt jest "naszego" typu.
package pl.jaceklaskowski.wicket;

import org.apache.log4j.Logger;
import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.RequiredTextField;
import org.apache.wicket.model.CompoundPropertyModel;

import pl.jaceklaskowski.wicket.entities.Osoba;

public class PrzedstawSie extends WebPage {

private static final long serialVersionUID = 1L;

private transient Logger logger = Logger.getLogger(PrzedstawSie.class.getName());

public PrzedstawSie(final PageParameters params) {
CompoundPropertyModel model = new CompoundPropertyModel(new Osoba());
Form loginForm = new Form("dane", model) {
private static final long serialVersionUID = 1L;

protected void onSubmit() {
Osoba osoba = (Osoba) getModel().getObject();
logger.info(osoba);
((WicketDemoSession) getSession()).setOsoba(osoba);
if (!continueToOriginalDestination()) {
setResponsePage(new Powitanie(getModelObjectAsString()));
}
}
};
RequiredTextField imie = new RequiredTextField("login");
loginForm.add(imie);
add(loginForm);
}
}
Na uwagę zasługuje linia ((WicketDemoSession) getSession()).setOsoba(osoba);, gdzie pobieram sesję i ustawiam w niej egzemplarz typu Osoba. Przy okazji tworzenia tej nowej strony w aplikacji musiałem użyć słowa kluczowego transient, które było moim pierwszym jego użyciem. To też zaliczam jako plus dla Wicketa.

Baczne oko zauważy użycie metody public boolean org.apache.wicket.Component.continueToOriginalDestination(), która zwróci true, jeśli przerwaliśmy wcześniej obsługę żądania (przepływ) za pomocą org.apache.wicket.RestartResponseAtInterceptPageException. Jego zgłoszenie powoduje przerwanie obsługi bieżącego żądania i przekierowanie do wskazanej w konstruktorze wyjątku strony. Najbardziej naturalne użycie tego rodzaju przerwania to obsługa uwierzytelnienia użytkownika.
package pl.jaceklaskowski.wicket;

import java.util.Arrays;

import org.apache.log4j.Logger;
import org.apache.wicket.PageParameters;
import org.apache.wicket.RestartResponseAtInterceptPageException;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.RequiredTextField;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.model.CompoundPropertyModel;

import pl.jaceklaskowski.wicket.entities.Osoba;

public class DaneOsobowe extends WebPage {

private static final long serialVersionUID = 1L;

private transient Logger logger = Logger.getLogger(DaneOsobowe.class.getName());

public DaneOsobowe(PageParameters params) {
// pobranie danych z sesji
WicketDemoSession session = (WicketDemoSession) getSession();
Osoba osoba = session.getOsoba();
if (osoba == null) {
throw new RestartResponseAtInterceptPageException(PrzedstawSie.class);
}
logger.info("Dane osoby: " + osoba);
CompoundPropertyModel model = new CompoundPropertyModel(osoba);
Form loginForm = new Form("daneOsobowe", model) {
private static final long serialVersionUID = 1L;

protected void onSubmit()
{
// TODO: miejsce dla JPA
Osoba osoba = (Osoba) getModel().getObject();
logger.info(osoba);
((WicketDemoSession) getSession()).setOsoba(osoba);
setResponsePage(new Powitanie(getModelObjectAsString()));
}
};
TextField imie = new TextField("imie");
// określenie pola obowiązkowego
imie.setRequired(true);
loginForm.add(imie);
// pomocnicza klasa wykonująca setRequired(true)
TextField nazwisko = new RequiredTextField("nazwisko");
loginForm.add(nazwisko);
TextField login = new RequiredTextField("login");
login.setRequired(true);
loginForm.add(login);
// lista rozwijalna z danymi z Osoba.getMiejscowosci (model formularza to Osoba)
DropDownChoice choice = new DropDownChoice("miejscowosc",
Arrays.asList(new String[] { "Warszawa", "Krak\u00f3w",
"Wroc\u0142aw", "Pozna\u0144", "Szczecin", "Gda\u0144sk" })) {

private static final long serialVersionUID = 1L;

// wyślij wybór z listy po zmianie wyboru do serwera
@Override
protected boolean wantOnSelectionChangedNotifications() {
return true;
}

protected void onSelectionChanged(final Object newSelection)
{
System.out.println("Miejscowosc: " + newSelection);
}
};
choice.setRequired(true);
loginForm.add(choice);
add(loginForm);
// panel komunikatów
add(new FeedbackPanel("komunikaty"));
}
}
Jeśli wywołanie strony DaneOsobowe nie będzie związane z egzemplarzem Osoba w sesji nastąpi przerwanie do klasy-strony PrzedstawSie, w której po pomyślnym zatwierdzeniu formularza umieszczam wymagany obiekt i wykonanie wspomnianej metody continueToOriginalDestination() spowoduje powrót do wykonania poprzednio przerwanej strony - powraca do przerwanego wątku. Jeśli wywołana zostanie strona PrzedstawSie bezpośrednio, metoda continueToOriginalDestination() zwróci false i nastąpi przekierowanie do strony Powitanie. Proste, nieprawdaż?