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ż?

4 komentarze:

  1. hmm... jak by tu zacząć, trochę się zawiodłem, to co pokazałeś o wykorzystaniu sesji to jakiś koszmar... ;-)
    po pierwsze obowiązek rozszerzenia konkretnej klasy (co się stało z interfejsami, które są kontraktami między twórcą fw a użytkownikiem??)
    druga spraw to pamiętanie o nadpisaniu newSession(), jeśli zapomnimy to będzie niezły bajzel ;-)
    trzecia sprawa, to jednak mamy rzutowanie (WicketDemoSession) getSession(), a już myślałem, że Wicket mnie zaskoczy...
    po czwarte, jak to testować, chodzi mi o sterowanie, a nie o logikę businessową?
    kolejna rzecz, to sterowanie przepływem za pomocą wyjątków??!!?? czy to nie jakiś anty-pattern? chyba, że coś zmieniło i obecenie wyjątki są dozwolone w takich sytuacjach?

    OdpowiedzUsuń
  2. siek! miałem kliknąć podgląd a nie publikuj ... kontynuując
    wiem, że te pytania to raczej do twórców Wicketa, niż do ciebie Jacku, jednak jestem zawiedziony tym elementem, miałem nadzieję, że będzie to ładniej zrealizowane ...
    i jeszcze jedna rzecz, to kod strony rozrósł się niemiłosiernie, a strona w sumie nic nie robi ;-)

    OdpowiedzUsuń
  3. No cóż, uczę się Wicketa i to co prezentuję może być jedynie namiastką jego możliwości i to w najgorszym wydaniu. Moim głównym przewodnikiem jest na razie książka Pro Wicket, która dotyczy wersji Wicket sprzed jego migracji do ASF, więc liczę na więcej uproszczeń niż te, które zaprezentowałem.

    Konkretna klasa to aplikacja wicketowa. To jest czyste programowanie w Javie, więc wszystkie techniki dozwolone. Nie napisałem tego wcześniej, bo nie do końca to czuję, ale Wicketa określa się jako szkielet niezarządzany, podczas, gdy Spring, JSF są szkieletami zarządzanymi, w których nie możesz sobie tak po prostu wykonać new i oczekiwać, że szkielet będzie działał (oczekuj wyjaśnień na blogu dlaczego wrzuciłem Springa i JSF do jednego wora).

    Pamiętanie o newSession jest konieczne. Cóż takie prawa Wicketa, aby uprościć tworzenie aplikacji za pewną cenę. I to jest ta cena. W Javie jest podobnie - jeśli nie zadeklarujesz, że realizujesz interfejs Serializable, a cała klasa jest faktycznie gotowa do serializacji, to Java zgłosi wyjątek. Możnaby więcej takich przykładów podać.

    Z rzutowaniem się zgodzę - mnie też to odrzuciło. Chciałbym, aby wprowadzono typy generyczne, ale prawdę powiedziawszy nie wiem, jak mogliby to zrealizować. Za słaby jestem z tego tematu widocznie.

    Temat testowania niebawem. Sam wciąż o tym myślę. Zresztą ostatnio nawet pisałem o Selenium i Twill (to od strony testowania stron webowych), a interesuje mnie również testowanie niewebowe (takie jednostkowe bez założenia, że port 80 to środowisko pracy).

    Temat wyjątków nie jest mi znany w ogóle poza org.apache.wicket.RestartResponseAtInterceptPageException, który tak na prawdę nie jest wyjątkiem per se, a sposobem na wskazanie, że coś ma się fajnego zdarzyć - przekierowanie. Dla mnie to jakby starać się złapać komara na siatkę na zakupy. Pewnie się da, ale nie do tego spodziewałbym się użyć siatki na zakupy ;-)

    Bardzo cieszą mnie Twoje komentarze. Czym bardziej żywiołowe tym większy z nich pożytek - dopingują do pracy i ustawiają ich kierunek. Ja wciąż jestem pod wrażeniem podejściem Wicketa i nie zamierzam mu jeszcze odpuścić. Ciekaw jestem ilu jest w Polsce użytkowników Wicketa?

    OdpowiedzUsuń
  4. Myślę, że nie jest to twoja wina, bazujesz na książce, którą kupią tysiące czytelników i na jej podstawie będą tworzyć swoje aplikacje w Wicketcie, więc moim zdaniem to autor jest odpowiedzialny, że prezentuje taki styl tworzenia programu, który mi nie odpowiada, a możliwe, że innym tak - kwestia gustu ;-)

    Wicketa w jakiś sposób porównuję do Struts2 stąd moje pytanie o interfejsy, POJO, etc. A Spring w sumie robi to samo co new() tylko deklaratywnie ;-)

    Właśnie, newSession(), "brzydal jeden", już oznaczenie klasy interfejsem, tak jak Serializable() jest o wiele "ładniejsze", a jak zapomnisz tego zrobić to dostaniesz wyjątek przy kompilacji, co niestety nie będzie miało przy newSession() :-(

    Rzutowanie to niestety wymóg, Wicket 1.3.1 bazuje na Java 1.4 więc o generyczności można zapomnieć ;-)

    Wracając do wyjątków, to RestartResponseAtInterceptPageException bazuje na RuntimeException co w moim rozumieniu oznacza, że środowisko uruchomieniowe napotkało błąd i sygnalizuje się to właśnie przez RuntimeException :D Rozumiejąc w tym toku, to brak zalogowanego użytkownika, wg twórców Wicketa, oznacza błąd środowiska Java??? :D
    Kolejna rzecz to wygenerowanie wyjątku jest bardzo kosztowne (w sensie pamięciowym i obciążenia) i ogólnie wyjątek oznacza błąd, a moim zdaniem, to że użytkownik nie jest zalogowany to nie błąd, tylko normalna sytuacja ;-)

    Cieszę się, że cieszą cię moje komentarze, sam wiele się uczę na twoich wywodach :D

    OdpowiedzUsuń