Groovy

Eine Übersicht über die Skriptsprache Groovy.

Einführung

Groovy ist eine Skriptsprache, die es recht gut schafft, die Vorteile von Java und von Skriptsprachen zu vereinen.

Groovy wird nicht interpretiert, sondern bei der Ausführung in eine Java-Klasse übersetzt. D.h. jede Groovy-Klasse ist eigentlich eine ganz normale Java-Klassen. Dadurch hat Groovy eine sehr gute Performance - fast so gut wie nativer Java-Code. "Fast" deshalb, weil ein kleiner Overhead dazukommt: Zum einen die Übersetzung des Skripts und zum andern die zusätzliche Softwareschicht, die für Groovy-Features nötig ist, welche kein direktes Java-Äquivalent haben.

Durch diese enge Verwandtschaft mit Java, kann man in Groovy alle Vorzüge von Java ausnutzen: Die Plattformunabhängigkeit, die mächtige Standardbibliothek und natürlich die riesige Auswahl von Third-Party-Bibliotheken.

Links

Sprachbesonderheiten

Groovy bietet die typischen Sprachfeatures einer Skriptsprache. Das fängt damit an, dass man das Semikolon, das ein Statement abschließt, weglassen kann, wenn man will.

Auch die in Java recht strenge Typisierung kann in Groovy aufgeweicht werden. D.h. man muss Variablen keinen Typ geben und man kann per "Duck-typing" auf Bean-Properties oder Methoden zugreifen, ohne dass vorher ein Cast nötig ist.

int typedVar1 = 15;
File typedVar2 = new File("myfile.txt")
def untypedVar = new File("myfile.txt")

// Methodenzugriff auf ein Objekt in einer untypisierten Variable per Duck-typing
println untypedVar.exists()

Der Zugriff auf Bean-Properties ist vereinfacht: Man muss nicht explizit den Getter bzw. Setter einer Bean aufrufen, sondern kann statt dessen mit der kompakteren Property-Notation darauf zugreifen:

// Getter / Setter style
File file = new File("myfile.txt")
println file.getAbsolutePath()
file.setLastModified(new Date().getTime());

// Property style
File file = new File("myfile.txt")
println file.absolutePath
file.lastModified = new Date().time;

Bei Verwendung der Property-Notation prüft Groovy zunächst, ob es ein public-Field mit diesem Namen gibt. Wenn nein, dann wird der passende Getter bzw. Setter aufgerufen.

Das Beispiel zeigt auch noch zwei weitere Erleichterungen, die - typisch Groovy - eigentlich nur eine Kleinigkeit sind, einem aber so oft über den Weg laufen, dass sie eine echte Erleichterung sind.

Zum einen wird nicht wie bei Java per Default nur das Package java.lang eingebunden, sondern die Packages java.lang, java.util, java.io, java.net, groovy.lang und groovy.util sowie die Klassen java.math.BigInteger und java.math.BigDecimal.

Zum anderen gibt es für den Ausdruck System.out.println die Abkürzung println. Damit kann man sehr schnell mal eine kleine Debug-Ausgabe machen, ohne dass man immer so einen riesigen Schlonz hinschreiben muss.

Literal-Schreibweise für Arrays und Maps

Für Arrays und Maps gibt es eine Literalschreibweise. Damit lassen sich diese wesentlich kompakter angeben. Arrays werden dabei als List angelegt, d.h. ihre Größe kann dynamisch verändert werden.

FIXME Welchen Typ hat so ein Array genau? FIXME Umwandeln in echtes Array: [1, 2, 5, 7] as int[]

// Literalschreibweise von Maps
def map = [:]
map.key = "value";
println map["key"]
println map.key

def filledMap = [ a:15, b:"some string" ]

// Literalschreibweise von Arrays
def array = []
array.add(2);
array.add(5);
println array[0]
println array[1]

def filledArray = [ 2, 5, 15, "some string" ]

Groovy-Beans

In Java sind Beans einfach nur eine Konvention. In Groovy wird diese Konvention auch im Sprachumfang berücksichtigt. Zum einen durch die Property-Notation beim Zugriff auf Bean-Properties (siehe oben). Zum andern jedoch auch bei der Definition von Bean-Properties.

