January 26, 2009

Elegancki CRUD w jednej akcji Struts2 część 2/2


Kontynuuję moją opowieść o Struts2, zamykając wątek z części pierwszej.

W tak zwanym międzyczasie zacząłem czytać "Groovy in Action", potem zobaczyłem też wpis na blogu Jacka Laskowskiego. Cóż, zaryzykuję stwierdzenie, że z punktu widzenia wygody programowania, wsparcia języka dla typowych czynności programistycznych (ale niestety również wydajności), Groovy ma się do Javy jak Java do C++. Jednym słowem jestem mocno oczarowany finezją tego języka: pętla for odchodzi do lamusa, dostajemy za to operator statku kosmicznego "<=>" i znakomite wsparcie dla kolekcji). Grails z kolei wzbudziło we mnie wątpliwość, czy kilkanaście stron tekstu by opisać CRUDa w Struts2, jednym z najpopularniejszych frameworków webowych w Javie to nie przesada...

Jednak do Grooviego na pewno wrócę, a teraz dokończmy naszą aplikację webową w staroświeckiej i nieporadnej Javie (autor ww. książki ostrzegał, że po pierwszym zetknięciu z Groovy właśnie tak będzie wyglądał powrót do Javy ;-)). Przypomnę, że w pierwszej części artykułu zdołaliśmy zaledwie skonfigurować naszą aplikację i zdefiniować stos interceptorów. Co prawda ten domyślny też by się nadał, ale przymiotnik "elegancki" w tytule zobowiązuje :-).

Zajmijmy się teraz konfiguracją właściwej akcji. Na razie bez pisania kodu dodajmy naszą nową akcję zajmującą się całym cyklem życia obiektu do deskryptora struts-config.xml. Jak zostało już wcześniej zdradzone, jedna klasa akcji zawiera szereg metod takich jak list(), save() czy show(), a każda z nich odpowiada jednej logicznej akcji. Tag <action> posiada co prawda atrybut method, dzięki czemu definiując akcję definiujemy nie tylko klasę, ale jednocześnie metodę (jeśli chcemy użyć innej niż execute()). Dzięki niemu można umieścić w jednej klasie kilka metod i "wyprodukować" dzięki temu kilka akcji. Jednak deklarowanie wszystkich takich akcji explicite byłoby męczące, trudne w utrzymaniu i spotęgowałoby jedynie ilość XMLa. Na szczęście istnieje możliwość definiowania szeregu akcji za pomocą masek, co wyjaśni poniższy przykład:


<action name="movie/*" method="{1}" class="moviesAction">
<!-- [...] -->
</action>


Nazwa akcji posiada maskę "*", a nazwa metody odwołuje się do zmiennej {1}, która, co jest chyba intuicyjne, odpowiada wartości pasującej do wspomnianej maski. Oznacza to, że jeśli przykładowo użytkownik wpisze adres "movie/vote", Struts2 wywoła metodę vote() (oczywiście jeśli takowa istnieje i ma odpowiednią sygnaturę) klasy akcji. Z tą klasą też coś namieszałem - nie jest to bynajmniej pełna nazwa klasy z pakietem tylko… nazwa beanu springowego. Nie będę się jednak zajmował integracją Spring-Struts2, bo ani nie jest to przedmiotem naszego tutoriala, ani też nie stanowi specjalnej trudności. Szczegółów proszę szukać w kodzie źródłowym.

Poszło szybko, zamiast definiować osobno 6 akcji (bo tyle ostatecznie napiszemy), różniących się jedynie metodą, wystarczył jeden generyczny zapis. By jednak uzupełnić mapowania rezultatów akcji na widoki, zastanówmy się jednak co mogą zwracać nasze poszczególne akcje. Ja przyjąłem następujące założenia:

list - przejście do widoku listy wszystkich filmów

redirectList - jw. ale poprzez wysłanie użytkownikowi kodu redirect. Przydatne np. gdy usuwamy film i chcemy przenieść użytkownika na listę filmów, pozbawioną już właśnie usuniętego obiektu. Zwykłe przeniesienie na widok jak w przypadku rezultatu list spowodowałoby, że w URLu ciągle widniałby adres movie/delete?id=123, co niechybnie doprowadziłoby do błędy w przypadku odświeżenia strony (wzorzec GET after POST).

input - wyświetlenie widoku umożliwiającego edycję szczegółów filmu. Przyjąłem przy tym, że na stosie wartości powinna się znaleźć zmienna id - jeśli jej wartość jest zerowa, widok służy do wprowadzenia danych nowego filmu. Inna wartość oznacza edycję już istniejącego filmu. Trochę konserwatywnie, ale pliki JSP zawsze można dopasować do własnych potrzeb.

show - wysyła użytkownikowi komunikat redirect do akcji movie/show zawierający parametr id. Przydatne po akcjach zmieniających stan filmu, gdy chcemy wyświetlić jego szczegóły w trybie tylko tylko do odczytu. Znowu nie można zwyczajnie przekierować akcji do widoku ze względu na "trefny URL" (patrz rezultat redirectList)

