03 września 2008

JSF 1.2 - rozdział 7.4 NavigationHandler

Rozdział 7 specyfikacji JavaServer Faces przedstawia API z pakietu javax.faces.application, które stanowi podwaliny dla mechanizmu rozszerzania (w tym i zmiany) zachowania działania szkieletu JSF. Wielokrotnie spotykam się z opinią o bardzo nieprzyjaznym dla programisty rozwiązaniu jakim jest JSF i tyleż samo napotykam w nim interesujących konstrukcji programistycznych. Przykładem możliwości jakie można uzyskać w JSF jako szkielecie aplikacyjnym może być JBoss Seam, którego głównym celem jest (a może jedynie początkowo była) integracja technologi interfejsu użytkownika w Java EE - JavaServer Faces 1.2 - oraz technologii komponentów biznesowych opartych o Enterprise JavaBeans (EJB) 3.0. Dodając do tego projekty facelets oraz RestFaces, którymi parałem się przez ostatnie tygodnie i coraz bardziej intrygowało mnie, co spowodowało, że stało się to możliwe. Wystarczy dostarczyć pewne rozszerzenie (ala wtyczkę) do JSF deklarując w faces-config.xml i już JSF może działać inaczej niż byśmy mogli sobie początkowo wyobrazić. Na pierwszy ogień poszedł javax.faces.application.NavigationHandler, który opisany jest w rozdziale 7.4 NavigationHandler specyfikacji JavaServer Faces 1.2.

NavigationHandler jest to część szkieletu aplikacyjnego JSF uruchamiania do obsługi łańcucha znakowego będącego logicznym identyfikatorem zwróconym przez wykonaną akcję w aplikacji i wybraniem nowego widoku do wyświetlenia. Jeśli wynik akcji jest null ten sam widok, który spowodował wykonanie akcji zostanie ponownie wyświetlony. W zasadzie najlepiej możnaby to uzmysłowić prezentując sygnaturę jedynej metody abstrakcyjnej klasy NavigationHandler:
 public void handleNavigation(FacesContext context, String fromAction, String outcome)
Na podstawie przekazanego kontekstu FacesContext oraz wyniku outcome działania akcji fromAction handleNavigation podejmuje decyzję o kolejnej stronie do wyświetlenia.

Implementacja JSF musi dostarczyć domyślną realizację NavigationHandler, który pozwala na zdefiniowanie przepływów (przejść między stronami w aplikacji) w plikach konfiguracyjnych (domyślnie faces-config.xml). Do konfiguracji przepływu służy znacznik <navigation-rule>. Znacznik może zawierać opcjonalny <from-view-id>, który akceptuje wartości wyrażenia regularnego dla aktualnego identyfikatora widoku postaci dosłownej (pełne dopasowanie)
 <navigation-rule>
<from-view-id>/stworzKategorie.jsp</from-view-id>
<navigation-case>
<from-action>#{kategoriaAgent.stworz}</from-action>
<to-view-id>/wyswietl.jsp</to-view-id>
</navigation-case>
</navigation-rule>
, początek widoku (!) zakończonego gwiazdką
 <navigation-rule>
<from-view-id>/stworz*</from-view-id>
<navigation-case>
<from-outcome>success</from-outcome>
<to-view-id>/wyswietl.jsp</to-view-id>
</navigation-case>
</navigation-rule>
, gwiazdkę, która określa wszystkie możliwe widoki (strony)
 <navigation-rule>
<from-view-id>*</from-view-id>
<navigation-case>
<from-action>#{kategoriaAgent.stworz}</from-action>
<to-view-id>/wyswietl.jsp</to-view-id>
</navigation-case>
</navigation-rule>
lub po prostu bez określenia <from-view-id>, co przekłada się również na dowolny widok:
 <navigation-rule>
<navigation-case>
<from-outcome>success</from-outcome>
<to-view-id>/wyswietl.jsp</to-view-id>
</navigation-case>
</navigation-rule>
W ramach <navigation-rule> znajduje się dowolna liczba <navigation-case>, które precyzują dopasowanie.

Możliwe jest określenie wielu <navigation-rule> dotyczących tego samego <from-view-id>, ale niepoprawnym jest określenie tej samej kombinacji <from-*> dla danego <from-view-id>.

Zwrócenie null z akcji informuje NavigationHandler, aby nie przeszukiwał reguł i ponownie wyświetlił aktualny widok.

Elementy <from-outcome> oraz <from-action> odpowiadają parametrom wejściowym metody handleNavigation.

Możliwe jest skorzystanie z elementu <redirect/> do określenia przekierowania aktualnego żądania do zadanej strony, po którym następuje zatrzymanie przetwarzania żądania JSF (wywołując javax.faces.context.FacesContext.responseComplete()). Wyjątkiem jest środowisko portletowe, gdzie przekierowania nie są dozwolone.
 <navigation-rule>
<from-view-id>/stworz*</from-view-id>
<navigation-case>
<from-outcome>success</from-outcome>
<to-view-id>/wyswietl.jsp</to-view-id>
</navigation-case>
<navigation-case>
<from-outcome>failure</from-outcome>
<to-view-id>/failure.jsp</to-view-id>
<redirect />
</navigation-case>
</navigation-rule>
Brak <navigation-rule>, która pasuje do parametrów metody handleNavigation nie zmienia aktualnego widoku (podobnie jak wartość null).

Znalezienie pasującego widoku powoduje zbudowanie nowego, tracąc stan poprzedniej (uwaga: może być czasochłonne).

Podmiana domyślnego NavigationHandler następuje programowo poprzez wykonanie metody javax.faces.application.Application.setNavigationHandler(NavigationHandler handler) lub deklaratywnie w faces-config.xml w sekcji <application>.
 <application>
<navigation-handler>klasa.realizujaca.NavigationHandler</navigation-handler>
</application>
Można wyobrazić sobie implementację, której konfigurację przepływów definiujemy w faces-config.xml oraz bazie danych. Dobrym źródłem wiedzy nt. temat będzie z pewnością wątek How the way for create a new NavigationHandler w stosunkowo młodym forum GlassFish WebTier. Możnaby również oprzeć NavigationHandler na regułach pisanych w Groovy czy umożliwić kreowanie przepływów z poziomu interfejsu użytkownika. Chętnie zapoznałbym się z wdrożonymi pomysłami.

Pozostaje życzyć sobie, żeby konstruowanie nowych kontrolek JSF było równie proste. Jest to bodajże najbardziej pracochłonne zadanie w JSF. Na razie jest nie na moje siły i wydaje się, że jedynie facelets mógłby być panaceum (acz wprowadza własny ViewHandler i znaczniki JSP należy migrować do znaczników facelets, co ponownie może być nie lada wyzwaniem).