Groovy
Instalacja
- Ściągnij Groovy 1.0 ze strony http://groovy.codehaus.org/Download (lub lokalnie)
- Ustaw zmienną środowiskową
GROOVY_HOME
na katalog, gdzie znajduje się rozpakowana dystrybucja - Dodaj
GROOVY_HOME/bin
do zmiennejPATH
- Ustaw zmienną
JAVA_HOME
tak, by wskazywała na JDK - Uruchom groovysh (konsola) lub groovyConsole (Swing)
Wtyczka do Eclipse'a
http://groovy.codehaus.org/Eclipse+Plugin
Cechy języka
-
println "Hello World!"
Średnik niepotrzebny. Chyba, że chcemy napisać kilka instrukcji w jednej linii.
Każda instrukcja do czegoś się wylicza.
println
wylicza się donull
. - Dynamiczne typowanie:
- x = 1
- println x
- x = new java.util.Date()
- println x
- x = -3.1499392
- println x
- x = false
- println x
- x = "Hi"
- println x
- Mapy i listy definiuje się bardzo prosto:
- myList = [1776, -1, 33, 99, 0, 928734928763]
- // int[] a = {1, 2, 3}; - nie zadziala!
- println myList.size()
- scores = [ "Brett":100, "Pete":"Did not finish", "Andrew":86.87934 ]
- // Sposoby dostępu do mapy:
- println scores["Pete"]
- println scores.Pete
- // Przypisanie wartości:
- scores["Pete"] = 3
- Inna składnia pętli:
- for (i in 0..len-1)
- // albo
- for (i in 0..<len)
==
oznacza jav'oweequals
. Tożsamość obiektów sprawdzamy za pomocą metodyis
.return
jest opcjonalny.- Metody i klasy są domyślnie publiczne
throws
nie jest sprawdzane przez kompilator.- Domyślne importy:
java.io.*
java.lang.*
java.math.BigDecimal
java.math.BigInteger
java.net.*
java.util.*
groovy.lang.*
groovy.util.*
Closures - programowanie funkcyjne
Jest to anonimowy blok kodu wykonywalnego. Może brać argumenty i zwraca wartość jak funkcja. Może korzystać ze zmiennych widocznych w otaczającym kontekście. Można napisać:
{ square -> square * square }
Składnia:
{ [closureArguments->] statements }
closureArguments
to opcjonalna lista argumentów oddzielonych przecinkami. Jeśli zostanie pominięta, będziemy
mieli closure jednoargumentową (z dostępem do parametru poprzez nazwę it
, np. { println it }
).
W Javie należałoby zapisać ten kod w otaczającej klasie i metodzie.
Blok można przypisać na zmienną:
- def c = { it * it } // domyślny parametr it - zawsze występuje
- c(2) // zwróci 4
- c.call(2) // to samo co wyżej
Przypisaną na zmienną closure można potem przekazać jako parametr:
- public class GVector extends java.util.Vector {
- public void apply( c ){
- for (i in 0..<size()){
- this[i] = c(this[i])
- }
- }
- }
- def gVect = new GVector()
- gVect.add(2)
- gVect.add(3)
- gVect.add(4)
- gVect.apply{ value -> println(value) }
Trochę bardziej złożony przykład:
- class Employee {
- def salary
- }
- def highPaid(emps) {
- def threshold = 150
- return emps.findAll{ e -> e.salary > threshold }
- }
- def emps = [180, 140, 160].collect{ val -> new Employee(salary:val) }
- println emps.size() // prints 3
- println highPaid(emps).size() // prints 2
Zmienne związane z closure są dla niej dostępne nawet, gdy zostanie zwrócona na zewnątrz otaczającego kontekstu.
Przykład:
- public class A {
- private int member = 20;
- private String method()
- {
- return "hello";
- }
- def publicMethod (String name_)
- {
- def localVar = member + 5;
- def localVar2 = "Parameter: ${name_}";
- return {
- println "${member} ${name_} ${localVar} ${localVar2} ${method()}"
- }
- }
- }
- A sample = new A();
- def closureVar = sample.publicMethod("Xavier");
- closureVar();
- The above code will print out:
- 20 Xavier 25 Parameter: Xavier hello
Closures pochodzą od klasy groovy.lang.Closure
. Są tzw. First Class Objects.
Z closure można związać na stałe parametry za pomocą tzw. currying'u, np.:
- def c = { arg1, arg2-> println "${arg1} ${arg2}" }
- def d = c.curry("foo")
- d("bar")
Jak zdefiniować metodę, która bierze closure?
- class SomeCollection {
- public void each (Closure c)
- }
Można teraz wywołać each()
z definicją closure poza nawiasami okrągłymi:
- SomeCollection stuff = new SomeCollection();
- stuff.each() { println it }
Bardziej tradycyjna składnia jest też dostępna. W Groovy'm można unikać nawiasów w wielu sytuacjach, zatem poniższe kombinację są też dopuszczalne:
- SomeCollection stuff = new SomeCollection();
- stuff.each { println it } // Look ma, no parens
- stuff.each ({ println it }) // Strictly traditional
Zakresy
- // lets use an exclusive range
- range = 5..<8
- assert range.size() == 3
- assert range.get(2) == 7
- assert range[2] == 7
- assert range instanceof java.util.List
- assert range.contains(5)
- assert ! range.contains(8)
- (1..10).each { i ->
- println "Hello ${i}"
- }
Zakresy mogą być także użyte w połączeniu ze switch ... case
.
- switch (years) {
- case 1..10: interestRate = 0.076; break;
- case 11..25: interestRate = 0.052; break;
- default: interestRate = 0.037;
- }
Operator *.
assert [1, 3, 5] == ['a', 'few', 'words']*.size()
Indeksowanie
- def text = "nice cheese gromit!"
- def x = text[-1] // ujemne indeksy - indeksowanie od końca
- assert x == "!"
- def name = text[-7..-2]
- assert name == "gromit"
- def text = "nice cheese gromit!"
- def name = text[3..1] // tak też można - odwracanie indeksów
- assert name == "eci"
Dynamiczne obiekty (Expandos)
Jest to obiekt, który nie musi mieć z góry zdefiniowanych właściwości i metod.
- def player = new Expando()
- player.name = "Dierk"
- player.greeting = { "Hello, my name is $name" }
- println player.greeting()
Wyrażenia regularne
Groovy wspomaga wyrażenia regularne za pomocą wyrażenia ~wzorzec
,
które kompiluje się do obiektu Javy Pattern
z podanego łańcucha znakowego.
Groovy posiada też operator =~
, które tworzy obiekt Matcher
oraz ==~
, który dopasowuje wyrażenie regularne.
- import java.util.regex.Matcher
- import java.util.regex.Pattern
- assert "cheesecheese" =~ "cheese"
- assert "cheesecheese" =~ /cheese/
- assert "cheese" == /cheese/ /*they are both string syntaxes*/
- // lets create a regex Pattern
- def pattern = ~/foo/
- assert pattern instanceof Pattern
- assert pattern.matcher("foo").matches()
- // lets create a Matcher
- def matcher = "cheesecheese" =~ /cheese/
- assert matcher instanceof Matcher
- answer = matcher.replaceAll("edam")
- // lets do some replacement
- def cheese = ("cheesecheese" =~ /cheese/).replaceFirst("nice")
- assert cheese == "nicecheese"
Iteracja po String'ach, mapach, listach, ...
- // iterowanie po mapie
- def map = ['abc':1, 'def':2, 'xyz':3]
- x = 0
- for ( e in map ) {
- x += e.value
- }
- assert x == 6
- // iterowanie po wartościach mapy
- x = 0
- for ( v in map.values() ) {
- x += v
- }
- assert x == 6
- // iterowanie po znakach w łańcuchu
- def text = "abc"
- def list = []
- for (c in text) {
- list.add(c)
- }
- assert list == ["a", "b", "c"]
Switch'owanie po wszystkim
- def x = 1.23
- def result = ""
- switch ( x ) {
- case "foo":
- result = "found foo" //UWAGA: bez break'a!
- case "bar":
- result += "bar"
- case [4, 5, 6, 'inList']:
- result = "list"
- break
- case 12..30:
- result = "range"
- break
- case Integer:
- result = "integer"
- break
- case Number:
- result = "number"
- break
- default:
- result = "default"
- }
- assert result == "number"
Operator ?.
Gdy przechodzimy po skomplikowanym grafie obiektów i nie chcemy, by rzucany był NullPointerException
, możemy użyć operatora ?.
zamiast .
.
- def foo = null
- def bar = foo?.something?.myMethod()
- assert bar == null
Grails
Cel
Celem Grails jest zapewnienie podobnego do Rails środowiska, które wygląda bardziej znajomo dla programistów Javy, dzięki czemu przejście do tego środowiska ma być łagodniejsze.
Grails bazuje na technologiach takich jak Spring (MVC), Hibernate (mapowanie obiektów i relacji), SiteMesh (interfejs użytkownika).
Inne elementy, na których opiera się Grails, to Dynamic Tag Libraries, Grails Object Relational Mapping, Groovy Server Pages oraz Scaffolding.
Instalacja
- Ściągnij Grails ze strony http://grails.codehaus.org/Download (lub lokalnie)
- Utwórz zmienną środowiskową
GRAILS_HOME
i ustaw tak, by wskazywała katalog z rozpakowanym archiwum - Dodaj do zmiennej
PATH
katalogbin
z kataloguGRAILS_HOME
- W linii poleceń wpisz
grails
Tworzenie projektu
Aby utworzyć projekt należy wpisać w konsoli grails create-app
.
Grails zapyta o nazwę projektu (w przykładzie będzie to my-project
) i utworzy strukturę projektu.
%PROJECT_HOME% + grails-app + conf ---> configuration artifacts like data sources + controllers ---> controller artifacts + domain ---> domain classes + i18n ---> message bundles for i18n + services ---> services + taglib ---> tag libraries + util ---> special utility classes (e.g., codecs, etc.) + views ---> views + layouts ---> layouts + hibernate ---> optional hibernate config + lib + spring ---> optional spring config + src + groovy ---> optional; location for Groovy source files (of types other than those in grails-app/*) + java ---> optional; location for Java source files + war + WEB-INF
Grails i Eclipse
Grails automatycznie tworzy Eclipse'owe projekty przy tworzeniu aplikacji.
Bug! Nie należy używać katalogu głównego ani ścieżek ze spacjami.
W Eclipse przejdź do Windows -> Preferences... -> Java -> Build path -> Classpath Variables -> New
i dodaj zmienną GRAILS_HOME
.
Konfiguracja projektu
Źródła danych
W katalogu grails-app/conf
utworzone zostały pliki konfiguracyjne dla źródeł danych.
Domyślnie ustawione są na bazę danych HSQLDB działającą w pamięci operacyjnej
(zobacz: grails-app/conf/DevelopmentDataSource.groovy
).
Jeśli chcemy użyć innej bazy danych, wystarczy zmienić odpowiednie wartości w plikach konfiguracyjnych
oraz dodać sterownik bazy danych do katalogu lib
projektu.
Domain Classes - encje
Domain class jest to klasa utrwalana w bazie danych. Domyślnie wszystkie jej atrybuty są zapamiętywane.
Aby utworzyć domain class, w katalogu głównym projektu wykonaj polecenie grails create-domain-class
.
Zostaniesz poproszony o podanie nazwy klasy (wpisz np. Book
).
Klasa zostanie utworzona w katalogu grails-app/domain
.
Można dodać teraz dane testowe.
Żeby to zrobić, należy zmienić plik grails-app/conf/ApplicationBootStrap.groovy
:
- class ApplicationBootStrap {
- def init = { servletContext ->
- // Create some test data
- new Book(author:"Stephen King",title:"The Shining").save()
- new Book(author:"James Patterson",title:"Along Came a Spider").save()
- }
- def destroy = {
- }
- }
Kontrolery
Kontrolery obsługują zapytania i mapują URL'e.
Aby wygenerować kontroler oraz widok (strony GSP) należy wykonać grails generate-all
i wpisać nazwę encji (Book
z przykładu).
Kontroler zostanie wygenerowany do pliku grails-app/controllers/BookController.groovy
.
Otwórz ten plik i zmień go w następujący sposób, by wykorzystać dynamiczny scaffolding:
- class BookController {
- def scaffold = Book
- }
Uruchamianie projektu
- Z linii poleceń
Wykonaj
grails run-app
. - Z Eclipse'a
Z listy
Run
wybierz opcjęRun...
Z listy przycisku
Run
wybierz opcjęRun...
. WJava Applications
wybierzRun
. Zmiany w projekcie wykonane pod Eclipse'em będą automatycznie ładowane.
Zostanie uruchomiony silnik Jetty na porcie 8080
.
Aby uruchomić Jetty na innym porcie np. 9090 wpisz -Dserver.port=9090 run-app
Lista książek jest dostępna pod adresem http://localhost:8080/my-project/book/list.
Grails Object Relational Mapping (GORM)
Grails wykorzystuje Hibernate 3, ale nie trzeba nic konfigurować.
Domain Class
Jak widzieliśmy w przykładzie jest to zwykła klasa.
W czasie wykonania dostaje jednak dwa dodatkowe atrybuty: id
oraz version
.
Domyślnie wszystkie atrybuty są obowiązkowe oraz są utrwalane w bazie danych. Aby zmienić to zachowanie, wystarczy stworzyć listy wyjątków w klasie:
- class Book {
- static optionals = [ "releaseDate" ]
- static transients = [ "digitalCopy" ]
- Author author
- String title
- String author
- Date releaseDate
- File digitalCopy
- }
Można także ustawić wartości domyślne oraz zmienić nazwę mapowanej tabeli:
- class Book {
- static withTable = "book_table" // tabela będzie mapowana do book_table
- //...
- String title = ''
- String author = '[author unknown]' // domyślna wartość
- //etc..
- }
Modelowanie związków pomiędzy encjami
- Relacje 1-1
Poniższy kod utworzy relację jednokierunkową:
- class Face {
- Nose nose
- }
Do tego, by relacja była dwukierunkowa, wystarczy napisać:
- class Nose {
- Face face
- }
- Relacje 1-n
Stwórzmy encję
Author
. Można być autorem wielu książek.- class Author {
- static hasMany = [ books : Book ]
- String name
- }
Nie trzeba nic zmieniać w klasie
Book
. - Relacje n-m
Można je definiować za pomocą
hasMany
po obu stronach relacji. Przynajmniej jedna ze stron musi mieć atrybutbelongsTo
, który decyduje o tym, która z encji jest "właścicielem" związku, a więc, która będzie powodowała kaskadę uaktualnień.- class Book {
- static belongsTo = Author
- static hasMany = [authors:Author]
- }
- // Author jest 'posiadaczem' związku
- class Author {
- static hasMany = [books:Book]
- }
- // kaskada spowoduje dodanie nowej książki do bazy danych
- new Author(..)
- .addBook(new Book(..))
- .save()
Nie można dodać jednak nowego autora do książki i spodziewać się kaskady.
Walidacja danych
Ograniczenia na dane nakłada się za pomocą closure constraints
, np.:
- class User {
- Long id
- Long version
- String login
- String password
- String email
- Date age
- static constraints = {
- login(length:5..15,blank:false,unique:true)
- password(length:5..15,blank:false)
- email(email:true,blank:false)
- age(min:new Date(),nullable:false)
- }
- }
Walidujemy dane za pomocą metody validate()
.
Domyślnie waliduje także save()
.
Wprowadzone ograniczenia mają wpływ na generowany widok, np. zamiast pól tekstowych generowane są listy, lub zakresy wartości
- def user = new User()
- // populate properties
- if(user.validate()) {
- // do something with user
- }
- else {
- user.errors.allErrors.each {
- println it
- }
- }
Więcej o kontrolerach
Kontrolery można tworzyć automatycznie za pomocą polecenia
grails create-controller
. Taki sam efekt będzie miało utworzenie
kontrolera ręcznie. Mapowanie URL'i jest automatyczne.
- class BookController { // mapuje na <...>/book
- def list = { // mapuje na <...>/book/list
- // do controller logic
- // create model
- return model
- };
- def index = { // domyślna akcja
- redirect(action:list)
- }
- }
Podczas wykonania kontroler dostaje metody i parametry, pozwalające na dostęp do sesji, czy też parametrów żądania a także na logowanie błędów, itp. Pełny spis metod i parametrów znajduje się tu.
- class BookController {
- def find = {
- def findBy = params["findBy"] // parametry żądania
- def appContext = servletContext["appContext"]
- def loggedUser = session["logged_user"] // parametry sesji
- // do stuff
- // return model
- return model
- };
- }
Kontekst "Flash" jest to pomysł wzięty z Rails, polegający na wprowadzeniu tymczasowego składu dla atrybutów, które mają być dostępne tylko przy następnym żądaniu. Przykład użycia:
- def delete = {
- def b = Book.get( params['id'] )
- if(!b) {
- flash['message'] = "User not found for id ${params['id']}"
- redirect(action:list)
- }
- ... // remaining code
- }
Kontroler powinien zwrócić pewien model, którego używa widok przy renderowaniu strony. Model jest mapą:
- def show = {
- def b = Book.get( params['id'] )
- return [ book : b ]
- }
Jeśli nie będzie zwrócony żaden model, użyte zostaną atrybuty kontrolera.
Widoki
Widok realizowany jest za pomocą stron GSP lub JSP. Zasadnicza różnica pomiędzy nimi jest taka, że w stronach GSP skryptlety pisze się w Groovy'm oraz dostępne są dodatkowe tagi. Tutaj znajduje się lista dostępnych tagów.
Za podłączenie widoków do kontrolerów odpowiada mechanizm konwencji. Na przykład dla kontrolera:
- class BookController {
- def list = {
- ["books" : Book.list() ]
- }
- }
odpowiedni widok znajduje się w katalogu grails-app/views/book
i ma nazwę list.gsp
.
- <html>
- <head>
- <title>Book list</title>
- </head>
- <body>
- <h1>Book list</h1>
- <table>
- <tr>
- <th>Title</th>
- <th>Author</th>
- </tr>
- <g:each in="${books}">
- <tr>
- <td>${it.title}</td>
- <td>${it.author}</td>
- </tr>
- </g:each>
- </table>
- </body>
- </html>
Layouts
Układ strony można stosować w Grails poprzez SiteMesh. Polega to na dodaniu
tagu meta
z nazwą layout
:
- <html>
- <head>
- <meta name="layout" content="main"></meta>
- </head>
- <body>This is my content!</body>
- </html>
Layout nazwany main.gsp
należy umieścić w katalogu grails-app/views/layouts
.
- <html>
- <head>
- <title><g:layoutTitle default="An example decorator" /></title>
- <g:layoutHead />
- </head>
- <body onload="${pageProperty(name:'body.onload')}">
- <div class="menu"><!--Jakieś wspólne menu--></menu>
- <div class="body">
- <g:layoutBody />
- </div>
- </div>
- </body>
- </html>