14 lutego 2009

Rozdział 4. "Understanding Controllers" z "The Definitive Guide to Grails, Second Edition"

Kolejny rozdział 4. "Understanding Controllers" to 40 stron mnóstwa wiadomości z podwórka Grails, a dokładniej o kontrolerach. Mimo wcześniejszej lektury "Beginning Groovy and Grails" (patrz Book review: Beginning Groovy and Grails: From Novice to Professional). Już poprzedni rozdział było trudno zrelacjonować w kilku(nastu) zdaniach, a ten nie będzie łatwiejszy. Ostrzegałem.

Kontroler w Grails jest klasą, która jest odpowiedzialna za obsługę żądań w aplikacji (nie powinno to nikogo dziwić, jeśli przypomnieć sobie, że Grails to realizacja wzorca MVC). Instancje kontrolera tworzone są na nowo dla każdego żądania, więc nie zajmujemy się wcale kwestią odporności kodu kontrolera na wielowątkowość. Klasy kontrolerów znajdują się w grails-app/controllers. Nazwa klasy musi kończyć się Controller. Kontrolery nie mają potrzeby rozszerzać jakiejkolwiek klasy bazowej czy implementować specjalny intefrejs.

Akcje w kontrolerze definiowane są jako pola, do których przypisuje się blok kodu w postaci domknięcia Groovy.

Jeśli nie wskazano akcji do wykonania w URL, Grails wykona domyślną akcję. Wyznaczenie domyślnej akcji to następująca sekwencja: pojedyncza akcja w kontrolerze staje się domyślną, przy większej liczbie jest to index lub dowolna inna wskazana przez właściwość defaultAction, np. (Listing 4-3):
 class SampleController {
def defaultAction = 'list'
def list = {}
def index = {}
}
Właściwość log jest wstrzeliwana do każej klasy kontrolera jako egzemplarz org.apache.commons.logging.Log.

Dowolny wyjątek w Groovy jest wyjątkiem niekontrolowanym (ang. unchecked/runtime exception), stąd nie trzeba zajmować się w ogóle ich przechwytywaniem (chyba, że zajdzie taka potrzeba). Przechwycenie wyjątku w Groovy jest identyczne jak w Javie - blok try-catch.

Lista domyślnych atrybutów dostępnych w kontrolerze (nazwy są samowyjaśniające się): actionName, actionUri, controllerName, controllerUri, flash, log, params, request, response, session i servletContext. Odczyt/zapis parametrów/atrybutów w wielu z nich, odbywa się za pomocą operatorów "dot dereference" czy "subscript", np. request.myAttr, albo session.myAttr = 'pewna-wartość'.

Wyróżniamy 4 zasięgi działania obiektów w Grails - request (pojedyncze żądanie), flash (obecne i przyszłe żądanie), session (sesja) oraz servletContext (współdzielone w całej aplikacji przez całe jej istnienie).

Wyświetlenie komunikatu w odpowiedzi na żądanie z poziomu kontrolera to render 'tutaj tekst do wyświetlenia' z opcjonalnym parametrem contentType, np. (Listing 4-11):
 render 'Witaj użytkowniku'
render text: '<b>Witaj użytkowniku</b>', contentType: 'text/xml'
Warto zauważyć, że wykonanie metody z parametrami w Groovy nie wymaga umieszczenia ich w nawiasach.

Wskazanie danego widoku do wyświetlenia w render (standardowo będzie to strona gsp, której nazwa odpowiada nazwie metody w katalogu odpowiadającym nazwie kontrolera), to zadanie dla parametru view. Możliwe jest wskazanie dedykowanego modelu.
 render(view: "display", model: [ song:Song.get(params.id) ])
Można również wskazać adres widok względnego do grails-app/views, np.
 render(view: "/common/song", model: [ song:Song.get(params.id) ])
