MVC


Wstęp

Platforma jPALIO wspiera tworzenie aplikacji w oparciu o wzorzec projektowy MVC (Model View Controller). Jest to wzorzec, który doskonale sprawdza się w przypadku aplikacji webowych. Zapewnia rozdzielenie warstwy modelu danych, warstwy prezentacji oraz warstwy logiki biznesowej.

Podstawowym komponentem w tym wzorcu jest kontroler. Jest on odpowiedzialny za sterowanie obsługą żądania, wywoływanie komponentów realizujących logikę biznesową związanych z danym żądaniem, przygotowanie modelu danych dla widoku i następnie wywołanie widoku.

Widok w jPALIO MVC to lista obiektów jPALIO (zazwyczaj jednoelementowa), które są wywoływane jeden po drugim. Lista tych obiektów może być dynamicznie modyfikowana podczas przetwarzania.

Kontroler

Klasą bazową dla wszystkich kontrolerów jPALIO MVC jest klasa jpalio.mvc.Controller. Aby stworzyć kontroler należy w obiekcie jPALIO utworzyć klasę dziedziczącą po tej klasie. Sterowanie obsługą danego żądania odbywa się poprzez adnotacje umieszczone przy metodach kontrolera oraz ich argumentach. Tylko oznaczone metody biorą udział w procesie obsługi żądania.

O tym jaka metoda będzie obsługiwac dane żądanie decydują następujące czynniki:

Jedyną czynnością jaką należy wykonać aby żądania wysyłane do danej strony jPALIO były obsługiwane przez kontroler jest ustawienie obiektu jPALIO zawierającego daną klasę kontrolera jako obiekt ciała strony jPALIO. Utworzenie nowego kontrolera nie wymaga restartu środowiska jPALIO.

Przykład bardzo prostego kontrolera bazującego na nazwach metod (żadna z metod nie ma argumentów)

package samples.features.userInterface.mvc;

import palio.*
import palio.modules.*
import jpalio.mvc.*
import jpalio.mvc.annotations.*
 
public class SimpleController extends Controller {
 
    @RequestHandler(view=["samples.features.userInterface.mvc.defaultHandlerView"], defaultHandler=true)
    public void someDefaultHandler() {
        
    }

    @RequestHandler(view=["samples.features.userInterface.mvc.firstRequestHandlerView"])
    public void firstRequestHandler() {
        
    }

    @RequestHandler(view=["samples.features.userInterface.mvc.secondRequestHandlerView"])
    public void secondRequestHandler() {
        
    }

    // Handling unhandled exceptions
    protected void handleException(Exception ex) {
        // Do some exception handling
        println "An exception occured: ${ex.getClass().getName()}"
    }
}

Adnotacja jpalio.mvc.annotations.RequestHandler

Adnotacja jpalio.mvc.annotations.RequestHandler służy do oznaczenia metod kontrolera, które powinny brać udział w procesie obsługi żądań skierowanych do danego kontrolera. Dodatkowo definiuje ona statyczny widok oraz uprawnienia jakie powinien posiadać użytkownik z bieżącej sesji aby można było wykonać daną metodę. Poniżej przedstawiono zestawienie wszystkich pól tej adnotacji wraz z opisem.

Nazwa Opis Wartość domyślna
view Lista kodów obiektów jPALIO, które zostaną umieszczone na początku listy obiektów widoku brak
finalView Lista kodów obiektów jPALIO, które zostaną umieszczone na końcu listy obiektów widoku brak
privs Przywileje wymagane do wywołania danej metody. Wystarczy, że użytkownik w bieżącej sesji będzie posiadał jeden z wymienionych przywilejów. brak
defaultHandler Znacznik określający czy do danej metody powinno być przekierowane żądanie w momencie gdy żadna z metod nie została dopasowana. Tylko jedna z metod kontrolera może być oznaczona jako domyślna. false

Przykłady w Groovy:

@RequestHandler(view=["samples.features.userInterface.mvc.objectsList"], defaultHandler=true)
@RequestHandler(view=["samples.features.userInterface.mvc.objectContent"])

Widok

Widok w jPALIO MVC to lista obiektów jPALIO, które wywoływane są jeden po drugim w ostatnim etapie przetwarzania żądania. W najprostszej opcji widokiem jest pojedynczy obiekt jPALIO. Zadaniem tych obiektów jest wygenerowanie widoku (zazwyczaj interfejsu użytkownika) na bazie modelu danych przygotowanych w kontrolerze. Obiekty jPALIO implementujące widok wskazywane są poprzez adnotację RequestHandler, a dokładniej poprzez jej pola view i finalView.