So muss man in Groovy nicht umständlich für jede Property ein privates Feld anlegen und den Zugriff darauf mit Gettern und Settern ermöglichen. Statt dessen gibt man die Properties einfach an - in einem Einzeiler. Groovy generiert dann automatisch die entsprechenden Getter und Setter.

Beispiel (Groovy-Code):

class Customer {
  // properties
  Integer id
  String name
  Date dob

  // sample usage code
  static void main(args) {
    def customer = new Customer(id:1, name:"Gromit", dob:new Date())
    println("Hello ${customer.name}")
  }
}

(Die Original-Version dieses Codes kommt aus der Groovy-Dokumentation)

Äquivalenter Java-Code:

import java.util.Date;

public class Customer {
  // properties
  private Integer id;
  private String name;
  private Date dob;

  public Integer getId() {
    return this.id;
  }

  public String getName() {
    return this.name;
  }

  public Date getDob() {
    return this.dob;
  }

  public void setId(Integer id) {
    this.id = id;
  }

  public void setName(String name) {
    this.name = name;
  }

  public void setDob(Date dob) {
    this.dob = dob;
  }

  // sample usage code
  public static void main(String[] args) {
    Customer customer = new Customer();
    customer.setId(1);
    customer.setName("Gromit");
    customer.setDob(new Date());

    System.out.println("Hello " + customer.getName());
  }
}

(Die Original-Version dieses Codes kommt aus der Groovy-Dokumentation)

Wenn man will, kann man trotzdem einen Getter oder Setter anlegen - entweder um darin noch weiteren Code auszuführen, oder aber um die Sichtbarkeit der Property zu steuern.

Beispiele für verschiedene Sichtbarkeiten von Gettern und Settern:

class Foo {
  // read only property
  final String name = "John"

  // read only property with public getter and protected setter
  Integer amount
  protected void setAmount(Integer amount) { this.amount = amount }

  // dynamically typed public property
  def cheese
}

Closures

FIXME Closures

def closure = { ... }
def closureWithParams = { a, b -> ... }

// Explizite closure
def handler = { key, value -> println key + " = " + value }
[ a:"b", "Hallo":"Otto" ].each(handler)

// Inline closure
[ a:"b", "Hallo":"Otto" ].each { key, value -> println key + " = " + value }

// Mit Parameter-Namen
def sum = 0;
[ 1, 5, 7 ].each { value -> sum += value }
println "Sum: " + sum

// Nutzung des Default-Parameter-Namens "it"
def sum = 0;
[ 1, 5, 7 ].each { sum += it }
println "Sum: " + sum

Kontext-Beispiel:

def text = ""
[ "a", "b", "c" ].each { value -> text += value + ", " }
println text
deviceProc.controllerUpdate = { ce -> println "I was just called with event $ce" }
Processor deviceProc = ...
deviceProc.addControllerListener(new ControllerListener() {
   public void controllerUpdate(ControllerEvent ce) {
      ...
   }
}

Anonyme Klassen

FIXME Anonyme Klassen über Maps definieren

Strings

Details zu Strings siehe: Groovy Strings (mit vielen API-Beispielen)

In Groovy gibt es vier verschiedene Arten, einen String anzugeben:

Beispiel - die verschiedenen String-Typen:

def str1 = "string"
def str2 = 'string'
def str3 = /string/
def str4 = """multiline
string"""

Strings können mit == verglichen werden. Es muss nicht zwingend equals oder equalsIgnoreCase verwendet werden:

assert "Karl" == /Karl/;

assert "Hallo\nWelt" == """Hallo
Welt""";

Substrings:

assert 'abcdefg'[ 3..5 ] == 'def'
assert 'abcdefg'[ 1, 3..5 ] == 'bdef'
assert 'abcdefg'[-5..-2] == 'cdef'

Variablen in Strings

In Strings mit einfachen oder doppelten Anführungszeichen können Variablen direkt in den Strings verwendet werden. Dazu schreibt man ein $ und anschließend den Variablennamen.

Man kann auch einen komplexeren Ausdruck angeben, muss diesen dann aber in geschweite Klammern setzen.

Beispiel - Variablen in Strings:

def bla = 5;
def blubb = "Karl"
def file = new File("myfile.txt")

println "$blubb has $bla new entries in ${file.absolutePath}"

Erweiterte API

Strings haben in Groovy eine erweiterte API gegenüber dem normalen Java-String.

FIXME Beispiele für nützliche String-Funktionen: String.split, Array.join

Weitere Details siehe API der String-Klasse

reguläre Ausdrücke

Außerdem bietet Groovy native Unterstützung für reguläre Ausdrücke:

def str = "KSC gegen Bayern München: 10:2"
def regex = "(.*) gegen (.*): (\\d+):(\\d+)"
def matcher = (str =~ regex)

def data = [
  club1: matcher[0][1],
  club2: matcher[0][2],
  score1: matcher[0][3],
  score2: matcher[0][4]
];

Hinweis: matcher ist ein java.util.regex.Matcher.

So kann man auch einfache Regex-Tests machen (Wenn der Regex-Tests fehl schlägt, ist der Ausdruck null, was als false interpretiert wird):

if (file.name =~ /\.txt$/) {
  ...
}

Verarbeitung von Datenstrukturen

FIXME Collection-Klassen können einfach iteriert werden.

FIXME Chaining von sort, einfache Comparator

Groovy als Shell-Skript verwenden

Um Groovy direkt als Shell-Skript zu verwenden, muss man nur einen Execution-Header hinzufügen. So kann man ein Groovy-Skript z.B. auch als Cron-Skript ausführen lassen, indem man es einfach beispielsweise im Verzeichnis /etc/cron.daily/ ablegt - ein Editieren der crontab bleibt einem dabei erspart.

Beispiel /etc/cron.daily/myscript.groovy:

#!/usr/bin/groovy

println "Hello!"

Integration von ant in Groovy

Für viele Jobs gibt es sehr praktische Ant-Tasks, mit deren Hilfe man in einem Ant-Skript in wenigen Zeilen z.B. ein Verzeichnis kopieren kann. All diese Ant-Tasks kann man auch direkt in einem Groovy-Skript verwenden.

Beispiel:

def ant = new AntBuilder()

ant.copy(todir: targetDir, preservelastmodified:true) {
  fileset(dir: basedir) {
    include(name: "source/**")
  }
}

Siehe auch:

Integration von Groovy in ant

Ant-Hello-World

Hier ein kleines Hello-World-Beispiel für die Integration von Groovy in ant.

Dateistruktur:

+- build.xml
   +- groovy
   |  +- Main.groovy
   +- lib
      +- antlr-2.7.7.jar     (from groovy distribution)
      +- asm-3.2.jar         (from groovy distribution)
      +- groovy-1.7.0.jar    (from groovy distribution)

Haupt-Groovy-Klasse groovy/Main.groovy:

class Main {
  static void hello(String who) {
    println "Hello $who"
  }
}

Ant-Build-Datei (build.xml) - Hello World:

<?xml version="1.0" encoding="UTF-8"?>
<project basedir=".">

  <path id="groovy.classpath">
    <fileset dir="lib">
      <include name="*.jar" />
    </fileset>
  </path>

  <taskdef name="groovy"
           classname="org.codehaus.groovy.ant.Groovy"
           classpathref="groovy.classpath"/>

  <target name="my-target" description="TODO">
    <groovy>
      <classpath>
        <pathelement location="groovy"/>
      </classpath>

      Main.hello("world");
    </groovy>
  </target>

</project>

Im groovy-Tag kann man beliebigen Groovy-Code unterbringen. Allerdings stimmen die Zeilennummern bei Stack-Traces nicht mit den Zeilennummern im Ant-Skript überein. Daher bietet es sich an, den eigentlichen Code in eine extra .groovy-Datei auszulagern und sich im Ant-Skript auf ein Bißchen Glue-Code zu beschränken.

Interaktion Groovy <-> Ant

Ant-Build-Datei (build.xml) - komplexere Interaktion zwischen Groovy und Ant:

<?xml version="1.0" encoding="UTF-8"?>
<project basedir=".">

  <property name="my-ant-property" location="some/path"/>

  <path id="groovy.classpath">
    <fileset dir="lib">
      <include name="*.jar" />
    </fileset>
  </path>

  <taskdef name="groovy"
           classname="org.codehaus.groovy.ant.Groovy"
           classpathref="groovy.classpath"/>

  <target name="my-target" description="TODO">
    <fileset id="txtfiles" dir="." includes="**/*.txt"/>

    <groovy>
      <classpath>
        <pathelement location="groovy"/>
      </classpath>

      // Reading ant properties
      def antProp = properties['my-ant-property']

      // Reading ant filesets
      def txtFileArr = project.references.txtfiles
      txtFileArr.each{ fileRes ->
        File file = fileRes.file
        println "Processing ${file.absolutePath} ..."
      }

      // Failing the ant task
      throw new Exception("Task failed")
    </groovy>
  </target>

</project>

Weblinks:

Externe Prozesse aufrufen

Siehe auch:

// Kommando einfach als String definieren
def cmd = [ "java", "-jar", "build/lib/css-validator/css-validator.jar", "file://my.css" ]

// Kommando ausführen und warten, bis es beendet ist
def proc = cmd.execute()
proc.waitFor()

// Output als String abgreifen
def output = proc.in.text
output.split("\n").each { line ->
  ...
}

Output direkt ausgeben (siehe Quelle):

class StreamGobbler extends Thread {
    InputStream is

    StreamGobbler(InputStream is) {
        this.is = is
    }

    public void run() {
        try {
            InputStreamReader isr = new InputStreamReader(is)
            BufferedReader br = new BufferedReader(isr)
            String line = null
            while ( (line = br.readLine()) != null) {
                System.out.println(line)
            }
        } catch (IOException exc) {
            exc.printStackTrace()
        }
    }
}

void execCmd(cmd) {
    def proc = cmd.execute()

    new StreamGobbler(proc.getErrorStream()).start()
    new StreamGobbler(proc.getInputStream()).start()

    proc.waitFor()
    def exitCode = proc.exitValue()

    if (exitCode != 0) {
        fail("Executing ${cmd[0]} failed with exit code ${exitCode}");
    }
}

// Usage examples
execCmd("some-command-needing-user-input.sh")  // Command is a String
execCmd([ "java", "-jar", "myjar.jar" ])       // Command is an array of Strings

Dateien lesen und schreiben

Details siehe API der File-Klasse

def file = new File('sample.txt')

// Read complete File
String content = file.getText('utf-8')

// Read file line by line
file.withReader('utf-8') { reader ->
  String line;
  while ((line = reader.readLine()) != null) {
    ...
  }
}

// Write complete file
file.withWriter('utf-8') { writer ->
  writer << content
}

// Write file line by line
file.withWriter('utf-8') { writer ->
  writer.writeLine 'Adding this text to the file.'
}

XML / XPath

Hinweise:

import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.*

private void parseXml() {
  def builder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
  def xpath = XPathFactory.newInstance().newXPath()

  File xmlFile = new File("example.xml")
  def rootElem = builder.parse(xmlFile).documentElement
  xpath.evaluate('/some/xpath', rootElem, XPathConstants.NODESET).each { elem ->
    println elem.getAttribute("name") + " has text: " + elem.textContent
  }
}
import javax.xml.transform.*
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult

private void writeXml() {
  File xmlFile = new File("example.xml")
  def rootElem = ...

  Transformer xmlTransformer = TransformerFactory.newInstance().newTransformer();
  xmlTransformer.setOutputProperty(OutputKeys.INDENT, "yes");   // Pretty-print
  xmlFile.withWriter('utf-8') { writer ->
    xmlTransformer.transform(new DOMSource(rootElem), new StreamResult(writer))
  }
}

DirectoryScanner von Ant verwenden

Siehe auch: DirectoryScanner-API

import org.apache.tools.ant.DirectoryScanner

...

long classLastModified = getMaxLastModified(new File(...), [ "class/**" ] )

...

private long getMaxLastModified(File basedir, def includes) {
  DirectoryScanner scanner = new DirectoryScanner()
  scanner.addDefaultExcludes()
  scanner.basedir = basedir
  scanner.includes = includes
  scanner.scan()

  long maxLastModified = -1
  scanner.includedFiles.each { fileName ->
    maxLastModified = Math.max(maxLastModified, new File(basedir, fileName).lastModified())
  }
  return maxLastModified;
}