Groovy als Web-Backend

Aus Jonas Notizen Webseite
Version vom 14. März 2019, 11:14 Uhr von Admin (Diskussion | Beiträge) (Kopiert von eigenen Notizen)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Zur Navigation springen Zur Suche springen

Inhaltsverzeichnis

Einführung

Installieren

  • Mithilfe vom SDKMAN kann man Grails ganz einfach auf sein System installieren:

$ sdk install grails

App erstellen

$ grails create-app APP-NAME erstellt im derzeitigem Verzeichnis einen neuen Ordner mit dem entsprechenden APP-NAMEn.

Wenn man jetzt in den erstellten Projekt-Ordner wechselt ($ cd APP-NAME) kann man hier seine $ grails ... Befehle für das Projekt ausführen.

Interaktive Konsole

Wenn man hingegen nur $ grails ausführt gelangt man in eine interaktive Konsole von Grails.
Diese hat den Vorteil, dass Grails nicht für jeden Befehl eine neue JVM starten muss. Zudem brauchen die Befehle auch keinen grails vorne-dran mehr.

"Konventionen über Konfiguration"

"Konventionen über Konfiguration" heißt dass der Ort/Name einer Datei zur Identifikation der "Objekt-Rolle" genutzt wird.
Aus diesem Grund sollte man sich mit dem Aufbau von Grails bekannt machen, um zu wissen wo was rein gehört.

App starten

Grails-App können durch den eingebauten Tomcat-Server mit dem Befehl $ grails run-app gestartet werden.

App testen

Die $ grails create-* Befehle erstellen jeweils eine dazugehörige Unit-Testing Klasse unter src/test/groovy. Natürlich muss man noch selber die Logik hinter den Tests einfügen, wenn man dies hat kann man den Test mithilfe von $ grails test-app starten.

App deployen

Grail-Apps können auf mehrere Weisen bereitgestellt werden.

  • Wenn man einen Traditionällen Container (Wie Tomcat, Jetty, etc.) benutzt kann man ganz einfach ein sog. "Web Application Archive" (.war-Datei) erstellen, welches alle App-Daten in eine Datei zusammenfast. (Wie ein .java Archiv)

$ grails war erstellt die .war-Datei im Verzeichniss build/libs.

  • Grails inkludiert eine eigene eingebette Version von Tomcat innerhalb der WAR-Datei, welches zu Problemen führen kann wenn der eigentliche Server eine andere Version von Tomcat hat.

Wenn man dies nicht will kann man den Scope innerhalb von build.gradle wie folgt ändern:

provided "org.springframework.boot:spring-boot-starter-tomcat"

Grails baut standardmäßig auf Tomcat-8 APIs auf. Diese Version kann man auch unter build.gradle innerhalb der dependencies {}-Sektion wie folgt auf zB. Tomcat-7 ändern:

ext['tomcat.version'] = '7.0.59'

WAR-Datei starten

Mit dem Java-Befehl java -Dgrails.env=prod -jar DEINE-WAR-DATEI.war kann man nun ganz einfach seine erstellte .war-Datei ausführen.
Beispiel: java -Dgrails.env=prod -jar build/libs/mywar-0.1.war

Unterstützte Java EE-Container

Grails läuft auf jeden Container der Servlet 3.0 (oder darüber) unterstützt, wie:

  • Tomcat 7
  • GlassFish 3
  • Resin 4
  • JBOSS 6
  • Jetty 8
  • Oracle Weblogic 12c
  • IBM WebSphere 8.0

Generierung eines Applikations-Bereichs

Der Befehl $ grails generate-all (PACKET.)KLASSEN-NAME erstellt den Controller, dessen Unit-Tests sowie die Assozierten Views.
Dieser Befehl ist nur für die Start-Erleichterung, natürlich kann man auch alle Datei-Skelette manuell erstellen.






Konfiguration

Obwohl Grails dem Schema "convention-over-configuration" folgt, kommt man natürlich nicht ohne Konfigurations-Dateien aus.
Für einen schnelles Hello-World (oÄ.) reichen die Standard-Konfigurationen komplett aus. Möge man dann aber eine MySQL-Verbindung, eigene Konfig-Werte (etc) machen muss man dies Grails natürlich mitteilen.

Grundlegende Konfiguration

Grails Konfigurations-Dateien sind in 2 Sektionen aufgeteilt:

  • Build-Konfiguration (build.gradle)
  • Runtime-Konfiguration (grails-app/conf/application.yml)

Um im Code auf die Runtime-Konfig zuzugreifen gibt es eine spezielle öffentliche Variable namens grailsApplication vom Typ GrailsApplication. Beispiel:

class MyController {
    def hello() {
        def recipient = grailsApplication.config.getProperty('foo.bar.hello')

        render "Hello ${recipient}"
    }
}

Die config-Eigenschaft ist eine Instanz der Klasse Config welche viele nützliche Funktionen bietet.
Insbesondere Interessant ist die getProperty-Methode, bei der man auch einen Fallback-Wert angeben kann falls kein Wert in der Konfig angegeben ist:

class MyController {

    def hello(Recipient recipient) {
        //Retrieve Integer property 'foo.bar.max.hellos', otherwise use value of 5
        def max = grailsApplication.config.getProperty('foo.bar.max.hellos', Integer, 5)

        //Retrieve property 'foo.bar.greeting' without specifying type (default is String), otherwise use value "Hello"
        def greeting = grailsApplication.config.getProperty('foo.bar.greeting', "Hello")

        def message = (recipient.receivedHelloCount >= max) ?
          "Sorry, you've been greeted the max number of times" :  "${greeting}, ${recipient}"
        }

        render message
    }
}

Zugehörige Konfigurations-Werte kann man zudem mit Springs @Value-Annotation in eine Variable injecten.

import org.springframework.beans.factory.annotation.*

class MyController {
    @Value('${foo.bar.hello}')
    String recipient

    def hello() {
        render "Hello ${recipient}"
    }
}


DataSource

Weil Grails auf Java aufbaut sollte man ein bisschen Verständniss von JDBC (Java Database Connectivity) besitzen.

Treiber einfügen

Wenn man eine andere Datenbank als H2 nimmt benötigt mani noch einen JDBC-Treiber. Für MySQL wäre des Connector/J. Treiber kommen normalerweise in Form eines Java-Archivs. Am besten fügt man diese Treiber mithilfe eines Maven-Repos in seine dependencies ein:

dependencies {
    runtime 'mysql:mysql-connector-java:5.1.29'
}

Wenn man die JAR erfolgreich eingebunden hat kann man sich jetzt mit der Weise bekannt machen wie Grails Datenbanken-Aktionen verwaltet.

Datasource Einrichten

In grails-app/conf/DataSource.groovy kann man einstellen, wie Grails die Domain-Klassen-Objekte speichert.

