Search

Dark theme | Light theme
Showing posts with label GroovyGoodness:Advanced. Show all posts
Showing posts with label GroovyGoodness:Advanced. Show all posts

April 23, 2014

Groovy Goodness: Restricting Script Syntax With SecureASTCustomizer

Running Groovy scripts with GroovyShell is easy. We can for example incorporate a Domain Specific Language (DSL) in our application where the DSL is expressed in Groovy code and executed by GroovyShell. To limit the constructs that can be used in the DSL (which is Groovy code) we can apply a SecureASTCustomizer to the GroovyShell configuration. With the SecureASTCustomizer the Abstract Syntax Tree (AST) is inspected, we cannot define runtime checks here. We can for example disallow the definition of closures and methods in the DSL script. Or we can limit the tokens to be used to just a plus or minus token. To have even more control we can implement the StatementChecker and ExpressionChecker interface to determine if a specific statement or expression is allowed or not.

In the following sample we first use the properties of the SecureASTCustomizer class to define what is possible and not within the script:

package com.mrhaki.blog

import org.codehaus.groovy.control.customizers.SecureASTCustomizer
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.ast.stmt.*
import org.codehaus.groovy.ast.expr.*
import org.codehaus.groovy.control.MultipleCompilationErrorsException

import static org.codehaus.groovy.syntax.Types.PLUS
import static org.codehaus.groovy.syntax.Types.MINUS
import static org.codehaus.groovy.syntax.Types.EQUAL

// Define SecureASTCustomizer to limit allowed
// language syntax in scripts.
final SecureASTCustomizer astCustomizer = new SecureASTCustomizer(
    // Do not allow method creation.
    methodDefinitionAllowed: false,

    // Do not allow closure creation.
    closuresAllowed: false,

    // No package allowed.
    packageAllowed: false,

    // White or blacklists for imports.
    importsBlacklist: ['java.util.Date'],
    // or importsWhitelist
    staticImportsWhitelist: [],
    // or staticImportBlacklist
    staticStarImportsWhitelist: [],
    // or staticStarImportsBlacklist

    // Make sure indirect imports are restricted.
    indirectImportCheckEnabled: true,

    // Only allow plus and minus tokens.
    tokensWhitelist: [PLUS, MINUS, EQUAL],
    // or tokensBlacklist

    // Disallow constant types.
    constantTypesClassesWhiteList: [Integer, Object, String],
    // or constantTypesWhiteList
    // or constantTypesBlackList
    // or constantTypesClassesBlackList
    
    // Restrict method calls to whitelisted classes.
    // receiversClassesWhiteList: [],
    // or receiversWhiteList
    // or receiversClassesBlackList
    // or receiversBlackList

    // Ignore certain language statement by
    // whitelisting or blacklisting them.
    statementsBlacklist: [IfStatement],
    // or statementsWhitelist

    // Ignore certain language expressions by
    // whitelisting or blacklisting them.
    expressionsBlacklist: [MethodCallExpression]
    // or expresionsWhitelist
)

// Add SecureASTCustomizer to configuration for shell.
final conf = new CompilerConfiguration()
conf.addCompilationCustomizers(astCustomizer)

// Create shell with given configuration.
final shell = new GroovyShell(conf)

// All valid script.
final result = shell.evaluate '''
def s1 = 'Groovy'
def s2 = 'rocks'
"$s1 $s2!"
'''

assert result == 'Groovy rocks!'

// Some invalid scripts.
try {
    // Importing [java.util.Date] is not allowed
    shell.evaluate '''
    new Date() 
    '''
} catch (MultipleCompilationErrorsException e) {
    assert e.message.contains('Indirect import checks prevents usage of expression')
}


try {
    // MethodCallExpression not allowed
    shell.evaluate '''
    println "Groovy rocks!" 
    '''
} catch (MultipleCompilationErrorsException e) {
    assert e.message.contains('MethodCallExpressions are not allowed: this.println(Groovy rocks!)')
}

To have more fine-grained control on which statements and expression are allowed we can implement the StatementChecker and ExpressionChecker interfaces. These interfaces have one method isAuthorized with a boolean return type. We return true if a statement or expression is allowed and false if not.

package com.mrhaki.blog

