TSM - Introducere în Grails (III)

Tavi Bolog - Development Lead at Nokia

Sper că articolele precedente despre Grails v-au trezit interesul asupra framework-ului. Acesta este ultimul articol din seria "Introducere în Grails", prezentând următoarele topicuri:

Validatori custom

În articolele precendente am discutat despre constrângeri care se aplică obiectelor domeniu. Grails oferă un set bogat de constrângeri: nullable, blank, size, etc. În cazul în care dorim validări adiționale asupra obiectelor noastre de domeniu, va trebui să ne scriem validatorii noștri.

Să încercăm să adăugam un validator custom care să nu permită folosirea cuvântului "test" ca parte a unui mesaj pe care vrem să îl postăm. În primul rând va trebui să specificăm folosirea unui validator custom:

static constraints = {
  message size:5..100, blank:false, validator:
  {val, obj -> obj.messageValidator(val)}	
}	

Apoi va trebui să definim metoda de validare în clasa Message:

private messageValidator(val) {
 if (val.toLowerCase().contains("test")) {
   return "test.is.not.allowed"
 }
 return true
}

Validatorul primește ca parametru valoarea de validat (în cazul nostru textul mesajului de postat). Dacă textul este ok relativ la regulile noastre, metoda de validare va returna "true". Orice altă expresie returnată va fi considerată ca validare eșuată la apelarea metodei "validate" pentru obiectul nostru Message și aplicația va afișa un mesaj de eroare. Vom vedea în secțiunea următoare cum să customizăm mesajele aplicației noastre, adăugând suport pentru internationalizare.

Internaționalizarea

Majoritatea aplicațiilor web sunt destinate utilizatorilor care vorbesc limbi diferite. De asemenea, mesajele default pe care Grails le pune la dispoziție pentru a trata erori, nu sunt întotdeauna user-friendly (vezi mai sus). Suportul de internaționalizare al Grails se bazează pe Spring MVC. Setările de limbă sunt implementate folosind obiectul Locale, asociat cu sesiunea unui user.

Grails stochează pachetele de mesaje în folderul grails-app/i18n ca fișiere de proprietăți Java. Convenția este că pachetele de mesaje încep cu "messages" și se termină cu numele locale-ului. Câteva exemple:

Pentru aplicația noastră vom folosi doar două fișiere:

Să luam exemplul de mai sus, unde mesajul de eroare pus la dispoziție de Grails nu a fost foarte user friendly și să adăugam textul nostru de eroare în fișierele de proprietăți.

Deschide https://github.com/tavibolog/GrailsSocialNetwork/tree/master/grails-app/i18n/messages.properties și adaugă:

message.name=My message
test.is.not.allowed=Test is not allowed!

Acum încearcă să postezi un mesaj care conține cuvântul "test". Vom observa două lucruri:

test.is.not.allowed=Test nu este admis!
message.name=Mesajul meu

După cum ați observat am mai adăugat o pereche cheie-valoare pentru un nou text și am dori să o folosim în fișierele gsp. Aceasta este foarte simplu în Grails. Deschide https://github.com/tavibolog/GrailsSocialNetwork/blob/master/grails-app/views/message/create.gsp și caută stringul "Message" și înlocuiește-l cu: . Acest tag va cere Grails să caute în fișierele de internaționalizare după o cheie numită "message.name" și să o folosească în funcție de setarea de limbă curentă.

Aplicația noastră nu are un selector de limbă, dar Grails ne ajută să testăm schimbarea setării de limbă, expunând un parametru de request, numit "lang" care poate fi folosit pentru selectarea limbii în aplicația noastră. Dacă dorim să schimbăm limba aplicației în română, va trebui să facem un request folosind "lang=ro", ca și mai jos:

http://localhost:8080/GrailsSocialNetwork/message/create?lang=ro

Pagina de creare a unui mesaj va arăta semi-internaționalizat (iar mesajul de eroare venit de la validatorul custom va fi afișat de asemenea în română):

Câteodată avem nevoie de suport de internaționalizare la nivelul controllerului.

Să încercăm să adăugăm suport de internaționalizare pentru mesajul de eroare afișat în cazul în care combinația user-parolă nu este validă.

