Groovy
Groovy-Scripting im CMS.
- API
- Bibliothek – Übersicht
- CMS-spezifische Helfer
- Groovy Syntax und Beispiele
- Groovy – Übersicht
- Logging
- Timings
- Tips
- Verwendungsmöglichkeiten
API
eine gekapselte Klasse mit statischen Methoden, die zusammengehörige Funktionalitäten enthältMyApi (bei class und logName muss noch angepasst werden.
import com.batix.Log
import com.batix.action.GroovyAction
import com.batix.modul.Customer
import com.batix.modul.SystemVariables
import com.batix.table.TableMetadata
import com.batix.tags.GroovyTag
import javax.servlet.ServletContext
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import javax.servlet.http.HttpSession
import java.sql.Connection
class MyApi {
private static GroovyAction.GroovyContext getContext() {
return GroovyAction.context
}
private static GroovyTag getIncludeTag() {
return context.includeTag
}
private static GroovyAction getAction() {
return context.action
}
private static ServletContext getApplication() {
return context.application
}
private static HttpServletRequest getRequest() {
return context.request
}
private static HttpServletResponse getResponse() {
return context.response
}
private static HttpSession getSession() {
return context.session
}
private static Customer getWeb() {
return context.web
}
private static SystemVariables getVariables() {
return context.variables
}
private static Connection getConn() {
return action?.connection ?: includeTag?.checkConnection()
}
/* Logging start */
private static def logName = "MyApi"
//noinspection GroovyUnusedAssignment
private static void logD(String msg, HttpServletRequest request = null) { Log.debug("[${logName}] ${msg}", request) }
//noinspection GroovyUnusedAssignment
private static void logI(String msg, HttpServletRequest request = null) { Log.info("[${logName}] ${msg}", request) }
//noinspection GroovyUnusedAssignment
private static void logN(String msg, HttpServletRequest request = null) { Log.notice("[${logName}] ${msg}", request) }
//noinspection GroovyUnusedAssignment
private static void logW(String msg, HttpServletRequest request = null) { Log.warn("[${logName}] ${msg}", request) }
//noinspection GroovyUnusedAssignment
private static void logE(String msg, Exception ex = null, HttpServletRequest request = null) { Log.error("[${logName}] ${msg}", request, ex) }
/* Logging end */
// Beispiel: öffentliche Konstante
public static final def ID_STATUS_OFFEN = "15FBFE52655"
// Beispiel: öffentliche Methode
static Map info(String vid) { return null }
}
Bibliothek – Übersicht
Hier werden wiederverwendbare Code-Schnipsel und Helferlein gesammelt. Diese müssen, falls nicht anders angegeben, einfach nur rauskopiert werden und im eigenen Code (am besten ganz oben) eingefügt werden.
- API — eine gekapselte Klasse mit statischen Methoden, die zusammengehörige Funktionalitäten enthält
- BatixUtils
- Logging — um einfach Logausgaben zu erzeugen, die den Name des Scripts vorangestellt haben
- Timings — nützlich, um einzelne Code-Teile zu profilen und so herauszufinden, was genau langsam ist
CMS-spezifische Helfer
Bestimmte CMS-Klassen wurden um Utilities zur einfacheren Verwendung erweitert.
Importe
Einige häufig genutzte Klassen werden automatisch importiert, z.B. java.sql.Connection, TableMetadata und BatixRecord.
Objekte
Dem Groovy-Script stehen automatisch folgende Objekte zur Verfügung:
-
includeTag- Referenz zum ausführendenbx:groovyTag, ansonstennull-
es können hier z.B. Tag-Parameter gelesen werden:
Template
<bx:groovy includes="MyReport" format="html" />Groovy-Baustein
String format = includeTag.getStringParameter("format") println("gewähltes Format: $format")
-
-
action- Referenz zum ausführenden Groovy-Action, ansonstennull-
kann z.B. benutzt werden, um Script-Attribute zu setzen oder das Action abzubrechen
action.setScriptAttribute("thing", "value") action.cancel = true
-
-
application- derServletContext -
request- der aktuelleHttpServletRequest-
z.B. um Request-Parameter auszulesen, die der Seite oder dem Action übergeben wurden
String kid = request.getParameter("kid")
-
-
response- die aktuelleHttpServletResponse- falls ein Action verwendet wird:
action.originalResponsebenutzen, um z.B. direkt Bytes zu schreiben
- falls ein Action verwendet wird:
-
session- die aktuelle Session, falls vorhanden -
pageData- enthält Infos zum AufrufString file = pageData.filename // z.B. "detail.htm" -
navElement- der aufgerufene Menüpunkt (NavigationElement) -
web- das Projekt (Customer) -
variables- die Systemeinstellungen (SystemVariables)-
Das sind die pro Projekt überschreibbaren Variablen
String portalId = variables.getVariable("PortalID")
-
Connection
Um einfach an eine Datenbank-Connection zu kommen, egal ob der Groovy-Baustein in einem Action oder Tag (oder sogar den Entwickler-Tools) verwendet wird, kann folgender Block verwendet werden:
Connection.with { conn ->
// hier Code, der conn benutzt
}
Die Connection wird nach dem Block automatisch wieder geschlossen bzw. zurückgegeben.
TableMetadata holen
Eine Instanz von TableMetadata lässt sich einfach zu einem Container erzeugen, in dem Entweder der Titel, der Tabellenname oder die ID des Containers benutzt wird (es wird eine TableMetadata-Instanz zurückgegeben):
// funktioniert alles gleich
def tmdKunden = TableMetadata["Kunden"]
def tmdKunden = TableMetadata["bxc_kunden"]
def tmdKunden = TableMetadata["15525037CCA"]
// auch mit Punkt-Schreibweise verfügbar
def tmdKunden = TableMetadata.Kunden
def tmdKunden = TableMetadata.bxc_kunden
def tmdKunden = TableMetadata."15525037CCA" // als String, da sonst Syntax Error wegen Ziffer am Anfang
BatixRecord holen
Ein bestimmter Datensatz aus einem Container, kann durch seine ID einfach geholt werden (es wird eine BatixRecord-Instanz zurückgegeben):
def recKunde = tmdKunden["15525037CCB"]
Einen neuen Datensatz erzeugt man mit den üblichen Methoden:
def recKunde = tmdKunden.createNewRecord(conn, com.batix.util.UniqueID.createHexId(), true)
Datensatz-Felder lesen / schreiben
Hat man einen BatixRecord kann man auf einfache Art und Weise die Datensatz-Felder lesen und ändern:
// Text
String name = recKunde.Name.string
recKunde.Titel.string = "Dr."
// Zahl
int alter = recKunde.Alter.number // Ganzzahl-Feld
recKunde.Alter.number = 18
double rabatt = recKunde.Rabatt.number // Dezimalzahl-Feld
recKunde.Rabatt.number = 5.83
// Datum, Datum + Uhrzeit, Uhrzeit oder Zeitraum
Date bday = recKunde.Geburtstag.date
recKunde.lastUpdate.date = new Date()
// Häkchen
boolean archived = recKunde.archiviert.state
recKunde.Newsletter.state = false
// Einzelverknüpfung (Bild, Dokument, Datensatz)
String picId = recKunde.Foto.string
String statusId = recKunde.Status.string
recKunde.Rolle.string = "15525037CCC"
// Mehrfachverknüpfung
MultipleLinkField leistungen = recKunde.Leistungen
// Untercontainer
SubListField vertraege = recKunde.Vertraege
// zum Schluss Update nicht vergessen
recKunde.updateRecordInDatabase(conn)
// oder Anlegen, falls es ein neuer Datensatz war
recKunde.createRecordInDatabase(conn, false) // false = Default-Werte der Container-Felder übernehmen
Durch die angesprochenen Probleme mit Typ-Infos, kann es passieren, dass bei Datum/Zeit-Feldern der Setter uneindeutig ist, falls null übergeben wird (da es dort mehrere setDate Methoden gibt).
Falls eine Variable übergeben wird, die auch null sein kann, wird daher beim Setzen von Datum/Zeit Feldern empfohlen, die setDate Methode aufzurufen und einen expliziten Cast hinzuzufügen:
Date birthday = ...
recKunde.Geburtstag.setDate(birthday as Date)
Mehrere Datensätze filtern
Ähnlich dem XML-Filter mit bx:containerfilter können in Groovy Datensätze gefiltert werden. Der Methode werden als erster Parameter eine Liste an Kriterien (bestehend aus einer drei-Elementigen Liste pro Kriterium mit: Feld(ern), Vergleichstyp und Vergleichswert) und als zweiter Parameter eine Map von Optionen übergeben. Das Ergebnis is ein (evtl. leeres) Array aus BatixRecords.
Ab Version 2.6.8 kann bei der Kriterien-Liste jeweils ein vierter Parameter angegeben werden (true oder false), welcher dem required-Attribut in der XML entspricht. Das ist nützlich, falls man direkt Sachen aus dem Request o.ä. als Vergleichswert angibt: ["Name", 1, request.getParameter("name"), true] (in späteren Versionen führt ein leerer Vergleichswert ohne explizite required-Angabe dann zum Abbruch)
BatixRecord[] recs = tmdKunden.findRecords([
["Leistungen", 5, 0], // Feld darf nicht leer sein
["Newsletter", 4, 1], // Feld muss angehakt sein
[["Name", "Vorname"], 1, "muster"] // mindestens eins der beiden Felder muss "muster" enthalten
], [
orderby: "Kundennummer", // optional, Sortierungsfeld (Standard ID)
desc: true, // optional, Sortierrichtung (Standard aufsteigend)
index: 5, // optional, Startindex (Null-basiert)
limit: 20, // optional, maximale Anzahl Ergebnisse
active: true, // optional, aktive Datensätze suchen? (Standard ja)
inactive: true // optional, inaktive Datensätze suchen? (Standard nein)
])
Die Zahlen entsprechen den Typen des normalen Filters. Es müssen nicht alle Parameter angegeben werden, ein kurzes Beispiel ist (in Groovy steht [:] für eine leere Map):
BatixRecord[] recs = tmdKunden.findRecords([
["archived", 4, 1] // alle archivierten Kunden finden
], [:])
Ab Version 2.7.0 können auch mehrere Sortierungs-Kriterien angegeben werden. Dabei sind folgende Formate für orderby möglich (desc wird dann ignoriert):
BatixRecord[] recs = tmdKunden.findRecords([], [
orderby: [
"Firma", // aufsteigend
["Name"], // aufsteigend
["Vorname", true] // absteigend
]
])
Einen Datensatz filtern
Ab Version 2.7.0 ist es auch möglich nur den ersten Datensatz zu laden, anstatt alle:
BatixRecord rec = tmdKunden.findRecord([
// siehe oben
], [
// siehe oben
])
Rückgabewert ist entweder ein BatixRecord oder null.
Batix-Quelltext ausführen
Will man schnell einen String evaluieren, der Batix-Tags enthält, kann diese Methode benutzt werden:
String code = '<bx:math>1 + 1</bx:math>'
String result = code.evaluateAsBatixSource()
Der Standard Mime-Typ ist text/html, um einen anderen zu benutzen (manche Tags machen unterschiedliche Sachen, je nach Mime-Typ der Seite) kann dieser als Parameter mitgegeben werden: evaluateAsBatixSource("text/plain").
Groovy Syntax und Beispiele
Nebst den hier am häufigsten genutzten Elementen gibt es noch viele weitere Funktionen und Helfer, siehe dazu auch die offizielle Doku (auf die Version achten).
Syntax
Klammern um Parameter sowie das Semikolon am Ende der Zeile können weggelassen werden. Empfehlung: Klammern verwenden, Semikolon weglassen.
println "Hello, World"
println("Hello, World") // Empfehlung
println("Hello, World");
Typen müssen nicht explizit angegeben werden (def entspricht quasi Object in Java bzw. var in JavaScript).
Empfehlung: Typen explizit angeben, falls die IDE sie nicht automatisch erkennt und demtentsprechend keine sinnvollen Vorschläge für Syntax-Completion macht.
def x = 11.8 // erst Zahl
println(x)
x = "Test" // dann String
println(x.toUpperCase())
Typen werden, soweit möglich automatisch umgewandelt, es müssen keine expliziten Casts verwendet werden (hier kann aber das Schlüsselwort as verwendet werden).
Es erfolgt auch ein automatisches Boxing / Unboxing (int <--> Integer etc.).
def i = "5" as int
eineListe.add(12)
Der Gleichheits-Operator in Groovy (==) wird im Hintergrund zu einem .equals() Aufruf umgewandelt.
String a = "test"
String b = "te" + "st"
println(a == b) // true, in Java: a.equals(b)
Strings können von Anführungszeichen ("), Apostrophen (') oder Slashes (/) umschlossen sein.
Empfehlung: Das verwenden, was im String nicht vorkommt, um nicht escapen zu müssen (Slashes, falls Anführungszeichen und Apostrophe vorkommen bzw. für Reguläre Ausdrücke).
"str" == 'str' == /str/
Es gibt auch Mehrzeilige Strings. Ein Backslash (\) bewirkt, dass der Zeilenumbruch vor der ersten Zeile im Code nicht mit ausgegeben wird.
println("""
Zeile 1
Zeile 2
"""
println("""\
einzige Zeile"""
In Strings mit Anführungszeichen (einfach oder dreifach für mehrzeilige Strings) können Code-Platzhalter verwendet werden, die durch ihren Wert ersetzt werden.
def user = "alice"
println("Hi $user")
println("aktueller Timestamp: ${new Date().time}")
Closures
Closures entsprechen in etwa den functions in JavaScript oder Lambda-Funktionen in Java 8. Es muss dabei nicht explizit return angegeben werden, der Rückgabewert des letzten Statements wird als Rückgabewert der Closure benutzt. Parameter müssen nicht typisiert werden.
// keine Parameter
def c1 = {
"Hello"
}
c1()
def c2 = { name ->
"Hello, $name!"
}
c2("World")
Methoden, die eine Closure als einzigen oder letzten Parameter akzeptieren, kann die Closure direkt übergeben werden (keine runden Klammern nötig).
"...".eachLine { line ->
println(line.reverse())
}
Falls die Closure genau ein Parameter übergeben bekommt, so muss dieser nicht benannt werden, der Standardname ist "it".
"...".eachLine {
println(it.reverse())
}
Spaceship-Operator
Vergleiche (compareTo) erledigt der Spaceship-Operator, das vereinfacht die Implementation eines Comparators.
println 5 <=> 10 // Java: 5.compareTo(10)
println "def" <=> "abc" // Java: "def".compareTo("abc")
println null <=> 10 // gibt keine Exception
Beispiel: System Properties filtern
System.properties
.findAll { it.key.startsWith("user") }
.sort { x, y -> x.key <=> y.key }
.each { println it }
Get / Set
Es werden automatisch JavaBeans-Accessors erzeugt bzw. angesprochen, d.h. man muss nicht getProp() oder setProp() aufrufen, sondern kann einfach prop schreiben.
// Lesen
println(obj.thing) // entspricht obj.getThing()
println("...".empty) // entspricht "...".isEmpty()
// Schreiben
obj.thing = null // entspricht obj.setThing(null)
Arrays / Lists / Maps / Ranges / Collections
Für Listen und Maps gibt es eine native Syntax.
Liste
def a = [1, 2, 3] // eine ArrayList im Hintergrund
println(a[1]) // einfach auf Elemente zugreifen
println([].size()) // eine leere Liste
Map
def m = [language: "Groovy", version: 2.1] // eine LinkedHashMap
println m["language"] // einfach auf Elemente zugreifen
println m.version // einfach auf Elemente zugreifen
println([:].size()) // eine leere Map
Mit Ranges kann man einfach Zahlenbereiche generieren.
(1..10).each { println(it) } // 1 bis 10
(1..<10).each { println(it) } // 1 bis 9
(5..-5).each { println(it) } // absteigend
Außerdem können mittels Ranges teile von Strings und Collections extrahiert werden.
def a = [1, 2, 3, "a", "b", "c"]
println a[-2] // b
println a[2..4] // [3, a, b]
println a[2..1] // [3, 2]
println a[-2..2] // [b, a, 3]
def s = "the quick brown fox"
println s[4..9, -3..-1] // quick fox
Um zu überprüfen, ob eine Collection ein Element enthält, kann der in Operator verwendet werden.
println(5 in [1, 5, 10]) // true
Diesen gibt es auch in der verkürzten Variante der for-Schleife.
def a = [1, 5, 10]
for (i in a) {
println(i)
}
Groovy bringt viele Hilfsmethoden für Collections mit.
[5, 4, 8, 1, 4, 5, 2].sort().unique().reverse() // Sortieren, Doppler entfernen, Reihenfolge invertieren
(1..10).take(3).drop(1) // nur die ersten 3 Elemente nehmen und davon dann das erste wegschmeißen
[4, 7, 1].min() // Minimum finden
[cents: 5, dime: 2, quarter: 3].max { it.value } // Maximum finden und dabei festlegen, was verglichen werden soll pro Element
(1..100).sum() // Summe bilden
["abc", "def", "abcdef"].findAll { it.startsWith("a") } // alle Elemente finden, die einer Bedingung entsprechen
["abc", "abcdef"].collect { it.length() } == [3, 6] // Elemente transformieren
['a', 'b'] << [1, 2] // [a, b, [1, 2]] Element anhängen
['a', 'b'] + [1, 2] // [a, b, 1, 2] Listen zusammenführen
[1, 2, 3].join("~") // 1~2~3 Elemente mittels Trenner zusammenführen
def a = [1, 2, 3, 4, 5]
println(a.any { it > 3 }) // true, erfüllt mindestens ein Element die Bedingung?
println(a.every { it < 5 }) // false, erfüllen alle Elemente die Bedingung?
Wenn man nicht immer prüfen will, ob ein Key in einer Map ist, bevor man ihn benutzt, kann man ein Default-Wert festlegen. Dieser wird dann unter dem Key automatisch in der Map angelegt, sobald das erste Mal auf ihn zugegriffen wird.
def map = [:].withDefault {
new Date()
}
println map.test // Zeit 1
sleep(2000);
println map.test // immer noch Zeit 1
println map.test2 // Zeit 2
RegEx
Reguläre Ausdrücke haben auch eine verkürzte Syntax. Slashes werden hier nur der Lesbarkeit / Identifizierbarkeit verwendet, es sind immer noch normale Strings (nicht wie in JavaScript, wo mit Slashes direkt reguläre Ausdrücke erzeugt werden).
// Pattern erzeugen
Pattern p = ~/\d+/
println(p.matcher("123").matches())
// Matcher erzeugen
Matcher m = "123" =~ /\d(\d)\d/
println(m.matches())
println(m.group(1))
// Direkt matchen
println("123" ==~ /\d+/) // true
Null-Dereferenzierung
Um NullPointerExceptions vorzubeugen, ohne dauernd mit if auf null prüfen zu müssen, empfiehlt sich der ?. Operator. Dieser stoppt, sobald der Ausdruck vor dem Fragezeichen null ist und gibt null zurück.
class Person {
def name
}
def p
println(p?.name?.length()) // null
p = new Person()
println(p?.name?.length()) // null
p.name = "Tester"
println(p?.name?.length()) // 6
Elvis-Operator
Default Werte können mit dem Elvis-Operator (?:) vereinfacht werden. Dies entspricht dem Ternary-If, wobei die Bedingung und der True-Teil identisch sind.
null ?: 5 // 5 --> null ? null : 5
false ?: "test" // test --> false ? false : "test"
4 ?: 8 // 4 --> 4 ? 4 : 8
Switch
Das switch Statement wurde auch erweitert.
def testSwitch(val) {
switch (val) {
case ~/^Switch.*Groovy$/: return 'Pattern match'
case BigInteger: return 'Class isInstance'
case 60..90: return 'Range contains'
case [21, 'test', 9.12]: return 'List contains'
case 42.056: return 'Object equals'
case { it instanceof Integer && it < 50 }:
return 'Closure boolean'
default: return 'Default'
}
}
println testSwitch("Switch to Groovy")
println testSwitch(42G)
println testSwitch(70)
println testSwitch('test')
println testSwitch(42.056)
println testSwitch(20)
println testSwitch('default')
JSON
Mit JSON kann sehr einfach gearbeitet werden.
JSON lesen
def slurper = new groovy.json.JsonSlurper()
def json = slurper.parseText("""
{
"name": "Mustermann",
"ids": [5, 10, 11]
}
""")
println(json.name) // Mustermann
println(json.ids[1]) // 10
In neueren Groovy-Versionen liefert der JsonSlurper eine LazyMap zurück, die nicht serialisiert werden kann (falls etwas in der Session gespeichert werden soll). Die Variante mit HashMap ist noch als JsonSlurperClassic verfübar.
Ein JSON-String ist auch schnell aus normalen Objekten (Strings, Lists, Maps, ...) erzeugt:
JSON schreiben
def builder = new groovy.json.JsonBuilder()
def obj = [
name: "Mustermann",
ids: [5, 10, 11]
]
builder(obj)
println(builder.toPrettyString()) // JSON von oben
Als Abkürzung kann die Hilfsklasse JsonOutput verwendet werden.
JsonOutput
import groovy.json.JsonOutput
def obj = [
name: "Mustermann",
ids: [5, 10, 11]
]
def jsonStr = JsonOutput.toJson(obj)
println(jsonStr) // alles auf einer Zeile, spart Bytes
println(JsonOutput.prettyPrint(jsonStr)) // lesbar
XML
Für XML-Input gibt es 2 Verarbeitungsmöglichkeiten: XMLParser und XMLSlurper (siehe dazu auch die Groovy Doku).
XmlParser parst das komplette XML und baut eine Struktur im RAM auf, was es erlaubt Teile der XML oft und dabei schnell abzufragen (falls dem XML-Dokument Nodes aktualisiert oder hinzugefügt werden sollen, wird auch XmlParser empfohlen):
XML lesen (mit XmlParser)
def parser = new XmlParser()
def root = parser.parseText("""
<person>
<access read='true' write='false' />
<data>
<name>Mustermann</name>
</data>
<data>
<name>Musterfrau</name>
</data>
</person>
""")
println(root.access.@write[0]) // false
println(root.data[1].name.text()) // Musterfrau
XmlSlurper hingegen wertet das XML nur partiell und on-demand aus - somit wird RAM gespart, häufige Zugriffe hingegen dauern ggf. länger (dafür können Abfragen aber etwas kürzer geschrieben werden):
XML lesen (mit XmlSlurper)
def slurper = new XmlSlurper()
def root = slurper.parseText("""
<person>
<access read='true' write='false' />
<data>
<name>Mustermann</name>
</data>
<data>
<name>Musterfrau</name>
</data>
</person>
""")
println(root.access.@write) // false
println(root.data[1].name) // Musterfrau
Um XML-Dokumente programmatisch zusammenzubauen, bietet such MarkupBuilder an:
XML schreiben
def writer = new StringWriter()
def builder = new groovy.xml.MarkupBuilder(writer)
builder.person {
access(read: true, write: false)
data {
name("Mustermann")
}
data {
name("Musterfrau")
}
}
println(writer.toString()) // XML von oben
Um schnell einzelne Teile einer XML zu ändern und wieder in einen String zu überführen, kann der XmlParser und die Hilfsklasse XmlUtil verwendet werden. Hier ein Beispiel, um Werte zum Validation-Feld hinzuzufügen:
XML parsen, bearbeiten, zurückschreiben
import groovy.xml.XmlUtil
// XML parsen
def rootNode = new XmlParser().parseText("""
<result size="1">
<field name="Kontakt_Email" resultcode="1" bin="00000001" hex="0x01" />
</result>
""")
// Attribut ändern
rootNode.@size++
// Node hinzufügen (Parameter sind Name der Node sowie Attribute als Map)
rootNode.appendNode("field", [
"name": "Kontakt_Name",
"resultcode": "1",
"bin": "00000001",
"hex":"0x01"
])
// wieder XML-String erzeugen
println(XmlUtil.serialize(rootNode))
/* Ergebnis:
<?xml version="1.0" encoding="UTF-8"?><result size="2">
<field name="Kontakt_Email" resultcode="1" bin="00000001" hex="0x01"/>
<field name="Kontakt_Name" resultcode="1" bin="00000001" hex="0x01"/>
</result>
*/
SQL
Auch mit SQL kann einfacher gearbeitet werden.
java.sql.Connection conn = ...
def sql = new groovy.sql.Sql(conn)
def foo = "fruit"
sql.eachRow("SELECT * FROM food WHERE type=${foo}") { // Parameter werden automatisch SQL-Encoded
println("I like ${it.name}") // gibt Spalte "name" aus
}
Um ein automatisches SQL-Encoden zu gewährleisten, darf der Query-String nicht aus mehreren Strings zusammengebaut werden, sondern darf nur ein String mit Platzhaltern sein.
Überladene Methoden und Typen
Da Groovy Methoden dynamisch aufruft, zur Laufzeit aber nicht mehr die genauen Typen aus dem Quellcode kennt (wegen Type Erasure etc.), kann es vorkommen, dass mehrere Methoden gefunden werden, die zum Aufruf passen.
Das ist meistens der Fall, wenn null übergeben wird, hier weiß Groovy nur, dass es ein NullObject ist, kennt aber nicht den genauen Typ:
String str = null
println(str.getClass().simpleName) // NullObject
Beispielsweise gibt es diese zwei Methoden:
void setDate(String str) { ... }
void setDate(Date date) { ... }
Erfolgt nun in Groovy der Aufruf folgendermaßen:
// null als Literal
obj.setDate(null)
obj.date = null
// oder als Variable
String str = null
obj.date = str
Dann weiß Groovy nicht, welche Methode es genau aufrufen soll, das sowohl String als auch Date zu NullObject passen (null bzw. NullObject kann in beides umgewandelt werden).
Frühere Groovy-Versionen haben einfach irgendeine Methode genommen, neue Versionen werfen eine Exception. Da dies nicht rückwärts-kompatibel ist, bringen CMS-Versionen, welche neuere Groovy-Versionen enthalten, stattdessen einen Fehler im Log, funktionieren aber weiterhin mit altem Code (es wird irgendeine Methode genommen).
Falls so ein Szenario auftritt, kann man Groovy aber helfen, in dem man beim Aufruf den Typ explizit als Cast hinzufügt (es muss hier die Methode aufgerufen werden, beim Property-Setter geht die Typ-Info wieder verloren):
obj.setDate(str as String) // OK
obj.setDate((String)str) // OK
// obj.date = str as String // geht nicht
// obj.date = (String)str // geht nicht
Groovy – Übersicht
Seit Version 2.6.2 ist es möglich die Scriptsprache Groovy im CMS zu verwenden (sowohl in Actions als auch in Templates). Zu beachten ist, dass je CMS-Version ggf. eine andere Groovy-Version eingebunden ist - dazu die .jar Datei im lib-Verzeichnis prüfen oder in Entwickler-Tools ausführen: println(GroovySystem.version).
Groovy ist eine dynamische Sprache mit vereinfachter Syntax und Funktionalitäten (ähnlich JavaScript) und vielen schon eingebauten Utilities. Außerdem wurden bestimmte Sachen des CMS erweitert, damit man sie einfach in Groovy benutzen kann (z.B. Datensätze bearbeiten).
- Verwendungsmöglichkeiten
- Groovy Syntax und Beispiele
- CMS-spezifische Helfer
- Tips
- Bibliothek – Übersicht
Logging
Es können logI("text") für INFO-Meldungen etc. verwendet werden. logE kann als zweiter Parameter zusätzlich noch eine Exception übergeben werden.
Zur besseren Übersicht im Log wird immer der Scriptname vorangestellt.
logName noch anpassen
es muss noch com.batix.Log importiert werden
Zum Kopieren (für Script)
/* Logging start */
def logName = "myScript"
//noinspection GroovyUnusedAssignment
def logD = { String msg, HttpServletRequest request = null -> Log.debug("[${logName}] ${msg}", request) }
//noinspection GroovyUnusedAssignment
def logI = { String msg, HttpServletRequest request = null -> Log.info("[${logName}] ${msg}", request) }
//noinspection GroovyUnusedAssignment
def logN = { String msg, HttpServletRequest request = null -> Log.notice("[${logName}] ${msg}", request) }
//noinspection GroovyUnusedAssignment
def logW = { String msg, HttpServletRequest request = null -> Log.warn("[${logName}] ${msg}", request) }
//noinspection GroovyUnusedAssignment
def logE = { String msg, Exception ex = null, HttpServletRequest request = null -> Log.error("[${logName}] ${msg}", request, ex) }
/* Logging end */
Zum Kopieren (für Klassen)
/* Logging start */
private static def logName = "myScript"
//noinspection GroovyUnusedAssignment
private static void logD(String msg, HttpServletRequest request = null) { Log.debug("[${logName}] ${msg}", request) }
//noinspection GroovyUnusedAssignment
private static void logI(String msg, HttpServletRequest request = null) { Log.info("[${logName}] ${msg}", request) }
//noinspection GroovyUnusedAssignment
private static void logN(String msg, HttpServletRequest request = null) { Log.notice("[${logName}] ${msg}", request) }
//noinspection GroovyUnusedAssignment
private static void logW(String msg, HttpServletRequest request = null) { Log.warn("[${logName}] ${msg}", request) }
//noinspection GroovyUnusedAssignment
private static void logE(String msg, Exception ex = null, HttpServletRequest request = null) { Log.error("[${logName}] ${msg}", request, ex) }
/* Logging end */
Beispiel
def user = BxUser.findInstance(request)
if (!user) {
logE("no user found")
return
}
logI("found user ${user.id}")
Timings
Zwischendrin immer addTiming("kleine Info was gemacht wurde") aufrufen.
Es können auch einzelne Durchläufe einer Schleife gemessen werden: addTiming("Info zur Schleife", "Info zum Durchlauf").
So werden die aufeinanderfolgende Timings, bei denen der erste Parameter identisch ist, gruppiert.
Am Ende gibt getTimings() eine Zusammenfassung als String zurück.
Für Schleifen wird die Gesamtdauer sowie die Anzahl der Durchläufe und die mittlere Dauer ausgegeben. Außerdem werden die Top und Flop 3 der Durchläufe (nach Dauer) angezeigt.
Zum Kopieren
/* Timings start */
def timings = []
def timingLast = new Date().time
def addTiming = { String title, String subTitle = null ->
def duration = new Date().time - timingLast
if (subTitle) {
def value = [
duration: duration,
title: subTitle
]
if (!timings || timings[-1].title != title) {
timings << [
title: title,
values: []
]
}
timings[-1].values << value
value.index = timings[-1].values.size()
} else {
timings << [
duration: duration,
title: title
]
}
timingLast = new Date().time
}
Closure<String> getTimings = {
def res = new StringBuilder()
res << "Timings:\n"
def timingTotal = 0
def emptyPrefix = " " * 27
def maxDetails = 6
timings.each { t ->
if (t.values) {
List vals = t.values.sort { a, b ->
a.duration <=> b.duration ?:
a.index <=> b.index
}
long sum = vals.sum { it.duration } as long
double avg = sum / t.values.size()
timingTotal += sum
res << String.format("%8d ms (+%8d ms) %s (%dx ~%.2f ms)\n", timingTotal, sum, t.title, t.values.size(), avg)
if (vals.size() > maxDetails) vals = [*vals[0..(maxDetails / 2 - 1)], null, *vals[-(maxDetails / 2)..-1]]
vals.each { val ->
res << emptyPrefix
if (val) res << String.format("#%-4d %4dms %s\n", val.index, val.duration, val.title)
else res << "[...]\n"
}
} else {
timingTotal += t.duration
res << String.format("%8d ms (+%8d ms) %s\n", timingTotal, t.duration, t.title)
}
}
def lastDuration = new Date().time - timingLast
timingTotal += lastDuration
res << String.format("%8d ms total\n", timingTotal)
return res.toString()
}
/* Timings end */
Beispiel
// do stuff
addTiming("startup")
// do more stuff
addTiming("init")
// do loop stuff 1
addTiming("fetch info", "load one...")
// do loop stuff 2
addTiming("fetch info", "load two...")
// ...
// do database stuff
addTiming("finalizing")
// do final stuff
Erzeugt dann z.B. mit logI(getTimings()) sowas:
Timings:
1 ms (+ 1 ms) startup
94 ms (+ 93 ms) init
753 ms (+ 659 ms) fetch info (7x ~94,14 ms)
#4 15ms load 15F53263D95
#5 16ms load 15F53263DA5
#3 17ms load 15F53263D86
[...]
#2 20ms load 15F53263D75
#6 78ms load 15F53263DF3
#1 496ms load 15F53263D61
2598 ms (+ 1845 ms) finalizing
2605 ms total
Tips
Hier gibt es lose gesammelte Tricks und Kniffe.
Bessere Exceptions im Log
Um Stacktraces von unnötigem Groovy-Meta-Ballast zu befreien und so direkt lesbar zu machen, gibt es StackTraceUtils.deepSanitize. Statt die Exception direkt an Log o.ä. zu übergeben, einfach diese Methode drumherum packen und schon verschwinden unnötige Zeilen aus dem Stacktrace.
import com.batix.Log
import org.codehaus.groovy.runtime.StackTraceUtils
// ...
try {
// ...
} catch (Exception ex) {
Log.error("my error message", StackTraceUtils.deepSanitize(ex))
}
Session
Manchmal gibt es Probleme in Groovy, wenn es keine Session gibt, weil standardmäßig kein Sessioncookie mehr erzeugt wird. Dadurch ist die Groovy-Variable session nun manchmal leer und muß im GroovyAction mit if (!session) session = request.session gefüllt werden und in <bx:groovy> mit session = includeTag.forceSession() Das müßte auch noch irgendwo in die Hilfe eingetragen werden.
Wenn man keine Session im JSP braucht sollte man oben <%@page session="false"%> hinzufügen. Sonst wird auf der Seite, auf der das JSP eingebunden ist, ein Session-Cookie erzeugt.
Verwendungsmöglichkeiten
Groovy-Code kann in Actions (Actionbaustein "Groovy ausführen") oder in Quelltexten (bx:groovy) eingesetzt werden. Es gibt auch die Möglichkeit in den Entwickler-Tools schnell Groovy-Code auszuführen.
Um Ausgaben zu erzeugen, kann einfach println verwendet werden:
- Tag: der Text wird in die Seite geschrieben
- Action: der Text wird ausgegeben, falls
action.sendJSPOutput()aufgerufen wurde, ansonsten kann auchaction.originalResponseverwendet werden - Entwickler-Tools: der Text wird im Output auf der Seite angezeigt
Außerdem ist es jeweils möglich zentrale Groovy-Bausteine aus dem aktuellen Projekt einzubinden (unter Dokumentvorlagen > Groovy), um Code nachzunutzen - dies ist das gleiche Prinzip wie bei Textbausteinen.