Groovy

From Evgeny Goldin

Jump to: navigation, search

Contents

Multiple assignment


def (a, b) = [a, b]
def (int a, String b) = [3, "Opa"]
 
def a, b
(a, b) = {[ 'aaa', 'bbb' ]}()
 
(a, b) = [b, a]


'for' loops

for (i in 0..9)
     println i
 
for (int i = 0; i < 10; i++)
     println i


Operators


Builders

def builder = new groovy.xml.MarkupBuilder()
 
def bobsFriend = {
 person {
   name "fred"
   age 40
 }
}
 
builder.person {
 name "bob"
 age 30
 friend bobsFriend
}


Categories


class StringCategory { static String   lower(String self)     { self.toLowerCase() }}
class NumberCategory { static Distance getMeters(Number self) { new Distance(number: self) }} // getMeters() => "meters"
 
use ( StringCategory ) { assert "test" == "TeSt".lower() }
use ( NumberCategory ) { def dist = 300.meters }
 
 
use ( groovy.xml.dom.DOMCategory ) {
  assert html.head.title.text() == 'Test'
  assert html.body.p.text()     == 'This is a test.'
  assert html.find{ it.tagName  == 'body' }.tagName == 'body'
  assert html.getElementsByTagName('*').grep{ it.'@class' }.size() == 2
}
 
 
use ( groovy.time.TimeCategory ) {
    println 1.minute.from.now
    println 10.hours.ago
    println new Date() - 3.months
}


Groovy JDK


MOP


Dynamic Property access / Method Invocation

/**
 * Dynamic property access
 */
obj."$property"
obj[(property)]
obj."$property" = value
 
/**
 * Dynamic method invocation
 */
object."$methodName"( *args )
object.invokeMethod( methodName, *args )
 
 
/**
 * Dynamic object creation
 */
def <T> T construct(Class<T> clazz, Map args) { clazz.newInstance(args) }
assert 'groovy.util.Expando' == construct(Expando, [a:'aa']).class.name
assert 'aa'                  == construct(Expando, [a:'aa']).a


MOP Classes


MetaProgramming Hooks


Method Description Usages
getProperty (String property) Intercepts every property read access
setProperty (String property, Object newValue) Intercepts every property write access
invokeMethod (String name, args) Intercepts every method access
propertyMissing (String name[, value]) Intercepts failed property access. Maximum performance penalty!
methodMissing (String name, args) Intercepts failed method dispatch. Maximum performance penalty!


Inspecting MetaClass

String.metaClass.methods.each    { println it.name }
String.metaClass.properties.each { println it.name }
 
println Foo.metaClass.getMetaMethods()         // List<MetaMethod>
println Foo.metaClass.getProperties()          // List<MetaProperty>
println Foo.metaClass.getMetaMethod( '..' )    // MetaMethod
println Foo.metaClass.getMetaProperty( '..' )  // MetaProperty
println Foo.metaClass.respondsTo ( new Foo(), "bar"  )
println Foo.metaClass.hasProperty( new Foo(), "prop" )


  • respondsTo only works for "real" methods and those added via ExpandoMetaClass and not for cases where invokeMethod or methodMissing are overridden. It is impossible in these cases to tell if an object responds to a method without actually invoking the method.


groovy.util.Expando

  • Dynamically expandable JavaScript-like bean.
  • Groovydoc, src


def ex             = new Expando()
ex.propertyName    = 'Some Value'
ex.anotherPropName = 'Another Value'
ex.printDate       = { String header -> println "[$header]: [${ new Date() }]" }
 
assert 'Some Value'    == ex.propertyName
assert 'Another Value' == ex.anotherPropName
 
ex.printDate( "Date" )
 
def bean = new Expando( name:"James", location:"London", id:123 )
assert "James" == bean.name
assert 123     == bean.id
assert "aaa"   == new Expando( someMethod: { "aaa" } ).someMethod()


groovy.lang.ExpandoMetaClass


groovy> println String.metaClass
groovy> String.metaClass.aaa = { "aaa" }
groovy> println String.metaClass
 
org.codehaus.groovy.runtime.HandleMetaClass@1a503f[groovy.lang.MetaClassImpl@1a503f[class java.lang.String]]
groovy.lang.ExpandoMetaClass@1205d8d[class java.lang.String]



/**
 * Borrowing methods
 */
