Groovy Tables
Overview
Groovy Tables is a groovy library which allows you to create lists of objects using a table like grammar. It was written primarily for use when writing test cases.
How to use
Gradle
repositories {
mavenCentral()
}
dependencies {
testCompile group: 'org.tools4j', name: 'groovy-tables', version: '1.6'
}
Maven
<dependency>
<groupId>org.tools4j</groupId>
<artifactId>groovy-tables</artifactId>
<version>1.6</version>
<scope>test</scope>
</dependency>
Examples
Object creation
The following example creates a list of Book objects
List<Book> books = GroovyTables.createListOf(Book.class).fromTable {
author | title | cost | year
"Jane Austen" | "Pride and Prejudice" | 12.95 | 1813
"Harper Lee" | "To Kill a Mockingbird" | 14.95 | 1960
"F. Scott Fitzgerald" | "The Great Gatsby" | 12.95 | 1925
"Charlotte Brontë" | "Jane Eyre" | 6.95 | 1847
"George Orwell" | "1984" | 8.95 | 1949
"J.D. Salinger" | "The Catcher in the Rye" | 6.95 | 1951
"William Shakespeare" | "Romeo and Juliet" | 5.95 | 1597
}
If you wish to, you can create your own reusable constructor method, giving you an even more concise syntax, e.g:
private List<Book> books(Closure closure){
return GroovyTables.createListOf(Book.class).fromTable(closure)
}
...and in your test method:
List<Book> books = books {
author | title | cost | year
"Jane Austen" | "Pride and Prejudice" | 12.95 | 1813
"Harper Lee" | "To Kill a Mockingbird" | 14.95 | 1960
"F. Scott Fitzgerald" | "The Great Gatsby" | 12.95 | 1925
"Charlotte Brontë" | "Jane Eyre" | 6.95 | 1847
"George Orwell" | "1984" | 8.95 | 1949
"J.D. Salinger" | "The Catcher in the Rye" | 6.95 | 1951
"William Shakespeare" | "Romeo and Juliet" | 5.95 | 1597
}
Here is another example creating a list of quotes:
List<Quote> quotes = GroovyTables.createListOf(Quote).fromTable {
symbol | price | quantity
"AUD/USD" | 1.0023 | 1200000
"AUD/USD" | 1.0024 | 1400000
"AUD/USD" | 1.0026 | 2000000
"AUD/USD" | 1.0029 | 5000000
}
By default groovytabledsl finds a suitable constructor or static factory method to create instances of the given class. You can give the api a filter to 'force' a certain mode of construction. This example passes a filter which only accepts constructors. e.g.
List<Book> books = GroovyTables.createFromTable(Book.class, ConstructionMethodFilter.CONSTRUCTORS, {
author | title | cost | year
"Jane Austen" | "Pride and Prejudice" | 12.95 | 1813
"Harper Lee" | "To Kill a Mockingbird" | 14.95 | 1960
"F. Scott Fitzgerald" | "The Great Gatsby" | 12.95 | 1925
"Charlotte Brontë" | "Jane Eyre" | 6.95 | 1847
"George Orwell" | "1984" | 8.95 | 1949
"J.D. Salinger" | "The Catcher in the Rye" | 6.95 | 1951
"William Shakespeare" | "Romeo and Juliet" | 5.95 | 1597
});
The filter is a just a Predicate so you are free to create your own filters. The ConstructionMethodFilter also provides some method handy 'chainable' methods to help build filters.
List<Book> books = GroovyTables.createFromTable(Book.class, ConstructionMethodFilter.filter().withStaticFactoryMethods().withName("create"), {
author | title | cost | year
"Jane Austen" | "Pride and Prejudice" | 12.95 | 1813
"Harper Lee" | "To Kill a Mockingbird" | 14.95 | 1960
"F. Scott Fitzgerald" | "The Great Gatsby" | 12.95 | 1925
"Charlotte Brontë" | "Jane Eyre" | 6.95 | 1847
"George Orwell" | "1984" | 8.95 | 1949
"J.D. Salinger" | "The Catcher in the Rye" | 6.95 | 1951
"William Shakespeare" | "Romeo and Juliet" | 5.95 | 1597
});
The field names (column headings) are only used when the api attempts to call setter methods after constructing an object. So if field names are omitted, the API will simply not attempt to construct an instance using reflection.
Closure chaining
A new recently added feature, you can chain a closure at the end of the table, to consume the table. e.g.
GroovyTables.withTable {
side | symbol | price | qty
Side.BUY | "AUD/USD" | 1.0023 | 1200000
Side.BUY | "AUD/USD" | 1.0022 | 1400000
Side.BUY | "AUD/USD" | 1.0020 | 2000000
Side.BUY | "AUD/USD" | 1.0019 | 5000000
Side.SELL | "AUD/USD" | 1.0025 | 1100000
Side.SELL | "AUD/USD" | 1.0026 | 1600000
Side.SELL | "AUD/USD" | 1.0028 | 2020000
}.forEachRow {
quoteBook.getSide(side).add(new Quote(symbol: symbol, price: price, quantity: qty))
}
There is also the option of calling a chained closure, with explicitly defined closure arguments. (For this you must not specify column headings):
GroovyTables.withTable {
Side.BUY | "AUD/USD" | 1.0023 | 1200000
Side.BUY | "AUD/USD" | 1.0022 | 1400000
Side.BUY | "AUD/USD" | 1.0020 | 2000000
Side.BUY | "AUD/USD" | 1.0019 | 5000000
Side.SELL | "AUD/USD" | 1.0025 | 1100000
Side.SELL | "AUD/USD" | 1.0026 | 1600000
Side.SELL | "AUD/USD" | 1.0028 | 2020000
}.forEachRow { Side side, String symbol, double price, int qty ->
quoteBook.getSide(side).add(new Quote(symbol: symbol, price: price, quantity: qty))
}
Simple array creation
You can also create simple lists of arrays. e.g.
final List<Object[]> listOfArrays = GroovyTables.createListOfArrays {
1 | 2 | 3
2 | 3 | 5
55 | 5 | 60
}
println listOfArrays
Output:
[[1, 2, 3], [2, 3, 5], [55, 5, 60]]
Some details
Methods of construction
There are three methods of construction. Class Constructors, Static Factory Methods, and Reflection
- Class Constructors - The API takes constructors as a preference compared to the other two methods. The API will look at each constructor and will compare the parameters of the constructor, with the given arguments. If the given arguments can be coerced into the list of parameters in the constructor, then that constructor is deemed a candidate.
- Static Factory Methods - The API first builds a list of static class methods, which return a type which matches the class we are constructing. Then, the same as for contructors, the method's parameters are compared with the given arguments to discover matches.
- Reflection - The API will first look to see if a zero arg constructor exists. If it does, it will then see if a suitable setter exists for each argument given (this is why field names are required when the Reflection method is used). If a setter cannot be found for a field, then the API checks whether a field can be accessed directly. Once it has confirmed that each column has a corresponding field that can be accessed, reflection is deemed a construction candidate.
How a construction method is selected
Suitable construction methods are analyzed before construction takes place. A decision is then made regarding the most suitable construction method. This decision is made based on:
- Any construction method filter that the caller has passed. (No filter is passed by default).
- The type of construction. Class Constructors take precedence over Static Factory Methods which take precedence over straight Reflection
- Whether any argument coercion is required. For example, a static factory method whose parameters exactly match the types passed as arguments in the table, will take precedence over a constructor which requires that an Integer argument be cast to a Long constructor parameter.
A construction method is selected separately for each 'line' of the table. In the future we might cache last used construction methods but initial performance testing deemed little benefit was gained in terms of milliseconds of execution.
Turning on logging
If you want to debug/understand what groovy-tables is doing, you can turn logging on. Logging at the moment just goes to System.out
org.tools4j.groovytables.Logger.setCurrentLevel(org.tools4j.groovytables.Logger.Level.DEBUG)
Acknowledgments
Thanks to Christian Baranowski whose blog post here: http://tux2323.blogspot.co.uk/2013/04/simple-table-dsl-in-groovy.html, inspired the fancy usage of operator overloading to achieve the table like grammar.