- •Credits
- •About the Authors
- •About the Reviewers
- •www.PacktPub.com
- •Table of Contents
- •Preface
- •Introduction
- •Installing Groovy on Windows
- •Installing Groovy on Linux and OS X
- •Executing Groovy code from the command line
- •Using Groovy as a command-line text file editor
- •Running Groovy with invokedynamic support
- •Building Groovy from source
- •Managing multiple Groovy installations on Linux
- •Using groovysh to try out Groovy commands
- •Starting groovyConsole to execute Groovy snippets
- •Configuring Groovy in Eclipse
- •Configuring Groovy in IntelliJ IDEA
- •Introduction
- •Using Java classes from Groovy
- •Embedding Groovy into Java
- •Compiling Groovy code
- •Generating documentation for Groovy code
- •Introduction
- •Searching strings with regular expressions
- •Writing less verbose Java Beans with Groovy Beans
- •Inheriting constructors in Groovy classes
- •Defining code as data in Groovy
- •Defining data structures as code in Groovy
- •Implementing multiple inheritance in Groovy
- •Defining type-checking rules for dynamic code
- •Adding automatic logging to Groovy classes
- •Introduction
- •Reading from a file
- •Reading a text file line by line
- •Processing every word in a text file
- •Writing to a file
- •Replacing tabs with spaces in a text file
- •Deleting a file or directory
- •Walking through a directory recursively
- •Searching for files
- •Changing file attributes on Windows
- •Reading data from a ZIP file
- •Reading an Excel file
- •Extracting data from a PDF
- •Introduction
- •Reading XML using XmlSlurper
- •Reading XML using XmlParser
- •Reading XML content with namespaces
- •Searching in XML with GPath
- •Searching in XML with XPath
- •Constructing XML content
- •Modifying XML content
- •Sorting XML nodes
- •Serializing Groovy Beans to XML
- •Introduction
- •Parsing JSON messages with JsonSlurper
- •Constructing JSON messages with JsonBuilder
- •Modifying JSON messages
- •Validating JSON messages
- •Converting JSON message to XML
- •Converting JSON message to Groovy Bean
- •Using JSON to configure your scripts
- •Introduction
- •Creating a database table
- •Connecting to an SQL database
- •Modifying data in an SQL database
- •Calling a stored procedure
- •Reading BLOB/CLOB from a database
- •Building a simple ORM framework
- •Using Groovy to access Redis
- •Using Groovy to access MongoDB
- •Using Groovy to access Apache Cassandra
- •Introduction
- •Downloading content from the Internet
- •Executing an HTTP GET request
- •Executing an HTTP POST request
- •Constructing and modifying complex URLs
- •Issuing a REST request and parsing a response
- •Issuing a SOAP request and parsing a response
- •Consuming RSS and Atom feeds
- •Using basic authentication for web service security
- •Using OAuth for web service security
- •Introduction
- •Querying methods and properties
- •Dynamically extending classes with new methods
- •Overriding methods dynamically
- •Adding performance logging to methods
- •Adding transparent imports to a script
- •DSL for executing commands over SSH
- •DSL for generating reports from logfiles
- •Introduction
- •Processing collections concurrently
- •Downloading files concurrently
- •Splitting a large task into smaller parallel jobs
- •Running tasks in parallel and asynchronously
- •Using actors to build message-based concurrency
- •Using STM to atomically update fields
- •Using dataflow variables for lazy evaluation
- •Index
Using Groovy Language Features
Defining data structures as code in Groovy
An important and powerful part of Groovy is its implementation of the Builder pattern.
This pattern was made famous by the seminal work Design Patterns: Elements of Reusable Object-Oriented Software; Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides.
With builders, data can be defined in a semi-declarative way. Builders are appropriate for the generation of XML, definition of UI components, and anything that is involved with simplifying the construction of object graphs. Consider:
Teacher t = new Teacher('Steve') Student s1 = new Student('John') Student s2 = new Student('Richard') t.addStudent(s1)
t.addStudent(s2)
There are a few issues with the previous code; verbosity and the lack of a hierarchical relationship between objects. This is what we can do with a Builder in Groovy:
teacher ('Jones') { student ('Bob') student ('Sue')
}
Out of the box, Groovy includes a suite of builders for most of the common construction tasks that we might encounter:
ff MarkupBuilder for building an XML-style tagged output (see the Constructing XML content recipe in Chapter 5, Working with XML in Groovy)
ff DOMBuilder for constructing a WC3 DOM tree in memory from the GroovyMarkup-like syntax (http://groovy.codehaus.org/GroovyMarkup)
ff JsonBuilder for building data structures using the JSON format (see the
Constructing JSON messages with JsonBuilder recipe in Chapter 6, Working with JSON in Groovy)
ff SwingBuilder to build Swing-based UIs
ff ObjectGraphBuilder to construct a graph of objects that follow the Java Beans rules
Builders are the fundamental blocks for creating DSLs (Domain Specific Languages) in Groovy. Martin Fowler, in his book Domain-Specific Languages, defines a DSL as a computer programming language of limited expressiveness focused on a particular domain. The limits of a DSL are not bound to its usefulness, but rather to its scope within the domain. A typical example of this contraposition is SQL: the language has enough expressiveness to operate on a database, but it lacks the eloquence to write an operating system.
108
www.it-ebooks.info
Chapter 3
A DSL is a small scale, specifically-focused language, rather than a general purpose language like Java. Chapter 9, Metaprogramming and DSLs in Groovy, contains two recipes that show how you to create DSLs in great detail.
In this recipe, we are going to explore how builders can simplify the creation of an object hierarchy for testing purposes.
Getting ready
Generating test data is a tedious task, especially when we need realistic data that can be used to simulate different situations in our code. Normally, it all starts from a domain model that has to be manually built and fed to some function:
Book book1 = new Book() book.id = 200
book.title = 'Twenty Thousand Leagues Under the Sea' book.author = 'Jules Verne'
Book book2 = new Book() book.id = 201
...
...you get the idea. Test cases quickly become an endless list of hard-to-read object graph definitions and before you know it, your tests are very hard to maintain.
In this recipe, we want to create a simple DSL mainly based on Builders to draw our domain model without the Java Bean ceremony and, as a bonus, be able to generate random data using different strategies.
This is our domain model:
import groovy.transform.*
@Canonical
class ShoppingCart { List<Book> items = [] User user
Address shippingData
}
@Canonical class Book { Long id
String title BigDecimal price
}
109
www.it-ebooks.info
Using Groovy Language Features
@Canonical class User { Long id
String name Address address }
@Canonical class Address {
String street String city String country
}
It's a simplistic domain model for a books e-commerce site. Our goal is to build a DSL that uses the metaprogramming features of Groovy to express the object graph in a concise way.
def shoppingCart = new ECommerceTestDataBuilder().build { items(2) {
title RANDOM_TITLE
id RANDOM_ID, 100, 200000 price 100
}
user {
id RANDOM_ID, 1,500 firstName RANDOM_STRING lastName RANDOM_STRING address RANDOM_US_ADDRESS
}
}
The preceding snippet generates a ShoppingCart object containing two books. Each book has a random title fetched from the amazing Gutenberg project (http://www.gutenberg. org/), a random unique ID with values ranging from 100 to 200000, and a fixed price, set to 100.
How to do it...
First of all, let's create a new Groovy file named randomTestData.groovy and paste the domain model classes defined in the previous paragraph.
1.In the same file, following the domain classes, add the definition for the new builder: class ECommerceTestDataBuilder {
}
110
www.it-ebooks.info
Chapter 3
2.Add the main builder method, build, to the body of the class:
ShoppingCart shoppingCart def books = []
ShoppingCart build(closure) { shoppingCart = new ShoppingCart() closure.delegate = this
closure()
shoppingCart.items = books shoppingCart
}
3.The next method to add is the one for defining the number of books to add to the shopping cart:
void items (int quantity, closure) { closure.delegate = this quantity.times {
books << new Book() closure()
}
}
4.Add the methodMissing method, which is a key part of the DSL architecture, as explained in the next section:
def methodMissing(String name, args) { Book book = books.last()
if (book.hasProperty(name)) {
def dataStrategy = isDataStrategy(args) if (dataStrategy) {
book.@"$name" = dataStrategy.execute()
}else {
book.@"$name" = args[0]
}
}else {
throw new MissingMethodException(
name,
ECommerceTestDataBuilder,
args)
}
}
111
www.it-ebooks.info
Using Groovy Language Features
5.The last method to add for the ECommerceTestDataBuilder class is required for adding random data generation strategies to our DSL:
def isDataStrategy(strategyData) { def strategyClass = null
try {
if (strategyData.length == 1) {
strategyClass = strategyData[0].newInstance()
}else {
strategyClass = strategyData[0].
newInstance(*strategyData[1,-1])
}
if (!(strategyClass instanceof DataPopulationStrategy)) {
strategyClass = null
}
} catch (Exception e) {
}
strategyClass
}
6.The builder code is complete. Now let's add a couple of strategy classes to the script and the main strategy interface:
interface DataPopulationStrategy { def execute()
}
class RANDOM_TITLE implements DataPopulationStrategy {
def titleCache = []
def ignoredTitleWords = ['Page', 'Sort', 'Next']
void getRandomBookTitles() {
def slurper = new XmlSlurper() slurper.setFeature(
'http://apache.org/xml/features/' + 'nonvalidating/load-external-dtd', false)
def dataUrl = 'http://m.gutenberg.org' + '/ebooks/search.mobile'
def orderBy = '/?sort_order=random'
def htmlParser = slurper.parse("${dataUrl}${orderBy}") htmlParser.'**'.findAll{ it.@class == 'title'}.each {
if (it.text().tokenize().disjoint(ignoredTitleWords)) {
112
www.it-ebooks.info
Chapter 3
titleCache << it.text()
}
}
}
def execute() {
if (titleCache.size==0) { randomBookTitles
}
titleCache.pop()
}
}
class RANDOM_ID implements DataPopulationStrategy {
Long minVal
Long maxVal
RANDOM_ID (min, max) { minVal = min
maxVal = max
}
def execute() {
double rnd = new Random().nextDouble() minVal + (long) (rnd * (maxVal - minVal))
}
}
7.It is time to put our code to the test:
def shoppingCart = new ECommerceTestDataBuilder().build { items(5) {
title RANDOM_TITLE
id RANDOM_ID, 100, 200000 price 100
}
}
assert shoppingCart.items.size == 5 shoppingCart.items.each {
assert it.price == 100
assert it.id > 100 && it.id < 200000
}
113
www.it-ebooks.info
Using Groovy Language Features
How it works...
The domain model's classes are standard Groovy Beans annotated with the @Canonical annotation. The annotation is discussed in detail in the Writing less verbose Java Beans with Groovy Beans recipe. In short, it adds an implementation of equals, hashCode, and toString, along with a tuple constructors, to a bean.
@Canonical class Book { Long id
String title BigDecimal price
}
Book b = new Book(2001, 'Pinocchio', 22.3) println b.toString()
The preceding code snippet will print:
Book(2001, Pinocchio, 22.3)
The method build that was displayed at step 2 is the builder's entry method:
def shoppingCart = new ECommerceTestDataBuilder().build {
...
}
The build method takes a closure as only argument. The closure is where most of the magic happens. Let's dig into the closure code:
items(5) {
title RANDOM_TITLE
id RANDOM_ID, 100, 200000 price 100
}
The items method that was defined at step 3 is invoked with two arguments: the number of books to create, and another closure where the random data strategies are defined. In Groovy, if the last argument of a method is a closure, it does not need to be inside the parentheses of the invoked method:
def doSomething(int i, Closure c) { c(i)
}
something(i) { // closure code
}
114
www.it-ebooks.info
Chapter 3
You may have noticed that both methods, build and item, have a call to the delegate method of the closure just before the closure is invoked:
closure.delegate = this closure()
The delegate method allows you to change the scope of the closure so that the methods invoked from within the closure are delegated directly to the builder class.
Inside the items block, we define the (random) values that we want to be assigned to each property of the Book object. The properties are only defined in the Book object but are not visible by the Builder. So how is the Builder able to resolve a call to the title or price property of Book and assign a value? Every method invoked from inside the items block, in fact, does not exist.
Thanks to Groovy's metaprogramming capabilities, we can intercept method calls and create methods on the fly. In particular, the most common technique for intercepting calls in Groovy is to implement the methodMissing method on a Groovy class. This method can be considered as a net for undefined functions in a class. Every time a call is executed against a missing method, the runtime routes the call to the methodMissing routine, just before throwing a MissingMethodException exception. This offers a chance to define an implementation for these ghost methods.
Let's take a closer look:
title RANDOM_TITLE
The method title does not exist in the Builder code. When an invocation to this method is executed from within the closure, the dispatcher, before giving up and throwing a MissingMethodException, tries to see if methodMissing can be used to resolve the method.
Groovy allows you to call a method and omit the parentheses if there is at least one parameter and there is no ambiguity. This is the case for the title method, which can be written as title(RANDOM_TITLE), but obviously the DSL is much more readable without parentheses.
Inside the Builder's methodMissing method, the code does the following:
1.Fetches the last book created by the items method.
2.Checks that the book has a property named as the missing method, using the hasProperty method on the object itself.
3.If the missing method is named as one of the book's properties, the code either directly assigns the parameter passed after the missing method to the appropriate property of Book, or tries to resolve a random strategy through the isDataStrategy method (step 7).
115
www.it-ebooks.info
Using Groovy Language Features
The object property is accessed through the @ operator, which accesses the field directly bypassing the mutators (getters and setters):
book.@"$name" = (dataStrategy) ? dataStrategy.execute() : args[0]
In the previous code snippet, the field is populated with the value defined in the DSL (for example, price 100, or by the result of the random data strategy call). The random data strategy classes must implement the DataPopulationStrategy (step 8). The interface exposes only one execute method. If a strategy requires more arguments, these have to be passed through the constructor (see RANDOM_ID strategy, where the minimum and maximum values are set via the class' constructor). The method isDataStrategy is invoked for
each field specified in the items block. The class accesses the argument passed after the field specification:
title RANDOM_TITLE
It tries to instantiate the class as a DataPopulationStrategy instance. The argument passed to the property must match the class name of the strategy class in order for the strategy resolution to work.
The isDataStrategy method employs a small trick to instantiate the random data strategy class in case the DSL specifies additional arguments, such as:
id RANDOM_ID, 1,500
In the previous snippet, the id field will be populated by the result of the RANDOM_ID strategy that will generate a random number between 1 and 500. If the strategy has no arguments, the class is instantiated with newInstance:
strategyClass = strategyData[0].newInstance()
The strategyData variable corresponds to the args variable of the methodMissing function, which is the caller. args is a list of arguments containing whatever values are passed from the DSL. For instance, id RANDOM_ID, 1, 500 corresponds to calling a method with the following signature:
def id(Object... args) { }
This is why we call newInstance on the first element of the strategyData variable. If the args list contains more than one argument, we assume that the strategy class requires the additional values, so the strategy class is instantiated in this fashion:
strategyClass = strategyData[0].newInstance(*strategyData[1,-1])
In this one-liner, we take advantage of several features of Groovy. The first one is the possibility of calling newInstance with an array of objects. Java makes creating classes dynamically with a constructor much more cumbersome. The second feature is the spread operator. The spread operator (*) is used to tear a List apart into single elements.
116
www.it-ebooks.info