Lista obiektów widoków została podzielona na dwie listy. Pierwsza stanowi początek głównej listy (view), a druga stanowi koniec listy (finalView). Lista obiektów widoku może być budowana dynamicznie w trakcie przetwarzania żądania. Służą do tego następujące metody klasy jpalio.mvc.Controller:

setView
Nadpisuje początkową listę obiektów widoku
appendView
Dodaje kolejny obiekt/obiekty do początkowej listy obiektów widoku
setFinalView
Nadpisuje końcową listę obiektów widoku
appendFinalView
Dodaje kolejny obiekt/obiekty do końcowej listy obiektów widoku

 

Adnotacja jpalio.mvc.annotations.RequestParam

Adnotacja jpalio.mvc.annotations.RequestParam służy do oznaczania argumentów metod kontrolera. Powinna być ona używana tylko w przypadku argumentów metod oznaczonych jako RequestHandler. Adnotacja ta pełni ważną rolę w algorytmie dopasowania bieżącego żądania do danej metody. Adnotacja ta umożliwia także wiązanie parametrów żądania do argumentów metody. Poniżej przedstawiono zestawienie wszystkich pól tej adnotacji wraz z opisem.

Nazwa Opis Wartość domyślna
name Nazwa parametru w kontekście bieżącego żądania, która ma zostać związana z danym argumentem metody kontrolera.
brak
value Wymagana wartość parametru o nazwie określonej za pomocą pola name brak
required Określa czy dany parametr jest wymagany aby dana metoda została dopasowana do żądania.
true
format Pole używane do konwertowania wartości parametrów do typów zadeklarowanych w metodzie (np. typu java.util.Date) brak

Przykład w Groovy:

@RequestParam(name="_RowID") Long objectId
@RequestParam(name="objectCode") String objectCode

Adnotacja jpalio.mvc.annotations.RequestBean

Adnotacja jpalio.mvc.annotations.RequestBean służy do oznaczania argumentów metod kontrolera. Powinna być ona używana tylko w przypadku argumentów metod oznaczonych jako RequestHandler. Adnotacja ta umożliwia wiązanie parametrów żądania do pól klasy obiektu podanego jako argument. Klasa powinna być zgodna z konwencją JavaBeans. Adnotacja ta nie pełni żadnej roli w algorytmie dopasowania bieżącego żądania do danej metody. Poniżej przedstawiono zestawienie wszystkich pól tej adnotacji wraz z opisem.

Nazwa Opis Wartość domyślna
name Nazwa beana. Stanowi ona prefiks dla parametrów żądania, które mają zostać użyte przy wiązaniu wartości do pól klasy. Pole to nie jest obowiązkowe.
brak

Przykład:

// Without name
@RequestBean SampleBean bean
// With name
@RequestBean(name="bean") SampleBean bean

Algorytm dopasowania metody kontrolera do żądania

Jeżeli w konkteście żądania ustawiony jest parametr requestHandler to wybierana jest metoda o nazwie równej wartości tego parametru z największą liczbą dopasowanych wymaganych parametrów oznaczonych adnotacją RequestParam.

Jeżeli żadna z metod o nazwie równej wartości parametru requestHandler nie zostanie wybrana, wywoływana jest metoda oznaczona jako domyślna (defaultHandler).

W przypadku gdy w kontekście żądania nie jest ustawiony parametr o nazwie requestHandler, metoda wybierana jest tylko na podstawie adnotacji RequestParam argumentów. Wybierana jest metoda o największej liczbie dopasowanych wymaganych parametrów.

W przypadku gdy dopasowane zostaną dwie metody o takiej samej liczbie wymaganych parametrów wyrzucany jest wyjątek. Jest to sytuacja wyjątkowa, która nie powinna mieć miejsca.

Poniższy diagram w sposób ogólny przedstawia omawiany algorytm.

Przekazywanie modelu do widoku

Przekazywanie parametrów modelu do widoku może odbywać się na dwa sposoby. Pierwszym z nich są zmienne globalne. Klasa bazowa dla wszystkich kontrolerów jpalio.mvc.Controller posiada metodę setParam, która ustawia podany parametr. Wywołanie tej metody jest równoznaczne z wywołaniem metody setParam z modułu Palio.

// Method from controller
this.setParam("paramName", 123)
// Method from Palio module
palio.modules.Palio.setParam("paramName", 123)

Drugim, zalecanym sposobem jest przekazywanie parametrów do obiektów widoku. Aby przekazać parametry należy w metodzie obsługującej żądanie (request handler) zwrócić tablicę typu Object[] lub Collection. Elementy tej tablicy/kolekcji zostaną automatycznie użyte jako argumenty podczas wywoływania kolejnych obiektów jPALIO składających się na widok. Poniżej przedstawiono przykład.

