13 marca 2009

Relacja z rozdziału 6. o mapowaniu URL w Grails z DGG2

Lektura książki The Definitive Guide to Grails, Second Edition idzie dalej. Wspominałem już, że wstrzymałem lekturę, aby nadrobić jej relację na blogu, co zmusza mnie do dwu-, a czasem nawet kilkukrotnego wertowania tych samych stron. I dobrze, bo zdecydowanie poprawia zrozumienie tematu. Dodając do tego projekt nauczyciel i mam(y) wszystko potrzebne do poznawania Grails. Zauważyłem również, że Grails znalazł swoje miejsce na innych polskich blogach, więc spragnionych wiedzy grailsowej zapraszam na strony Michała Piotrowskiego (blog Stronnice Chlebika) oraz Mateusza Mrozewskiego (blog Blog IT - Mateusz Mrozewski). Ich styl pisania znacznie odbiega od mojego - jest krócej i bardziej treściwie, i z przykładami (!)

Tym razem przejdziemy do konfiguracji adresów URL w Grails. Rozdział 6. "Mapping URLs" pozwala zrozumieć, w jaki sposób można definiować relację pomiędzy URL a jego kontrolerem i akcją z możliwością przesyłania własnych parametrów.

Pierwsza część URL (po kontekście aplikacji webowej) to nazwa kontrolera, a kolejna (opcjonalna) nazwa jego akcji (domknięcia), np. dla poniższego kontrolera PierwiastekController:
 package pl.jaceklaskowski.nauczyciel

class PierwiastekController {
def index = {
render 'Wywołano index'
}
}
adres http://localhost:8080/nauczyciel/pierwiastek/index to wykonanie jego akcji index. Jeśli nie podano opcjonalnej nazwy akcji, zostanie wykonana akcja domyślna kontrolera. Ostatni, opcjonalny, element URL to identyfikator id.

Definicja składowych URL znajduje się w grails-app/conf/UrlMappings.groovy.

Jeśli korzystamy z opcjonalnych składowych (za pomocą operatora dereferencji w Groovy - ?) w URL to muszą one być na końcu wzorca.

Zmienna to składowa URL poprzedzona $. Mapowanie może zawierać tekst statyczny. Nazwa kontrolera i jego akcji nie muszą występować w URL. Za pomocą bloku static mappings w skrypcie UrlMappings możemy przypisać wykonanie wskazanego kontrolera i jego akcji, np. (Listing 6-3):
 class UrlMappings {
static mappings = {
"/pierwiastek/$id" {
controller = 'pierwiastek'
action = 'show'
}
}
}
Powyższa konfiguracja jest równoznaczna z
 class UrlMappings {
static mappings = {
"/pierwiastek/$id"(controller = 'pierwiastek', action = 'show')
}
}
}
Wybór należy do nas, który ze sposobów bardziej do nas przemawia.

W Grails preferuje się adresy typu /pierwiastek/potas zamiast typowych /pierwiastek?nazwa=potas. Przekazanie wartości "potas" w parametrze nazwa to konfiguracja:
 class UrlMappings {
static mappings = {
"/pierwiastek/$nazwa"(controller = 'pierwiastek', action = 'show')
}
}
}
Dostęp do zmiennej jest możliwy przez zmienną params, bez względu, czy przekazaliśmy parametr przez mapowanie w UrlMappings (jako część URL), czy bezpośrednio jako parametr żądania.