Folgende Konfigurationen kann man in einer dataSource einfügen:

  • driverClassName - Klassenname des JDBC-Treibers (zB. "com.mysql.jdbc.Driver")
  • username, password - Login-Daten um die JDBC-Verbindung aufzubauen
  • url - Die JDBC-URL der Datenbank
  • dbCreate - Wie/Ob man die Tabelle je nach dem Domain-Klassen-Model erstellen soll
    • create - Falls sich das Datenbank-Schema verändert hat, wird die Tabelle entleert und mithilfe des neuen Schemas erstellt
    • create-drop - Gleich wie create, leert die Datenbank hingegen bei jedem neustart (Egal ob sich das Schema geändert hat oder nicht)
    • update - Aktualisiert das Model der Tabelle, falls es sich verändert hat (Die Daten bleiben erhalten, Die Domainklassen-Tabelle muss existieren) Notiz: Solch eine Weise veträgt natürlich nicht viele Änderung des Schemas. (zB. Beim ändern des Namens einer Spalte bleibt die alte Spalte mit den existierenden Daten und die neue wird einfach hinzugefügt)
    • validate - Warnt vor Änderungen, aber verändert das Datenbank-Schema bei Veränderung nicht
  • pooled - Ob man einen Pool von Verbindungen benutzen sollte (Standard: true)
  • logSql - Leitet SQL-Logs nach stdout um
  • formatSql - Ob SQL-Logs "schön?" formatiert werden sollen
  • dialect - TODO
  • readOnly - TODO
  • transactional - TODO
  • persistenceInterceptor - TODO
  • properties - Extra Einstellungen für den DatenSource-Bean. (Siehe Tomcat-Pool Dokumentation oder Javadoc Dokumentation)
  • jmxExport - TODO

Zum Testen nutzt man meißt

Typische MySQL Konfiguration

Eine Typische MySQL-Konfiguration wäre:

dataSource {
    pooled = true
    dbCreate = "update"
    url = "jdbc:mysql://localhost:3306/my_database"
    driverClassName = "com.mysql.jdbc.Driver"
    dialect = org.hibernate.dialect.MySQL5InnoDBDialect
    username = "username"
    password = "password"
    properties {
       jmxEnabled = true
       initialSize = 5
       maxActive = 50
       minIdle = 5
       maxIdle = 25
       maxWait = 10000
       maxAge = 10 * 60000
       timeBetweenEvictionRunsMillis = 5000
       minEvictableIdleTimeMillis = 60000
       validationQuery = "SELECT 1"
       validationQueryTimeout = 3
       validationInterval = 15000
       testOnBorrow = true
       testWhileIdle = true
       testOnReturn = false
       jdbcInterceptors = "ConnectionState;StatementCache(max=200)"
       defaultTransactionIsolation = java.sql.Connection.TRANSACTION_READ_COMMITTED
    }
}

Komplexere MySQL Konfiguration

Oder ein Beispiel einer eher komplexeren Konfiguration:

dataSource {
    pooled = true
    dbCreate = "update"
    url = "jdbc:mysql://localhost:3306/my_database"
    driverClassName = "com.mysql.jdbc.Driver"
    dialect = org.hibernate.dialect.MySQL5InnoDBDialect
    username = "username"
    password = "password"
    properties {
       // Documentation for Tomcat JDBC Pool
       // http://tomcat.apache.org/tomcat-7.0-doc/jdbc-pool.html#Common_Attributes
       // https://tomcat.apache.org/tomcat-7.0-doc/api/org/apache/tomcat/jdbc/pool/PoolConfiguration.html
       jmxEnabled = true
       initialSize = 5
       maxActive = 50
       minIdle = 5
       maxIdle = 25
       maxWait = 10000
       maxAge = 10 * 60000
       timeBetweenEvictionRunsMillis = 5000
       minEvictableIdleTimeMillis = 60000
       validationQuery = "SELECT 1"
       validationQueryTimeout = 3
       validationInterval = 15000
       testOnBorrow = true
       testWhileIdle = true
       testOnReturn = false
       ignoreExceptionOnPreLoad = true
       // http://tomcat.apache.org/tomcat-7.0-doc/jdbc-pool.html#JDBC_interceptors
       jdbcInterceptors = "ConnectionState;StatementCache(max=200)"
       defaultTransactionIsolation = java.sql.Connection.TRANSACTION_READ_COMMITTED // safe default
       // controls for leaked connections
       abandonWhenPercentageFull = 100 // settings are active only when pool is full
       removeAbandonedTimeout = 120
       removeAbandoned = true
       // use JMX console to change this setting at runtime
       logAbandoned = false // causes stacktrace recording overhead, use only for debugging
       // JDBC driver properties
       // Mysql as example
       dbProperties {
           // Mysql specific driver properties
           // http://dev.mysql.com/doc/connector-j/en/connector-j-reference-configuration-properties.html
           // let Tomcat JDBC Pool handle reconnecting
           autoReconnect=false
           // truncation behaviour
           jdbcCompliantTruncation=false
           // mysql 0-date conversion
           zeroDateTimeBehavior='convertToNull'
           // Tomcat JDBC Pool's StatementCache is used instead, so disable mysql driver's cache
           cachePrepStmts=false
           cacheCallableStmts=false
           // Tomcat JDBC Pool's StatementFinalizer keeps track
           dontTrackOpenResources=true
           // performance optimization: reduce number of SQLExceptions thrown in mysql driver code
           holdResultsOpenOverStatementClose=true
           // enable MySQL query cache - using server prep stmts will disable query caching
           useServerPrepStmts=false
           // metadata caching
           cacheServerConfiguration=true
           cacheResultSetMetadata=true
           metadataCacheSize=100
           // timeouts for TCP/IP
           connectTimeout=15000
           socketTimeout=120000
           // timer tuning (disable)
           maintainTimeStats=false
           enableQueryTimeouts=false
           // misc tuning
           noDatetimeStringSync=true
       }
    }
}

Unterschiedliche Konfiguration je nach Phase (Produktion/Testen)

Wenn man einen dataSource {}-Block einfach in die Datei einfügt, gilt diese egal wie die App gestartet wurde.
In Grails kann man jedoch unterschiedliche Konfigurationen einstellen:

environments {
    production {
        dataSource {
            url = "jdbc:mysql://liveip.com/liveDb"
            //...
        }
    }
    test {
        dataSource {
            url = "jdbc:mysql://localhost.com/testDb"
            //...
        }
    }
}

Hiermit hat man seine eigene Abschottung zum Testen und muss sich nicht fürchten die Production-Datenbank zu vernichten.


Eigene Scripts

Mit dem Befehl $ grails create-script NAME kann man sein eigenes Skript erstellen, welches standardmäßig in src/main/scripts/ gespeichert wird.

description()

Die description()-Methode wird für die Ausgabe vom grails help Befehl und um den Nutzern zu helfen.
Beispiel vom generate-all Befehl:

description( "Generates a controller that performs CRUD operations and the associated views" ) {
  usage "grails generate-all <<DOMAIN CLASS>>"
  flag name:'force', description:"Whether to overwrite existing files"
  argument name:'Domain Class', description:'The name of the domain class'
}

Beispiel-Befehl

Ein Beispiel anhand des create-script-Befehls:

description( "Creates a Grails script" ) {
  usage "grails create-script <<SCRIPT NAME>>"
  argument name:'Script Name', description:"The name of the script to create"
  flag name:'force', description:"Whether to overwrite existing files"
}

def scriptName = args[0]
def model = model(scriptName)
def overwrite = flag('force') ? true : false

render  template: template('artifacts/Script.groovy'),
        destination: file("src/main/scripts/${model.lowerCaseName}.groovy"),
        model: model,
        overwrite: overwrite

model()

Wenn man model() mit einem Class/String/File/Resource-Parameter aufruft erhält man eine neue Instanz von der Klasse Model.
Die Model Klasse hat hilfreiche Funktionen zur Code generierung.

Scripts innerhalb eines Scripts aufrufen

Grails hat schon von Anfang an eine große Anzahl an Skripts, welche man ganz einfach aufrufen kann.
Der folgende Code ruft den Befehl testApp mit dem Parametern --debug-jvm auf.

testApp('--debug-jvm')


Die Web-Ebene

= Controller

Für jede Seite braucht man einen speziellen Controller, welcher sich um die einkommenden Web-Requests kümmert und Aktionen (Responses) wie Umleitungen (Redirects), das rendern einer Seite (View), uvm. erledigt
Mit dem Befehl $ grails create-controller (PACKET.)KLASSEN-NAME erstellt man das Skelett eines Controllers, welches in grails-app/controllers/APP-NAME/DEIN/PACKET/KLASSEN-NAME.groovy gespeichert wird. (Dieser Befehl ist nur zur vereinfachten Erstellung, man kann es auch manuell oder mit einer IDE machen)


Actions

Das Standard URL-Mapping Konfiguration versichert dass der Name des Controllers sowie jede Methode zum entsprechendem URI-Pfad gebunden wird.
Das folgende Beispiel hiermit unter ".../Book/index" erreichbar:

package myapp

class BookController {
    def index() { }
}

Ein Controller kann mehrere öffentliche Methoden haben, welche sich (Wie oben beschrieben) jeweils zur einer URI binden.

= Standard-Aktion

  • Wenn es nur eine Aktion (aka. Methode) gibt, wird diese als Standard-Aktion anerkannt
  • Wenn es eine Aktion namens "index" gibt, wird diese als Standard-Aktion anerkannt
  • Alternativ kann man auch eine eigenen Standard setzten, indem man static defaultAction = "DEINE-METHODE" in den Quellcode des Controllers einfügt.

Scopes

Scopes sind HashMap-Ähnliche strukturen, in welche man Variablen speichern kann. Für Controller stehen folgende Scope-Variablen zur Verfügung:

servletContext: Statisch, für alle gleich

Dieser Scope lässt uns Daten speichern, welche über die gesamte Web-App gleich statisch erreichbar sind. Das servletContext-Objekt ist eine Instanz vom Standardmäßigen Java-Objekt ServletContext.

Es ist nützlich um:

  • Applikations-Attribute zu speichern
  • lokale Server-Resourcen zu laden und
  • Informationen vom Servlet-Container zu erhalten.

session: Für jede Sitzung anders

Das session-Objekt ist eine Instanz von [1] der Servlet API.

Es ist nützlich um Attribute der derzeitigen Sitzung eines Klientens zu speichern, wie zB. der Login (Name/Passwort).

request: Mitgesendete Informationen

Das request-Objekt ist eine Instanz von HttpServletRequest der Servlet API.

Es ist nützlich um:

  • Mitgesendete Request-Header-Felderdaten zu bekommen
  • Request-bezogene Attribute zwischen-zu-speichern und
  • Informationen des aktuellen Klienten zu erhalten

Grails fügt einige zusätzliche Funktionen zum HttpServletRequest-Objekt hinzu

  • XML - Eine Instanz der GPathResult welches erlaubt einkommende XML-Anfragen zu verarbeiten (Parsen) - Nützlich für REST
  • JSON - Eine Instanz des JSONObjects welches erlaubt einkommente JSON-Anfragen zu verarbeiten (Parsen) - Nützlich für JSON und REST
  • isRedirected() - Gibt true zurück wenn diese Anfrage nach einem Request fordert.
  • get - Gibt true zurück wenn die Anfrage ein GET-Request ist
  • post - Gibt true zurück wenn die Anfrage ein POST-Request ist
  • each - Implementation von Groovys each-Methode um über gewisse Request-Objekte zu iterieren.
  • find - Implementation von Groovys find-Methode um ein gewisses Request-Objekt zu finden (Ohne komplett richtigen Hashkey-Namen)
  • findAll - Implementation von Groovys findAll-Methode um gewisse Request-Objekte zu finden (Ohne komplett richtigen Hashkey-Namen)
  • xhr - Gibt true zurück wenn die Anfrage ein AJAX-Request ist.

Würde der Body der Anfrage folgenden XML-Code besitzen:

<book>
   <title>The Stand</title>
</book>

Können wir dies ganz einfach mithilfe des request-Objekts serialisieren:

def title = request.XML?.title
render "The Title is $title"

(params): Veränderliche Mitgesendete Informationen

Obwohl das request-Objekt die gleiche Funktion bietet, ist diese manchmal nützlich für Daten-Bindung an Objekte. Veränderliche, Mehrdimensionale Map von einkommenden Request-Parametern (CGI).

flash: Speicher zwischen 2 Anfragen

Eine temporäre "Zwischenspeicher"-Map welche die Daten einer einzelnen Sitzung für die nächste Anfrage (Von der Sitzung) speichert.

Dieses Pattern ist nützlich für HTTP-Redirects (Speziell für Redirect After Post) und um Daten einfach für den nächsten mitzuschicken.

Scope eines Controllers

Neu erstellte Controller haben den Standard-Scope-Wert "singleton" (Änderbar in application.yml). Verfügbare Controller-Scopes sind:

  • prototype (Standard) - Für jeden Request wird ein neuer Controller erstellt
  • session - Für jede Sitzung wird nur ein Controller-Objekt erstellt
  • singleton - Für die gesamte Zeit gibt es nur eine Instanz des Controllers (Wird geraten für Aktionen wie Methoden)

Man kann den Scope eines Individuellen Controllers auch manuell ändern, indem man static scope = "DEIN-SCOPE" in den Quellcode des Controllers einfügt.

Model und Ansicht (Model and View)