Przekierowanie to metoda redirect, która akceptuje mapę jako argument wejściowy, w której umieszczamy niezbędne parametry - action (akcja do wykonania), controller (kontroler), id (parametr id w przekierowaniu), params (mapa obowiązujących parametrów), uri (względny adres przekierowania), url (bezwzględny adres przekierowania).

Kiedy akcja kontrolera zwraca mapę, staje się ona bieżącym modelem dla widoku, np. (Listing 4-14):
 def show = {
[ song: Song.get(params.id) ]
}
Warto zaznaczyć, że w Groovy ostatnie wyrażenie to wartość zwracana (czyli return jest opcjonalny). W tym przypadku widok będzie mógł odczytać song, który wskazany jest w tym modelu.

Można tak (Listing 4-17):
 def save = {
def album = new Album()
album.genre = params.genre
album.title = params.title
...
}
jednakże dla wielu parametrów może to być bardzo pracochłonne, więc łatwiej przypisać parametry z żądania korzystając z automatycznie generowanego w Grails konstruktora:
 def save = {
def album = new Album(params)
...
}
W tym momencie pojawia się uwaga dotycząca potencjalnych ataków, przy wykorzystaniu tego automatycznego ustawiania właściwości z parametrów żądania, które występuje również w Ruby on Rails, Spring MVC, WebWork i in. Rozwiązaniem jest metoda bindData (o której za moment) oraz dokładna kontrola poprawności.

Można również w prosty sposób przypisać wszystkie wartości atrybutów już istniejącego egzemplarza klasy dziedzinowej z wykorzystaniem automatycznie dodawanego atrybutu properties, np.
 def update = {
def album = Album.get(params.id)
album.properties = params
album.save()
}
Przypisane zostaną jedynie parametry, które należą do pustej (domyślnej) przestrzeni nazw, tj. nazwy parametrów są proste, np. title, name. Poprzedzenie nazwy parametru pewną nazwą (przestrzenią klasy dziedzinowej), np.
 <input type="text" name="album.title" />
<input type="text" name="artist.title" />
to możliwość pobrania jedynie pewnego zbioru parametrów, np.
 def album = new Album( params["album"] )
def artist = new Artist( params["artist"] )
Bez względu na przestrzenie nazw parametrów możemy zawężać automatyczne przypisanie wartości do określonych parametrów z bindData, np. (Listing 4-21):
 bindData(album, params, [include:"title"])
include oznacza przypisanie jedynie parametru title, podczas gdy exclude to wyłączenie podanych z przypisania. Możemy również zawęzić rozważane parametry do zadanej przestrzeni nazw parametrów, np.:
 bindData(album, params, [include:"title"], "album")
Kontrola wartości jest oparta na mechaniźmie springowego org.springframework.validation. Każdorazowa kontrola poprawności instancji klasy dziedzinowej to stworzenie obiektu org.springframework.validation.Errors, który jest następnie do niej przypisywany. Wypisanie wszystkich błędów to:
 album.errors.allErrors.each { println it.code }
Możemy jedynie sprawdzić, czy klasa ma jakiekolwiek błędy z hasErrors(), np.
 if (album.hasErrors()) println "Błędy!"
W GSP będzie to znacznik <g:renderErrors>, np.
 <g:renderErrors bean="${album}" />
Na stronie 82. w podrozdziale "Working with Command Objects" pojawia się materiał o obiektach-polecenie (ang. command object), których opisu nie znalazłem w książce "Beginning Groovy and Grails". Całkowite novum!

Klasa polecenia to klasa, która ma wszystkie cechy klasy dziedzinowej, poza trwałością, tj. możliwe jest automatyczne przypisywanie wartości oraz kontrola ich poprawności. Możemy definiować warunki klasy polecenia i sprawdzać ich poprawność, jak w klasie dziedzinowej.

Klasa polecenia znajduje się w katalogu grails-app/controllers, a nawet w samej klasie kontrolera (Groovy wspiera umieszczanie wielu klas w pojedynczym pliku, co może przydać się, jeśli klasa polecenia wykorzystywana jest jedynie przez pojedynczy kontroler). Nazwa klasy polecenia kończy się Command.