import org.codehaus.groovy.control.customizers.SecureASTCustomizer
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.ast.stmt.*
import org.codehaus.groovy.ast.expr.*
import org.codehaus.groovy.control.MultipleCompilationErrorsException

import static org.codehaus.groovy.control.customizers.SecureASTCustomizer.ExpressionChecker
import static org.codehaus.groovy.control.customizers.SecureASTCustomizer.StatementChecker


// Define SecureASTCustomizer.
final SecureASTCustomizer astCustomizer = new SecureASTCustomizer()

// Define expression checker to deny 
// usage of variable names with length of 1.
def smallVariableNames = { expr ->
    if (expr instanceof VariableExpression) {
        expr.variable.size() > 1
    } else {
        true
    }
} as ExpressionChecker

astCustomizer.addExpressionCheckers smallVariableNames


// In for loops the collection name
// can only be 'names'.
def forCollectionNames = { statement ->
    if (statement instanceof ForStatement) {
        statement.collectionExpression.variable == 'names'
    } else {    
        true
    }
} as StatementChecker

astCustomizer.addStatementCheckers forCollectionNames


// Add SecureASTCustomizer to configuration for shell.
final CompilerConfiguration conf = new CompilerConfiguration()
conf.addCompilationCustomizers(astCustomizer)

// Create shell with given configuration.
final GroovyShell shell = new GroovyShell(conf)

// All valid script.
final result = shell.evaluate '''
def names = ['Groovy', 'Grails']
for (name in names) {
    print "$name rocks! " 
}

def s1 = 'Groovy'
def s2 = 'rocks'
"$s1 $s2!"
'''

assert result == 'Groovy rocks!'

// Some invalid scripts.
try {
    // Variable s has length 1, which is not allowed.
    shell.evaluate '''
    def s = 'Groovy rocks'
    s
    '''
} catch (MultipleCompilationErrorsException e) {
    assert e.message.contains('Expression [VariableExpression] is not allowed: s')
}


try {
    // Only names as collection expression is allowed.
    shell.evaluate '''
    def languages = ['Groovy', 'Grails']
    for (name in languages) {
        println "$name rocks!" 
    }
    '''
} catch (MultipleCompilationErrorsException e) {
    assert e.message.contains('Statement [ForStatement] is not allowed')
}

Code written with Groovy 2.2.2.

April 4, 2014

Groovy Goodness: Closure as Writable

In a previous post we learned about the Writable interface and how the GString implementation implements this interface. In Groovy we can also use a closure as an implementation of the Writable interface. The Closure class has the method asWritable() that will return a version of the closure with an implementation of the writeTo() method. The Writer object that is used as an argument for the writeTo() method will be passed as argument to the closure. The asWritable() method also adds a toString() implementation for the closure to return the result of a closure as a String.

In the following code we write a sample make() method. The make() method return a Writable closure. The closure is only executed when the writeTo() or toString() method is invoked.

Writable make(Map binding = [:], Closure template) {
    // Use asWritable() to make the closure
    // implement the Writable interface.
    def writableTemplate = template.asWritable()
    
    // Assing binding map as delegate so we can access
    // the keys of the maps as properties in the 
    // closure context.
    writableTemplate.delegate = binding
    
    // Return closure as Writable.
    writableTemplate
}

// Use toString() of Writable closure.
assert make { Writer out -> out <<  "Hello world!" }.toString() == 'Hello world!'

// Provide data for the binding.
// The closure is not executed when the 
// make method is finished.
final writable = make(user:'mrhaki', { out ->
    out.println "Welcome ${user},"
    out.print "Today on ${new Date(year: 114, month: 3, date: 4).format('dd-MM-yyyy')}, "
    out.println "we have a Groovy party!"
})

// We invoke toString() and now the closure
// is executed.
final result = writable.toString()

assert result == '''Welcome mrhaki,
Today on 04-04-2014, we have a Groovy party!
'''

// Append contents to a file.
// NOTE: The leftShift (<<) operator on File is implemented
// in Groovy to use the File.append() method.
// The append() method creates a new Writer and
// invokes the write() method which 
// is re-implemented in Groovy if the argument
// is a Writable object. Then the writeTo() method
// is invoked:
// Writer.write(Writable) becomes Writable.writeTo(Writer).
// So a lot of Groovy magic allows us to use the following one-liner
// and still the writeTo() method is used on Writable.
new File('welcome.txt') << writable