success - domyślny rezultat, wyświetla szczegóły filmu. Przydatny np. gdy klikamy na liście na konkretny film - odświeżenie widoku nie zrobi krzywdy aplikacji.

Bogatsi o taką wiedzę możemy ją zakodować w XMLu :-):


<action name="movie/*" method="{1}" class="moviesAction">
<result name="list">list.jsp</result>
<result name="redirectList" type="redirectAction">movie/list</result>
<result name="input">input.jsp</result>
<result name="show" type="redirectAction">
<param name="actionName">movie/show</param>
<param name="id">${id}</param>
</result>
<result>show.jsp</result>
</action>


Tam gdzie mówiliśmy o wysłaniu użytkownikowi komunikatu redirect, używany jest rezultat typu redirectAction, w pozostałych przypadkach domyślny rezultat przekazuje sterowanie do pliku JSP. Zastanawiające może być jedynie mapowanie rezultatu show. Otóż chcemy wysłać do użytkownika komunikat redirect do akcji movie/show. Jednak nic mu po tym, jeśli nie będzie wiedział, o film z jakim id ma poprosić. Stąd dodatkowy parametr, który de facto spowoduje odesłanie komunikatu redirect do URLa movie/show?id=123, gdzie 123 to wartość dostępna pod nazwą id na stosie wartości. Zapis klamrowy "${id}" jest konieczny, ponieważ domyślnie w tym miejscu Struts2 nie interpretuje napisów jako wyrażeń OGNL.

Z wielkim smutkiem oznajmiam, że koniec programowania w XMLu, wracamy do Javy :-). Ponieważ wszystkie akcje typu CRUD są podobne, postanowiłem stworzyć klasę bazową AbstractCrudAction<E> definiującą podstawowe operacje, implementującą wspólne funkcje i kilka pożytecznych interfejsów. Aha, typ generyczny E to typ obiektu dziedziny, jakim zarządzamy, czyli w naszym wypadku Movie. Oto nasza klasa bazowa w całej okazałości:


public abstract class AbstractCrudAction<E> extends ActionSupport implements ModelDriven<E>, Preparable {

public static final String LIST = "list";
public static final String REDIRECT_LIST = "redirectList";
public static final String SHOW = "show";

protected E model;

protected List<E> list;

public E getModel() {
return model;
}

public List<E> getList() {
return list;
}

public String create() {
return INPUT;
}

public abstract void prepareList();

public String list() {
return LIST;
}

public abstract void prepareShow();

public String show() {
if(model == null) {
addActionError(getText("error.not_found"));
return REDIRECT_LIST;
}
return SUCCESS;
}

public abstract void prepareEdit();

public String edit() {
return INPUT;
}

public abstract void prepareDelete();

public abstract String delete();

public abstract void prepareSave();

public abstract String save();

public abstract void prepareUpdate();

public abstract String update();

public void prepare() throws Exception {
}
}


Warto nadmienić o kilku, nie do końca jasnych elementach tej klasy. Po pierwsze implementuje ona dwa interfejsy, oba niezmiernie ciekawe. ModelDriven<E>, za sprawą interceptora modelDriven (polecam poczytanie kodu źródłowego klasy ModelDrivenInterceptor, dobry start do zrozumienia działania interceptorów i pisania własnych) rozdziela logikę (akcję) od modelu danych (w naszym wypadku filmu). Brzmi strasznie a ogranicza się do tego, że interceptor dla wszystkich akcji implementujących ten interfejs wywołuje metodę getModel(), która powinna zwrócić typ E, i umieszcza zwrócony obiekt na szczycie stosu wartości. Ma to ogromną zaletę nad ręcznym implementowanie metody getMovie() czy podobnej - ponieważ nasz obiekt dziedziny znajduje się bezpośrednio na szycie stosu, w widoku możemy używać prostych wyrażeń OGNL takich jak "title" czy w przyszłości "actors[0]" - gdybyśmy w akcji mieli metodę getMovie(), używalibyśmy odpowiednio "movie.title", "movie.actors[0]", etc. Oczywiście zgodnie z działaniem stosu wartości, jeśli dana właściwość nie zostanie znaleziona w modelu na szycie stosu, framework szuka niżej we właściwościach akcji itd.

Interfejs ModelDriven przypomina zatem FormBeany ze Struts1, jednak znacznie lepiej zaprojektowane. Zatem jeśli Wasza akcja ewidentnie zajmuje się określonym obiektem dziedziny, nie ma sensu tworzyć dla niego specjalnej metody get*() w klasie (lub przemapowywać właściwości obiektu na właściwości klasy).

Z metodą getModel() i interceptorem modelDriven jest jednak pewien problem: interceptor działa przed akcją, zatem jeśli przyszło Wam do głowy przypisać zmiennej zwracanej przez getModel() wartość w metodzie execute() lub analogicznej, to nie zadziała. Wcześniej interceptor wywoła getModel() i zingoruje zwróciny null - żaden pożytek :-). Oczywiście nie wspominałbym o tym, gdybym nie znał rozwiązania, które przy okazji zwiększy jakość naszej akcji - interfejs Preparable!