Person.metaClass.buyHouse = new MortgageLender().&borrowMoney
 
 
/**
 * Adding methods
 */
Number.metaClass.multiply       =  { Amount amount -> amount.times(delegate) }
Number.metaClass.div            =  { Amount amount -> amount.inverse().times(delegate) }
Book.metaClass.titleInUpperCase << {-> title.toUpperCase() } // "Appends" the new method, if the method already exists an exception will be thrown
Book.metaClass.titleInUpperCase =  {-> title.toUpperCase() } // Replaces an instance method
 
Person.metaClass.whatIsThis     = { String arg -> .. }
Person.metaClass.whatIsThis    << { int arg    -> .. } // Overload
Person.metaClass.whatIsThis     = { int arg    -> .. } // Replace
 
Number.metaClass {
    multiply  { Amount amount -> amount.times(delegate) }
    div       { Amount amount -> amount.inverse().times(delegate) }
    div    << { Number factor -> delegate.divide(factor) }
    div    << { Amount factor -> delegate.divide(factor) }
    method << { String s ->  .. } << { Integer i ->  .. }
}
 
 
/**
 * Adding static methods
 */
Dog.metaClass.static.create = { new Dog() }
Dog.metaClass {
    'static' {
        fqn { delegate.name }
    }
}
 
 
/**
 * Adding properties
 */
Dog.metaClass.getBreed = { 'Poodle' }   // new Dog().breed
Book.metaClass.author  = "Stephen King" // Property is mutable and has both a setter and getter. It is stored in a ThreadLocal WeakHashMap, don't expect the value to stick around forever!
Car.metaClass {
    lastAccessed = null
    invokeMethod = { String name, args ->
        ..
        delegate.lastAccessed = new Date()
        ..
    }
}
 
 
/**
 * Adding constructor
 */
Dog.metaClass.constructor   = { String name  -> new Dog ( name : name  ) }
Book.metaClass.constructor << { String title -> new Book( title: title ) }
Book.metaClass.constructor  = { new Book() } // StackOverflowError!
Book.metaClass.constructor  = { org.springframework.beans.BeanUtils.instantiateClass(Book) } // Bypass StackOverflowError
 
 
/**
 * Dynamic name creation
 */
Person.metaClass."changeNameTo${methodName}" = {-> delegate.name = "Bob" }
classes.findAll { it.name.endsWith( 'Codec' ) }.each { codec ->
    Object.metaClass."encodeAs${codec.name-'Codec'}"   = { codec.newInstance().encode(delegate) } // encodeAsHtml,   encodeAsJavaScript,   etc
    Object.metaClass."decodeFrom${codec.name-'Codec'}" = { codec.newInstance().decode(delegate) } // decodeFromHtml, decodeFromJavaScript, etc
}
 
 
/**
 * Overriding MOP hooks
 */
Stuff.metaClass.invokeMethod          = { String name, args       -> .. }
Person.metaClass.getProperty          = { String name             -> .. }
Person.metaClass.setProperty          = { String name, value      -> .. }
Number.metaClass.methodMissing        = { String methodName, args -> .. }
Dog.metaClass.static.methodMissing    = { String methodName, args -> .. }
Dog.metaClass.static.propertyMissing  = { String propertyName     -> .. }
Stuff.metaClass.'static'.invokeMethod = { String name, args       -> .. }
 
 
/**
 * Adding methods to interfaces
 * Requires ExpandoMetaClass.enableGlobally()!
 */
List.metaClass.sizeDoubled  = {-> delegate.size() * 2 }
HttpSession.metaClass.getAt = { String key -> delegate.getAttribute(key) }
HttpSession.metaClass.putAt = { String key, Object val -> delegate.setAttribute(key, val) }
 
 
/**
 * Adding Methods to instance
 */
def str = "hello test"
def s   = "aaaaaaaaaa"
 
str.metaClass.test = { "test" }
s.metaClass {
   aaa { "aaa" }
   bbb { "bbb" }
}
 
assert "test" == str.test()
assert "aaa"  == s.aaa()
assert "bbb"  == s.bbb()
 
x = "aaaaaaaaaa"
x.metaClass.map = [:]
x.map.y = 5
assert 5 == x.map.y
 
 
/**
 * Custom casting
 */
String.metaClass.asType = { Long cl -> .. }
 
 
/**
 * Undo metaprogramming
 */