assert new File('welcome.txt').text == '''Welcome mrhaki,
Today on 04-04-2014, we have a Groovy party!
'''

Code written with Groovy 2.2.2

September 10, 2013

Groovy Goodness: Running Scripts from a JAR Archive

We can run Groovy scripts from the command-line with the groovy command followed by the name of Groovy script file. We could already run script with the http: protocol as described in this blog post. Since Groovy 2.1 we can even run scripts that are packaged in an archive with the jar: protocol.

Let's create first a simple Groovy script and package it in a sample.jar file.

// File: cookies.groovy

println 'Running from inside a JAR: '
def cookies = ['Cookie', 'Biscuit'].collect { it.toUpperCase() }.join(',')
println cookies

We can use the jar command to place the file cookies.groovy in sample.jar:

$ jar cvf sample.jar cookies.groovy
added manifest
adding: cookies.groovy(in = 153) (out= 126)(deflated 17%)
$ jar tvf sample.jar
     0 Fri Sep 06 14:50:24 CEST 2013 META-INF/
    68 Fri Sep 06 14:50:24 CEST 2013 META-INF/MANIFEST.MF
   153 Fri Sep 06 14:48:44 CEST 2013 cookies.groovy
$

We now have a single Groovy script, but we can place more scripts inside the JAR file if we want to. To run a specific Groovy script file we must use the jar: protocol. We must specify the location of the JAR file with the file: or http: protocols followed by !/. In some shells the ! symbol is a special character and we might need to escape it. For example on Mac OSX we need to invoke the following command to run our script placed in the JAR file:

$ groovy jar:file:sample.jar'!'/cookies.groovy
Running from inside a JAR:
COOKIE,BISCUIT

We can place the JAR file on a web server and use the jar:http://<address>/sample.jar!/cookies.groovy syntax to run the scripts remotely.

Code written with Groovy 2.1.6

May 28, 2013

Groovy Goodness: @DelegatesTo For Type Checking DSL

Groovy 2.1 introduced the @DelegatesTo annotation. With this annotation we can document a method and tell which class is responsible for executing the code we pass into the method. If we use @TypeChecked or @CompileStatic then the static type checker of the compiler will use this information to check at compile-time if the code is correct. And finally this annotation allows an IDE to give extra support like code completion.

Suppose we have the following class Reservation with the method submit(). The method accepts a closure with methods that need to be applied with the instance of the Reservation class. This is a very common pattern for writing simple DSLs in Groovy.

class Reservation {
    private Date date
    private String event
    private String attendee
    
    void date(final Date date) { this.date = date }
    void event(final String event) { this.event = event }
    void attendee(final String attendee) { this.attendee = attendee }
    
    /** Submit a reservation. 
      * @param config Configuration for reservation, invoking method class on Reservation.
      */
    static void submit(final Closure config) {
        final Reservation reservation = new Reservation()
        reservation.with config
    }

}

class Event {
    /** Use Reservation configuration DSL to submit a reservation. */
    void submitReservation() {
        Reservation.submit {
            date Date.parse('yyyyMMdd', '20130522')
            event 'Gr8Conf'
            attendee 'mrhaki'
            reserved true
        }
    }
}

final event = new Event()
event.submitReservation()

When we look at the code we might already see there is an error. In the Event.submitReservation() method we have the line reserved true, which will try to invoke the reserve() method of the Reservation class. But that method is not defined. When we run the application we get the expected error:

Exception thrown
May 28, 2013 6:58:36 AM org.codehaus.groovy.runtime.StackTraceUtils sanitize
WARNING: Sanitizing stacktrace:
groovy.lang.MissingMethodException: No signature of method: Reservation.reserved() is applicable for argument types: (java.lang.Boolean) values: [true]

To get an error for this line at compile-time we must add some annotation so the Groovy compiler can do static type checking on our code. We change the code and get the following sample:

import groovy.transform.*

class Reservation {
    private Date date
    private String event
    private String attendee
    
    void date(final Date date) { this.date = date }
    void event(final String event) { this.event = event }
    void attendee(final String attendee) { this.attendee = attendee }
    