Interfejs ten działa analogicznie do interfejsu modelDriven - specjalny interceptor sprawdza, czy nasza akcja przypadkiem nie implementuje Preparable i jeśli tak - wywołuje metodę prepare() tego interfejsu. Jednak jak widać na powyższym kodzie, my z tej funkcjonalności nie korzystamy. Mamy jednak po jednej metodzie prepare*() dla każdej metody akcji: prepareList(), prepareSave(), etc. Jak łatwo się domyśleć, metody te są wołane tylko dla akcji związanych z określoną metodą - podczas gdy samo prepare() jest najpierw dla każdej akcji.

Jak widać we wszystkich wypadkach metody prepare*() są abstrakcyjne, a właściwe metody akcji nie zawsze! Dla przykładu metoda prepareEdit() najpierw przygotowuje dane dla akcji edit(), (chociażby ładując obiekt z bazy danych). O ile tylko konkretna klasa (np. MoviesAction) wie jaki obiekt należy załadować, o tyle implementacja metody edit() jest zawsze taka sama - i można ją zaimplementować w klasie bazowej.

Takie podejście nie tylko zapewnia ładniejszy kod, separując fazę przygotowania danych i inicjalizacji innych struktur od właściwej logiki biznesowej. Dzięki wspomnianemu stosowi interceptorów paramsPrepareParams, możemy zastosować pewną bardzo elegancką sztuczkę. Niestety jej omówienie nieco odbiega poza zakres tego tutorialu, zainteresowanych odsyłam do opisu w pliku struts-default.xml w JARze ze Strutsami - ew. kiedyś może skuszę się na pełniejszy opis. W skrócie sztuczka polega na tym, że najpierw framework czyta z requestu jedynie id, potem w prepareUpdate() ładujemy oryginalny obiekt z bazy by w drugim uruchomieniu interceptora params nanieść na oryginalny obiekt zmiany nadesłane od użytkownika. Całość jedynie zapisujemy w update().

Tytułem wyjaśnienia - zmienna model została już omówiona, natomiast zmienna list jest wykorzystywana jedynie przy wyświetlaniu listy. Okazuje się bowiem, że wszystkie pozostałe akcje korzystają z pojedynczej instancji klasy modelu, a tylko list() potrzebuje całej listy. Mała, ale chyba wybaczalna niekonsekwencja. Wytłumaczę się również z terminologii - edit() i create() to akcje wyświetlające formularze edycji, natomiast update() i save() służą do zapisania odpowiednio zmian lub nowego obiektu.

Może jeszcze wytłumaczę się ze szczątkowej logiki w show(). Otóż klasa zakłada, że zaimplementowana w klasie dziedziczącej metoda prepareShow() zajmie się zapisaniem w zmiennej model obiektu do wyświetlenia. Jeśli metoda tego nie zrobiła lub nie odnalazła odpowiedniego obiektu - show() wraca do widoku listy z komunikatem o błędzie. Tutaj widać zastosowanie interceptora store: dodajemy komunikat o błędzie, ale potem robimy redirect do listy - bez tego interceptora komunikat by zniknął.

Podkreślę po raz pierwszy, ale nie ostatni, że ta jedna klasa wspierać będzie CRUD dla dowolnych obiektów modelu, uwalniając nas od kilku męczących szczegółów. Pora zatem przejść do konkretnej implementacji:


public class MoviesAction extends AbstractCrudAction<Movie> {

private MoviesDao moviesDao;

private long id;

public void setId(long id) {
this.id = id;
}

public long getId() {
return id;
}

@Override
public void prepareList() {
list = moviesDao.getAllMovies();
}

@Override
public void prepareShow() {
model = moviesDao.getMovie(id);
}

@Override
public void prepareEdit() {
model = moviesDao.getMovie(id);
}

@Override
public void prepareSave() {
model = new Movie();
}

@Override
public String save() {
moviesDao.saveMovie(model);
return SHOW;
}

@Override
public void prepareUpdate() {
model = moviesDao.getMovie(id);
}

@Override
public String update() {
moviesDao.updateMovie(model);
return SHOW;
}

@Override
public void prepareDelete() {
model = moviesDao.getMovie(id);
}

@Override
public String delete() {
moviesDao.deleteMovie(model);
return REDIRECT_LIST;
}

public void setMoviesDao(MoviesDao moviesDao) {
this.moviesDao = moviesDao;
}
}


MoviesDao jest interfejsem wstrzykiwanym przez Springa - konkretna implementacja nie jest istotna. Najważniejsze, że dotarliśmy szczęśliwie do celu: oto właściwa akcja implementująca pełen cykl CRUD składa się z samych jednolinijkowców, żadnej logiki, sama esencja - odczyt bądź zapis z wykorzystaniem DAO (dla przeciwników tego wzorca, wstrzyknięcie bezpośrednio EntityManagera też by się sprawdziło). Jeśli chcemy zaimplementować CRUD dla innego obiektu z modelu, właściwie wystarczy zaimplementować również prostą akcję dziedziczącą po AbstractCrudAction<E>.