@RequestHandler(view=["samples.features.userInterface.mvc.objectsList"], defaultHandler=true)
public Object[] objectsList() {
return [Groovy.module("sql").read("select CODE from P_OBJECTS order by CODE")]
}

Przykładowa treśc obiektu widoku

$#params(List objectsList)

<h1>Objects list</h1>

$=(@row, (Object[]) null)
...
})
 

Wiązanie danych żądania

jPALIO MVC wspiera mechanizm wiązania parametrów żądania do pól klasy argumentu oznaczonego adnotacją RequestBean. Adnotacją tą może być oznaczonych wiele argumentów metody obsługującej żądanie. W przypadku gdy podana klasa zawiera pola o typach, które nie mogą zostać automatycznie przekonwertowane z danych żądania na wartość danego typu (pola złożone lub na przykład data (konwersja zależy od formatu w jakim została przesłana) należy zarejestrować własny edytor dla danego pola. Aby tego dokonać należy nadpisać metodę initDataBinder w klasie bazowej kontrolera.

Przykładowa implementacja metody initBinder:

protected void initDataBinder(String beanName, jpalio.commons.bind.DataBinder dataBinder) {
if ("bean".equals(beanName)) {
dataBinder.registerEditor(SampleBean.class, new SampleBeanEditor())
}
if ("bean1".equals(beanName)) {
dataBinder.registerEditor(SampleBean.class, new SampleBeanEditor())
}
}

Klasa edytora:

public class SampleBeanEditor extends java.beans.PropertyEditorSupport {
    
    @Override
    public void setAsText(String text) {
        setValue(new SampleBean(id:Long.valueOf(text)));
    }
    
}

Nazwa beana pełni kluczową rolę przy wiązaniu danych. Stanowi ona prefiks nazwy dla wszystkich parametrów użytych do wiązania. W przypadku gdy nazwa nie jesta podana używane są wszystkie parametry.

Pełny przykład

Klasa palio.mvc.SampleBean:

package palio.mvc;

public class SampleBean {

Long id

String name

String description

SampleBean internalBean

public String toString() {
return "SampleBean[id:${id}, name=${name}, description=${description}, internalBean=${internalBean}]"
}

}

 

Formularz:

<form method="post" action="$page.url($currentPageCode())">
<input type="text" name="bean.id" value="123">
<input type="text" name="bean.name" value="Bean name">
<input type="text" name="bean.description" value="Bean description">

<select name="bean.internalBean">
<option value="1">bean with id = 1</option>
<option value="2">bean with id = 2</option>
<option value="3">bean with id = 3</option>
</select>

<button type="submit" name="requestHandler" value="beanTest">Send</button>

</form>

 

Metoda beanTest w kontrolerze:

@RequestHandler(view="palio.mvc.beanTest")
public Object[] beanTest(
@RequestBean(name="bean") SampleBean bean,
) {
// Do anything ...
return [bean]
}

 

Treść obiektu palio.mvc.beanTest:

$#params(palio.mvc.SampleBean bean)

<h1>$@bean.toString()</h1>

 

Przykładowa inicjalizacja mechanizmu wiązania danych oraz implementacja edytora została przedstawiona powyżej.

Obsługa wyjątków

Obsługa wyjątków w kontrolerze jPALIO MVC jest realizowana w sposób bardzo elastyczny. Poniżej przedstawiono możliwe sytuacje oraz sposoby ich obsługi.

Wyjątki w metodzie obsługi żądania (request handler)

Metody obługujące żądania powinny same obsługiwać wyjątki. Wyjątek wyrzucony poza metodę zostanie obsłużony przez mechanizm obsługi nieobsłużonych wyjątków (patrz niżej). Przykładowa obsługa wyjątku może wyglądać w sposób następujący:

 

try {
    setParam("objectsList", Groovy.module("sql").read("palio", "select aid, code from p_objects order by code"))
} catch (Exception ex) {
    setParam("exception", ex)
    setView("palio.mvc.errorView")
}

 

Nieobsłużone wyjątki

Klasa jpalio.mvc.Controller posiada abstrakcyjną metodę handleException. Każdy kontroler powinien posiadać implementację tej metody. Istnieje pełna dowolności jej implementacji. Może być to np. logowanie błędu do określonego logu na poziomie FATAL lub przekierowanie do obiektu obsługi błędów lub raportowanie poprzez wysłanie wiadomości email, SMS.

Dobrą praktyką jest stworzenie bazowego kontrolera dla wszystkich kontrolerów w danym projekcie z zaimplementowaną obsługą wyjątków nieobsłużonych. Dzięki temu wszystkie nieobsłużone wyjątki będą obsługiwane w jednolity sposób.

Przykładowa implementacja:

 

protected void handleException(Exception ex) {
    // Log message
    palio.Logger.getLogger(getClass().getName()).fatal("Unhandled exception occured", ex)
    
    // Send email message
    // palio.Groovy.module("email").sendMessage(...)
    
    // Invoke error handler object
    palio.Groovy.object("samples.features.userInterface.mvc.errorHandler", ex.getCause())
}

Przykładowa implementacja obiektu prezentacji błędu:

 

 

$#params(java.lang.Exception ex)

<html>
<head>
</head>

<body>
<h1 style="color:red;">Exception occured. Please contact with application administrator.</h1>
<h2>$@ex.getMessage()</h2>

</body>
</html>

 

jPALIO MVC vs jPALIO Classic

Poniżej przedstawiono dwie implementacje obiektu podpiętego pod stronę jPALIO implementującego wzorzec MVC. Jedna steruje wykonaniem strony z użyciem standardowych instrukcji sterujących jPALIO, druga zaś z wykorzystaniem wsparcia jPALIO MVC. Obie wersje obsługują dokładnie te same funkcjonalności. Przykłady te mogą sugerować, że zastosowanie jPALIO MVC wprowadza niepotrzebne utrudnienia. Należy pamiętać jednak o tym, że używając jPALIO MVC wprowadzamy pewną konwencję programowania, która jest z góry narzucona i ciężko jest (nawet największemu bałaganiarzowi) od niej odstąpić. W przypadku implementacji bez użycia jPALIO MVC (drugi przykład) z dużym prawdopodobieństwem można stwierdzić, że każdy z programistów będzie programował wg swojej własnej konwencji, a co gorsza może programować bez żadnej konwencji.

Implementacja z użyciem wsparcia jPALIO MVC

import palio.*
import palio.modules.*
import jpalio.mvc.*
import jpalio.mvc.annotations.*

public class SampleController extends Controller {

// Method is matched only when "requestHandler" parameter has "listObject" value
@RequestHandler(view="palio.mvc.objectsList")
public void listObjects() {
setParam("objectsList", Groovy.module("sql").read("palio", "select id, code from p_objects order by code"))
}

// Method is matched only when "requestHandler" parameter has "listPages" value and user has required privileges
@RequestHandler(view="palio.mvc.pagesList", privs=["palio.mvc.pagesList"])
public void listPages() {
setParam("pagesList", Groovy.module("sql").read("palio", "select id, code from p_pages order by code"))
}

// Method is matched only when "requestHandler" parameter has "objectContent" value and "_RowID" parameter is not empty
@RequestHandler(view="palio.mvc.objectContent")
public void objectContent(
@RequestParam(name="_RowID") Long objectId
) {
Object[] row = Groovy.module("sql").readLine("palio", "select tag from p_objects where id=?", [objectId] as Object[])
setParam("objectContent", row[0])
}

// Default handler
@RequestHandler(defaultHandler=true, view="palio.mvc.page")
public void defaultHandler() {
// Nothing to do. Display only given view
}

// Handling unhandled exceptions
protected void handleException(Exception ex) {
// Do some exception handling
println "An exception occured: ${ex.getClass().getName()}"
}
}

 

Implementacja bez użycia wsparcia MVC (jPALIO Classic)

$try({
$// Execute only when "requestHandler" parameter has "listObject"
$if($==("listObjects", $requestHandler), {
$=(objectsList, $sql.read("palio", "select id, code from p_objects order by code"))
$*palio.mvc.objectsList
$return()
})

$// Executed only when "requestHandler" parameter has "listPages" value and user has required privileges
$if($==("listPages", $requestHandler), {
$ifNot($user.hasPriv("palio.mvc.pagesList", (Long) null), {
$error.throwError("Insufficient privileges to execute this operation")
})
$=("pagesList", $sql.read("palio", "select id, code from p_pages order by code"))
$*palio.mvc.pagesList
$return()
})

$// Executed only when "requestHandler" parameter has "objectContent" value and "_RowID" parameter is not empty
$if($&&($==("objectContent", $requestHandler), $isNotNull($_RowID)), {
$=(@row, $sql.readLine("palio", "select tag from p_objects where id=?", [$toLong((String) $_RowID)]))
$=(objectContent, $@row[0])
$*palio.mvc.objectContent
$return()
})

$// Default handler
$*palio.mvc.page


}, {
$// Handling unhandled exceptions
An exception occured: $error.getMessage()
})