Ein Model ist eine Map welche zum rendern genutzt werden kann. Die Schlüssel in der Map korrespondieren mit dem Variablen-Namen über dessen sie von zB. einer .gsp-Datei erreichbar sind.
Es gibt einige Wege um ein Model zu übergeben:

  • Explizit: Wenn seine Methode gleich wie die View-Datei heißt, übergibt man das Model einfach wie beim returnen mit folgendem Syntax: [Var1: Data1, Var2: Data2, ...]
def VIEW_NAME() {
   [book: Book.get(params.id)] // Gleich wie render(view: "VIEW_NAME", model: [book: Book.get(params.id)])
}
  • Eine erweiterte/bessere Weiße wäre es, Springs ModelAndView zu benutzen.
import org.springframework.web.servlet.ModelAndView

def index() {
    // get some books just for the index page, perhaps your favorites
    def favoriteBooks = ...

    // forward to the list view to show them
    return new ModelAndView("/book/list", [ bookList : favoriteBooks ])
}

Views selber selektieren mit der render()-Methode

In den vorherhigen 2 Beispielen haben wir nirgendswo angegeben welche view mit dem Map-Daten gerendert werden soll.
Dies liegt an Grails Konventionen. Im folgenden Beispiel würde Grails unter grails-app/views/book/show.gsp nachsuchen:

class BookController {
    def show() {
         [book: Book.get(params.id)]
    }
}

Falls wir aber nicht wollen, das Grails sich den Namen der View-Datei mithilfe des Methoden-Namens sucht, können wir die vielfältige render()-Methode nutzen.

def show() {
    def map = [book: Book.get(params.id)]
    render(view: "display", model: map)
}

Wenn Grails keine .gsp-Datei finden kann, sucht es auch nach .jsp-Dateien.

Views für Namespaced-Controller selektieren

Wenn ein Controller einen eigenen namespace gesetzt hat, wie zB. 'business', schaut Grails

  • zuerst nach ob die View-Datei unter grails-app/views/business/... zu finden ist
  • und falls nicht sucht es unter dem normalen Namespace grails-app/views/... nach.

Beispiel:

class ReportingController {
    static namespace = 'business'

    def humanResources() {
        // This will render grails-app/views/business/reporting/humanResources.gsp
        // if it exists.

        // If grails-app/views/business/reporting/humanResources.gsp does not
        // exist the fallback will be grails-app/views/reporting/humanResources.gsp.

        // The namespaced GSP will take precedence over the non-namespaced GSP.

        [numberOfEmployees: 9]
    }


    def accountsReceivable() {
        // This will render grails-app/views/business/reporting/numberCrunch.gsp
        // if it exists.

        // If grails-app/views/business/reporting/numberCrunch.gsp does not
        // exist the fallback will be grails-app/views/reporting/numberCrunch.gsp.

        // The namespaced GSP will take precedence over the non-namespaced GSP.

        render view: 'numberCrunch', model: [numberOfEmployees: 13]
    }
}

Redirects

Aktionen können mithilfe der Controller-Methode redirect() umgeleitet werden. Beispiel:

class OverviewController {

    def login() {}

    def find() {
        if (!session.user)
            redirect(action: 'login')
            return
        }
        ...
    }
}

Die redirect() fordert einer der folgenden angaben:

  • Der Name der Aktion (Sowie den namens des Controllers, falls sich die Aktion in einem anderen Controller befindet)
// Redirects to the "index()"-Action in the "home"-Controller
redirect(controller: 'home', action: 'index')
  • URI zu einer Resource relativ vom Kontext-Pfad:
// Redirect to an explicit URI
redirect(uri: "/login.html")
  • Eine Komplette URL:
// Redirect to a URL
redirect(url: "http://grails.org")

Parameter können von einer zu der anderen Aktion mithilfe des params-Arguments übergeben werden:

redirect(action: 'myaction', params: [myparam: "myvalue"])

Diese werden dann in den Controller-Scope params (Siehe oben) gespeichert.

Aktionen chainen

Fungiert ähnlich wie redirect, indem es auf eine andere Aktion springt und diese ausführt.
Chaining hingegen erlaubt es uns das Model beizubehalten und Model-Daten zusätzlich zu übergeben.

Beispiel:

class ExampleChainController {

    def first() {
        chain(action: second, model: [one: 1])
    }

    def second () {
        chain(action: third, model: [two: 2])
    }

    def third() {
        [three: 3])
    }
}

Resultiert in das Model

[one: 1, two: 2, three: 3]

, wenn die Aktion "first()" aufgerufen wird.

Wie beim redirect kann man hier auch wieder Daten in das params-Scope einspeisen:

chain(action: "action1", model: [one: 1], params: [myparam: "param1"])

Die render()-Methode

Die render()-Methode ist sehr flexibel und kann viele Argumente hingegennehmen.
Vom anzeigen eines einfachen Textes bis hin zur render eines Views/Templates.

// renders text to response
render "some text"

// renders text for a specified content-type/encoding
render(text: "<xml>some xml</xml>", contentType: "text/xml", encoding: "UTF-8")

// render a template to the response for the specified model
def theShining = new Book(title: 'The Shining', author: 'Stephen King')
render(template: "book", model: [book: theShining])

// render each item in the collection using the specified template
render(template: "book", collection: [b1, b2, b3])

// render a template to the response for the specified bean
def theShining = new Book(title: 'The Shining', author: 'Stephen King')
render(template: "book", bean: theShining)

//! render the view with the specified model
def theShining = new Book(title: 'The Shining', author: 'Stephen King')
render(view: "viewName", model: [book: theShining])

// render the view with the controller as the model
render(view: "viewName")

// render some markup to the response
render {
    div(id: "myDiv", "some text inside the div")
}

// render some XML markup to the response
render(contentType: "text/xml") {
    books {
         for (b in books) {
             book(title: b.title, author: b.author)
         }
    }
}

//! render a JSON ( http://www.json.org ) response with the builder attribute:
render(contentType: "application/json") {
    book(title: b.title, author: b.author)
}

//! render with status code
render(status: 503, text: 'Failed to update book ${b.id}')

//! render a file
render(file: new File(absolutePath), fileName: "book.pdf")


Data Binding

"Binding" ist das Verfahren, einkommende Request-Parameter an ein Objekt zu binden. "Data Binding" sollte auch die Konversion von String auf den jeweiligen Typen übernehmen können, weil alle Request-Parameter ja nur Strings sind aber ein Objekt ja auch Integers (etc) haben kann.

// Klasse 
class Person {
    String firstName
    String lastName
    Integer age
}

// Bindung
def bindingMap = [firstName: 'Peter', lastName: 'Gabriel', age: 63]

def person = new Person(bindingMap)

assert person.firstName == 'Peter'
assert person.lastName == 'Gabriel'
assert person.age == 63

Mit Unterklassen

Dieser Binder kann auch unterklassen verwalten:

// Klassen
class Person {
    String firstName
    String lastName
    Integer age
    Address homeAddress
}