No, nie tylko - jeszcze warstwa prezentacji, która siłą rzeczy musi się różnić. Ale o niej mówić nie będę, zainteresowanych JSPami odsyłam do kodu źródłowego. Warto zerknąć na list.jsp (użyłem displaytaga, biblioteki, której warto poświęcić osobny wpis… kiedyś) oraz na input.jsp (jeden prosty atrybut validate="true" i Strutsy wygenerują nam śliczną walidację po stronie w klienta w JavaScripcie). Właśnie: nie wspomniałem też o walidacji (nie mogłem skorzystać z adnotacji, ponieważ musiałbym nimi udekorować obiekt dziedziny, co jest kiepskim pomysłem) oraz internacjonalizacji. Znowu odsyłam do kodu aplikacji.

To by było na tyle, niestety z przykrością muszę powiadomić o kolejnym zgrzycie w wersji 2.1.6, który już wcześniej dawał mi się we znaki. Przy wysyłaniu redirect do klienta z parametrem id = ${id}, chociaż zupełnie poprawne i działa, powoduje pojawienie się logu na poziomie ERROR z komunikatem:

2009-01-25 22:21:55,947 ERROR [CommonsLogger.java:27] : Unable to set parameter [id] in result of type [org.apache.struts2.dispatcher.ServletActionRedirectResult]
Caught OgnlException while setting property 'id' on type 'org.apache.struts2.dispatcher.ServletActionRedirectResult'. - Class: ognl.ObjectPropertyAccessor
File: ObjectPropertyAccessor.java
Method: setProperty
Line: 132 - ognl/ObjectPropertyAccessor.java:132:-1
at com.opensymphony.xwork2.ognl.OgnlUtil.internalSetProperty(OgnlUtil.java:392)
[…]

Nie jest to nasz błąd, zwyczajnie twórcy Struts2 nie mogą się zdecydowanie, na jakim poziomie zalogować tą informację: Using the Redirect Action Result with parameters to the target action causes an OGNL warning. BTW mój problem z myślnikiem w groupId okazał się być przypadłością mavena, mogłem sprawdzić z innym archetypem, mea culpa. Podziękowania dla Łukasza Lenarta za komentarz.



Zgodnie z obietnicą kod przykładowej aplikacji, wystarczy mvn jetty:run by odrobinkę sobie poklikać. Jeśli w moim zdecydowanie przydługim opisie pominąłem jakiś ważny szczegół, proszę o informację.

P.S.: Mój Eclipse Ganymede (3.4.1) wyświetla idiotyczny błąd w plikach JSP korzystających z displaytaga:

Syntax error on token "}", delete this token

W linii… 0 pliku JSP. Jeśli ktoś spotkał się z podobną przypadłością (albo jeszcze lepiej udało mu się ją zwalczyć), byłbym wdzięczny za info :-).

January 23, 2009

Elegancki CRUD w jednej akcji Struts2 część 1/2

Elegancki CRUD w jednej akcji Struts2 część 1/2

Wreszcie pojawiło się Struts2 w stabilnej wersji z gałęzi 2.1.x. Branch ten wprowadza wiele nowości, dlatego z niecierpliwością czekałem na edycją oznaczoną literkami GA miast beta. Chyba programiści się nieco pośpieszyli z oznaczeniem Struts 2.1.6 mianem gotowego produkcyjnie, ale nie przeszkodzi to nam w przedstawieniu krótkiego tuto riala tego znakomitego frameworku.

Postanowiłem przedstawić Wam sposób na implementację całego procesu CRUD określonego obiektu dziedziny za pomocą jednej akcji. Pojedyncza klasa będzie zatem odpowiedzialna za cały cykl życia obiektu: utworzenie, edycję, podgląd, przeglądanie i usuwanie. Jako przykład wymyśliłem sobie portal filmowy i oczywiście obiekt Movie:


public class Movie {
private long id;
private String title;
private Calendar released;
private String director;
private Integer length;
//get/set
}


Ale od początku, zaczynamy od utworzenia szkieletu projektu za pomocą mavena:

mvn archetype:create -DgroupId=com.blogspot.nurkiewicz.film-portal -DartifactId=web -DarchetypeGroupId=org.apache.struts -DarchetypeArtifactId=struts2-archetype-starter -DarchetypeVersion=2.0.11.2-SNAPSHOT -DremoteRepositories=http://people.apache.org/repo/m2-snapshot-repository