    /** Submit a reservation. 
      * @param config Configuration for reservation, invoking method class on Reservation.
      */
    static void submit(@DelegatesTo(Reservation) final Closure config) {
        final Reservation reservation = new Reservation()
        reservation.with config
    }

}

@TypeChecked
// @CompileStatic - will also do static type checking
class Event {
    /** Use Reservation configuration DSL to submit a reservation. */
    void submitReservation() {
        Reservation.submit {
            date Date.parse('yyyyMMdd', '20130522')
            event 'Gr8Conf'
            attendee 'mrhaki'
            reserved true
        }
    }
}

final event = new Event()
event.submitReservation()

When the code is compiled we immediately get a compilation error:

1 compilation error:

[Static type checking] - Cannot find matching method Event#reserved(boolean). Please check if the declared type is right and if the method exists.
 at line: 26, column: 13

Wow, this useful! We find errors in our DSL before the code is run.

When we create this code in an IDE like IntelliJ IDEA we also get code completion in the Reservation.submit() method invocation in the Event class. The following screenshot shows code completion and a red font for reserved to indicate the compilation error.


Code written in Groovy 2.1.3

January 30, 2013

Groovy Goodness: Adding Extra Methods Using Extension Modules

Groovy 2.0 brought us extension modules. An extension module is a JAR file with classes that provide extra methods to existing other classes like in the JDK or third-party libraries. Groovy uses this mechanism to add for example extra methods to the File class. We can implement our own extension module to add new extension methods to existing classes. Once we have written the module we can add it to the classpath of our code or application and all the new methods are immediately available.

We define the new extension methods in helper classes, which are part of the module. We can create instance and static extension methods, but we need a separate helper class for each type of extension method. We cannot mix static and instance extension methods in one helper class. First we create a very simple class with an extension method for the String class. The first argument of the extension method defines the type or class we want the method to be added to. The following code shows the method likeAPirate. The extension method needs to be public and static even though we are creating an instance extension method.

// File: src/main/groovy/com/mrhaki/groovy/PirateExtension.groovy
package com.mrhaki.groovy

class PirateExtension {
    static String likeAPirate(final String self) {
        // List of pirate language translations.
        def translations = [
            ["hello", "ahoy"], ["Hi", "Yo-ho-ho"],
            ['are', 'be'], ['am', 'be'], ['is', 'be'],
            ['the', "th'"], ['you', 'ye'], ['your', 'yer'],
            ['of', "o'"]
        ]
        
        // Translate the original String to a 
        // pirate language String.
        String result = self
        translations.each { translate ->
                result = result.replaceAll(translate[0], translate[1])
        }
        result
    }
}

Next we need to create an extension module descriptor file. In this file we define the name of the helper class, so Groovy will know how to use it. The descriptor file needs to be placed in the META-INF/services directory of our module archive or classpath. The name of the file is org.codehaus.groovy.runtime.ExtensionModule. In the file we define the name of our module, version and the name of the helper class. The name of the helper class is defined with the property extensionClasses:

# File: src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule
moduleName = pirate-module
moduleVersion = 1.0
extensionClasses = com.mrhaki.groovy.PirateExtension

Now are extension module is ready. The easiest way to distribute the module is by packaging the code and descriptor file in a JAR file and put it in a artifact repository manager. Other developers can then use build tools like Gradle or Maven to include the extension module in their projects and applications. If we use Gradle to create a JAR file we only needs this small build script:

// File: build.gradle
apply plugin: 'groovy'

repositories.mavenCentral()

dependencies {
    // Since Gradle 1.4 we don't use the groovy configuration
    // to define dependencies. We can simply use the
    // compile and testCompile configurations.
    compile 'org.codehaus.groovy:groovy-all:2.0.6'
}

Now we can invoke $ gradle build and we got ourselves an extension module.

Let's add a test for our new extension method. Because we use Gradle the test classpath already will contain our extension module helper class and descriptor file. In our test we can simply invoke the method and test the results. We are going to use Spock to write a simple specification:

// File: src/test/groovy/com/mrhaki/groovy/PirateExtensionSpec.groovy
package com.mrhaki.groovy

import spock.lang.Specification

class PirateExtensionSpec extends Specification {

    def "likeAPirate method should work as instance method on a String value"() {
        given:
        final String originalText = "Hi, Groovy is the greatest language of the JVM."

        expect:
        originalText.likeAPirate() == "Yo-ho-ho, Groovy be th' greatest language o' th' JVM."
    }

}