Autorzy proponują korzystać z konwencji "podkreślnik-zamiast-%20", aby adres typu /zwiazek/chlorek wapnia miał postać /zwiazek/chlorek_wapnia zamiast /zwiazek/chlorek%20wapnia. Wystarczy połączyć wcześniejsze mapowanie (z static mappings) z wykonaniem metody replaceAll('_', ' '), aby dostać się do poprawnej wartości parametru, tj.
 class PierwiastekController {
def show = {
def pierwiastek = Pierwiastek.findByNazwa(params.nazwa.replaceAll('_', ' '))
render "Symbol pierwiastka: ${pierwiastek.symbol}, nazwa: ${pierwiastek.nazwa}"
}
}
Poza tym, możliwe jest definiowanie nowych zmiennych żądania, jak gdyby były one w nim przekazane. W ten sposób wykonanie URL rozpoczynającego się /pokazPierwiastek/<nazwa> miało inny skutek niż wykonanie /pokazPierwiastekPelny/<nazwa>. Dla użytkownika końcowego wyróżnikiem jest adres URL, podczas, gdy jego obsługą zajmuje się kontroler, który wywołany zostanie z właściwymi parametrami, tj.
 class UrlMappings {
static mappings = {
"/pokazPierwiastek/$nazwa(controller = 'pierwiastek', action = 'wyswietl') {
format = 'prosty'
}
"/pokazPierwiastekPelny/$nazwa(controller = 'pierwiastek', action = 'wyswietl') {
format = 'pelny'
}
}
}
W ten sposób, obsługa akcji wyswietl w kontrolerze PierwiastekController podejmowałaby decyzję o ilości wyświetlanych danych na bazie parametru format (${params.format}).

Możliwe jest związanie adresu z konkretnym widokiem - stroną GSP. Użyteczne w sytuacji, kiedy widok nie potrzebuje modelu oraz żadna z akcji kontrolera nie musi być wcześniej wykonana. Mapowanie jest identyczne do poprzednich, z kontrolerami i ich akcjami, z tą różnicą, że zamiast action definiujemy parametr view, np.:
 class UrlMappings {
static mappings = {
"/"(view: '/welcome')
}
}
W wyniku zostanie wyświetlona strona grails-app/views/welcome.gsp. Jeśli poza view określimy również parametr controller, wtedy strona GSP będzie tą, która związana jest z danym kontrolerem, np.:
 class UrlMappings {
static mappings = {
"/wyszukaj"(view: 'wyszukaj', controller: 'pierwiastek')
}
}
Dla /wyszukaj zostanie wyświetlona strona grails-app/views/pierwiastek/wyszukaj.gsp *bez* wcześniejszego wykonania akcji w kontrolerze.

Poza tym, możemy w (pod)sekcji constraints nadawać warunki jakie muszą spełniać parametry żądania, aby doszło do wykonania kontrolera, albo wyświetlenia strony, np.:
 class UrlMappings {
static mappings = {
"/pierwiastek/$masaAtomowa(controller = 'pierwiastek', action = 'wyswietl') {
constraints {
masaAtomowa matches: /[0-9]{2}/
}
}
}
}
Mechanizm jest podobny do warunków w klasach dziedzinowych. I podobnie jak w klasach dziedzinowych, wszystkie warunki muszą zajść, aby wykonane zostało mapowanie.

UWAGA: Podobieństwo między warunkami w klasach dziedzinowych, a tymi w mapowaniu URL jest niezwykle subtelne - w klasach dziedzinowych mamy do czynienia ze statyczną zmienną, której przypisujemy domknięcie (jako jej wartość), a w mapowaniu URL wykonujemy metodę constraints przekazując jej parametr będący domknięciem. Uwaga na brak znaku równości. Cuda języka Groovy.