I ochoczo zmieniamy wersję Struts2 z 2.0.11.2 na 2.1.6, Javę na 1.6 i JUnit na 4.5. Budujemy projekt i… pierwszy kłopot. W groupId uzyłem myślnika com.blogspot.nurkiewicz.film-portal, a archetyp mavenowy bez zastanowienia wygenerował pakiet z myślnikiem… Nie wiem czy to problem z archetypem Struts2 czy ogólnie mavenowy. Póki co zgłosiłem zespołowi Struts2 (WW-2965 - Maven archetype produces malformed Java code when dashes occur in groupId).

Drobnostka, po niewielkich poprawek przystępujemy do prac właściwych. Najpierw konieczne zmiany we właściwościach projektu (struts.properties):

struts.locale=pl_PL
struts.enable.SlashesInActionNames=true
struts.action.extension=action,

Pierwsza właściwość jest oczywista, bez niej z jakichś powodów Struts2 używał nieco innego Locale przy konwersji daty na String a innego przy operacji odwrotnej, co skutkowało błędami walidacji... Co prawda szukałem przyczyny dość długo debugując kod frameworku, jednak przyjrzę się temu problemowi jeszcze kiedy indziej.

Druga włącza możliwość używania slashy w nazwach akcji - otwiera to przed nami bardzo ciekawe możliwości, o czym zaraz. I wreszcie ostatni parametr… Uważny czytelnik zauważy przecinek na końcu - o niego właśnie chodzi :-). De facto pozwalamy Strutsom na używanie akcji bez rozszerzenia .action w adresach. Po cóż nam te dziwne ustawienia? Otóż dzięki nim nasza aplikacja webowa zyska "niemal przyjazne" adresy, przykładowo:

http://localhost:8080/web/movie/create

Zamiast standardowego:

http://localhost:8080/web/createMovie.action

Prawda, że ładniej? :-) Czas odkryć karty - stworzymy jedną akcję MoviesAction, która będzie miała zamiast jednej metody execute() szereg metod odpowiadających odpowiednim funkcjom cyklu życia obiektu, np. create(), update(), show(). Każda z tych metod będzie tworzyła logicznie jedną akcję dostępną odpowiednio pod nazwą: movie/create, movie/update czy movie/show. Ponadto dodamy metodę list() do przeglądania wszystkich filmów. Potrzebujemy jednak jeszcze kilku, nie do końca trywialnych zabiegów. Przede wszystkim serce aplikacji, czyli struts.xml - dodajemy do domyślnego pakietu następujące deklaracje konfiguracyjne:

<interceptors>
<interceptor-stack name="crudStack">
<interceptor-ref name="paramsPrepareParamsStack">
<param name="validation.excludeMethods">list,create</param>
<param name="workflow.excludeMethods">list,create</param>
</interceptor-ref>
<interceptor-ref name="store">
<param name="operationMode">AUTOMATIC</param>
</interceptor-ref>
</interceptor-stack>
</interceptors>

<default-interceptor-ref name="crudStack" />

<default-action-ref name="index" />

<global-results>
<result name="error">/common/error.jsp</result>
</global-results>

<global-exception-mappings>
<exception-mapping exception="java.lang.Throwable" result="error" />
</global-exception-mappings>

<action name="index">
<result type="redirectAction">movie/list</result>
</action>

Na widok takiej dawki XMLa zapewne powiało grozą, albo jeszcze gorzej, przypomniało się Wam EJB 2.1 ;-). Nie jest jednak tak źle - pierwszy element, interceptor-stack, definiujemy stos interceptorów, jakiego chcemy używać. Jak działa stos interceptorów i dlaczego jest tak ważny to temat na zupełnie osobny artykuł - omówię zatem jedynie różnice w stosunku do stosu domyślnego. Po pierwsze zamiast defaultStack jako bazę wybrałem paramsPrepareParamsStack - o zmyślnej sztuczce tego stosu opowiem przy okazji akcji update().

Druga ważna zmiana to dodanie interceptora store, który jest zdefiniowany w struts-default.xml, jednak nie należy do żadnego gotowego stosu interceptorów - chociaż są plany, by w gałęzy 2.2 Strutsów był już w stosie domyślnym, właśnie z taką konfiguracją. A co robi? Bardzo sprytną rzecz - otóż jeśli w naszej akcji dodamy jakieś komunikaty (addActionMessage() bądź addActionError()) przepadną one jeśli wyślemy do klienta komunikat redirect (mają one bowiem zasięg pojedynczego żądania). Wyobraźmy sobie jednak dowolną akcję, która modyfikuje bazę danych (a zatem do dobrego smaku należy zastosowanie wzorca GET after POST) i chce poinformować użytkownika o sukcesie właśnie takim komunikatem. Niestety - nie jest to możliwe, ponieważ zaraz po modyfikacji wykonuje redirect i dodany chwilę wcześniej komunikat przepada. Właśnie taki, całkiem częsty scenariusz, obsługuje MessageStoreInterceptor: przed wykonaniem redirecta zachowuje wszystkie komunikaty w sesji by potem automatycznie - przy następnym żądaniu - odczytać je i dodać do strony. Sprytne, prawda?