We add the dependency to Spock in our Gradle build file:

// File: build.gradle
apply plugin: 'groovy'

repositories.mavenCentral()

dependencies {
    // Since Gradle 1.4 we don't use the groovy configuration
    // to define dependencies. We can simply use the
    // compile and testCompile configurations.
    compile 'org.codehaus.groovy:groovy-all:2.0.6'

    testCompile 'org.spockframework:spock-core:0.7-groovy-2.0'
}

We can run $ gradle test to run the Spock specification and test our new extension method.

To add a static method to an existing class we need to add an extra helper class to our extension module and an extra property to our descriptor file to register the helper class. The first argument of the extension method define the type we want to add a static method to. In the following helper class we add the extension method talkLikeAPirate() to the String class.

// File: src/main/groovy/com/mrhaki/groovy/PirateStaticExtension.groovy
package com.mrhaki.groovy

class PirateStaticExtension {
    static String talkLikeAPirate(final String type) {
        "Arr, me hearty,"
    }
}

We change the descriptor file and add the staticExtensionClasses property:

# File: src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule
moduleName = pirate-module
moduleVersion = 1.0
extensionClasses = com.mrhaki.groovy.PirateExtension
staticExtensionClasses = com.mrhaki.groovy.PirateStaticExtension

In our Spock specification we add an extra test for our new static method talkLikeAPirate() on the String class:

// File: src/test/groovy/com/mrhaki/groovy/PirateExtensionSpec.groovy
package com.mrhaki.groovy

import spock.lang.Specification

class PirateExtensionSpec extends Specification {

    def "likeAPirate method should work as instance method on a String value"() {
        given:
        final String originalText = "Hi, Groovy is the greatest language of the JVM."

        expect:
        originalText.likeAPirate() == "Yo-ho-ho, Groovy be th' greatest language o' th' JVM."
    }

    def "talkLikeAPirate method should work as static method on String class"() {
        expect:
        "Arr, me hearty, Groovy rocks!" == String.talkLikeAPirate() + " Groovy rocks!"
    }

}

Written with Groovy 2.1

September 26, 2012

Groovy Goodness: Using Implicit call() Method

In Groovy we can invoke an implicit call() method on a Groovy object. We can leave out the call method name and just use (). We can use meta programming to add an implementation for the call() method to a class. In the following example script we add an implementation for the call() method with a single parameter to the String class. The implementation returns the element found at the range specified by the argument when we invoke the method:

String.metaClass.call = { range ->
    delegate[range]
}

def value = 'Groovy is Gr8'
assert value(0) == 'G'
assert value(10) == 'G'
assert value(4) == value[4]
assert value.call(1) == value(1)
assert value(0..5) == 'Groovy'

Inspired by the examples http://groovyconsole.appspot.com/view.groovy?id=21006 and http://groovyconsole.appspot.com/script/21005 we can also write our own class and implement the call() method. This can for example be used in DSLs.

class StringConverter {
    def value
    
    def value(s) {
        value = s
        this
    }

    /** Convert characters in value property if cond is true */    
    def upper(cond) {
        value = value.collect { cond(it) ? it.toUpperCase() : it }.join()
    }

    def call(callable) {
        callable
    }
}

def converter = new StringConverter()
converter.with {
    value 'mrhaki' upper { it < 'm' }
    // Equivalent to:
    // value('mrhaki') upper { it < 'm' }
    // or
    // value('mrhaki').call(upper { it < 'm' })
    // or
    // value('mrhaki').call(upper({ it < 'm' }))
}
assert converter.value == 'mrHAKI'

converter.with {
    value('jdriven') upper { it == 'j' || it == 'd' }

    assert value == 'JDriven'
}

(Code written with Groovy 2.0.4)

November 23, 2011

Groovy Goodness: Magic Package to Add Custom MetaClass

Groovy is very dynamic. We can add methods to classes at runtime that don't exist at compile time. We can add our own custom MetaClass at startup time of our application if we follow the magic package naming convention. The naming convention is groovy.runtime.metaclass.[package].[class]MetaClass. For example if we want to change the behavior of the java.lang.String class, then we must write a custom MetaClass with the package name groovy.runtime.metaclass.java.lang and class name StringMetaClass. We can do the same for classes we create ourselves. For example if we have a class myapp.RunApp than the custom metaclass implementation RunAppMetaClass would be in package groovy.runtime.metaclass.myapp.

