1. Command chains
Groovy lets you omit parentheses around the arguments of a
method call for top-level statements. “command chain” feature extends this by allowing us to chain such
parentheses-free method calls, requiring neither parentheses around arguments, nor dots between the chained calls.
The general idea is that a call like a b c d
will actually be equivalent to a(b).c(d)
. This
also works with multiple arguments, closure arguments, and even named arguments. Furthermore, such command chains can
also appear on the right-hand side of assignments. Let’s have a look at some examples
supported by this new syntax:
// equivalent to: turn(left).then(right)
turn left then right
// equivalent to: take(2.pills).of(chloroquinine).after(6.hours)
take 2.pills of chloroquinine after 6.hours
// equivalent to: paint(wall).with(red, green).and(yellow)
paint wall with red, green and yellow
// with named parameters too
// equivalent to: check(that: margarita).tastes(good)
check that: margarita tastes good
// with closures as parameters
// equivalent to: given({}).when({}).then({})
given { } when { } then { }
It is also possible to use methods in the chain which take no arguments, but in that case, the parentheses are needed:
// equivalent to: select(all).unique().from(names)
select all unique() from names
If your command chain contains an odd number of elements, the chain will be composed of method / arguments, and will finish by a final property access:
// equivalent to: take(3).cookies
// and also this: take(3).getCookies()
take 3 cookies
This command chain approach opens up interesting possibilities in terms of the much wider range of DSLs which can now be written in Groovy.
The above examples illustrate using a command chain based DSL but not how to create one. There are various strategies that you can use, but to illustrate creating such a DSL, we will show a couple of examples - first using maps and Closures:
show = { println it }
square_root = { Math.sqrt(it) }
def please(action) {
[the: { what ->
[of: { n -> action(what(n)) }]
}]
}
// equivalent to: please(show).the(square_root).of(100)
please show the square_root of 100
// ==> 10.0
As a second example, consider how you might write a DSL for simplifying
one of your existing APIs. Maybe you need to put this code in front of
customers, business analysts or testers who might be not hard-core Java
developers. We’ll use the Splitter
from the Google
Guava libraries project as it
already has a nice Fluent API. Here is how we might use it out of the
box:
@Grab('com.google.guava:guava:r09')
import com.google.common.base.*
def result = Splitter.on(',').trimResults(CharMatcher.is('_' as char)).split("_a ,_b_ ,c__").iterator().toList()
It reads fairly well for a Java developer but if that is not your target audience or you have many such statements to write, it could be considered a little verbose. Again, there are many options for writing a DSL. We’ll keep it simple with Maps and Closures. We’ll first write a helper method:
@Grab('com.google.guava:guava:r09')
import com.google.common.base.*
def split(string) {
[on: { sep ->
[trimming: { trimChar ->
Splitter.on(sep).trimResults(CharMatcher.is(trimChar as char)).split(string).iterator().toList()
}]
}]
}
now instead of this line from our original example:
def result = Splitter.on(',').trimResults(CharMatcher.is('_' as char)).split("_a ,_b_ ,c__").iterator().toList()
we can write this:
def result = split "_a ,_b_ ,c__" on ',' trimming '_\'
2. Operator overloading
Various operators in Groovy are mapped onto regular method calls on objects.
This allows you to provide your own Java or Groovy objects which can take advantage of operator overloading. The following table describes the operators supported in Groovy and the methods they map to.
Operator | Method |
---|---|
|
a.plus(b) |
|
a.minus(b) |
|
a.multiply(b) |
|
a.power(b) |
|
a.div(b) |
|
a.mod(b) |
|
a.or(b) |
|
a.and(b) |
|
a.xor(b) |
|
a.next() |
|
a.previous() |
|
a.getAt(b) |
|
a.putAt(b, c) |
|
a.leftShift(b) |
|
a.rightShift(b) |
|
a.rightShiftUnsigned(b) |
|
b.isCase(a) |
|
a.asBoolean() |
|
a.bitwiseNegate() |
|
a.negative() |
|
a.positive() |
|
a.asType(b) |
|
a.equals(b) |
|
! a.equals(b) |
|
a.compareTo(b) |
|
a.compareTo(b) > 0 |
|
a.compareTo(b) >= 0 |
|
a.compareTo(b) < 0 |
|
a.compareTo(b) <= 0 |
3. Script base classes
3.1. The Script class
Groovy scripts are always compiled to classes. For example, a script as simple as:
println 'Hello from Groovy'
is compiled to a class extending the abstract groovy.lang.Script
class. This class contains a single abstract
method called run. When a script is compiled, then its body will become the run method, while the other methods
found in the script are found in the implementing class. The Script
class provides base support for integration
with your application through the Binding
object, as illustrated in this example:
def binding = new Binding() (1)
def shell = new GroovyShell(binding) (2)
binding.setVariable('x',1) (3)
binding.setVariable('y',3)
shell.evaluate 'z=2*x+y' (4)
assert binding.getVariable('z') == 5 (5)
1 | a binding is used to share data between the script and the calling class |
2 | a GroovyShell can be used with this binding |
3 | input variables are set from the calling class inside the binding |
4 | then the script is evaluated |
5 | and the z variable has been "exported" into the binding |
This is a very practical way to share data between the caller and the script, however it may be insufficient or not
practical in some cases. For that purpose, Groovy allows you to set your own base script class. A base script class
has to extend groovy.lang.Script
and be a single abstract method type:
abstract class MyBaseClass extends Script {
String name
public void greet() { println "Hello, $name!" }
}
Then the custom script base class can be declared in the compiler configuration, for example:
def config = new CompilerConfiguration() (1)
config.scriptBaseClass = 'MyBaseClass' (2)
def b = new Binding()
def shell = new GroovyShell(this.class.classLoader, b, config) (3)
shell.evaluate """
setName 'Judith' (4)
greet()
"""
1 | create a custom compiler configuration |
2 | set the base script class to our custom base script class |
3 | then create a GroovyShell using that configuration |
4 | the script will then extend the base script class, giving direct access to the name property and greet method |
3.2. The @BaseScript annotation
As an alternative, it is also possible to use the @BaseScript
annotation directly into a script:
import groovy.transform.BaseScript
@BaseScript MyBaseClass baseScript
setName 'Judith'
greet()
where @BaseScript
should annotate a variable which type is the class of the base script. Alternatively, you can set
the base script class as a member of the @BaseScript
annotation itself:
@BaseScript(MyBaseClass)
import groovy.transform.BaseScript
setName 'Judith'
greet()
3.3. Alternate abstract method
We have seen that the base script class is a single abstract method type that needs to implement the run
method. The
run
method is executed by the script engine automatically. In some circumstances it may be interesting to have a base
class which implements the run
method, but provides an alternative abstract method to be used for the script body.
For example, the base script run
method might perform some initialization before the run
method is executed. This
is possible by doing this:
abstract class MyBaseClass extends Script {
int count
abstract void scriptBody() (1)
def run() {
count++ (2)
scriptBody() (3)
count (4)
}
}
1 | the base script class should define one (and only one) abstract method |
2 | the run method can be overriden and perform a task before executing the script body |
3 | run calls the abstract scriptBody method which will delegate to the user script |
4 | then it can return something else than the value from the script |
If you execute this code:
def result = shell.evaluate """
println 'Ok'
"""
assert result == 1
Then you will see that the script is executed, but the result of the evaluation is 1
as returned by the run
method of the base class. It is even clearer if you use parse
instead of evaluate
, because it would allow you to
execute the run
method several times on the same script instance:
def script = shell.parse("println 'Ok'")
assert script.run() == 1
assert script.run() == 2
4. Adding properties to numbers
In Groovy number types are considered equal to any other types. As such, it is possible to enhance numbers by adding properties or methods to them. This can be very handy when dealing with measurable quantities for example. Details about how existing classes can be enhanced in Groovy are found in the extension modules section or the categories section.
An illustration of this can be found in Groovy using the TimeCategory
:
use(TimeCategory) {
println 1.minute.from.now (1)
println 10.hours.ago
def someDate = new Date() (2)
println someDate - 3.months
}
1 | using the TimeCategory , a property minute is added to the Integer class |
2 | similarily, the months method returns a groovy.time.DatumDependentDuration which can be used in calculus |
Categories are lexically bound, making them a great fit for internal DSLs.
5. @DelegatesTo
5.1. Explaining delegation strategy at compile time
@groovy.lang.DelegatesTo
is a documentation and compile-time annotation aimed at:
-
documenting APIs that use closures as arguments
-
providing type information for the static type checker and compiler
The Groovy language is a platform of choice for building DSLs. Using closures, it’s quite easy to create custom control structures, as well as it is simple to create builders. Imagine that you have the following code:
email {
from 'dsl-guru@mycompany.com'
to 'john.doe@waitaminute.com'
subject 'The pope has resigned!'
body {
p 'Really, the pope has resigned!'
}
}
One way of implementing this is using the builder strategy, which
implies a method, named email
which accepts a closure as an argument.
The method may delegate subsequent calls to an object that implements
the from
, to
, subject
and body
methods. Again, body
is a
method which accepts a closure as an argument and that uses the builder
strategy.
Implementing such a builder is usually done the following way:
def email(Closure cl) {
def email = new EmailSpec()
def code = cl.rehydrate(email, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
the EmailSpec
class implements the from
, to
, … methods. By
calling rehydrate
, we’re creating a copy of the closure for which we
set the delegate
, owner
and thisObject
values. Setting the owner
and the this
object is not very important here since we will use the
DELEGATE_ONLY
strategy which says that the method calls will be
resolved only against the delegate of the closure.
class EmailSpec {
void from(String from) { println "From: $from"}
void to(String... to) { println "To: $to"}
void subject(String subject) { println "Subject: $subject"}
void body(Closure body) {
def bodySpec = new BodySpec()
def code = body.rehydrate(bodySpec, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
}
The EmailSpec
class has itself a body
method accepting a closure that is cloned and executed. This is what
we call the builder pattern in Groovy.
One of the problems with the code that we’ve shown is that the user of
the email
method doesn’t have any information about the methods that
he’s allowed to call inside the closure. The only possible information
is from the method documentation. There are two issues with this: first
of all, documentation is not always written, and if it is, it’s not
always available (javadoc not downloaded, for example). Second, it
doesn’t help IDEs. What would be really interesting, here, is for IDEs
to help the developer by suggesting, once they are in the closure body,
methods that exist on the email
class.
Moreover, if the user calls a method in the closure which is not defined
by the EmailSpec
class, the IDE should at least issue a warning (because
it’s very likely that it will break at runtime).
One more problem with the code above is that it is not compatible with static type checking. Type checking would let the user know if a method call is authorized at compile time instead of runtime, but if you try to perform type checking on this code:
email {
from 'dsl-guru@mycompany.com'
to 'john.doe@waitaminute.com'
subject 'The pope has resigned!'
body {
p 'Really, the pope has resigned!'
}
}
Then the type checker will know that there’s an email
method accepting
a Closure
, but it will complain for every method call inside the
closure, because from
, for example, is not a method which is defined
in the class. Indeed, it’s defined in the EmailSpec
class and it has
absolutely no hint to help it knowing that the closure delegate will, at
runtime, be of type EmailSpec
:
@groovy.transform.TypeChecked
void sendEmail() {
email {
from 'dsl-guru@mycompany.com'
to 'john.doe@waitaminute.com'
subject 'The pope has resigned!'
body {
p 'Really, the pope has resigned!'
}
}
}
will fail compilation with errors like this one:
[Static type checking] - Cannot find matching method MyScript#from(java.lang.String). Please check if the declared type is right and if the method exists. @ line 31, column 21. from 'dsl-guru@mycompany.com'
5.2. @DelegatesTo
For those reasons, Groovy 2.1 introduced a new annotation
named @DelegatesTo
. The goal of this annotation is to solve both the
documentation issue, that will let your IDE know about the expected
methods in the closure body, and it will also solve the type checking
issue, by giving hints to the compiler about what are the potential
receivers of method calls in the closure body.
The idea is to annotate the Closure
parameter of the email
method:
def email(@DelegatesTo(EmailSpec) Closure cl) {
def email = new EmailSpec()
def code = cl.rehydrate(email, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
What we’ve done here is telling the compiler (or the IDE) that when the
method will be called with a closure, the delegate of this closure will
be set to an object of type email
. But there is still a problem: the
defaut delegation strategy is not the one which is used in our method.
So we will give more information and tell the compiler (or the IDE) that
the delegation strategy is also changed:
def email(@DelegatesTo(strategy=Closure.DELEGATE_ONLY, value=EmailSpec) Closure cl) {
def email = new EmailSpec()
def code = cl.rehydrate(email, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
Now, both the IDE and the type checker (if you are using @TypeChecked
)
will be aware of the delegate and the delegation strategy. This is very
nice because it will both allow the IDE to provide smart completion, but
it will also remove errors at compile time that exist only because the
behaviour of the program is normally only known at runtime!
The following code will now pass compilation:
@TypeChecked
void doEmail() {
email {
from 'dsl-guru@mycompany.com'
to 'john.doe@waitaminute.com'
subject 'The pope has resigned!'
body {
p 'Really, the pope has resigned!'
}
}
}
5.3. DelegatesTo modes
@DelegatesTo
supports multiple modes that we will describe with examples
in this section.
5.3.1. Simple delegation
In this mode, the only mandatory parameter is the value which says to
which class we delegate calls. Nothing more. We’re telling the compiler
that the type of the delegate will always be of the type documented
by @DelegatesTo
(note that it can be a subclass, but if it is, the
methods defined by the subclass will not be visible to the type
checker).
void body(@DelegatesTo(BodySpec) Closure cl) {
// ...
}
5.3.2. Delegation strategy
In this mode, you must specify both the delegate class and a
delegation strategy. This must be used if the closure will not be called
with the default delegation strategy, which is Closure.OWNER_FIRST
.
void body(@DelegatesTo(strategy=Closure.DELEGATE_ONLY, value=BodySpec) Closure cl) {
// ...
}
5.3.3. Delegate to parameter
In this variant, we will tell the compiler that we are delegating to another parameter of the method. Take the following code:
def exec(Object target, Closure code) {
def clone = code.rehydrate(target, this, this)
clone()
}
Here, the delegate which will be used is not created inside the exec
method. In fact, we take an argument of the method and delegate to it.
Usage may look like this:
def email = new Email()
exec(email) {
from '...'
to '...'
send()
}
Each of the method calls are delegated to the email
parameter. This is
a widely used pattern which is also supported by @DelegatesTo
using a
companion annotation:
def exec(@DelegatesTo.Target Object target, @DelegatesTo Closure code) {
def clone = code.rehydrate(target, this, this)
clone()
}
A closure is annotated with @DelegatesTo
, but this time, without
specifying any class. Instead, we’re annotating another parameter
with @DelegatesTo.Target
. The type of the delegate is then determined
at compile time. One could think that we are using the parameter type,
which in this case is Object
but this is not true. Take this code:
class Greeter {
void sayHello() { println 'Hello' }
}
def greeter = new Greeter()
exec(greeter) {
sayHello()
}
Remember that this works out of the box without having to annotate
with @DelegatesTo
. However, to make the IDE aware of the delegate
type, or the type checker aware of it, we need to add @DelegatesTo
.
And in this case, it will now that the Greeter
variable is of
type Greeter
, so it will not report errors on the sayHello
method even if the exec method doesn’t explicitely define the target as
of type Greeter. This is a very powerful feature, because it prevents
you from writing multiple versions of the same exec
method for
different receiver types!
In this mode, the @DelegatesTo
annotation also supports the strategy
parameter that we’ve described upper.
5.3.4. Multiple closures
In the previous example, the exec
method accepted only one closure,
but you may have methods that take multiple closures:
void fooBarBaz(Closure foo, Closure bar, Closure baz) {
...
}
Then nothing prevents you from annotating each closure
with @DelegatesTo
:
class Foo { void foo(String msg) { println "Foo ${msg}!" } }
class Bar { void bar(int x) { println "Bar ${x}!" } }
class Baz { void baz(Date d) { println "Baz ${d}!" } }
void fooBarBaz(@DelegatesTo(Foo) Closure foo, @DelegatesTo(Bar) Closure bar, @DelegatesTo(Baz) Closure baz) {
...
}
But more importantly, if you have multiple closures and multiple arguments, you can use several targets:
void fooBarBaz(
@DelegatesTo.Target('foo') foo,
@DelegatesTo.Target('bar') bar,
@DelegatesTo.Target('baz') baz,
@DelegatesTo(target='foo') Closure cl1,
@DelegatesTo(target='bar') Closure cl2,
@DelegatesTo(target='baz') Closure cl3) {
cl1.rehydrate(foo, this, this).call()
cl2.rehydrate(bar, this, this).call()
cl3.rehydrate(baz, this, this).call()
}
def a = new Foo()
def b = new Bar()
def c = new Baz()
fooBarBaz(
a, b, c,
{ foo('Hello') },
{ bar(123) },
{ baz(new Date()) }
)
At this point, you may wonder why we don’t use the parameter names as references. The reason is that the information (the parameter name) is not always available (it’s a debug-only information), so it’s a limitation of the JVM. |
5.3.5. Delegating to a generic type
In some situations, it is interesting to instruct the IDE or the compiler that the delegate type will not be a parameter but a generic type. Imagine a configurator that runs on a list of elements:
public <T> void configure(List<T> elements, Closure configuration) {
elements.each { e->
def clone = configuration.rehydrate(e, this, this)
clone.resolveStrategy = Closure.DELEGATE_FIRST
clone.call()
}
}
Then this method can be called with any list like this:
@groovy.transform.ToString
class Realm {
String name
}
List<Realm> list = []
3.times { list << new Realm() }
configure(list) {
name = 'My Realm'
}
assert list.every { it.name == 'My Realm' }
To let the type checker and the IDE know that the configure
method calls the closure on each element of the list, you
need to use @DelegatesTo
differently:
public <T> void configure(
@DelegatesTo.Target List<T> elements,
@DelegatesTo(strategy=Closure.DELEGATE_FIRST, genericTypeIndex=0) Closure configuration) {
def clone = configuration.rehydrate(e, this, this)
clone.resolveStrategy = Closure.DELEGATE_FIRST
clone.call()
}
@DelegatesTo
takes an optional genericTypeIndex
argument that tells what is the index of the generic type that will
be used as the delegate type. This must be used in conjunction with @DelegatesTo.Target
and the index starts at 0. In
the example above, that means that the delegate type is resolved against List<T>
, and since the generic type at index
0 is T
and inferred as a Realm
, the type checker infers that the delegate type will be of type Realm
.
We’re using a genericTypeIndex instead of a placeholder (T ) because of JVM limitations.
|
6. Compilation customizers
6.1. Introduction
Whether you are using groovyc
to compile classes or a GroovyShell
,
for example, to execute scripts, under the hood, a compiler configuration is used. This configuration holds information
like the source encoding or the classpath but it can also be used to perform more operations like adding imports by
default, applying AST transformations transparently or disabling global AST transformations.
The goal of compilation customizers is to make those common tasks easy to implement. For that, the CompilerConfiguration
class is the entry point. The general schema will always be based on the following code:
import org.codehaus.groovy.control.CompilerConfiguration
// create a configuration
def config = new CompilerConfiguration()
// tweak the configuration
config.addCompilationCustomizers(...)
// run your script
def shell = new GroovyShell(config)
shell.evaluate(script)
Compilation customizers must extend the org.codehaus.groovy.control.customizers.CompilationCustomizer class. A customizer works:
-
on a specific compilation phase
-
on every class node being compiled
You can implement your own compilation customizer but Groovy includes some of the most common operations.
6.2. Import customizer
Using this compilation customizer, your code will have imports added transparently. This is in particular useful for scripts implementing a DSL where you want to avoid users from having to write imports. The import customizer will let you add all the variants of imports the Groovy language allows, that is:
-
class imports, optionally aliased
-
star imports
-
static imports, optionally aliased
-
static star imports
import org.codehaus.groovy.control.customizers.ImportCustomizer
def icz = new ImportCustomizer()
// "normal" import
icz.addImports('java.util.concurrent.atomic.AtomicInteger', 'java.util.concurrent.ConcurrentHashMap')
// "aliases" import
icz.addImport('CHM', 'java.util.concurrent.ConcurrentHashMap')
// "static" import
icz.addStaticImport('java.lang.Math', 'PI') // import static java.lang.Math.Pi
// "aliased static" import
icz.addStaticImport('pi', 'java.lang.Math', 'PI') // import static java.lang.Math.PI as pi
// "star" import
icz.addStarImports 'java.util.concurrent' // import java.util.concurrent.*
// "static star" import
icz.addStaticStars 'java.lang.Math' // import static java.lang.Math.*
A detailed description of all shortcuts can be found in org.codehaus.groovy.control.customizers.ImportCustomizer
6.3. AST transformation customizer
The AST transformation customizer is meant to apply AST transformations transparently. Unlike global AST transformations that apply on every class beeing compiled as long as the transform is found on classpath (which has drawbacks like increasing the compilation time or side effects due to transformations applied where they should not), the customizer will allow you to selectively apply a transform only for specific scripts or classes.
As an example, let’s say you want to be able to use @Log
in a script.
The problem is that @Log
is normally applied on a class node and a
script, by definition, doesn’t require one. But implementation wise,
scripts are classes, it’s just that you cannot annotate this implicit
class node with @Log
. Using the AST customizer, you have a workaround
to do it:
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import groovy.util.logging.Log
def acz = new ASTTransformationCustomizer(Log)
config.addCompilationCustomizers(acz)
That’s all! Internally, the @Log
AST transformation is applied to
every class node in the compilation unit. This means that it will be
applied to the script, but also to classes defined within the script.
If the AST transformation that you are using accepts parameters, you can use parameters in the constructor too:
def acz = new ASTTransformationCustomizer(Log, value: 'LOGGER')
// use name 'LOGGER' instead of the default 'log'
config.addCompilationCustomizers(acz)
As the AST transformation customizers works with objects instead of AST
nodes, not all values can be converted to AST transformation parameters.
For example, primitive types are converted to ConstantExpression
(that
is LOGGER
is converted to new ConstantExpression('LOGGER')
, but if
your AST transformation takes a closure as an argument, then you have to
give it a ClosureExpression
, like in the following example:
def configuration = new CompilerConfiguration()
def expression = new AstBuilder().buildFromCode(CompilePhase.CONVERSION) { -> true }.expression[0]
def customizer = new ASTTransformationCustomizer(ConditionalInterrupt, value: expression, thrown: SecurityException)
configuration.addCompilationCustomizers(customizer)
def shell = new GroovyShell(configuration)
shouldFail(SecurityException) {
shell.evaluate("""
// equivalent to adding @ConditionalInterrupt(value={true}, thrown: SecurityException)
class MyClass {
void doIt() { }
}
new MyClass().doIt()
""")
}
For a complete list of options, please refer to org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
6.4. Secure AST customizer
This customizer will allow the developer of a DSL to restrict the grammar of the language, to prevent users from using some constructs, for example. It is only “secure” in that sense only and it is very important to understand that it does not replace a security manager. The only reason for it to exist is to limit the expressiveness of the language. This customizer only works at the AST (abstract syntax tree) level, not at runtime! It can be strange at first glance, but it makes much more sense if you think of Groovy as a platform to build DSLs. You may not want a user to have a complete language at hand. In the example below, we will demonstrate it using an example of language that only allows arithmetic operations, but this customizer allows you to:
-
allow/disallow creation of closures
-
allow/disallow imports
-
allow/disallow package definition
-
allow/disallow definition of methods
-
restrict the receivers of method calls
-
restrict the kind of AST expressions a user can use
-
restrict the tokens (grammar-wise) a user can use
-
restrict the types of the constants that can be used in code
For all those features, the secure AST customizer works using either a whitelist (list of elements that are allowed) or a blacklist (list of elements that are disallowed). For each type of feature (imports, tokens, …) you have the choice to use either a whitelist or a blacklist, but you can mix whitelists and blacklists for distinct features. In general, you will choose whitelists (disallow all, allow selected).
import org.codehaus.groovy.control.customizers.SecureASTCustomizer
import static org.codehaus.groovy.syntax.Types.* (1)
def scz = new SecureASTCustomizer()
scz.with {
closuresAllowed = false // user will not be able to write closures
methodDefinitionAllowed = false // user will not be able to define methods
importsWhitelist = [] // empty whitelist means imports are disallowed
staticImportsWhitelist = [] // same for static imports
staticStarImportsWhitelist = ['java.lang.Math'] // only java.lang.Math is allowed
// the list of tokens the user can find
// constants are defined in org.codehaus.groovy.syntax.Types
tokensWhitelist = [ (1)
PLUS,
MINUS,
MULTIPLY,
DIVIDE,
MOD,
POWER,
PLUS_PLUS,
MINUS_MINUS,
COMPARE_EQUAL,
COMPARE_NOT_EQUAL,
COMPARE_LESS_THAN,
COMPARE_LESS_THAN_EQUAL,
COMPARE_GREATER_THAN,
COMPARE_GREATER_THAN_EQUAL,
].asImmutable()
// limit the types of constants that a user can define to number types only
constantTypesClassesWhiteList = [ (2)
Integer,
Float,
Long,
Double,
BigDecimal,
Integer.TYPE,
Long.TYPE,
Float.TYPE,
Double.TYPE
].asImmutable()
// method calls are only allowed if the receiver is of one of those types
// be careful, it's not a runtime type!
receiversClassesWhiteList = [ (2)
Math,
Integer,
Float,
Double,
Long,
BigDecimal
].asImmutable()
}
1 | use for token types from org.codehaus.groovy.syntax.Types |
2 | you can use class literals here |
If what the secure AST customizer provides out of the box isn’t enough
for your needs, before creating your own compilation customizer, you
might be interested in the expression and statement checkers that the
AST customizer supports. Basically, it allows you to add custom checks
on the AST tree, on expressions (expression checkers) or statements
(statement checkers). For this, you must
implement org.codehaus.groovy.control.customizers.SecureASTCustomizer.StatementChecker
or org.codehaus.groovy.control.customizers.SecureASTCustomizer.ExpressionChecker
.
Those interfaces define a single method called isAuthorized
, returning
a boolean, and taking a Statement
(or Expression
) as a parameter. It
allows you to perform complex logic over expressions or statements to
tell if a user is allowed to do it or not.
For example, there’s no predefined configuration flag in the customizer which will let you prevent people from using an attribute expression. Using a custom checker, it is trivial:
def scz = new SecureASTCustomizer()
def checker = { expr ->
!(expr instanceof AttributeExpression)
} as SecureASTCustomizer.ExpressionChecker
scz.addExpressionCheckers(checker)
Then we can make sure that this works by evaluating a simple script:
new GroovyShell(config).evaluate '''
class A {
int val
}
def a = new A(val: 123)
a.@val (1)
'''
1 | will fail compilation |
Statements can be checked using org.codehaus.groovy.control.customizers.SecureASTCustomizer.StatementChecker
Expressions can be checked using org.codehaus.groovy.control.customizers.SecureASTCustomizer.ExpressionChecker
6.5. Source aware customizer
This customizer may be used as a filter on other customizers. The
filter, in that case, is the org.codehaus.groovy.control.SourceUnit
.
For this, the source aware customizer takes another customizer as a
delegate, and it will apply customization of that delegate only and only
if predicates on the source unit match.
SourceUnit
gives you access to multiple things but in particular the
file being compiled (if compiling from a file, of course). It gives
you the potential to perform operation based on the file name, for
example. Here is how you would create a source aware customizer:
import org.codehaus.groovy.control.customizers.SourceAwareCustomizer
import org.codehaus.groovy.control.customizers.ImportCustomizer
def delegate = new ImportCustomizer()
def sac = new SourceAwareCustomizer(delegate)
Then you can use predicates on the source aware customizer:
// the customizer will only be applied to classes contained in a file name ending with 'Bean'
sac.baseNameValidator = { baseName ->
baseName.endsWith 'Bean'
}
// the customizer will only be applied to files which extension is '.spec'
sac.extensionValidator = { ext -> ext == 'spec' }
// source unit validation
// allow compilation only if the file contains at most 1 class
sac.sourceUnitValidator = { SourceUnit sourceUnit -> sourceUnit.AST.classes.size() == 1 }
// class validation
// the customizer will only be applied to classes ending with 'Bean'
sac.classValidator = { ClassNode cn -> cn.endsWith('Bean') }
6.6. Customizer builder
If you are using compilation customizers in Groovy code (like the
examples above) then you can use an alternative syntax to customize compilation.
A builder (org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder
)
simplifies the creation of customizers using a hierarchical DSL.
import org.codehaus.groovy.control.CompilerConfiguration
import static org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder.withConfig (1)
def conf = new CompilerConfiguration()
withConfig(conf) {
// ... (2)
}
1 | static import of the builder method |
2 | configuration goes here |
The code sample above shows how to use the builder. A static method, withConfig, takes a closure corresponding to the builder code, and automatically registers compilation customizers to the configuration. Every compilation customizer available in the distribution can be configured this way:
6.6.1. Import customizer
withConfig(configuration) {
imports { // imports customizer
normal 'my.package.MyClass' // a normal import
alias 'AI', 'java.util.concurrent.atomic.AtomicInteger' // an aliased import
star 'java.util.concurrent' // star imports
staticMember 'java.lang.Math', 'PI' // static import
staticMember 'pi', 'java.lang.Math', 'PI' // aliased static import
}
}
6.6.2. AST transformation customizer
withConfig(conf) {
ast(Log) (1)
}
withConfig(conf) {
ast(Log, value: 'LOGGER') (2)
}
1 | apply @Log transparently |
2 | apply @Log with a different name for the logger |
6.6.3. Secure AST customizer
withConfig(conf) {
secureAst {
closuresAllowed = false
methodDefinitionAllowed = false
}
}
6.6.4. Source aware customizer
withConfig(configuration){
source(extension: 'sgroovy') {
ast(CompileStatic) (1)
}
}
withConfig(configuration){
source(extensions: ['sgroovy','sg']) {
ast(CompileStatic) (2)
}
}
withConfig(configuration) {
source(extensionValidator: { it.name in ['sgroovy','sg']}) {
ast(CompileStatic) (2)
}
}
withConfig(configuration) {
source(basename: 'foo') {
ast(CompileStatic) (3)
}
}
withConfig(configuration) {
source(basenames: ['foo', 'bar']) {
ast(CompileStatic) (4)
}
}
withConfig(configuration) {
source(basenameValidator: { it in ['foo', 'bar'] }) {
ast(CompileStatic) (4)
}
}
withConfig(configuration) {
source(unitValidator: { unit -> !unit.AST.classes.any { it.name == 'Baz' } }) {
ast(CompileStatic) (5)
}
}
1 | apply CompileStatic AST annotation on .sgroovy files |
2 | apply CompileStatic AST annotation on .sgroovy or .sg files |
3 | apply CompileStatic AST annotation on files whose name is foo |
4 | apply CompileStatic AST annotation on files whose name is foo or bar |
5 | apply CompileStatic AST annotation on files that do not contain a class named Baz |
6.6.5. Inlining a customizer
Inlined customizer allows you to write a compilation customizer directly, without having to create a class for it.
withConfig(configuration) {
inline(phase:'CONVERSION') { source, context, classNode -> (1)
println "visiting $classNode" (2)
}
}
1 | define an inlined customizer which will execute at the CONVERSION phase |
2 | prints the name of the class node being compiled |
6.6.6. Multiple customizers
Of course, the builder allows you to define multiple customizers at once:
withConfig(configuration) {
ast(ToString)
ast(EqualsAndHashCode)
}
6.7. Config script flag
So far, we have described how you can customize compilation using
a CompilationConfiguration
class, but this is only possible if you
embed Groovy and that you create your own instances
of CompilerConfiguration
(then use it to create a
GroovyShell
, GroovyScriptEngine
, …).
If you want it to be applied on the classes you compile with the normal
Groovy compiler (that is to say with groovyc
, ant
or gradle
,
for example), it is possible to use a compilation flag named configscript
that takes a Groovy configuration script as argument.
This script gives you access to the CompilerConfiguration
instance before
the files are compiled (exposed into the configuration script as a variable named configuration
),
so that you can tweak it.
It also transparently integrates the compiler configuration builder above. As an example, let’s see how you would activate static compilation by default on all classes.
6.7.1. Static compilation by default
Normally, classes in Groovy are compiled with a dynamic runtime. You can activate static compilation
by placing an annotation named @CompileStatic
on any class. Some people would like to have this
mode activated by default, that is to say not having to annotated classes. Using configscript
,
this is possible. First of all, you need to create a file named config.groovy
into src/conf
with
the following contents:
withConfig(configuration) { (1)
ast(groovy.transform.CompileStatic)
}
1 | configuration references a CompilerConfiguration instance |
That is actually all you need. You don’t have to import the builder, it’s automatically exposed in the script. Then, compile your files using the following command line:
groovyc -configscript src/conf/config.groovy src/main/groovy/MyClass.groovy
We strongly recommand you to separate configuration files from classes,
hence why we suggest using the src/main
and src/conf
directories above.
6.8. AST transformations (TBD)
7. Custom type checking extensions
It may be interesting, in some circumstances, to provide feedback about wrong code to the user as soon as possible, that is to say when the DSL script is compiled, rather than having to wait for the execution of the script. However, this is not often possible with dynamic code. Groovy actually provides a practical answer to this known as type checking extensions.