class Address {
    String county
    String country
}

// Bindung
def bindingMap = [firstName: 'Peter', lastName: 'Gabriel', age: 63, homeAddress: [county: 'Surrey', country: 'England'] ]

def person = new Person(bindingMap)

assert person.firstName == 'Peter'
assert person.lastName == 'Gabriel'
assert person.age == 63
assert person.homeAddress.county == 'Surrey'
assert person.homeAddress.country == 'England'

Mit Arrays

Auch Arrays sind möglich:

// Klassen
class Band {
    String name
    static hasMany = [albums: Album] // One-To-Many Relation von Band-zu-Album (Für Datenbank-Relation wichtige Angabe!)
    List albums 
}

class Album {
    String title
    Integer numberOfTracks
}

// Bindung
def bindingMap = [name: 'Genesis',
                  'albums[0]': [title: 'Foxtrot', numberOfTracks: 6],
                  'albums[1]': [title: 'Nursery Cryme', numberOfTracks: 7]]

def band = new Band(bindingMap)

assert band.name == 'Genesis'
assert band.albums.size() == 2
assert band.albums[0].title == 'Foxtrot'
assert band.albums[0].numberOfTracks == 6
assert band.albums[1].title == 'Nursery Cryme'
assert band.albums[1].numberOfTracks == 7

Mit Maps

Sowie auch mit Maps:

// Klassen
class Album {
    String title
    static hasMany = [players: Player] // One-To-Many Relation von Album-zu-Player (Für Datenbank-Relation wichtige Angabe!)
    Map players 
}

class Player {
    String name
}

// Bindung
def bindingMap = [title: 'The Lamb Lies Down On Broadway',
                  'players[guitar]': [name: 'Steve Hackett'],
                  'players[vocals]': [name: 'Peter Gabriel'],
                  'players[keyboards]': [name: 'Tony Banks']]

def album = new Album(bindingMap)

assert album.title == 'The Lamb Lies Down On Broadway'
assert album.players.size() == 3
assert album.players.guitar.name == 'Steve Hackett'
assert album.players.vocals.name == 'Peter Gabriel'
assert album.players.keyboards.name == 'Tony Banks'


File-Uploads

.gsp-Code

Upload Form: <br />
<g:uploadForm action="upload">
   <input type="file" name="myFile" />
   <input type="submit" />
</g:uploadForm>
  • g:uploadForm fügt automatisch enctype="multipart/form-data" zum form-Tag hinzu.

Nun gibt es viele Möglichkeiten mit dem File-Upload umzugehen. Eine davon ist mithilfe von Springs MultipartFile.
(Diese Weise ist gut um Dateien zu transferieren und manipulieren weil man direkt die InputStream-Instanz bekommen kann.)

def upload() {
    def f = request.getFile('myFile')
    if (f.empty) {
        flash.message = 'file cannot be empty'
        render(view: 'uploadForm')
        return
    }

    f.transferTo(new File('/some/local/dir/myfile.txt'))
    response.sendError(200, 'Done')
}

Upload-Größen-Limit von Grail

Grails Standard-Wert für die maximale Dateigröße liegt bei 128KB. Wenn dieses Limit überschritten wird taucht folgender fehler auf:

org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException

Dieses Limit kann man in seiner application.yml folgendermaßen anpassen:

grails:
    controllers:
        upload:
            maxFileSize: 2000000
            maxRequestSize: 2100000
  • maxFileSize: Maximale größe einer Datei bei einem Request
  • maxRequestSize: Maximale Größe des gesamten Requests


URL-Mappings

In der Dokumentation ist die Konvention des URL-Mappings wie folgt: /controller/action/id.
Dies kann man aber auch sehr leicht für spezielle Seiten in der Datei grails-app/conf/UrlMappings.groovy umändern. Beispiel (URL "/product" auf die Aktion "list()" vom Controller "Product" leiten):

"/product"(controller: "product", action: "list")

Man kann die URL-Mappings auch verschachteln:

group "/store", {
    group "/product", {
        "/$id"(controller:"product")
    }
}


(Explizites) REST-Mapping

"/books"(resources:'book')

ist gleich wie

get "/books"(controller:"book", action:"index")
get "/books/create"(controller:"book", action:"create")
post "/books"(controller:"book", action:"save")
get "/books/$id"(controller:"book", action:"show")
get "/books/$id/edit"(controller:"book", action:"edit")
put "/books/$id"(controller:"book", action:"update")
delete "/books/$id"(controller:"book", action:"delete")


// Results in /books/1/authors/2
<g:link controller="author" action="show" method="GET" params="[bookId:1]" id="2">The Author</g:link>

Redirects

"/viewBooks"(redirect: '/books/list')
"/viewAuthors"(redirect: [controller: 'author', action: 'list'])
"/viewPublishers"(redirect: [controller: 'publisher', action: 'list', permanent: true])


Integrierte Variablen

"/product/$id"(controller: "product")

Im obrigen Fall wird Grails automatisch beim Besuchen von zB "/product/2" die Zahl in das params Scope reinsetzen, damit es vom Controller einfach genutzt werden kann:

class ProductController {
     def index() { render params.id }
}

Natürlich kann man auch mehere Variablen verwenden, wie bei diesem Beispiel eines Blogs:

"/$blog/$year/$month/$day/$id"(controller: "blog", action: "show")

Optionale Variablen

Indem man ein Fragezeichen am ende des Variabl-Namens anhängt schlägt das URL-Mapping auch zu, wenn der Nutzer diese nicht angegeben hat. Beispiel-Mapping:

"/$blog/$year?/$month?/$day?/$id?"(controller:"blog", action:"show")

Das obrige Beispiel-Mapping schlägt bei allen folgenden URLs zu:

/graemerocher/2007/01/10/my_funky_blog_entry
/graemerocher/2007/01/10
/graemerocher/2007/01
/graemerocher/2007
/graemerocher

Optionale File-Extension

Wenn man die Datei-Erweiterung auch ganz einfach in einer params-Scope Variable haben möchte, kann man dies durch das anhängen von der Optionalen Variable (.$format)?:

# Mapping
"/$controller/$action?/$id?(.$format)?"()

# Controller
def index() {
    render "extension is ${response.format}"
}


Selbst "eingepflanzte" Variablen

Man kann auch selber mithilfe von URL-Mapping Variablen in das params-Scope reinschreiben.

"/holiday/win" {
     id = "Marrakech"
     year = 2007
}


Dynamisch aufgelöste Variablen

Die Hard-Gecodeten, selbst eingeschriebenen Variablen sind nützlich. Aber manchmal will man ja Variablen reinmachen, die sich bei der Laufzeit verändern können.
Dies ist möglich indem man einen eigenen Block zur Variable hinzufügt:

"/holiday/win" {
     id = { params.id }
     isEligible = { session.user != null } // must be logged in
}

