The Groovy Development Kit
1. Working with IO
Groovy provides a number of helper methods for working with I/O. While you could use standard Java code in Groovy to deal with those, Groovy provides much more convenient ways to handle files, streams, readers, …
In particular, you should take a look at methods added to:
-
the
java.io.File
class : http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/File.html -
the
java.io.InputStream
class: http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/InputStream.html -
the
java.io.OutputStream
class: http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/OutputStream.html -
the
java.io.Reader
class: http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/Reader.html -
the
java.io.Writer
class: http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/Writer.html -
the
java.nio.file.Path
class: http://docs.groovy-lang.org/latest/html/groovy-jdk/java/nio/file/Path.html
The following section focuses on sample idiomatic constructs using helper methods available above but is not meant to be a complete description of all available methods. For that, please read the GDK API.
1.1. Reading files
As a first example, let’s see how you would print all lines of a text file in Groovy:
new File(baseDir, 'haiku.txt').eachLine { line ->
println line
}
The eachLine
method is a method added to the File
class automatically by Groovy and has many variants, for example
if you need to know the line number, you can use this variant:
new File(baseDir, 'haiku.txt').eachLine { line, nb ->
println "Line $nb: $line"
}
If for whatever reason an exception is thrown in the eachLine
body, the method makes sure that the resource
is properly closed. This is true for all I/O resource methods that Groovy adds.
For example in some cases you will prefer to use a Reader
, but still benefit from the automatic resource management
from Groovy. In the next example, the reader will be closed even if the exception occurs:
def count = 0, MAXSIZE = 3
new File(baseDir,"haiku.txt").withReader { reader ->
while (reader.readLine()) {
if (++count > MAXSIZE) {
throw new RuntimeException('Haiku should only have 3 verses')
}
}
}
Should you need to collect the lines of a text file into a list, you can do:
def list = new File(baseDir, 'haiku.txt').collect {it}
Or you can even leverage the as
operator to get the contents of the file into an array of lines:
def array = new File(baseDir, 'haiku.txt') as String[]
How many times did you have to get the contents of a file into a byte[]
and how much code does it require? Groovy
makes it very easy actually:
byte[] contents = file.bytes
Working with I/O is not limited to dealing with files. In fact, a lot of operations rely on input/output streams, hence why Groovy adds a lot of support methods to those, as you can see in the documentation.
As an example, you can obtain an InputStream
from a File
very easily:
def is = new File(baseDir,'haiku.txt').newInputStream()
// do something ...
is.close()
However you can see that it requires you to deal with closing the inputstream. In Groovy it is in general a better
idea to use the withInputStream
idiom that will take care of that for you:
new File(baseDir,'haiku.txt').withInputStream { stream ->
// do something ...
}
1.2. Writing files
Of course in some cases you won’t want to read but write a file. One of the options is to use a Writer
:
new File(baseDir,'haiku.txt').withWriter('utf-8') { writer ->
writer.writeLine 'Into the ancient pond'
writer.writeLine 'A frog jumps'
writer.writeLine 'Water’s sound!'
}
But for such a simple example, using the <<
operator would have been enough:
new File(baseDir,'haiku.txt') << '''Into the ancient pond
A frog jumps
Water’s sound!'''
Of course we do not always deal with text contents, so you could use the Writer
or directly write bytes as in
this example:
file.bytes = [66,22,11]
Of course you can also directly deal with output streams. For example, here is how you would create an output stream to write into a file:
def os = new File(baseDir,'data.bin').newOutputStream()
// do something ...
os.close()
However you can see that it requires you to deal with closing the output stream. Again it is in general a better
idea to use the withOutputStream
idiom that will handle the exceptions and close the stream in any case:
new File(baseDir,'data.bin').withOutputStream { stream ->
// do something ...
}
1.3. Traversing file trees
In scripting contexts it is a common task to traverse a file tree in order to find some specific files and do something with them. Groovy provides multiple methods to do this. For example you can perform something on all files of a directory:
dir.eachFile { file -> (1)
println file.name
}
dir.eachFileMatch(~/.*\.txt/) { file -> (2)
println file.name
}
1 | executes the closure code on each file found in the directory |
2 | executes the closure code on files in the directory matching the specified pattern |
Often you will have to deal with a deeper hierarchy of files, in which case you can use eachFileRecurse
:
dir.eachFileRecurse { file -> (1)
println file.name
}
dir.eachFileRecurse(FileType.FILES) { file -> (2)
println file.name
}
1 | executes the closure code on each file or directory found in the directory, recursively |
2 | executes the closure code only on files, but recursively |
For more complex traversal techniques you can use the traverse
method, which requires you to set a special flag
indicating what to do with the traversal:
dir.traverse { file ->
if (file.directory && file.name=='bin') {
FileVisitResult.TERMINATE (1)
} else {
println file.name
FileVisitResult.CONTINUE (2)
}
}
1 | if the current file is a directory and its name is bin , stop the traversal |
2 | otherwise print the file name and continue |
1.4. Data and objects
In Java it is not uncommon to serialize and deserialize data using the java.io.DataOutputStream
and
java.io.DataInputStream
classes respectively. Groovy will make it even easier to deal with them. For example, you could
serialize data into a file and deserialize it using this code:
boolean b = true
String message = 'Hello from Groovy'
// Serialize data into a file
file.withDataOutputStream { out ->
out.writeBoolean(b)
out.writeUTF(message)
}
// ...
// Then read it back
file.withDataInputStream { input ->
assert input.readBoolean() == b
assert input.readUTF() == message
}
And similarly, if the data you want to serialize implements the Serializable
interface, you can proceed with
an object output stream, as illustrated here:
Person p = new Person(name:'Bob', age:76)
// Serialize data into a file
file.withObjectOutputStream { out ->
out.writeObject(p)
}
// ...
// Then read it back
file.withObjectInputStream { input ->
def p2 = input.readObject()
assert p2.name == p.name
assert p2.age == p.age
}
1.5. Executing External Processes
The previous section described how easy it was to deal with files, readers or streams in Groovy. However in domains like system administration or devops it is often required to communicate with external processes.
Groovy provides a simple way to execute command line processes. Simply
write the command line as a string and call the execute()
method.
E.g., on a *nix machine (or a Windows machine with appropriate *nix
commands installed), you can execute this:
def process = "ls -l".execute() (1)
println "Found text ${process.text}" (2)
1 | executes the ls command in an external process |
2 | consume the output of the command and retrieve the text |
The execute()
method returns a java.lang.Process
instance which will
subsequently allow the in/out/err streams to be processed and the exit
value from the process to be inspected etc.
e.g. here is the same command as above but we will now process the resulting stream a line at a time:
def process = "ls -l".execute() (1)
process.in.eachLine { line -> (2)
println line (3)
}
1 | executes the ls command in an external process |
2 | for each line of the input stream of the process |
3 | print the line |
It is worth noting that in
corresponds to an input stream to the standard output of the command. out
will refer
to a stream where you can send data to the process (its standard input).
Remember that many commands are shell built-ins and need special handling. So if you want a listing of files in a directory on a Windows machine and write:
def process = "dir".execute()
println "${process.text}"
you will receive an IOException
saying Cannot run program "dir":
CreateProcess error=2, The system cannot find the file specified.
This is because dir
is built-in to the Windows shell (cmd.exe
) and
can’t be run as a simple executable. Instead, you will need to write:
def process = "cmd /c dir".execute()
println "${process.text}"
Also, because this functionality currently makes use of
java.lang.Process
undercover, the deficiencies of that class
must be taken into consideration. In particular, the javadoc
for this class says:
Because some native platforms only provide limited buffer size for standard input and output streams, failure to promptly write the input stream or read the output stream of the subprocess may cause the subprocess to block, and even deadlock
Because of this, Groovy provides some additional helper methods which make stream handling for processes easier.
Here is how to gobble all of the output (including the error stream output) from your process:
def p = "rm -f foo.tmp".execute([], tmpDir)
p.consumeProcessOutput()
p.waitFor()
There are also variations of consumeProcessOutput
that make use of StringBuffer
, InputStream
,
OutputStream
etc… For a complete list, please read the
GDK API for java.lang.Process
In addition, there is a pipeTo
command (mapped to |
to allow overloading) which lets the output stream of one process be fed
into the input stream of another process.
Here are some examples of use:
proc1 = 'ls'.execute()
proc2 = 'tr -d o'.execute()
proc3 = 'tr -d e'.execute()
proc4 = 'tr -d i'.execute()
proc1 | proc2 | proc3 | proc4
proc4.waitFor()
if (proc4.exitValue()) {
println proc4.err.text
} else {
println proc4.text
}
def sout = new StringBuilder()
def serr = new StringBuilder()
proc2 = 'tr -d o'.execute()
proc3 = 'tr -d e'.execute()
proc4 = 'tr -d i'.execute()
proc4.consumeProcessOutput(sout, serr)
proc2 | proc3 | proc4
[proc2, proc3].each { it.consumeProcessErrorStream(serr) }
proc2.withWriter { writer ->
writer << 'testfile.groovy'
}
proc4.waitForOrKill(1000)
println "Standard output: $sout"
println "Standard error: $serr"
2. Working with collections
Groovy provides native support for various collection types, including lists, maps or ranges. Most of those are based on the Java collection types and decorated with additional methods found in the Groovy development kit.
2.1. Lists
2.1.1. List literals
You can create lists as follows. Notice that []
is the empty list
expression.
def list = [5, 6, 7, 8]
assert list.get(2) == 7
assert list[2] == 7
assert list instanceof java.util.List
def emptyList = []
assert emptyList.size() == 0
emptyList.add(5)
assert emptyList.size() == 1
Each list expression creates an implementation of java.util.List.
Of course lists can be used as a source to construct another list:
def list1 = ['a', 'b', 'c']
//construct a new list, seeded with the same items as in list1
def list2 = new ArrayList<String>(list1)
assert list2 == list1 // == checks that each corresponding element is the same
// clone() can also be called
def list3 = list1.clone()
assert list3 == list1
A list is an ordered collection of objects:
def list = [5, 6, 7, 8]
assert list.size() == 4
assert list.getClass() == ArrayList // the specific kind of list being used
assert list[2] == 7 // indexing starts at 0
assert list.getAt(2) == 7 // equivalent method to subscript operator []
assert list.get(2) == 7 // alternative method
list[2] = 9
assert list == [5, 6, 9, 8,] // trailing comma OK
list.putAt(2, 10) // equivalent method to [] when value being changed
assert list == [5, 6, 10, 8]
assert list.set(2, 11) == 10 // alternative method that returns old value
assert list == [5, 6, 11, 8]
assert ['a', 1, 'a', 'a', 2.5, 2.5f, 2.5d, 'hello', 7g, null, 9 as byte]
//objects can be of different types; duplicates allowed
assert [1, 2, 3, 4, 5][-1] == 5 // use negative indices to count from the end
assert [1, 2, 3, 4, 5][-2] == 4
assert [1, 2, 3, 4, 5].getAt(-2) == 4 // getAt() available with negative index...
try {
[1, 2, 3, 4, 5].get(-2) // but negative index not allowed with get()
assert false
} catch (e) {
assert e instanceof IndexOutOfBoundsException
}
2.1.2. List as a boolean expression
Lists can be evaluated as a boolean
value:
assert ![] // an empty list evaluates as false
//all other lists, irrespective of contents, evaluate as true
assert [1] && ['a'] && [0] && [0.0] && [false] && [null]
2.1.3. Iterating on a list
Iterating on elements of a list is usually done calling the each
and eachWithIndex
methods, which execute code on each
item of a list:
[1, 2, 3].each {
println "Item: $it" // `it` is an implicit parameter corresponding to the current element
}
['a', 'b', 'c'].eachWithIndex { it, i -> // `it` is the current element, while `i` is the index
println "$i: $it"
}
In addition to iterating, it is often useful to create a new list by transforming each of its elements into
something else. This operation, often called mapping, is done in Groovy thanks to the collect
method:
assert [1, 2, 3].collect { it * 2 } == [2, 4, 6]
// shortcut syntax instead of collect
assert [1, 2, 3]*.multiply(2) == [1, 2, 3].collect { it.multiply(2) }
def list = [0]
// it is possible to give `collect` the list which collects the elements
assert [1, 2, 3].collect(list) { it * 2 } == [0, 2, 4, 6]
assert list == [0, 2, 4, 6]
2.1.4. Manipulating lists
Filtering and searching
The Groovy development kit contains a lot of methods on collections that enhance the standard collections with pragmatic methods, some of which are illustrated here:
assert [1, 2, 3].find { it > 1 } == 2 // find 1st element matching criteria
assert [1, 2, 3].findAll { it > 1 } == [2, 3] // find all elements matching critieria
assert ['a', 'b', 'c', 'd', 'e'].findIndexOf { // find index of 1st element matching criteria
it in ['c', 'e', 'g']
} == 2
assert ['a', 'b', 'c', 'd', 'c'].indexOf('c') == 2 // index returned
assert ['a', 'b', 'c', 'd', 'c'].indexOf('z') == -1 // index -1 means value not in list
assert ['a', 'b', 'c', 'd', 'c'].lastIndexOf('c') == 4
assert [1, 2, 3].every { it < 5 } // returns true if all elements match the predicate
assert ![1, 2, 3].every { it < 3 }
assert [1, 2, 3].any { it > 2 } // returns true if any element matches the predicate
assert ![1, 2, 3].any { it > 3 }
assert [1, 2, 3, 4, 5, 6].sum() == 21 // sum anything with a plus() method
assert ['a', 'b', 'c', 'd', 'e'].sum {
it == 'a' ? 1 : it == 'b' ? 2 : it == 'c' ? 3 : it == 'd' ? 4 : it == 'e' ? 5 : 0
// custom value to use in sum
} == 15
assert ['a', 'b', 'c', 'd', 'e'].sum { ((char) it) - ((char) 'a') } == 10
assert ['a', 'b', 'c', 'd', 'e'].sum() == 'abcde'
assert [['a', 'b'], ['c', 'd']].sum() == ['a', 'b', 'c', 'd']
// an initial value can be provided
assert [].sum(1000) == 1000
assert [1, 2, 3].sum(1000) == 1006
assert [1, 2, 3].join('-') == '1-2-3' // String joining
assert [1, 2, 3].inject('counting: ') {
str, item -> str + item // reduce operation
} == 'counting: 123'
assert [1, 2, 3].inject(0) { count, item ->
count + item
} == 6
And here is idiomatic Groovy code for finding the maximum and minimum in a collection:
def list = [9, 4, 2, 10, 5]
assert list.max() == 10
assert list.min() == 2
// we can also compare single characters, as anything comparable
assert ['x', 'y', 'a', 'z'].min() == 'a'
// we can use a closure to specify the sorting behaviour
def list2 = ['abc', 'z', 'xyzuvw', 'Hello', '321']
assert list2.max { it.size() } == 'xyzuvw'
assert list2.min { it.size() } == 'z'
In addition to closures, you can use a Comparator
to define the comparison criteria:
Comparator mc = { a, b -> a == b ? 0 : (a < b ? -1 : 1) }
def list = [7, 4, 9, -6, -1, 11, 2, 3, -9, 5, -13]
assert list.max(mc) == 11
assert list.min(mc) == -13
Comparator mc2 = { a, b -> a == b ? 0 : (Math.abs(a) < Math.abs(b)) ? -1 : 1 }
assert list.max(mc2) == -13
assert list.min(mc2) == -1
assert list.max { a, b -> a.equals(b) ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 } == -13
assert list.min { a, b -> a.equals(b) ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 } == -1
Adding or removing elements
We can use []
to assign a new empty list and <<
to append items to it:
def list = []
assert list.empty
list << 5
assert list.size() == 1
list << 7 << 'i' << 11
assert list == [5, 7, 'i', 11]
list << ['m', 'o']
assert list == [5, 7, 'i', 11, ['m', 'o']]
//first item in chain of << is target list
assert ([1, 2] << 3 << [4, 5] << 6) == [1, 2, 3, [4, 5], 6]
//using leftShift is equivalent to using <<
assert ([1, 2, 3] << 4) == ([1, 2, 3].leftShift(4))
We can add to a list in many ways:
assert [1, 2] + 3 + [4, 5] + 6 == [1, 2, 3, 4, 5, 6]
// equivalent to calling the `plus` method
assert [1, 2].plus(3).plus([4, 5]).plus(6) == [1, 2, 3, 4, 5, 6]
def a = [1, 2, 3]
a += 4 // creates a new list and assigns it to `a`
a += [5, 6]
assert a == [1, 2, 3, 4, 5, 6]
assert [1, *[222, 333], 456] == [1, 222, 333, 456]
assert [*[1, 2, 3]] == [1, 2, 3]
assert [1, [2, 3, [4, 5], 6], 7, [8, 9]].flatten() == [1, 2, 3, 4, 5, 6, 7, 8, 9]
def list = [1, 2]
list.add(3)
list.addAll([5, 4])
assert list == [1, 2, 3, 5, 4]
list = [1, 2]
list.add(1, 3) // add 3 just before index 1
assert list == [1, 3, 2]
list.addAll(2, [5, 4]) //add [5,4] just before index 2
assert list == [1, 3, 5, 4, 2]
list = ['a', 'b', 'z', 'e', 'u', 'v', 'g']
list[8] = 'x' // the [] operator is growing the list as needed
// nulls inserted if required
assert list == ['a', 'b', 'z', 'e', 'u', 'v', 'g', null, 'x']
It is however important that the +
operator on a list is not mutating. Compared to <<
, it will create a new
list, which is often not what you want and can lead to performance issues.
The Groovy development kit also contains methods allowing you to easily remove elements from a list by value:
assert ['a','b','c','b','b'] - 'c' == ['a','b','b','b']
assert ['a','b','c','b','b'] - 'b' == ['a','c']
assert ['a','b','c','b','b'] - ['b','c'] == ['a']
def list = [1,2,3,4,3,2,1]
list -= 3 // creates a new list by removing `3` from the original one
assert list == [1,2,4,2,1]
assert ( list -= [2,4] ) == [1,1]
It is also possible to remove an element by passing its index to the remove
method, in which case the list is mutated:
def list = ['a','b','c','d','e','f','b','b','a']
assert list.remove(2) == 'c' // remove the third element, and return it
assert list == ['a','b','d','e','f','b','b','a']
In case you only want to remove the first element having the same value in a list, instead of removing all
elements, you can call the remove
method passing the value:
def list= ['a','b','c','b','b']
assert list.remove('c') // remove 'c', and return true because element removed
assert list.remove('b') // remove first 'b', and return true because element removed
assert ! list.remove('z') // return false because no elements removed
assert list == ['a','b','b']
As you can see, there are two remove
methods available. One that takes an integer and removes an element
by its index, and another that will remove the first element that matches the passed value. So what should we
do when we have a list of integers? In this case, you may wish to use removeAt
to remove an element by its
index, and removeElement
to remove the first element that matches a value.
def list = [1,2,3,4,5,6,2,2,1]
assert list.remove(2) == 3 // this removes the element at index 2, and returns it
assert list == [1,2,4,5,6,2,2,1]
assert list.removeElement(2) // remove first 2 and return true
assert list == [1,4,5,6,2,2,1]
assert ! list.removeElement(8) // return false because 8 is not in the list
assert list == [1,4,5,6,2,2,1]
assert list.removeAt(1) == 4 // remove element at index 1, and return it
assert list == [1,5,6,2,2,1]
Of course, removeAt
and removeElement
will work with lists of any type.
Additionally, removing all the elements in a list can be done by calling the clear
method:
def list= ['a',2,'c',4]
list.clear()
assert list == []
Set operations
The Groovy development kit also includes methods making it easy to reason on sets:
assert 'a' in ['a','b','c'] // returns true if an element belongs to the list
assert ['a','b','c'].contains('a') // equivalent to the `contains` method in Java
assert [1,3,4].containsAll([1,4]) // `containsAll` will check that all elements are found
assert [1,2,3,3,3,3,4,5].count(3) == 4 // count the number of elements which have some value
assert [1,2,3,3,3,3,4,5].count {
it%2==0 // count the number of elements which match the predicate
} == 2
assert [1,2,4,6,8,10,12].intersect([1,3,6,9,12]) == [1,6,12]
assert [1,2,3].disjoint( [4,6,9] )
assert ![1,2,3].disjoint( [2,4,6] )
Sorting
Working with collections often implies sorting. Groovy offers a variety of options to sort lists, from using closures to comparators, as in the following examples:
assert [6, 3, 9, 2, 7, 1, 5].sort() == [1, 2, 3, 5, 6, 7, 9]
def list = ['abc', 'z', 'xyzuvw', 'Hello', '321']
assert list.sort {
it.size()
} == ['z', 'abc', '321', 'Hello', 'xyzuvw']
def list2 = [7, 4, -6, -1, 11, 2, 3, -9, 5, -13]
assert list2.sort { a, b -> a == b ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 } ==
[-1, 2, 3, 4, 5, -6, 7, -9, 11, -13]
Comparator mc = { a, b -> a == b ? 0 : Math.abs(a) < Math.abs(b) ? -1 : 1 }
// JDK 8+ only
// list2.sort(mc)
// assert list2 == [-1, 2, 3, 4, 5, -6, 7, -9, 11, -13]
def list3 = [6, -3, 9, 2, -7, 1, 5]
Collections.sort(list3)
assert list3 == [-7, -3, 1, 2, 5, 6, 9]
Collections.sort(list3, mc)
assert list3 == [1, 2, -3, 5, 6, -7, 9]
Duplicating elements
The Groovy development kit also takes advantage of operator overloading to provide methods allowing duplication of elements of a list:
assert [1, 2, 3] * 3 == [1, 2, 3, 1, 2, 3, 1, 2, 3]
assert [1, 2, 3].multiply(2) == [1, 2, 3, 1, 2, 3]
assert Collections.nCopies(3, 'b') == ['b', 'b', 'b']
// nCopies from the JDK has different semantics than multiply for lists
assert Collections.nCopies(2, [1, 2]) == [[1, 2], [1, 2]] //not [1,2,1,2]
2.2. Maps
2.2.1. Map literals
In Groovy, maps (also known as associative arrays) can be created using the map literal syntax: [:]
:
def map = [name: 'Gromit', likes: 'cheese', id: 1234]
assert map.get('name') == 'Gromit'
assert map.get('id') == 1234
assert map['name'] == 'Gromit'
assert map['id'] == 1234
assert map instanceof java.util.Map
def emptyMap = [:]
assert emptyMap.size() == 0
emptyMap.put("foo", 5)
assert emptyMap.size() == 1
assert emptyMap.get("foo") == 5
Map keys are strings by default: [a:1]
is equivalent to ['a':1]
. This can be confusing if you define a variable
named a
and that you want the value of a
to be the key in your map. If this is the case, then you must escape
the key by adding parenthesis, like in the following example:
def a = 'Bob'
def ages = [a: 43]
assert ages['Bob'] == null // `Bob` is not found
assert ages['a'] == 43 // because `a` is a literal!
ages = [(a): 43] // now we escape `a` by using parenthesis
assert ages['Bob'] == 43 // and the value is found!
In addition to map literals, it is possible, to get a new copy of a map, to clone it:
def map = [
simple : 123,
complex: [a: 1, b: 2]
]
def map2 = map.clone()
assert map2.get('simple') == map.get('simple')
assert map2.get('complex') == map.get('complex')
map2.get('complex').put('c', 3)
assert map.get('complex').get('c') == 3
The resulting map is a shallow copy of the original one, as illustrated in the previous example.
2.2.2. Map property notation
Maps also act like beans so you can use the property notation to get/set
items inside the Map
as long as the keys are strings which are valid
Groovy identifiers:
def map = [name: 'Gromit', likes: 'cheese', id: 1234]
assert map.name == 'Gromit' // can be used instead of map.get('name')
assert map.id == 1234
def emptyMap = [:]
assert emptyMap.size() == 0
emptyMap.foo = 5
assert emptyMap.size() == 1
assert emptyMap.foo == 5
Note: by design map.foo
will always look for the key foo
in the map. This
means foo.class
will return null
on a map that doesn’t contain the class
key. Should you really want to know
the class, then you must use getClass()
:
def map = [name: 'Gromit', likes: 'cheese', id: 1234]
assert map.class == null
assert map.get('class') == null
assert map.getClass() == LinkedHashMap // this is probably what you want
map = [1 : 'a',
(true) : 'p',
(false): 'q',
(null) : 'x',
'null' : 'z']
assert map.containsKey(1) // 1 is not an identifier so used as is
assert map.true == null
assert map.false == null
assert map.get(true) == 'p'
assert map.get(false) == 'q'
assert map.null == 'z'
assert map.get(null) == 'x'
2.2.3. Iterating on maps
As usual in the Groovy development kit, idiomatic iteration on maps makes use of the each
and eachWithIndex
methods.
It’s worth noting that maps created using the map literal notation are ordered, that is to say that if you iterate
on map entries, it is guaranteed that the entries will be returned in the same order they were added in the map.
def map = [
Bob : 42,
Alice: 54,
Max : 33
]
// `entry` is a map entry
map.each { entry ->
println "Name: $entry.key Age: $entry.value"
}
// `entry` is a map entry, `i` the index in the map
map.eachWithIndex { entry, i ->
println "$i - Name: $entry.key Age: $entry.value"
}
// Alternatively you can use key and value directly
map.each { key, value ->
println "Name: $key Age: $value"
}
// Key, value and i as the index in the map
map.eachWithIndex { key, value, i ->
println "$i - Name: $key Age: $value"
}
2.2.4. Manipulating maps
Adding or removing elements
Adding an element to a map can be done either using the put
method, the subscript operator or using putAll
:
def defaults = [1: 'a', 2: 'b', 3: 'c', 4: 'd']
def overrides = [2: 'z', 5: 'x', 13: 'x']
def result = new LinkedHashMap(defaults)
result.put(15, 't')
result[17] = 'u'
result.putAll(overrides)
assert result == [1: 'a', 2: 'z', 3: 'c', 4: 'd', 5: 'x', 13: 'x', 15: 't', 17: 'u']
Removing all the elements of a map can be done by calling the clear
method:
def m = [1:'a', 2:'b']
assert m.get(1) == 'a'
m.clear()
assert m == [:]
Maps generated using the map literal syntax are using the object equals
and hashcode
methods. This means that
you should never use an object which hash code is subject to change over time, or you wouldn’t be able to get
the associated value back.
It is also worth noting that you should never use a GString
as the key of a map, because the hash code of a GString
is not the same as the hash code of an equivalent String
:
def key = 'some key'
def map = [:]
def gstringKey = "${key.toUpperCase()}"
map.put(gstringKey,'value')
assert map.get('SOME KEY') == null
Keys, values and entries
We can inspect the keys, values, and entries in a view:
def map = [1:'a', 2:'b', 3:'c']
def entries = map.entrySet()
entries.each { entry ->
assert entry.key in [1,2,3]
assert entry.value in ['a','b','c']
}
def keys = map.keySet()
assert keys == [1,2,3] as Set
Mutating values returned by the view (be it a map entry, a key or a value) is highly discouraged because success
of the operation directly depends on the type of the map being manipulated. In particular, Groovy relies on collections
from the JDK that in general make no guarantee that a collection can safely be manipulated through keySet
, entrySet
, or
values
.
Filtering and searching
The Groovy development kit contains filtering, searching and collecting methods similar to those found for lists:
def people = [
1: [name:'Bob', age: 32, gender: 'M'],
2: [name:'Johnny', age: 36, gender: 'M'],
3: [name:'Claire', age: 21, gender: 'F'],
4: [name:'Amy', age: 54, gender:'F']
]
def bob = people.find { it.value.name == 'Bob' } // find a single entry
def females = people.findAll { it.value.gender == 'F' }
// both return entries, but you can use collect to retrieve the ages for example
def ageOfBob = bob.value.age
def agesOfFemales = females.collect {
it.value.age
}
assert ageOfBob == 32
assert agesOfFemales == [21,54]
// but you could also use a key/pair value as the parameters of the closures
def agesOfMales = people.findAll { id, person ->
person.gender == 'M'
}.collect { id, person ->
person.age
}
assert agesOfMales == [32, 36]
// `every` returns true if all entries match the predicate
assert people.every { id, person ->
person.age > 18
}
// `any` returns true if any entry matches the predicate
assert people.any { id, person ->
person.age == 54
}
Grouping
We can group a list into a map using some criteria:
assert ['a', 7, 'b', [2, 3]].groupBy {
it.class
} == [(String) : ['a', 'b'],
(Integer) : [7],
(ArrayList): [[2, 3]]
]
assert [
[name: 'Clark', city: 'London'], [name: 'Sharma', city: 'London'],
[name: 'Maradona', city: 'LA'], [name: 'Zhang', city: 'HK'],
[name: 'Ali', city: 'HK'], [name: 'Liu', city: 'HK'],
].groupBy { it.city } == [
London: [[name: 'Clark', city: 'London'],
[name: 'Sharma', city: 'London']],
LA : [[name: 'Maradona', city: 'LA']],
HK : [[name: 'Zhang', city: 'HK'],
[name: 'Ali', city: 'HK'],
[name: 'Liu', city: 'HK']],
]
2.3. Ranges
Ranges allow you to create a list of sequential values. These can be
used as List
since
Range extends
java.util.List.
Ranges defined with the ..
notation are inclusive (that is the list
contains the from and to value).
Ranges defined with the ..<
notation are half-open, they include the
first value but not the last value.
Ranges defined with the <..
notation are also half-open, they include the
last value but not the first value.
Ranges defined with the <..<
notation are full-open, they do not include the
first value nor the last value.
// an inclusive range
def range = 5..8
assert range.size() == 4
assert range.get(2) == 7
assert range[2] == 7
assert range instanceof java.util.List
assert range.contains(5)
assert range.contains(8)
// lets use a half-open range
range = 5..<8
assert range.size() == 3
assert range.get(2) == 7
assert range[2] == 7
assert range instanceof java.util.List
assert range.contains(5)
assert !range.contains(8)
//get the end points of the range without using indexes
range = 1..10
assert range.from == 1
assert range.to == 10
Note that int ranges are implemented efficiently, creating a lightweight Java object containing a from and to value.
Ranges can be used for any Java object which implements java.lang.Comparable
for comparison and also have methods next()
and previous()
to return the
next / previous item in the range. For example, you can create a range of String
elements:
// an inclusive range
def range = 'a'..'d'
assert range.size() == 4
assert range.get(2) == 'c'
assert range[2] == 'c'
assert range instanceof java.util.List
assert range.contains('a')
assert range.contains('d')
assert !range.contains('e')
You can iterate on a range using a classic for
loop:
for (i in 1..10) {
println "Hello ${i}"
}
but alternatively you can achieve the same effect in a more Groovy idiomatic style, by iterating a range
with each
method:
(1..10).each { i ->
println "Hello ${i}"
}
Ranges can be also used in the switch
statement:
switch (years) {
case 1..10: interestRate = 0.076; break;
case 11..25: interestRate = 0.052; break;
default: interestRate = 0.037;
}
2.4. Syntax enhancements for collections
2.4.1. GPath support
Thanks to the support of property notation for both lists and maps, Groovy provides syntactic sugar making it really easy to deal with nested collections, as illustrated in the following examples:
def listOfMaps = [['a': 11, 'b': 12], ['a': 21, 'b': 22]]
assert listOfMaps.a == [11, 21] //GPath notation
assert listOfMaps*.a == [11, 21] //spread dot notation
listOfMaps = [['a': 11, 'b': 12], ['a': 21, 'b': 22], null]
assert listOfMaps*.a == [11, 21, null] // caters for null values
assert listOfMaps*.a == listOfMaps.collect { it?.a } //equivalent notation
// But this will only collect non-null values
assert listOfMaps.a == [11,21]
2.4.2. Spread operator
The spread operator can be used to "inline" a collection into another. It is syntactic sugar which often avoids calls
to putAll
and facilitates the realization of one-liners:
assert [ 'z': 900,
*: ['a': 100, 'b': 200], 'a': 300] == ['a': 300, 'b': 200, 'z': 900]
//spread map notation in map definition
assert [*: [3: 3, *: [5: 5]], 7: 7] == [3: 3, 5: 5, 7: 7]
def f = { [1: 'u', 2: 'v', 3: 'w'] }
assert [*: f(), 10: 'zz'] == [1: 'u', 10: 'zz', 2: 'v', 3: 'w']
//spread map notation in function arguments
f = { map -> map.c }
assert f(*: ['a': 10, 'b': 20, 'c': 30], 'e': 50) == 30
f = { m, i, j, k -> [m, i, j, k] }
//using spread map notation with mixed unnamed and named arguments
assert f('e': 100, *[4, 5], *: ['a': 10, 'b': 20, 'c': 30], 6) ==
[["e": 100, "b": 20, "c": 30, "a": 10], 4, 5, 6]
2.4.3. The star-dot `*.' operator
The "star-dot" operator is a shortcut operator allowing you to call a method or a property on all elements of a collection:
assert [1, 3, 5] == ['a', 'few', 'words']*.size()
class Person {
String name
int age
}
def persons = [new Person(name:'Hugo', age:17), new Person(name:'Sandra',age:19)]
assert [17, 19] == persons*.age
2.4.4. Slicing with the subscript operator
You can index into lists, arrays, maps using the subscript expression. It is interesting that strings are considered as special kinds of collections in that context:
def text = 'nice cheese gromit!'
def x = text[2]
assert x == 'c'
assert x.class == String
def sub = text[5..10]
assert sub == 'cheese'
def list = [10, 11, 12, 13]
def answer = list[2,3]
assert answer == [12,13]
Notice that you can use ranges to extract part of a collection:
list = 100..200
sub = list[1, 3, 20..25, 33]
assert sub == [101, 103, 120, 121, 122, 123, 124, 125, 133]
The subscript operator can be used to update an existing collection (for collection type which are not immutable):
list = ['a','x','x','d']
list[1..2] = ['b','c']
assert list == ['a','b','c','d']
It is worth noting that negative indices are allowed, to extract more easily from the end of a collection:
text = "nice cheese gromit!"
x = text[-1]
assert x == "!"
You can use negative indices to count from the end of the List, array, String etc.
def name = text[-7..-2]
assert name == "gromit"
Eventually, if you use a backwards range (the starting index is greater than the end index), then the answer is reversed.
text = "nice cheese gromit!"
name = text[3..1]
assert name == "eci"
2.5. Enhanced Collection Methods
In addition to lists, maps or ranges, Groovy offers a lot of additional methods for filtering, collecting, grouping, counting, … which are directly available on either collections or more easily iterables.
In particular, we invite you to read the Groovy development kit API docs and specifically:
3. Working with arrays
Groovy provides array support based on Java arrays with several extensions found in the Groovy development kit. The overall intention is that whether you are using an array or a collection, the code for working with the aggregate remains the same.
3.1. Arrays
3.1.1. Array literals
You can create arrays as follows. Notice that []
is also used as the empty array
expression when given an explicit array type.
Integer[] nums = [5, 6, 7, 8]
assert nums[1] == 6
assert nums.getAt(2) == 7 // alternative syntax
assert nums[-1] == 8 // negative indices
assert nums instanceof Integer[]
int[] primes = [2, 3, 5, 7] // primitives
assert primes instanceof int[]
def evens = new int[]{2, 4, 6} // alt syntax 1
assert evens instanceof int[]
def odds = [1, 3, 5] as int[] // alt syntax 2
assert odds instanceof int[]
// empty array examples
Integer[] emptyNums = []
assert emptyNums instanceof Integer[] && emptyNums.size() == 0
def emptyStrings = new String[]{} // alternative syntax 1
assert emptyStrings instanceof String[] && emptyStrings.size() == 0
var emptyObjects = new Object[0] // alternative syntax 2
assert emptyObjects instanceof Object[] && emptyObjects.size() == 0
3.1.2. Iterating on a list
Iterating on elements of a list is usually done calling the each
and eachWithIndex
methods, which execute code on each
item of a list:
String[] vowels = ['a', 'e', 'i', 'o', 'u']
var result = ''
vowels.each {
result += it
}
assert result == 'aeiou'
result = ''
vowels.eachWithIndex { v, i ->
result += v * i // index starts from 0
}
assert result == 'eiiooouuuu'
3.1.3. Other useful methods
There are numerous other GDK methods for working with arrays. Just be a little careful to read the documentation. For collections, there are some mutating methods which alter the original collection and others which produce new collections, leaving the original untouched. Since arrays are of a fixed size, we wouldn’t expect mutating methods which altered an array’s size. Often instead, such methods return collections. Here are some interesting array GDK methods:
int[] nums = [1, 2, 3]
def doubled = nums.collect { it * 2 }
assert doubled == [2, 4, 6] && doubled instanceof List
def tripled = nums*.multiply(3)
assert tripled == [3, 6, 9] && doubled instanceof List
assert nums.any{ it > 2 }
assert nums.every{ it < 4 }
assert nums.average() == 2
assert nums.min() == 1
assert nums.max() == 3
assert nums.sum() == 6
assert nums.indices == [0, 1, 2]
assert nums.swap(0, 2) == [3, 2, 1] as int[]
4. Working with legacy Date/Calendar types
The groovy-dateutil
module supports numerous extensions for working with
Java’s classic Date
and Calendar
classes.
You can access the properties of a Date
or Calendar
using the normal array index notation
with the constant field numbers from the Calendar
class as shown in the following example:
import static java.util.Calendar.* (1)
def cal = Calendar.instance
cal[YEAR] = 2000 (2)
cal[MONTH] = JANUARY (2)
cal[DAY_OF_MONTH] = 1 (2)
assert cal[DAY_OF_WEEK] == SATURDAY (3)
1 | Import the constants |
2 | Setting the calendar’s year, month and day of month |
3 | Accessing the calendar’s day of week |
Groovy supports arithmetic on and iteration between Date
and Calendar
instances as shown in the following example:
def utc = TimeZone.getTimeZone('UTC')
Date date = Date.parse("yyyy-MM-dd HH:mm", "2010-05-23 09:01", utc)
def prev = date - 1
def next = date + 1
def diffInDays = next - prev
assert diffInDays == 2
int count = 0
prev.upto(next) { count++ }
assert count == 3
You can parse strings into dates and output dates into formatted strings:
def orig = '2000-01-01'
def newYear = Date.parse('yyyy-MM-dd', orig)
assert newYear[DAY_OF_WEEK] == SATURDAY
assert newYear.format('yyyy-MM-dd') == orig
assert newYear.format('dd/MM/yyyy') == '01/01/2000'
You can also create a new Date or Calendar based on an existing one:
def newYear = Date.parse('yyyy-MM-dd', '2000-01-01')
def newYearsEve = newYear.copyWith(
year: 1999,
month: DECEMBER,
dayOfMonth: 31
)
assert newYearsEve[DAY_OF_WEEK] == FRIDAY
5. Working with Date/Time types
The groovy-datetime
module supports numerous extensions for working with
the Date/Time API
introduced in Java 8. This documentation refers to the data types defined by this API as
"JSR 310 types."
5.1. Formatting and parsing
A common use case when working with date/time types is to convert them to Strings (formatting) and from Strings (parsing). Groovy provides these additional formatting methods:
Method | Description | Example |
---|---|---|
|
For |
|
For |
|
|
For |
|
|
|
For |
|
For |
|
|
For |
|
|
|
For |
|
For |
|
|
For |
|
|
|
For |
|
For |
|
|
For |
|
|
|
Formats with
|
|
For parsing, Groovy adds a static parse
method to many of the JSR 310 types. The method
takes two arguments: the value to be formatted and the pattern to use. The pattern is
defined by the
java.time.format.DateTimeFormatter
API.
As an example:
def date = LocalDate.parse('Jun 3, 04', 'MMM d, yy')
assert date == LocalDate.of(2004, Month.JUNE, 3)
def time = LocalTime.parse('4:45', 'H:mm')
assert time == LocalTime.of(4, 45, 0)
def offsetTime = OffsetTime.parse('09:47:51-1234', 'HH:mm:ssZ')
assert offsetTime == OffsetTime.of(9, 47, 51, 0, ZoneOffset.ofHoursMinutes(-12, -34))
def dateTime = ZonedDateTime.parse('2017/07/11 9:47PM Pacific Standard Time', 'yyyy/MM/dd h:mma zzzz')
assert dateTime == ZonedDateTime.of(
LocalDate.of(2017, 7, 11),
LocalTime.of(21, 47, 0),
ZoneId.of('America/Los_Angeles')
)
Note that these parse
methods have a different argument ordering than the static
parse
method Groovy added to java.util.Date
.
This was done to be consistent with the existing parse
methods of the Date/Time API.
5.2. Manipulating date/time
5.2.1. Addition and subtraction
Temporal
types have plus
and minus
methods for adding or subtracting a provided
java.time.temporal.TemporalAmount
argument. Because Groovy maps the +
and -
operators
to single-argument methods of these names, a more natural expression syntax can be used to add and subtract.
def aprilFools = LocalDate.of(2018, Month.APRIL, 1)
def nextAprilFools = aprilFools + Period.ofDays(365) // add 365 days
assert nextAprilFools.year == 2019
def idesOfMarch = aprilFools - Period.ofDays(17) // subtract 17 days
assert idesOfMarch.dayOfMonth == 15
assert idesOfMarch.month == Month.MARCH
Groovy provides additional plus
and minus
methods that accept an integer argument,
enabling the above to be rewritten more succinctly:
def nextAprilFools = aprilFools + 365 // add 365 days
def idesOfMarch = aprilFools - 17 // subtract 17 days
The unit of these integers depends on the JSR 310 type operand. As evident above,
integers used with ChronoLocalDate
types like LocalDate
have a unit of
days.
Integers used with Year
and YearMonth
have a unit of
years and
months, respectively.
All other types have a unit of
seconds,
such as LocalTime
, for instance:
def mars = LocalTime.of(12, 34, 56) // 12:34:56 pm
def thirtySecondsToMars = mars - 30 // go back 30 seconds
assert thirtySecondsToMars.second == 26
5.2.2. Multiplication and division
The *
operator can be used to multiply Period
and Duration
instances by an
integer value; the /
operator can be used to divide Duration
instances by an integer value.
def period = Period.ofMonths(1) * 2 // a 1-month period times 2
assert period.months == 2
def duration = Duration.ofSeconds(10) / 5// a 10-second duration divided by 5
assert duration.seconds == 2
5.2.3. Incrementing and decrementing
The ++
and --
operators can be used increment and decrement date/time values by one unit. Since the JSR 310 types
are immutable, the operation will create a new instance with the incremented/decremented value and reassign it to the
reference.
def year = Year.of(2000)
--year // decrement by one year
assert year.value == 1999
def offsetTime = OffsetTime.of(0, 0, 0, 0, ZoneOffset.UTC) // 00:00:00.000 UTC
offsetTime++ // increment by one second
assert offsetTime.second == 1
5.2.4. Negation
The Duration
and Period
types represent a negative or positive length of time.
These can be negated with the unary -
operator.
def duration = Duration.ofSeconds(-15)
def negated = -duration
assert negated.seconds == 15
5.3. Interacting with date/time values
5.3.1. Property notation
The
getLong(TemporalField)
method of TemporalAccessor
types (e.g. LocalDate
,
LocalTime
, ZonedDateTime
, etc.) and the
get(TemporalUnit)
method of TemporalAmount
types (namely Period
and Duration
), can be invoked with
Groovy’s property notation. For example:
def date = LocalDate.of(2018, Month.MARCH, 12)
assert date[ChronoField.YEAR] == 2018
assert date[ChronoField.MONTH_OF_YEAR] == Month.MARCH.value
assert date[ChronoField.DAY_OF_MONTH] == 12
assert date[ChronoField.DAY_OF_WEEK] == DayOfWeek.MONDAY.value
def period = Period.ofYears(2).withMonths(4).withDays(6)
assert period[ChronoUnit.YEARS] == 2
assert period[ChronoUnit.MONTHS] == 4
assert period[ChronoUnit.DAYS] == 6
5.3.2. Ranges, upto
, and downto
The JSR 310 types can be used with the range operator.
The following example iterates between today and the LocalDate
six days from now,
printing out the day of the week for each iteration. As both range bounds are inclusive,
this prints all seven days of the week.
def start = LocalDate.now()
def end = start + 6 // 6 days later
(start..end).each { date ->
println date.dayOfWeek
}
The upto
method will accomplish the same as the range in the above example.
The upto
method iterates from the earlier start
value (inclusive) to the later end
value
(also inclusive), calling the closure with the incremented next
value once per iteration.
def start = LocalDate.now()
def end = start + 6 // 6 days later
start.upto(end) { next ->
println next.dayOfWeek
}
The downto
method iterates in the opposite direction, from a later start
value
to an earlier end
value.
The unit of iteration for upto
, downto
, and ranges is the same as the unit for addition
and subtraction: LocalDate
iterates by one day at a time,
YearMonth
iterates by one month, Year
by one year, and everything else by one second.
Both methods also support an optional a TemporalUnit
argument to change the unit of
iteration.
Consider the following example, where March 1st, 2018 is iterated up to March 2nd, 2018 using an iteration unit of months.
def start = LocalDate.of(2018, Month.MARCH, 1)
def end = start + 1 // 1 day later
int iterationCount = 0
start.upto(end, ChronoUnit.MONTHS) { next ->
println next
++iterationCount
}
assert iterationCount == 1
Since the start
date is inclusive, the closure is called with a next
date value of March 1st.
The upto
method then increments the date by one month, yielding the date, April 1st.
Because this date is after the specified end
date of March 2nd, the iteration stops immediately,
having only called the closure once. This behavior is the same for the downto
method except that
the iteration will stop as soon as the value of next
becomes earlier than the targeted end
date.
In short, when iterating with the upto
or downto
methods with a custom unit of iteration,
the current value of iteration will never exceed the end value.
5.3.3. Combining date/time values
The left-shift operator (<<
) can be used to combine two JSR 310 types into an aggregate type.
For example, a LocalDate
can be left-shifted into a LocalTime
to produce a composite
LocalDateTime
instance.
MonthDay monthDay = Month.JUNE << 3 // June 3rd
LocalDate date = monthDay << Year.of(2015) // 3-Jun-2015
LocalDateTime dateTime = date << LocalTime.NOON // 3-Jun-2015 @ 12pm
OffsetDateTime offsetDateTime = dateTime << ZoneOffset.ofHours(-5) // 3-Jun-2015 @ 12pm UTC-5
The left-shift operator is reflexive; the order of the operands does not matter.
def year = Year.of(2000)
def month = Month.DECEMBER
YearMonth a = year << month
YearMonth b = month << year
assert a == b
5.3.4. Creating periods and durations
The right-shift operator (>>
) produces a value representing the period or duration between the
operands. For ChronoLocalDate
, YearMonth
, and Year
, the operator yields
a Period
instance:
def newYears = LocalDate.of(2018, Month.JANUARY, 1)
def aprilFools = LocalDate.of(2018, Month.APRIL, 1)
def period = newYears >> aprilFools
assert period instanceof Period
assert period.months == 3
The operator produces a Duration
for the time-aware JSR types:
def duration = LocalTime.NOON >> (LocalTime.NOON + 30)
assert duration instanceof Duration
assert duration.seconds == 30
If the value on the left-hand side of the operator is earlier than the value on the right-hand side, the result is positive. If the left-hand side is later than the right-hand side, the result is negative:
def decade = Year.of(2010) >> Year.of(2000)
assert decade.years == -10
5.4. Converting between legacy and JSR 310 types
Despite the shortcomings of Date
, Calendar
, and TimeZone
types in the java.util
package
they are fairly common in Java APIs (at least in those prior to Java 8).
To accommodate use of such APIs, Groovy provides methods for converting between the
JSR 310 types and legacy types.
Most JSR types have been fitted with toDate()
and toCalendar()
methods for
converting to relatively equivalent java.util.Date
and java.util.Calendar
values.
Both ZoneId
and ZoneOffset
have been given a toTimeZone()
method for converting to
java.util.TimeZone
.
// LocalDate to java.util.Date
def valentines = LocalDate.of(2018, Month.FEBRUARY, 14)
assert valentines.toDate().format('MMMM dd, yyyy') == 'February 14, 2018'
// LocalTime to java.util.Date
def noon = LocalTime.of(12, 0, 0)
assert noon.toDate().format('HH:mm:ss') == '12:00:00'
// ZoneId to java.util.TimeZone
def newYork = ZoneId.of('America/New_York')
assert newYork.toTimeZone() == TimeZone.getTimeZone('America/New_York')
// ZonedDateTime to java.util.Calendar
def valAtNoonInNY = ZonedDateTime.of(valentines, noon, newYork)
assert valAtNoonInNY.toCalendar().getTimeZone().toZoneId() == newYork
Note that when converting to a legacy type:
-
Nanosecond values are truncated to milliseconds. A
LocalTime
, for example, with aChronoUnit.NANOS
value of 999,999,999 nanoseconds translates to 999 milliseconds. -
When converting the "local" types (
LocalDate
,LocalTime
, andLocalDateTime
), the time zone of the returnedDate
orCalendar
will be the system default. -
When converting a time-only type (
LocalTime
orOffsetTime
), the year/month/day of theDate
orCalendar
is set to the current date. -
When converting a date-only type (
LocalDate
), the time value of theDate
orCalendar
will be cleared, i.e.00:00:00.000
. -
When converting an
OffsetDateTime
to aCalendar
, only the hours and minutes of theZoneOffset
convey into the correspondingTimeZone
. Fortunately, Zone Offsets with non-zero seconds are rare.
Groovy has added a number of methods to Date
and Calendar
for converting into the various JSR 310 types:
Date legacy = Date.parse('yyyy-MM-dd HH:mm:ss.SSS', '2010-04-03 10:30:58.999')
assert legacy.toLocalDate() == LocalDate.of(2010, 4, 3)
assert legacy.toLocalTime() == LocalTime.of(10, 30, 58, 999_000_000) // 999M ns = 999ms
assert legacy.toOffsetTime().hour == 10
assert legacy.toYear() == Year.of(2010)
assert legacy.toMonth() == Month.APRIL
assert legacy.toDayOfWeek() == DayOfWeek.SATURDAY
assert legacy.toMonthDay() == MonthDay.of(Month.APRIL, 3)
assert legacy.toYearMonth() == YearMonth.of(2010, Month.APRIL)
assert legacy.toLocalDateTime().year == 2010
assert legacy.toOffsetDateTime().dayOfMonth == 3
assert legacy.toZonedDateTime().zone == ZoneId.systemDefault()
6. Handy utilities
6.1. ConfigSlurper
ConfigSlurper
is a utility class for reading configuration files defined in the form of Groovy scripts. Like it is
the case with Java *.properties
files, ConfigSlurper
allows a dot notation. But in addition, it allows for Closure scoped
configuration values and arbitrary object types.
def config = new ConfigSlurper().parse('''
app.date = new Date() (1)
app.age = 42
app { (2)
name = "Test${42}"
}
''')
assert config.app.date instanceof Date
assert config.app.age == 42
assert config.app.name == 'Test42'
1 | Usage of the dot notation |
2 | Usage of Closure scopes as an alternative to the dot notation |
As can be seen in the above example, the parse
method can be used to retrieve groovy.util.ConfigObject
instances. The
ConfigObject
is a specialized java.util.Map
implementation that either returns the configured value or a new ConfigObject
instance but never null
.
def config = new ConfigSlurper().parse('''
app.date = new Date()
app.age = 42
app.name = "Test${42}"
''')
assert config.test != null (1)
1 | config.test has not been specified yet it returns a ConfigObject when being called. |
In the case of a dot being part of a configuration variable name, it can be escaped by using single or double quotes.
def config = new ConfigSlurper().parse('''
app."person.age" = 42
''')
assert config.app."person.age" == 42
In addition, ConfigSlurper
comes with support for environments
. The environments
method can be used to hand over
a Closure instance that itself may consist of a several sections. Let’s say we wanted to create a particular configuration
value for the development environment. When creating the ConfigSlurper
instance we can use the ConfigSlurper(String)
constructor to specify the target environment.
def config = new ConfigSlurper('development').parse('''
environments {
development {
app.port = 8080
}
test {
app.port = 8082
}
production {
app.port = 80
}
}
''')
assert config.app.port == 8080
The ConfigSlurper environments aren’t restricted to any particular environment names. It solely depends on the
ConfigSlurper client code what value are supported and interpreted accordingly.
|
The environments
method is built-in but the registerConditionalBlock
method can be used to register other method names
in addition to the environments
name.
def slurper = new ConfigSlurper()
slurper.registerConditionalBlock('myProject', 'developers') (1)
def config = slurper.parse('''
sendMail = true
myProject {
developers {
sendMail = false
}
}
''')
assert !config.sendMail
1 | Once the new block is registered ConfigSlurper can parse it. |
For Java integration purposes the toProperties
method can be used to convert the ConfigObject
to a java.util.Properties
object that might be stored to a *.properties
text file. Be aware though that the configuration values are converted to
String
instances during adding them to the newly created Properties
instance.
def config = new ConfigSlurper().parse('''
app.date = new Date()
app.age = 42
app {
name = "Test${42}"
}
''')
def properties = config.toProperties()
assert properties."app.date" instanceof String
assert properties."app.age" == '42'
assert properties."app.name" == 'Test42'
6.2. Expando
The Expando
class can be used to create a dynamically expandable object. Despite its name it does not use the
ExpandoMetaClass
underneath. Each Expando
object represents a standalone, dynamically-crafted instance that can be
extended with properties (or methods) at runtime.
def expando = new Expando()
expando.name = 'John'
assert expando.name == 'John'
A special case occurs when a dynamic property registers a Closure
code block. Once being registered it can be invoked
as it would be done with a method call.
def expando = new Expando()
expando.toString = { -> 'John' }
expando.say = { String s -> "John says: ${s}" }
assert expando as String == 'John'
assert expando.say('Hi') == 'John says: Hi'
6.3. Observable list, map and set
Groovy comes with observable lists, maps and sets. Each of these collections trigger java.beans.PropertyChangeEvent
events when elements
are added, removed or changed. Note that a PropertyChangeEvent
is not only signalling that a certain event has
occurred, moreover, it holds information on the property name and the old/new value a certain property has been changed to.
Depending on the type of change that has happened, observable collections might fire more specialized PropertyChangeEvent
types. For example, adding an element to an observable list fires an ObservableList.ElementAddedEvent
event.
def event (1)
def listener = {
if (it instanceof ObservableList.ElementEvent) { (2)
event = it
}
} as PropertyChangeListener
def observable = [1, 2, 3] as ObservableList (3)
observable.addPropertyChangeListener(listener) (4)
observable.add 42 (5)
assert event instanceof ObservableList.ElementAddedEvent
def elementAddedEvent = event as ObservableList.ElementAddedEvent
assert elementAddedEvent.changeType == ObservableList.ChangeType.ADDED
assert elementAddedEvent.index == 3
assert elementAddedEvent.oldValue == null
assert elementAddedEvent.newValue == 42
1 | Declares a PropertyChangeEventListener that is capturing the fired events |
2 | ObservableList.ElementEvent and its descendant types are relevant for this listener |
3 | Registers the listener |
4 | Creates an ObservableList from the given list |
5 | Triggers an ObservableList.ElementAddedEvent event |
Be aware that adding an element in fact causes two events to be triggered. The first is of type ObservableList.ElementAddedEvent ,
the second is a plain PropertyChangeEvent that informs listeners about the change of property size .
|
The ObservableList.ElementClearedEvent
event type is another interesting one. Whenever multiple
elements are removed, for example when calling clear()
, it holds the elements being removed from the list.
def event
def listener = {
if (it instanceof ObservableList.ElementEvent) {
event = it
}
} as PropertyChangeListener
def observable = [1, 2, 3] as ObservableList
observable.addPropertyChangeListener(listener)
observable.clear()
assert event instanceof ObservableList.ElementClearedEvent
def elementClearedEvent = event as ObservableList.ElementClearedEvent
assert elementClearedEvent.values == [1, 2, 3]
assert observable.size() == 0
To get an overview of all the supported event types the reader is encouraged to have a look at the JavaDoc documentation or the source code of the observable collection in use.
ObservableMap
and ObservableSet
come with the same concepts as we have seen for ObservableList
in this section.