String.metaClass = null


Intercept + Cache + Invoke

Foo.metaClass.methodMissing = { String name, args ->         // Intercept
    def impl                = { Object[] varArgs  -> ... }
    Foo.metaClass."${name}" = impl                           // Cache
    impl( *args )                                            // Invoke
}


Runtime mixins

class DivingAbility { def dive() { ... }}
class FlyingAbility { def fly()  { ... }}
class JamesBondVehicle { .. }
 
JamesBondVehicle.mixin DivingAbility, FlyingAbility
new JamesBondVehicle().dive()
new JamesBondVehicle().fly()


AST Transformations



@Immutable

  • Immutable AST Macro
  • Class becomes final, all its fields become final, and you cannot change its state after construction


@Immutable final class Punter {
    String first, last
}
@Immutable class Coordinates {
    double lat, lng
}


@Delegate


/**
 * The Groovy compiler adds all of Date's methods to the Event class, and those methods simply delegate the call to the Date field.
 */
class Event
{
    @Delegate Date when
    String title, url
}
 
def df      = new SimpleDateFormat("yyyy/MM/dd")
def gr8conf = new Event(title: "GR8 Conference", url: "http://www.gr8conf.org",       when: df.parse("2009/05/18"))
def javaOne = new Event(title: "JavaOne",        url: "http://java.sun.com/javaone/", when: df.parse("2009/06/02"))
 
assert gr8conf.before(javaOne.when)
 
class Employee { def doTheWork() { "done" }}
class Manager  { @Delegate Employee slave = new Employee()}
assert new Manager().doTheWork() == "done"
 
 
/**
 * If the delegate is not a final class, it is even possible to make the Event class a subclass of Date simply by extending Date.
 */
class Event extends Date
{
    @Delegate Date when
    String title, url
}
 
 
/**
 * In the case you are delegating to an interface you don't need to explictely say you implement the interface of the delegate.
 * The @Delegate annotation fully supports polymorphism by promoting not only the delegate class's methods to the outer class, but the delegate's interfaces as well.
 */
class LockableList
{
    @Delegate                     private List list = []
//  @Delegate(interfaces = false) private List list = []
    @Delegate                     private Lock lock = new ReentrantLock()
}
 
assert new LockableList() instanceof Lock
assert new LockableList() instanceof List


@Singleton


public class T
{
    public static final T instance = new T();
    private T() {}
}
 
@Singleton class T {}
@Singleton(lazy = true) class T {}
 
T.instance


@Lazy


class Person { @Lazy              List pets = ['Cat', 'Dog', 'Bird']
               @Lazy(soft = true) List pets = ['Cat', 'Dog', 'Bird'] }
 
class Dude   { @Lazy              List pets = retrieveFromSlowDB()   }


@Newify


def buildTree() {
    new Tree(new Tree(new Leaf(1), new Leaf(2)), new Leaf(3))
}
 
@Newify([Tree, Leaf]) buildTree() {
    Tree(Tree(Leaf(1), Leaf(2)), Leaf(3))
}
 
@Newify([Coordinates, Path])
def build() {
    Path(
        Coordinates(48.824068, 2.531733),
        Coordinates(48.857840, 2.347212),
        Coordinates(48.858429, 2.342622)
    )
}


@Category, @Mixin

  • Category and Mixin transformations
  • groovy.lang.Category
  • Classes conforming to the conventional Groovy category conventions can be used within use statements or mixed in at compile time with the @Mixin transformation or at runtime with the mixin method on classes.
  • @Category only applies to one single type at a time, unlike classical categories which can be applied to any number of types.


@Category(Integer)
class IntegerOps { def triple() { this * 3 }}
use ( IntegerOps ) { assert 25.triple() == 75 }
 
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@Category( A )
class AUtils1 { def triple() { "[$this]".multiply(3) }}
 
@Category( A )
class AUtils2 { def duplicate() { "[$this]".multiply(2) }}
 
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@Mixin([ AUtils1, AUtils2 ])
@Mixin( AUtils1 )
class A { String toString(){ this.class.name }}
 
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
class A { String toString(){ this.class.name }}
A.mixin ([ AUtils1, AUtils2 ])
A.mixin AUtils1
 
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
assert "[A][A][A]" == new A().triple()
assert "[A][A]"    == new A().duplicate()