Im obrigen Fall wird der Code innerhalb der Blöcke erst evaluiert wenn die URL gematched wurde, und kann dadurch "mit logik versehn werden".

Constraints (Einschränkungen des Typens einer Variable)

Nehmen wir folgendes Beispiel:

static mappings = {
   "/$blog/$year?/$month?/$day?/$id?"(controller:"blog", action:"show")
}

Dieses Mapping funktioniert für:

/graemerocher/2007/01/10/my_funky_blog_entry

Aber auch für Werte, die wir vllt. nicht vorgesehen haben:

/graemerocher/not_a_year/not_a_month/not_a_day/my_funky_blog_entry

Dies wäre sehr problematisch und man müsste eine (komplexe) eigene Logik reincoden, damit bestimmte Variablen nur einen bestimmten Wert haben dürfen.
Zum Glück bieten uns URL-Mappings einen einfachen Weg für diese Einschränkung/Validation:

"/$blog/$year?/$month?/$day?/$id?" {
     controller = "blog"
     action = "show"
     constraints {
          year(matches:/\\\d{4}/)
          month(matches:/\\\d{2}/)
          day(matches:/\\\d{2}/)
     }
}

Error-Codes

Grails lässt uns zudem jeglichen HTTP-Fehlercode auf eine eigene Aktion (Seite) mappen:

"403"(controller: "errors", action: "forbidden")
"404"(controller: "errors", action: "notFound")
"500"(controller: "errors", action: "serverError")

Man kann auch auftauchende Exceptions auf eine eigene Seite mappen:

"500"(controller: "errors", action: "illegalArgument", exception: IllegalArgumentException)









Domain-Klassen

Domain-Klassen stellen ein Objekt dar, welches in einer Datenbank gespeichert und von dieser wieder deserialisiert werden kann.

  • Somit stellen Domain-Klassen das M im Model-View-Control (MVC) Paradigma dar

Das Skelett einer Domain-Klasse wird mit dem Befehl grails create-domain-class NAME-Befehl.

Funktionen

addTo

Fügt eine Relation zwischen zwei Domainklassen hinzu.
Syntax:.addTo[LISTE]([OBJEKT])
Beispiel:

def fictBook = new Book(title: "IT")
def nonFictBook = new Book(title: "On Writing: A Memoir of the Craft")
def a = new Author(name: "Stephen King")
             .addToFiction(fictBook)
             .addToNonFiction(nonFictBook)
             .save()

XXX

Setzt den Fehler-Cache der letzten Validationen zurück
Syntax:.clearErrors()
Beispiel:

def b = new Book(title: "The Shining")
b.validate()
if (b.hasErrors()) {

    // clear the list of errors
    b.clearErrors()

    // fix the validation issues
    b.author = "Stephen King"

    // re-validate
    b.validate()
}

count

Gibt die Anzahl an Reihen in der Domainklassen-Tabelle zurück
Syntax:.count()
Beispiel:

int bookCount = Book.count()

countBy & find...By

Siehe auch:

Dynamische Methode welches die Eigenschaften der Domainklasse nutzt um hilfreiche Such-Methoden zu erzeugen.
Syntax:.countBy[VARIABLE[OPERATOREN[ANDERES]]]
Die Folgenden Operatoren stehen zur Verfügung:

  • LessThan
  • LessThanEquals
  • GreaterThan
  • GreaterThanEquals
  • Between
  • Like
  • Ilike (i.e. ignorecase like)
  • IsNotNull
  • IsNull
  • Not
  • Equal
  • NotEqual
  • And
  • Or

Die folgenden Beispiele sind alle möglich:

// Domainklasse
class Book {
    String title
    Date releaseDate
    String author
}

// Checks
def c = Book.countByTitle("The Shining")
c = Book.countByTitleAndAuthor("The Sum of All Fears", "Tom Clancy")
c = Book.countByReleaseDateBetween(firstDate, new Date())
c = Book.countByReleaseDateGreaterThanEquals(firstDate)
c = Book.countByTitleLike("%Hobbit%")
c = Book.countByTitleNotEqual("Harry Potter")
c = Book.countByReleaseDateIsNull()
c = Book.countByReleaseDateIsNotNull()

delete

Löscht eine persistente Instanz
Syntax:.delete([flush: true/false])

  • Parameter "flush": Wenn auf "true" gesetzt wird die Aktion direkt durchgeführt

Beispiel:

def book = Book.get(1)
book.delete()

discard

Verwirft noch nicht gespeicherte Änderungen am lokalen Objekt.
Syntax:.discard()
Beispiel:

def b = Book.get(1)
b.title = "Blah"
b.discard() // changes won't be applied now

embedded

Erlaubt es Daten von anderen (Domain-)Klassen innerhalb der gleichen Tabelle zu speichern, anstatt wie normalerweise auf die Relation zu einer anderen Datenbank aufmerksam zu machen
Syntax:static embedded = ['KLASSEN-VARIABLE1', 'KLASSEN-VARIABLE2']
Beispiel:

# Domainklassen
class Person {

    String name
    Country bornInCountry
    Country livesInCountry

    static embedded = ['bornInCountry', 'livesInCountry']
}

// If you don't want an associated table created for this class, either
// define it in the same file as Person or put Country.groovy under the
// src/main/groovy directory.
class Country {
    String iso3
    String name
}

// Resultiert in einer Tabelle mit den Spalten: (Alle vom Typen VARCHAR(255))
// NAME
// BORN_IN_COUNTRY_ISO3
// BORN_IN_COUNTRY_NAME
// LIVES_IN_COUNTRY_ISO3
// LIVES_IN_COUNTRY_NAME

executeQuery

Führt HQL-Queries aus
Syntax:

  • .executeQuery(String query)
  • .executeQuery(String query, List positionalParams)
  • .executeQuery(String query, List positionalParams, Map metaParams)
  • .executeQuery(String query, Map namedParams)
  • .executeQuery(String query, Map namedParams, Map metaParams)

Parameter:

  • query - Eine HQL-Query
  • positionalParams - Eine Liste von Parametern für eine positionsparametrisierte HQL Abfrage
  • namedParams - Eine Map mit benannten Parametern für eine benannte parametrisierte HQL Abfrage
  • metaParams - Eine Map mit den Paginierungsparametern max oder / und offset sowie die Abfrageparameter im Hibernate-Modus readOnly, fetchSize, timeout und flushMode

Beispiel:

// simple query
Account.executeQuery("select distinct a.number from Account a")

// using with list of parameters
Account.executeQuery("select distinct a.number from Account a " +
                     "where a.branch = ? and a.created > ?",
                     ['London', lastMonth])

// using with a single parameter and pagination params
Account.executeQuery("select distinct a.number from Account a " +
                     "where a.branch = ?", ['London'],
                     [max: 10, offset: 5])