Wykorzystanie polecenia w kontrolerze sprowadza się do podania go jako pierwszy argument w akcji, np. (listing 4-24):
 def save = { AlbumCreateCommand cmd ->
...
}
Podczas wykonania akcji, tworzona jest nowa instancja polecenia, do której przypisywane są parametry żądania. Możemy skorzystać z automatycznie generowanej metody validate() do sprawdzenia, czy parametry żądania są spełniają nałożone warunki, np. (Listing 4-26):
 def save = { AlbumCreateCommand cmd ->
if (cmd.validate()) {
...
redirect(action: "show", id:album.id)
} else {
render(view: "create", model: [cmd:cmd])
}
}
Podobnie jak z klasami dziedzinowymi, mamy automatycznie związywany z klasą polecenia obiekt Errors, który w GSP odczytujemy przez
 <g:renderErrors bean="${cmd}" />
Za pomocą atrybutu allowedMethods określamy jakie typy żądania HTTP są dozwolone dla danej akcji (domyślnie wszystkie żadania są dozwolone), np. (Listing 4-28):
 class SomeController {
def allowedMethods = [action1: 'POST', action3: ['POST', 'DELETE']]
def action1 = {}
def action2 = {}
def action3 = {}
}
Niespełnienie warunku to kod błędu "405 Method Not Allowed" w odpowiedzi.

Obsługa przesłania pliku na serwer jest oparta na springowym org.springframework.web.multipart.MultipartHttpServletRequest i znacznikach - <g:uploadForm> (przypisuje formularzowi typ enctype na multipart/form-data) oraz <input> z atrybutem type="file", np. (Listing 4-30):
 <g:uploadForm action="upload">
<input type="file" name="myfile" />
<input type="submit" value="Upload!" />
</g:uploadForm>
Pobranie przesłanego pliku to wykonanie metody MultipartHttpServletRequest.getFile() z parametrem, który odpowiada wartości atrybutu name w input type="file", np. (Listing 4-31):
 def upload = {
// Zmienna file jest typu org.springframework.web.multipart.MultipartFile
// Brak pliku w żądaniu, to getFile() zwraca null
def file = request.getFile('myFile')
// file to przesłany plik
}
Grails, dzięki Spring MVC, automatycznie przypisuje przesłane pliki do atrybutu klasy dziedzinowej zgodnie z regułami - jeśli typ atrybutu to byte[], wtedy przypisane są bajty pliku, a kiedy String zawartość pliku będzie tekstem. Wystarczy nadać nazwę atrybutowi name w input z type="file", która odpowiada nazwie atrybutu klasy dziedzinowej i z pomocą automatycznego przypisywania wartości zlecenia (przez konstruktor i params) Grails zapisze zawartość do atrybutu, np.:
 // klasa dziedzinowa z polem art typu byte[]
class Album {
byte[] art
...
}

// input typu file z atrybutem name o nazwie art
<input type="file" name="art" />

// automatyczne przypisanie wartości przez konstruktor klasy dziedzinowej
def user = new Album(params)
Odczytanie treści zlecenia to request.inputStream.text.

Przesłanie binarnych danych do klienta (w odpowiedzi) to (Listing 4-36):
 def createZip = {
byte[] zip = ... // pobierz zawartość pliku binarnego
response.contentType = "application/octet-stream"
// przeciążony operator << w akcji!
response.outputStream << zip
response.outputStream.flush()
}
Reszta rozdziału przy kolejnej relacji. Zrobiło się trochę przy długo...

p.s. Nie zapomnieliśmy o konkursie Bloger Roku 2008? Wystarczy wejść na stronę do głosowania na Notatnik, gdzie podajemy nasz adres email i zatwierdzamy. Wiadomość z adresem potwierdzającym pojawia się w naszej skrzynce, klikamy w niego potwierdzając nasz wybór. I tyle. Dziękuję!