Our custom MetaClass is extended from DelegatingMetaClass and besides the name of the class and the package we can write our code the way we want.

We must first compile the custom MetaClass and then we must put it in the classpath of the application code that is going to use the MetaClass.

// File: StringMetaClass.groovy
package groovy.runtime.metaclass.java.lang

class StringMetaClass extends DelegatingMetaClass {

    StringMetaClass(MetaClass meta) {
        super(meta)
    }

    Object invokeMethod(Object object, String method, Object[] arguments) {
        if (method == 'hasGroovy') {
            object ==~ /.*[Gg]roovy.*/
        } else {
            super.invokeMethod object, method, arguments
        }
    }
}

The code that will use the delegating metaclass implementation:

// File: StringDelegateSample.groovy

// Original methods are still invoked.
assert 'mrhaki'.toUpperCase() == 'MRHAKI'

// Invoke 'hasGroovy' method we added via the DelegatingMetaClass.
assert !'Java'.hasGroovy()
assert 'mrhaki loves Groovy'.hasGroovy()
assert 'Groovy'.toLowerCase().hasGroovy()

First we compile our StringMetaClass:
$ groovyc StringMetaClass.groovy.

Next we can run the StringDelegateSample.groovy file if we put the generated class file in our classpath:
$ groovy -cp . StringDelegateSample


September 27, 2011

Groovy Goodness: Use inject Method on a Map

The inject() method is since Groovy 1.8.1 also available for Map objects. The closure arguments accepts two or three arguments. With the three-argument variant we get the key and value separately as arguments. Otherwise we get a map entry as closure argument.

// 3-argument closure with key, value.
def m = [user: 'mrhaki', likes: 'Groovy']
def sentence = m.inject('Message: ') { s, k, v ->
    s += "${k == 'likes' ? 'loves' : k} $v "
}

assert sentence.trim() == 'Message: user mrhaki loves Groovy'

// 2-argument closure with entry. 
def map = [sort: 'name', order: 'desc']
def equalSizeKeyValue = map.inject([]) { list, entry ->
    list << (entry.key.size() == entry.value.size())
}

assert equalSizeKeyValue == [true, false]

June 1, 2011

Groovy Goodness: Add Imports Transparently to Scripts with ImportCustomizer

Since Groovy 1.8.0 we can easily setup import statements for a Groovy compilation unit (for example a Groovy script file) without adding the imports to the script source. For example we have created domain classes in our own package. The script authors shouldn't know about the packages and just pretend the classes are accessible from the 'default' package.

We define an ImportCustomizer and use the addImport methods to add packages, classes, aliases to our script. The configured ImportCustomizer is added to a CompilerConfiguration object. We will see in future blog posts how we can even add more customizers to the CompilerConfiguration. We use the configuration when we create a GroovyShell and the information is applied to scripts we run with the created shell.

First we create a class and enum in the com.mrhaki.blog package. We compile the code so we have class files we can add to a classpath of another application.

// File: Post.groovy
// Compile with groovyc Post.groovy
package com.mrhaki.blog

class Post {
    String title
    Type type = Type.BLOG
}

enum Type {
    BLOG, NEWS
}

Next we create a Groovy script that will execute the following code:

// File: sample.groovy
// Article is imported as alias for com.mrhaki.blog.Post
def article = new Article(title: 'Groovy Goodness')

assert article.title == 'Groovy Goodness'
// BLOG is statically imported from com.mrhaki.blog.Type.*
assert article.type == BLOG
assert article.class.name == 'com.mrhaki.blog.Post'

Now we are ready to create a small application that will execute sample.groovy. We must add the com.mrhaki.blog.Post and com.mrhaki.blog.Type compiled classes to the classpath if we run the following script:

// File: RunScript.groovy
// Run with groovy -cp . RunScript
import org.codehaus.groovy.control.customizers.ImportCustomizer
import org.codehaus.groovy.control.CompilerConfiguration