Kolejna ciekawostka to ustawienie parametru excludeMethods interceptorów validation i workflow. Tutaj rozwiązujemy problem zbyt gorliwej walidacji. Otóż walidację w Struts2 definiuje się dla całej klasy akcji, nie da się bezpośrednio ograniczyć reguł walidacji (tak w XML, jak i w adnotacjach) do poszczególnych metod będących akcjami logicznymi. A to prowadzi do dziwnych błędów - np. przy wyświetlaniu ekranu do wprowadzenia nowego filmu na dzień dobry dostajemy błąd walidacji, że pole tytuł jest puste (taką regułę dodamy). Dlatego dla metody create (wprowadzanie nowego filmy) oraz list (wyświetlanie listy filmów) walidacja została wyłączona.

Przy okazji mrożąca krew w żyłach ciekawostka: Struts2 przeprowadza walidację w aż czterech różnych interceptorach… Najpierw interceptor params "przepisuje" wartości z requestu HTTP do pól dostępnych na stosie wartości (ValueStack), przy okazji zapisując w kontekście akcji błędy konwersji. Następnie conversionError konwertuje błędy z kontekstu na omawiane wcześniej komunikaty (addActionError()). Dalej interceptor (werble!) validation wykonuje walidację deklaratywną (XML i/lub adnotacje). Na samym końcu interceptor workflow uruchamia metodę validate() akcji… Uff… Na każdym z tych etapów pojawienie się błędów skutkuje przerwaniem dalszego przetwarzania interceptorów (a tym bardziej wywołania akcji) i natychmiastowym zwróceniem rezultatu INPUT, który powinien prowadzić z powrotem do felernego formularza z błędami. Chain-of-responsibility w całej krasie, :-)

Zamykamy konfigurację stosu interceptorów, od tej pory będzie już naszym najlepszym przyjacielem. Ustawiamy tak zdefiniowany stos jako domyślny (default-interceptor-ref) i wskazujemy domyślną akcję, jeśli użytkownik nie poda żadnej lub poda błędną. I tu przykre zaskoczenie. Skoro akcja index wykonuje jedynie redirect do akcji movie/list, to czemu nie ustawić od razu tej akcji jako domyślnej? Próbowałem… i otworzyłem kolejne zgłoszenie buga :-( WW-2963 - default-action-ref fails to find wildcard named actions.

W tym momencie chciałbym jeszcze zwrócić uwagę na brak atrybutu class w akcji index. Jest to zupełnie poprawna konstrukcja, zwyczajnie tworzymy akcję o pustej implementacji (używana jest klasa com.opensymphony.xwork2.ActionSupport, którą można przedefiniować używając taga <default-class-ref>). Nawet więcej - nie tylko poprawna, ale wręcz zalecana - zawsze powinniśmy kierować użytkownika do akcji, a nie od razu do JSP - uzyskamy spójne adresy URL, pliki JSP będą wzbogacone o kilka dodatkowych funkcji dodanych przez Struts (przejdą bowiem przez pełen stos interceptorów) oraz lepiej odesparujemy logikę od widoku.

Na koniec opowiem jeszcze o tagach <global-results> oraz <global-exception-mappings>, które znakomicie się uzupełniają. Ten drugi mapuje wyjątki rzucone przez nasze akcje bądź interceptory na rezultaty. Przykładowo jeśli nasza akcja rzuca wyjątkiem, nie musimy ręcznie go łapać i zwracać ERROR zamiast SUCCESS. Zrobi to za nas interceptor exception (to dlatego zawsze powinien być na samym dole stosu - by łapać wyjątki od wszystkich interceptorów i akcji nad nim), który złapie wyjątek dużo niżej i zamieni go na zdefiniowany rezultat. Jeśli dodamy do tego globalne mapowanie wskazanego rezultatu na określony widok, możemy uzyskać ładnie wyglądającą stronę z błędem (oksymoron?) zamiast błędu serwera i śladu stosu.

Tyle na dzisiaj, rozpisałem się niemiłosiernie o zwykłym CRUDzie, a nawet nie doszliśmy do akcji. Obiecuję, że w drugiej, ostatniej części dokończę przykład, może nawet napiszemy coś w Javie? ;-) Na pocieszenie dodam, że całą tą konfigurację robi się raz dla całego pakietu (zbioru akcji), a jeśli dodać do tego możliwość dziedziczenia pakietów, cały ten XML nie jest już nam taki straszny.

January 7, 2009

SCJD - Sun Certified Java Developer zdobyty!

Bez zbędnych wstępów, chciałem pochwalić się zdanym certyfikatem Sun Certified Java Developer (SCJD, CX-310-027), którego przyjemność zdawania umożliwiła mi moja ulubiona firma :-). Przyjemność tym większa, że wynik bardzo pozytywnie mnie zaskoczył (dodam, że próg wynosi 320/400):







SectionMaxActual
General Considerations:10090
Documentation:7070
Object-Oriented Design:3030
GUI:4031
Locking:8080
Data Store:4040
Network Server:4040
Total:400381