// using with Map of named parameters
Account.executeQuery("select distinct a.number from Account a " +
                     "where a.branch = :branch",
                     [branch: 'London'])

// using with Map of named parameters and pagination params
Account.executeQuery("select distinct a.number from Account a " +
                     "where a.branch = :branch",
                     [branch: 'London', max: 10, offset: 5])

// same as previous
Account.executeQuery("select distinct a.number from Account a " +
                     "where a.branch = :branch",
                     [branch: 'London'], [max: 10, offset: 5])

// tell underlying Hibernate Query object to not attach newly retrieved
// objects to the session, will only save with explicit `save`
Account.executeQuery("select distinct a.number from Account a",
                     null, [readOnly: true])

// time request out after 18 seconds
Account.executeQuery("select distinct a.number from Account a",
                     null, [timeout: 18])

// have Hibernate Query object return 30 rows at a time
Account.executeQuery("select distinct a.number from Account a",
                     null, [fetchSize: 30])

// modify the FlushMode of the Query (default is `FlushMode.AUTO`)
Account.executeQuery("select distinct a.number from Account a",
                     null, [flushMode: FlushMode.MANUAL])

executeUpdate

Aktualisiert die Datenbank mit DML-Style Operationen
Syntax:

  • .executeUpdate(String query)
  • .executeUpdate(String query, List positionalParams)
  • .executeUpdate(String query, Map namedParams)

Parameter:

  • query - Eine HQL-Query
  • positionalParams - Eine Liste von Parametern für eine positionsparametrisierte HQL Abfrage
  • namedParams - Eine Map von Parametern für eine positionsparametrisierte HQL Abfrage

Beispiel:

Account.executeUpdate("delete Book b where b.pages > 100")

Account.executeUpdate("delete Book b where b.title like ?",
                      ['Groovy In Action'])

Account.executeUpdate("delete Book b where b.author=?",
                      [Author.load(1)])

Account.executeUpdate("update Book b set b.title='Groovy In Action'" +
                      "where b.title='GINA'")

Account.executeUpdate("update Book b set b.title=:newTitle " +
                      "where b.title=:oldTitle",
                      [newTitle: 'Groovy In Action', oldTitle: 'GINA'])


exists

Schaut ob ein Objekt mit der angegebenen ID in der Datenbank existiert
Syntax:.exists(id)
Beispiel:

def accountId = ...
if (Account.exists(accountId)) {
    // do something
}


find & findAll

Siehe auch:

Gibt das erste gefundene Objekt der HQL-Query zurück
Syntax:

  • .find(String query)
  • .find(String query, List positionalParams)
  • .find(String query, List positionalParams, Map queryMaps)
  • .find(String query, Map namedParams)
  • .find(String query, Map namedParams, Map queryParams)
  • .find(String query, Map namedParams, Map queryParams)
  • .find([DOMAIN-KLASSE] example)
  • .find(Closure whereCriteria)

Parameter:

  • query - Eine HQL-Query
  • positionalParams - Eine Liste von Parametern für eine positionsparametrisierte HQL Abfrage
  • namedParams - Eine Map mit benannten Parametern für eine benannte parametrisierte HQL Abfrage
  • - Eine Map mit den Paginierungsparametern max oder / und offset sowie die Abfrageparameter im Hibernate-Modus readOnly, fetchSize, timeout und flushMode

Beispiel:

// Dan brown's first book
Book.find("from Book as b where b.author='Dan Brown'")

// with a positional parameter
Book.find("from Book as b where b.author=?", ['Dan Brown'])

// with a named parameter
Book.find("from Book as b where b.author=:author", [author: 'Dan Brown'])

// use the query cache
Book.find("from Book as b where b.author='Dan Brown'", [cache: true])
Book.find("from Book as b where b.author=:author",
          [author: 'Dan Brown'],
          [cache: true])

// query by example
def example = new Book(author: "Dan Brown")
Book.find(example)

// Using where criteria (since Grails 2.0)
Person p = Person.find { firstName == "Bart" }


findWhere & find...Where

Siehe auch:

Findet ein bestimmtest Objekt welches die Kriterien der Parameter erfüllt. Syntax:.
Beispiel:

// Domainklasse
class Book {

   String title
   Date releaseDate
   String author

   static constraints = {
      releaseDate nullable: true
   }
}

// Beispiel
def book = Book.findWhere(author: "Stephen King", title: "The Stand")

boolean isReleased = Book.findWhere(author: "Stephen King",
                                    title: "The Stand",
                                    releaseDate: null) != null


first & last

Gibt das erste/letzte Objekt in der Datenbank zurück
Syntax:.first/last(sort: '[VARIABLEN-NAMEN]')
Beispiel:

// retrieve the first person ordered by the identifier
def p = Person.first()

// retrieve the first person ordered by the lastName property
p = Person.first(sort: 'lastName')

// retrieve the first person ordered by the lastName property
p = Person.first('lastName')


get, read, refresh & getAll

Siehe auch:

Gibt die Instanz der Domainklasse mit der angegebenen ID zurück
Syntax:.get(ID)
Beispiel:

def b = Book.get(1)


getDirtyPropertyNames

Siehe auch:

Gibt die veränderten (Aber noch nicht in der Datenbank gespeicherten) Werte zurück
Syntax:.getDirtyPropertyNames()
Beispiel:

def b = Book.get(1)
someMethodThatMightModifyTheInstance(b)

def names = b.dirtyPropertyNames
for (name in names) {
    def originalValue = b.getPersistentValue(name)
    ...
}


hasMany

Definiert eine One-To-Many Relation zwischen 2 Klassen.
Syntax:static hasMany = [LISTEN-VARIABLE: TYP-VON-LISTE, ...]
Beispiel:

// Domainklasse
class Author {
    String name
    static hasMany = [books: Book]
}

// Methode
def a = Author.get(1)
for (book in a.books) { println book.title }


hasOne

Definiert eine Bidirektionale One-To-One Releation zwischen 2 Klassen, wobei der Fremdschlüssel zum Kind gehört.
Syntax:static hasOne = [VARIABLE: TYP-VON-VARIABLE]
Beispiel:

class Face {
    static hasOne = [nose: Nose]
}
class Nose {
    Face face
}

instanceOf

Checkt ob ein Domain-Objekt von einer anderen Domainklasse erstellt wurde. (Funktioniert auch wenn die erbende Klasse nichts anders macht)
Syntax:.
Beispiel:

// Domain-Klassen
class Container {
   static hasMany = [children: Child]
}
class Child {
   String name
   static belongsTo = [container: Container]
}
class Thing extends Child {}
class Other extends Child {}

// Methoden
def container = Container.get(id)
for (child in container.children) {
   if (child.instanceOf(Thing)) {
      // process Thing
   }
   else if (child.instanceOf(Other)) {
      // process Other
   }
   else {
      // handle unexpected type
   }
}


=== list === TODO
Syntax:.
Beispiel:


XXX


Syntax:.
Beispiel:


XXX


Syntax:.
Beispiel:

Constraints

In Grails sind Contraints ein weg zur Angabe wann ein Objekt Valide ist.
In Domain-Klassen werden Constraints innerhalb eines statisch öffentlichen Blocks definiert. Beispiel:

class User {
    ...

    static constraints = {
        login size: 5..15, blank: false, unique: true
        password size: 5..15, blank: false
        email email: true, blank: false
        age min: 18
    }
}

Domainklassen-Variablen im Contraint benutzen

Beim festlegen von Constraints in einem statischen Block darf man keine anderen Eigenschaften der Domainklasse benutzen.
Zum Beispiel könnte es passieren das man versucht:

class Response {
    Survey survey
    Answer answer

    static constraints = {
        survey blank: false
        answer blank: false, inList: survey.answers
    }
}

Aber man eine MissingPropertyException kriegt. Für solche angelegenheiten muss man einen sog. validator benutzen:

class Response {
    ...
    static constraints = {
        survey blank: false
        answer blank: false, validator: { val, obj -> val in obj.survey.answers }
    }
}

Constraints validieren

Um zu schauen ob alle Werte im validen Bereich liegen bietet eine Domain-Klasse die Methode validate():

def user = new User(params)

if (user.validate()) {
    // do something with user
}
else {
    user.errors.allErrors.each {
        println it
    }
}

Die errors-Eigenschaft einer Domainklasse ist eine Instanz von Springs Errors-Interface. Dieses Interface bietet Funktionen um über alle Validations-Fehler zu iterieren/bekommen.

(Notiz: .save() ruft auch .validate() und gibt den entsprechenden boolschen Wert )

Variablen validieren

Natürlich kann es auch auftauchen dass schon beim erstellen ein falscher Parameter übergeben wurde, wie zB. einen String anstatt eines Dates.
Dafür gibt es die Methode .hasErrors():

if (user.hasErrors()) {
    if (user.errors.hasFieldErrors("login")) {
        println user.errors.getFieldError("login").rejectedValue
    }
}

Liste aller Constraints und dessen Internationalisierbaren Fehlercodes

Services

In Grails sollte es eine Abtrennung zwischen "eigentlicher Logik" und "Anfragen behandeln".

  • Das behandeln von Anfragen (Redirects, Rendern von Templates/...) sollte den Controllern überlassen werden
    • Nur dies alleine könnte zur unübersichtlichkeit sowie mangelnder neu-benutzbarkeit führen
  • Die eigentliche Logik sollte jedoch in eigenen "Service"-Klassen liegen

Um eine Service-Klasse zu erstellen gibt es den Befehl $ grails create-service (PACKET.)KLASSEN-NAME.
Der Service-Klassen Stammbaum befindet sich unter grails-app/services/


Testing

Grails bietet eine große Erleichterung beim Testen seiner App.
Jeder create-* und generate-\*-Befehl erstellt automatisch einen zugehörigen Unit und integration-Test.
Beispiel: $ grails create-controller com.acme.app.simple erstellt

  • eine Controller-Klasse "grails-app/controllers/com/acme/app/SimpleController.groovy"
  • und die dazugehörige Unit-Test Klasse "src/test/groovy/com/acme/app/SimpleControllerSpec.groovy"
    • Aber natürlich muss man die Logik hinter den Tests selber einfügen!

Um die Unit-Tests zu starten muss man nurnoch mehr $ grails test-app eingeben.
Grails schreibt hier eine visuelle HTML sowie XML-Darstellung unter target/test-reports.
(Unit-Tests sind auch innerhalb der meisten IDEs möglich)

Man kann auch nur gewisse Tests starten, indem man das Objekt angibt. Beispiel: $ grails test-app some.org.*Controller

Unit Testing

Unit-Testing bedeuted Tests eines individuellen Code-Blocks/Funktion durchzuführen, ohne die umgebenden Sachen zu beachten. Unit-Tests führen normalerweise keine I/O-Operation wie Datenbanken, Sockets und Files aus, um Geschwindigkeit des Tests zu gewährleisten.

Integrations-Tests

Integrations-Tests unterscheiden sich zu Unit-Test, dass sie kompletten zugriff auf die komplette Grails umgebung haben. Integrations-Tests kann man mit dem Befehl $ grails create-integration-test NAME erstellen, und werden unter src/integration-test/groovy/NAME.groovy gespeichert.

Internationalisierung

Grails bietet Internationalisierung (i18n) ohne jegliche zusätzliche Plugins (Out-Of-Box). Quote von den Javadocs vom Locale-Objekt:

A Locale object represents a specific geographical, political, or cultural region.

Ein Locale setzt sich aus einem "Language Code" sowie einem "Country Code" zusammen. (Beispiel: "en_US")


Alle Nachrichten-Bündel sind im grails-app/i18n-Pfad hinterlegt. Der Syntax der Dateiname lautet messages_LANGUAGE-CODE_CITY-CODE.properties. (Beispiel: "messages_en_GB.properties" für Britisches Englisch.)

Standardmäßig schaut Grails im messages.properties-Bündel nach. Der Nutzer kann auf mehrere Arten aber auch ein anderes Locale anfordern (Falls vorhanden):

  • Accept-Language Attribut im HTTP-Header
  • lang-Parameter im Request, zB /book/list?lang=es
    • Grails wird diese "Einstellung" hierbei in einem Cookie des Users speichern, damit es spätere Anfragen besitzen

Message-Tag

Lokalisierungen werden mithilfe des g:message-Tags realisiert:

// .gsp-Datei
<g:message code="my.localized.content" />

// Deine Locale-Datei
my.localized.content=Hola, me llamo John. Hoy es domingo.

Message Argumente

Lokalisierungs-Texte können auch Argumente überbekommen:

// .gsp-Datei
<g:message code="my.localized.content" args="${ ['Juan', 'lunes'] }" />

// Deine Locale-Datei
my.localized.content=Hola, me llamo {0}. Hoy es {1}.

Das gleiche im Quellcode mithilfe vom MessageSource-Objekt.

import org.springframework.context.MessageSource

class MyappController {

    MessageSource messageSource

    def show() {
        def msg = messageSource.getMessage('my.localized.content', ['Juan', 'lunes'] as Object[], 'Default Message', request.locale)
    }

    //...
}

Oder, noch simpler, innerhalb eines Controllers mit der message()-Methode: (Funktioniert nur wenn GSP-Unterstützung vorhanden ist, welches eine REST-App nicht besitzt)

def show() {
    def msg = message(code: "my.localized.content", args: ['Juan', 'lunes'])
}


Sicherheit (Escaping)

Was Grails automatisch macht:

Grails bietet zudem weitere Codecs zum trivialen escapen beim rendern von Daten (Wie HTML, CSS, Javascript, URLs, ...)