Implementarea curentă (https://github.com/tavibolog/GrailsSocialNetwork/blob/master/grails-app/controllers/com/todaysoftmag/gsn/UserController.groovy ) este mai jos:

render(view: "login", model: [message: "Wrong username or password!"]) 

Localizarea este făcută folosindu-se tagul "message"; tot ce trebuie să facem este să apelăm tagul cu parametri necesari. Iată codul schimbat:

render(view: "login", model: [message: message(code:"error.wrong", args:[message(code: "error.usernameorpassword")])])

Deși pare puțin complicat, s-a vrut prezentarea suport de parametri în conținutul internaționalizat folosind Grails. În cazul nostru, vrem să afișăm un text definit de cheia "error.wrong", care are la rândul ei parametri, definiți în lista de argumente "args" - aceștia putând fi internaționalizați. În ambele cazuri, trebuie doar să apelăm tagul "message" cu parametri necesari: "code" - specifică textul de afișat și "args" - specifică eventuali parametri de înlocuit în textul internaționalizat.

Următorul pas este să definim noile string-uri în messages.properties:

error.wrong=Wrong {0}
error.usernameorpassword=username or passwordd

și messages_ro.properties:

error.wrong={0} sunt gresite
error.usernameorpassword=username sau parola

Acum, dacă încercăm să ne logăm cu o combinație invalidă de user și parolă, specificând o setare de limbă, aplicația va afișa un mesaj de eroare localizat.

Librării de tag-uri

Librăriile de tag-uri sunt o modalitate elegantă de a construi componente re-utilizabile în fișierele gsp. Librăriile de tag-uri sunt stocate în /grails-app/taglib. Librăriile de tag-uri sunt definite ca și clase Groovy și pot avea mai multe metode. Ele pot să aibă și namespace-uri pentru a nu intra în coliziune cu tag-urile standard Grails sau tag-uri definite de alți developeri.

Să încercăm să construim o librărie de tag-uri care va fi folosită să afișeze un mesaj de întâmpinare pentru userii aplicației noastre. Momentan acesta este implementată în list.gsp (https://github.com/tavibolog/GrailsSocialNetwork/blob/master/grails-app/views/message/list.gsp ):


Hi ${session.user}

Abordarea acesta are mai multe probleme:

Soluția este să folosim o librărie de tag-uri. Pentru a crea o librărie de taguri, rulăm următoarea comandă:

create-tag-lib com.todaysoftmag.gsn.Display

Aceasta va crea o librărie de taguri numită DisplayTagLib și fișierul de test asociat: DisplayTagLibTests.

package com.todaysoftmag.gsn
 
class DisplayTagLib {
static namespace = "display"
	
def greetings = {attrs ->
  if (session && session.user) {
    out << "
Hello, " + session.user + "!
" } else { out << g.link(controller: "user", action: "login"){"not signed in"} } } }

Să observăm acum implementarea și să o explicăm:

Acum că am definit librăria de tag-uri, să o și folosim în locul potrivit: main.gsp (https://github.com/tavibolog/GrailsSocialNetwork/blob/master/grails-app/views/layouts/main.gsp ).

Aceasta este scheletul care include toate paginile aplicației, deci pare locul potrivit să afișăm mesajul de întâmpinare:



Mapări de Urluri

Grails oferă un suport ușor de folosit pentru a manipula url-urile aplicației. "Locul de joacă" este UrlMappings.groovy, stocat în grails-app/conf (https://github.com/tavibolog/GrailsSocialNetwork/blob/master/grails-app/conf/UrlMappings.groovy ).

Până acum, aplicația noastră folosea ca și pagină de start pagina default pusă la dispoziție de Grails: http://localhost:8080/GrailsSocialNetwork . Acum, am dori să schimbăm asta și să afișam pagina de login. Mai mult, am dori să afișăm un nume mai simplu pentru url-ul de login, care este definit default ca și "/user/login". Am prefera ceva mai simplu, cum ar fi "/login".

Pentru a realiza acest lucru avem nevoie de câteva schimbări în fișierul de mapări de url-uri:

"/login" (controller: "user", action: "login")
 
"/"(view:"/user/login")
Prima modificare este pentru a avea un url de login mai simplu ("/login" în loc de "/user/login"). A doua modificare este pentru a cere Grails să afișeze view-ul de login de fiecare data când cineva accesează: http://localhost:8080/GrailsSocialNetwork - e.g. rădăcina aplicației.

Datorită acestor modificări, aplicația noastră arată mai bine când este accesată pentru prima dată:

Persistența

Până acum, aplicația noastră a folosit un mecanism de persistență în memorie (la fiecare restart datele se pierdeau). În această secțiune aș dori să descriu cum să adăugăm aplicației un data source persistent, folosim GORM. Configurarea data source-urilor se face în DataSource.groovy (https://github.com/tavibolog/GrailsSocialNetwork/blob/master/grails-app/conf/DataSource.groovy ).

Primul pas este să definim câteva configurări ale data source-ului, cum ar fi pooling, driverul folosit pentru conectare, username și parolă:

dataSource { pooled = true driverClassName = "com.mysql.jdbc.Driver" username = "tavi" password = "tavi" }

Toate aceste configurări pot fi suprascrise pentru fiecare mediu de dezvoltare. Să luam ca și exemplu, mediul de "development", cel pe care îl folosim când implementăm aplicația:

development {
    dataSource {
      dbCreate = "create-drop" 
       // one of "create"""create-drop",  
       //"update", ""validate", """
      url = "jdbc:mysql://localhost:3306/gsn"
        }
    }

Aici definim url-ul data source-ului, în cazul nostru un MySQL server, rulând o bază de date numită "gsn". De asemenea, când aplicația pornește, va șterge tabelele existente și va crea un nou set după cum este specificat de către clasele de domeniu: User și Message. Aceasta este foarte la îndemână de folosit pentru dezvoltare, dar de evitat într-un sistem de producție.

O ultimă configurare ar fi să permitem folosirea dependenței MySQL în proiectul nostru. În BuildConfig.groovy (https://github.com/tavibolog/GrailsSocialNetwork/blob/master/grails-app/conf/BuildConfig.groovy ), adăugăm conectorul MySQL ca și dependență dinamică:

dependencies {     
    runtime "mysql:mysql-connector-java:5.1.16"
}

Asumând că aveți o bază de date MySQL disponibilă la acest url: jdbc:mysql://localhost:3306/gsn și o combinație user/parolă să o accesați, să restartăm aplicația.

Hmm, Grails nu pornește din cauza unei erori:

| Error 2013-02-24 21:37:48,821 [pool-7-thread-1] ERROR hbm2ddl.SchemaExport  - Unsuccessful:
create table user (id varchar(255) not null auto_increment, version bigint not null, first_name varchar(10) not null, last_name varchar(10) not null, password varchar(10) not null, user_name varchar(10) not null unique, primary key (id))
| Error 2013-02-24 21:37:48,823 [pool-7-thread-1] ERROR hbm2ddl.SchemaExport  - Incorrect column specifier for column "id"

Din fericire, aceasta este ușor de fixat. Eroarea a apărut pentru că atributul "id" al clasei User este de tip String, dar GORM încearcă să îl seteze ca și AUTO_INCREMENT, însă doar tipurile numerice pot fi auto-incrementate în MySQL. Deci, să schimbăm tipul atributului nostru de la "String" la "long" și să lăsăm GORM și MySQL să se ocupe de generarea id-urilor noastre:

class User {
	long id // prior it  was String id = UUID.randomUUID().toString()
	String firstName
	String lastName
	String userName
	String password
…
}

Acum, aplicația va porni fără probleme. Dacă aruncăm o privire la baza de date "gsn", observăm că aceasta conține două tabele: "user" și "message"; GORM (via Hibernate) a creat tabele folosind atributele claselor domeniu și relațiile dintre ele. Datorită rutinei de ințializare din BootStrap.groovy, tabelele aplicației noastre au deja conținut pre-încărcat și sunt gata de folosit. De asemenea conținutul salvat acum poate supraviețui restartării unei aplicații.

Concluzie

În acest moment, am reușit să construim o aplicație folosind funcționalitătile de bază ale Grails și Groovy. Bine-înțeles, aplicația nu este pregătită de rulat în producție, dar este folositoare la învățarea unei nou framework de development.

Grails ajută developerii să devină productivi după câteva săptămâni de development la nivelul la care pot să construiască o aplicație web cu conținut dinamic. Bine-înțeles, universal Grails și Groovy este foarte vast și necesită mai mult timp pentru a-l stăpâni.

Sper că aceste trei articole v-au trezit interesul pentru a încerca Grails și Groovy în activitățile de web development. Acest framework a meritat investiția pentru echipa mea.

Bibliografie

http://grails.org/documentation

http://www.springsource.org/downloads/sts-ggts