Ale nie o wynikach chciałem napisać, a podzielić się wrażeniami. Ponieważ szczegóły zadanie są tajne/poufne :-) - raczej ogólnymi.

Przede wszystkim certyfikat, złożony z zadania projektowego i eseju, raczej nie nauczy Was super-nowego-frameworku, biblioteki czy technologii. Swing + RMI i zakaz korzystania z jakichkolwiek zewnętrznych bibliotek.

Ale chyba nie o to chodzi - ten certyfikat to głównie szlifowanie i sprawdzanie "miękkich" umiejętności programistycznych. Kod musi być dobrze napisany, udokumentowany, korzystać z całej palety wzorców projektowych. Trzeba wykazać się znajomością podstawowego API (strumienie, wątki, sieć), ale żadnego EJB, serwletów, JPA. I w tej materii wypada bardzo dobrze - sporo się nauczyłem na płaszczyźnie projektowania (z głównym naciskiem na czytelność, nawet kosztem wydajności), pisanie Javadoków weszło mi w krew (w projekcie każdy publiczny element kodu musi być udokumentowany).

Jeśli zatem chcecie sprawdzić umięjętności programistyczne jako takie, a nie znajomość takiego czy innego API, SCJD jest bardzo dobrym wyborem. Napisanie projektu sprawdzonego czujnym okiem egzaminatorów z Suna (jedyny obok SCEA certyfikat wymagający pisania żywego kodu) daje wiele satysfakcji, zwłaszcza przy pozytywnym wyniku. Sam temat projektu też jest dość ciekawy - z reguły nierelacyjna baza danych na wielodostępnym serwerze i klient w Swingu komunikujący się po RMI lub socketach.

Na koniec kilka uwag technicznych:
  • maven sprawdził się znakomicie w tym dość specyficznym projekcie. Budował JAR, generował Javadoki, uruchamiał testy, zarządzał zależnościami testowymi (innych mieć nie można) i budował za pomocą assembly wynikowy artefakt ze spakowanym programem, dokumentacją i źródłami.
  • Dokumentację użytkownika dostarczyłem w formacie HTML generując ją również mavenem z DocBooka za pomocą docbkx-maven-plugin.
  • Formatowanie kodu w Eclipse Ganymede poradziło sobie z momentami zakręconymi standardami Suna (np. "Four spaces should be used as the unit of indentation. The exact construction of the indentation (spaces vs. tabs) is unspecified. Tabs must be set exactly every 8 spaces (not 4)").
  • Przed napisaniem projektu przeczytałem (czasem kartkując) SCJD Exam with J2SE 5 (autorstwa Andrew Monkhouse i Terry Camerlengo). Raczej warto.
  • Całość (od pierwszej linijki kodu do ostatniej linijki docbookowego XMLa) zajęła mi miesiąc, w sumie ponad 10 KLOC.
To tyle, szczerze mogę polecić ten certyfikat, chociaż do najłatwiejszych (ani najtańszych) nie należy.

Freemarker - pierwsze kroki

Postanowiłem sporządzić krótki tutorial z jednym prostym przykładem pokazującym podstawowe możliwości Freemarkera. Wbrew pozorom nie stanąłem przed koniecznością migracji plików JSP na Freemarker, a użyłem tej biblioteki w klasycznej, konsolowej aplikacji Java SE. Jak się okazało, sprawdziła się znakomicie.

Bez zbędnego lania wody - Freemarker jest procesorem, który na wejściu otrzymuje model (zestaw obiektów Java) + szablon, a na wyjściu produkuje dokument będący szablonem uzupełnionym o odpowiednio sformatowane dane z modelu. Problem polega na konwersji obiektu Java do reprezentacji tekstowej. By być precyzyjnym, chodziło o translację następującego JavaBeanu:
package com.blogspot.nurkiewicz;

import java.util.List;

public class Procedure {

private boolean returns;

private String name;

private List<String> args;

/* konstruktory, gettery i settery*/

}

do kodu w języku Java. Dla przykładowych wartości właściwości: name="count", args=[x, y, size], returns=true, powinniśmy otrzymać następujący tekst:
public double subCount(double x, double y, double size);
Zacznijmy zatem od przygotowania środowiska. Podstawowym obiektem Freemarkera jest klasa Template. Co prawda dokumentacja poleca tworzenie jej instancji za pomocą klasy Configuration, jednak ponieważ domyślnie ma ona dość ubogie API (można ładować szablony jedynie z plików, i to bez przeszukiwania CLASSPATH), utworzymy obiekt klasy Template bezpośrednio:
Template template = new Template(null,
new InputStreamReader(Main.class.getResourceAsStream("procedure.ftl")),
new Configuration());
Kod ten został umieszczony w klasie com.blogspot.nurkiewicz.Main, zatem plik procedure.ftl będzie szukany w katalogu src/main/resource/com/blogspot/nurkiewicz.