// Add imports for script.
def importCustomizer = new ImportCustomizer()
// import static com.mrhaki.blog.Type.*
importCustomizer.addStaticStars 'com.mrhaki.blog.Type'
// import com.mrhaki.blog.Post as Article
importCustomizer.addImport 'Article', 'com.mrhaki.blog.Post'

def configuration = new CompilerConfiguration()
configuration.addCompilationCustomizers(importCustomizer)

// Create shell and execute script.
def shell = new GroovyShell(configuration)
shell.evaluate new File('sample.groovy')

May 27, 2011

Groovy Goodness: Command Chain Expressions for Fluid DSLs

Groovy 1.8 introduces command chain expression to further the support for DSLs. We already could leave out parenthesis when invoking top-level methods. But now we can also leave out punctuation when we chain methods calls, so we don't have to type the dots anymore. This results in a DSL that looks and reads like a natural language.

Let 's see a little sample where we can see how the DSL maps to real methods with arguments:

// DSL:
take 3.apples from basket
// maps to:
take(3.apples).from(basket)

// DSL (odd number of elements):
calculate high risk
// maps to:
calculate(high).risk
// or:
calculate(high).getRisk()

// DSL:
// We must use () for last method call, because method doesn't have arguments.
talk to: 'mrhaki' loudly()  
// maps to:
talk(to: 'mrhaki').loudly()

Implementing the methods to support these kind of DSLs can be done using maps and closures. The following sample is a DSL to record the time spent on a task at different clients:

worked 2.hours on design at GroovyRoom
developed 3.hours at OfficeSpace
developed 1.hour at GroovyRoom
worked 4.hours on testing at GroovyRoom

We see how to implement the methods to support this DSL here:

// Constants for tasks and clients.
enum Task { design, testing, developing }
enum Client { GroovyRoom, OfficeSpace }

// Supporting class to save work item info.
class WorkItem {
    Task task
    Client client
    Integer hours
}

// Support syntax 1.hour, 3.hours and so on.
Integer.metaClass.getHour = { -> delegate }
Integer.metaClass.getHours = { -> delegate }

// Import enum values as constants.
import static Task.*
import static Client.*

// List to save hours spent on tasks at
// different clients.
workList = []
 
def worked(Integer hours) {
    ['on': { Task task ->
        ['at': { Client client ->
            workList << new WorkItem(task: task, client: client, hours: hours)
        }]
    }]
}

def developed(Integer hours) {
    ['at': { Client client ->
        workList << new WorkItem(task: developing, client: client, hours: hours)
    }]
}

// -----------------------------------
// DSL
// -----------------------------------
worked 2.hours on design at GroovyRoom
developed 3.hours at OfficeSpace
developed 1.hour at GroovyRoom
worked 4.hours on testing at GroovyRoom


// Test if workList is filled
// with correct data.
def total(condition) {
    workList.findAll(condition).sum { it.hours }
}

assert total({ it.client == GroovyRoom }).hours == 7
assert total({ it.client == OfficeSpace }).hours == 3
assert total({ it.task == developing }).hours == 4
assert total({ it.task == design }).hours == 2
assert total({ it.task == testing }).hours == 4

August 26, 2010

Groovy Goodness: Store Closures in Script Binding

We have several ways to run (external) Groovy scripts from our code. For example we can use GroovyShell and the evaluate() method. If we want to pass variables to the script to be run we must create a Binding object and assign values to the variables and in our script we can use the variables. But if we assign closures to the variables we have methods available in our scripts. We can use it for example to define a DSL externally from the script that uses the DSL.

First we create a script with a simple DSL for sending text to the console or to a file:

// File: Output.groovy
console 'Groovy is great'

file('result.txt') { out ->
    out << 'Yes it is!'
}

Now we define the Groovy script that reads the script file and executes it:

// File: RunOutput.groovy
// Define DSL methods as closure variables in the binding.
def binding = new Binding()
binding.console = { String message ->
    println message
}
binding.file = { String fileName, Closure outputCode ->
    def outputFile = new File(fileName)
    outputFile.withWriter { writer ->
        outputCode.call writer
    }
}

// Create shell with the new binding.
def shell = new GroovyShell(binding)
def script = new File('Output.groovy')

// And execute
shell.evaluate script

We get the following output when we run the script:

$ groovy RunOutput
Groovy is great

And in the current directory we have a new file result.txt with the following contents: Yes it is!.