Mapowanie URL pozwala na związanie go z danym adresem URL z użyciem symbolów maski (wieloznacznik) - * (gwiazdka) lub ** (dwie gwiazdki), co oznacza cokolwiek spełniającego warunek bez konieczności przekazywania ciągu spełniającego maskę jako parametr żądania, np.: (Listing 6-14)
 class UrlMappings {
static mappings = {
"/images/*.jpg"(controller:'image')
}
}
co oznacza, że kontroler image jest związany z adresami typu /images/*.jpg, ale już nie z /images/podkatalog/*.jpg, który jest spełniony przy zastosowaniu podwójnej gwiazdki (dowolna hierarchia katalogów). Ciąg znaków spełniający maskę nie będzie związany z żadnym parametrem żądania. Jeśli jednak chcielibyśmy przypisać pasujący ciąg znaków do parametru, poprzedzamy symbol maski nazwą parametru, np.: (Listing 6-16)
 class UrlMappings {
static mappings = {
"/images/$pathToFile**.jpg"(controller: 'image')
}
}
Dla /images/photos/president.jpg parametr pathToFile ma wartość photos/president.

Istnieje możliwość związania różnych akcji kontrolera dla różnych typów żądań HTTP - GET, POST, PUT i DELETE. Zamiast sprawdzać w kontrolerze typ żądania HTTP za pomocą request.method możemy wskazać w action, która akcja kontrolera obsługuje dany typ, np.: (Listing 6-18):
 class UrlMappings {
static mappings = {
"/artist/$artistName" {
controller = 'artist'
action = [GET: 'show',
PUT: 'update',
POST: 'save',
DELETE: 'delete']
}
}
}
Wartością action jest mapa - typ żądania HTTP i akcja kontrolera.

Możemy również wiązać kody odpowiedzi HTTP, np. 404 czy 500 z danym widokiem bądź akcją kontrolera, np.: (Listing 6-20)
 class UrlMappings {
static mappings = {
"404"(controller:'store')
}
}
Domyślnie, Grails obsługuje kod 500 (Internal Error), który wyświetla widok /error (strona grails-app/views/error.gsp), która pozwala na namierzenie błędu w razie...błędu. Zakłada się, że jest użyteczne podczas tworzenia aplikacji i jej debugowaniu. W naszych stronach możemy korzystać z parametru exception (przykładem może być właśnie grails-app/views/error.gsp).

Do tej pory przedstawiony został mechanizm obsługi żądań do własnych URLi. Autorzy przedstawiają sposób tworzenia tych URLi za pomocą znaczników GSP - g:link. Wystarczy dostarczyć niezbędne parametry w atrybucie params wraz z odpowiednio zdefiniowanymi atrybutami action oraz controller, a g:link stworzy właściwy URL, np.: (Listing 6-22)
 class UrlMappings {
static mappings = {
"/showArtist/$artistName"(controller:'artist', action:'show')
}
}
Przy takim UrlMappings wystarczy skonstruować g:link w następujący sposób: (Listing 6-23)
 <g:link action='show'
controller='artist'
params="[artistName:${artist.name.replaceAll(' ', '_')}]">
${artist.name}
</g:link>
Plik UrlMappings można dzielić na mniejsze pliki. Wystarczy, aby miały taką samą strukturę - static mappings, znajdowały się w katalogu grails-app/conf i ich nazwa kończyła się na UrlMappings.

Testowanie własnych mapowań jest możliwe z klasą grails.test.GrailsUrlMappingsTestCase, która rozszerza z kolei klasę groovy.util.GroovyTestCase. Metoda assertForwardUrlMapping sprawdza, czy żądanie do zadanego URL jest obsługiwane przez właściwą akcję kontrolera, np.: (Listing 6-26)
 class ArtistUrlMappingTests extends grails.test.GrailsUrlMappingsTestCase {
void testShowArtist() {
assertForwardUrlMapping('/showArtist/Jeff_Beck',
controller:'artist', action:'display')
}
}
Jeśli mapowanie definiuje własne parametry żądania, np. wcześniej korzystaliśmy z artistName, wtedy sprawdzenie jest możliwe przez przekazanie opcjonalnego domknięcia do metody assertForwardUrlMapping z parametrami i ich żądanymi wartościami, np.
 class ArtistUrlMappingTests extends grails.test.GrailsUrlMappingsTestCase {
void testShowArtist() {
assertForwardUrlMapping('/showArtist/Jeff_Beck',
controller:'artist', action:'display') {
artistName = 'Jeff_Beck'
}
}
}
Sprawdzenie, czy mapowanie dla g:link da oczekiwany rezultat odbywa się za pomocą metody assertReverseUrlMapping, która działa i ma parametry analogiczne do assertForwardUrlMapping.

Sprawdzenie obu przypadków to wykonanie metody assertUrlMapping.

Klasa GrailsUrlMappingsTestCase wczytuje wszystkie mapowania aplikacji. Jeśli chcemy zawęzić zbiór testowanych mapowań, wystarczy zdefiniować w klasie testującej statyczną zmienną mappings, która przyjmuje pojedynczą nazwę klasy mapującej, albo ich listę, np.: (Listing 6-30):
 class ArtistUrlMappingsTests extends grails.test.GrailsUrlMappingsTestCase {
static mappings = [UrlMappings, ArtistUrlMappings]

//...
}
Więcej w dokumentacji Grails w Testing URL Mappings.