Posiadając obiekt template możemy przystąpić do zasilenia go modelem; wystarczy prosta mapa:
Map<String, Object> model = new HashMap<String, Object>();
final Procedure procedure = new Procedure(true, "count", Arrays.asList("x", "y", "size"));
model.put("proc", procedure);
Jak widać umieściliśmy w modelu, pod kluczem proc, instancję naszej klasy Procedure. Oznacza to, że w naszym szablonie będziemy mogli otrzymać wartości z tego obiektu posługując się prefiksem proc. Spróbujmy - przypominam, że plik procedure.ftl zawiera treść szablonu:
public double ${proc.name}(double ${proc.args[0]}, double ${proc.args[1]}, double ${proc.args[2]});
${proc.name} jest jednym z odwołań do modelu - w tym wypadku do właściwości name obiektu pod kluczem proc (czyli Freemarker wywoła Procedure.getName() na instancji klasy Procedure). Z kolei ${proc.args[0]} spowoduje javowe odwołanie getArgs().get(0). Jasne.

Do uruchomienia przykładu potrzebujemy jeszcze właściwego przetworzenia szablonu wraz z modelem oraz zdefiniowania dokąd ma trafić wynik (w naszym wypadku standardowe wyjście):
template.process(model, new OutputStreamWriter(System.out));
I wynik programu, nieco odbiegający od oczekiwań:
public double count(double x, double y, double size);
Nie dość, że brakuje prefiksu sub przed nazwą metody, to jeszcze zahardkodowaliśmy długość listy args oraz nie sprawdzamy właściwości returns, wartość której determinuje, czy metoda zwraca double czy void. Zacznijmy od tego. Freemarker udostępnia proste wyrażenia warunkowe, zwróćcie uwagę, że nie ma już potrzeby korzystania ze znaku dolara i nawiasów klamrowych:
public <#if proc.returns>double<#else>void</#if>
${proc.name}(${proc.args[0]}, ${proc.args[1]}, ${proc.args[2]});
Dla lepszej czytelności rozbiję szablon na linijki. Wyrażenie chyba oczywiste, Freemarker domyśla się, że trzeba wywołać metodę Procedure.isReturns(). Jednak zamiast złożonego warunku możemy zwyczajnie napisać:
${proc.returns?string("double", "void")}
co przypomina znany z wielu języków, także Javy, trójargumentowy operator warunkowy ?: .

Większym problemem jest nazwa metody - samo dodanie prefiksu nie wystarczy, ponieważ dodatkowo, zgodnie z notacją camel case, trzeba rozpocząć dostarczoną nazwę metody od wielkiej litery. Szczęśliwie, Freemarker potrafi sobie poradzić z tak prostym zabiegiem edycyjnym:
public ${proc.returns?string("double", "void")} sub${proc.name?cap_first}(
${proc.args[0]}, ${proc.args[1]}, ${proc.args[2]}
);
cap_first od capitalize first letter, Freemarker umożliwia nam jeszcze wiele innych transformacji, takich jak substring, dopełnianie czy obcinanie białych znaków. Tymczasem nasz wynik:
public double subCount(
x, y, size
);
Została tylko nieszczęsna lista. Po pierwsze musimy umieć iterować po liście dowolnej długości, po drugie ostatni element nie może się kończyć przecinkiem. Użyjemy w tym celu dyrektywy <#list> (istnieje również uboższa wersja: <#foreach>):
<#list proc.args as arg>
double ${arg},
</#list>);
Znowu dość prosty kod: dla każdego elementu z kolekcji proc.args (w każdej iteracji dany element jest widoczny pod kluczem arg) zostanie wydrukowana zawartość dyrektywy <#list>, czyli w naszym wypadku "double ${arg},". Efekt do przewidzenia, ale co zrobić z przecinkiem po ostatniej iteracji? Otóż dyrektywa <#list> wprowadza kilka dodatkowych zmiennych, m.in. swojsko brzmiącą arg_has_next. Czy trzeba tłumaczyć, że w każdej iteracji z wyjątkiem ostatniej przybiera ona wartość true? I czy muszę pokazywać pełny kod szablonu?
public ${proc.returns?string("double", "void")} sub${proc.name?cap_first}(
<#list proc.args as arg>
double ${arg}<#if arg_has_next>, </#if>
</#list>);
I tak oto Freemarker pomógł mi skrócić kod w Javie do zaledwie jednej linijki wykonującej odpowiedni szablon, zamknąłem widok i odizolowałem od modelu. A co najważniejsze, usunąłem pachnącą amatorką, zamotaną pętlę ze StringBuilderem, warunkami i tekstem przeplecionym z kodem w Javie - i chyba o to chodzi?

Mam nadzieję, że udało mi się przybliżyć składnię Freemarkera i zachęcić do stosowania tego narzędzia wszędzie tam, gdzie trzeba zamienić dane na tekst. Narzędzie to może również z powodzeniem zastępować XSLT: FreeMarker vs. XSLT.

Pełen kod źródłowy programu, maven friendly, 3,3 KiB.