@PackageScope

  • PackageScope transformation
  • To be able to expose a field with package-scope visibility, you can annotate your field with the @PackageScope annotation.


@Grab


@GrabResolver( name='neo4j-public-repo', root='http://m2.neo4j.org' )
@Grab( 'org.neo4j:neo4j-kernel:1.1.1' )
import org.neo4j.kernel.EmbeddedGraphDatabase
 
@Grab( group='org.ccil.cowan.tagsoup', module='tagsoup', version='0.9.7' )
def getHtml() { ... }
 
@Grab( 'net.sf.json-lib:json-lib:2.3:jdk15' )
def builder = new net.sf.json.groovy.JsonGroovyBuilder()


@Bindable, @Vetoable


AstBuilder


@WithLogging
def greet() {
  println "Hello World"
}
 
greet()
 
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.METHOD])
@GroovyASTTransformationClass(["gep.LoggingASTTransformation"])
public @interface WithLogging {}
 
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS)
public class LoggingASTTransformation implements ASTTransformation
{
 
  public void visit(ASTNode[] nodes, SourceUnit sourceUnit)
  {
      List methods = sourceUnit.getAST()?.getMethods()
      // find all methods annotated with @WithLogging
      methods.findAll { MethodNode method -> method.getAnnotations(new ClassNode(WithLogging)) }.each
      {
          MethodNode method ->
          ...
      }
  }
}
 
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
new AstBuilder().buildFromString(''' "Hello" ''')
 
new AstBuilder().buildFromCode { "Hello" }
 
List<ASTNode> nodes = new AstBuilder().buildFromSpec {
    block {
        returnStatement {
            constant "Hello"
        }
    }
}


Testing


Unit Tests


import static groovy.util.GroovyTestCase.*
 
class MyClass
{
    @Test
    void getAnything()
    {
       ...
       shouldFail( UnsupportedOperationException ){ badList << "will this work?" }
       ...
    }
}


Stubs


/**
 * Stub Person calls
 */
class Person { String first, last }
 
def stub = new StubFor(Person)
 
stub.demand.with {
   getFirst{ 'dummy' }
   getLast { 'name'  }
}
 
stub.use {
   def john = new Person(first:'John', last:'Smith')
   assert john.first == 'dummy'
   assert john.last  == 'name'
}
 
/**
 * Expect demanded call cardinalities to match demanded ranges.
 * Assert that all demanded method calls happened.
 */
stub.expect.verify()
 
 
/**
 * Demand calls to different methods
 */
stub.demand.getFirst{ 'dummy' }
stub.demand.getLast { 'name'  }
 
 
/**
 * When calls to the stubbed method should yield different results per call, add the respective demands in sequence
 */
stub.demand.getFirst { 'dummy' }
stub.demand.getFirst { 'ddddd' }
 
 
/**
 * Provide a range to specify how often the demanded closure should apply; the default is (1..1)
 */
stub.demand.getFirst(0..35) { 'dummy' }
 
 
/**
 * It is possible to react to the method arguments that CUT passes
 */
stub.demand.methodOne {
   number -> ( assert 0 == number % 2 )
   return 1
}


Mocks

  • Strict expectation of a mock verifies that all the demanded method calls happen in exactly the sequence of the specification.
  • The first method call that breaks this sequence causes the test to fail immediately.
  • There is no need to explicitly call the verify method, because that happens by default when the use{ .. } closure ends.
  • What gets asserted is whether the CUT follows a specified protocol when talking with the outside world. A protocol defines the rules that the CUT has to obey when calling the collaborator.
  • groovy.mock.interceptor.MockFor


def relay(request) {
    def farm = new SortableFarm()
    farm.sort()
    farm.getMachines()[0].send(request)
}
 
def farmMock = new MockFor(SortableFarm)
farmMock.demand.sort(){}
farmMock.demand.getMachines { [new Expando( send: {} )] }
farmMock.use { relay(null) }


Exceptions Handling


try { throw new Exception("bang!") } catch (any) { println "caught" }


Groovy Truth


Boolean Corresponding Boolean value is true
Matcher The matcher has a match
Collection The collection is non-empty
Map The map is non-empty
String, GString The string is non-empty
Number, Character The value is nonzero
None of the above Object.asBoolean() - object reference is non-null


Closures


Closure c = Collections.&binarySearch


DSL


Builders

Additional Data

Personal tools