-
Notifications
You must be signed in to change notification settings - Fork 9
Core
Core groovity is a java library that can be embedded in any application to provide scripting capabilities. Groovity scripts have a super-set of the syntax available in standard Groovy, meaning anything you can write in Groovy can be written the same way in Groovity, but Groovity adds some syntax features that go above and beyond.
- Setup
- Configuration
- Class Lifecycle
- Arguments
- Loading
- Load-time setup and verification
- Running
- Streaming Templates
- Logging
- Scheduling
- Reflection
To get started with Groovity core, create a folder to store your scripts, and in your java application use the GroovityBuilder to create an instance of Groovity. Groovity acts as a compiler and factory for groovy scripts.
import com.disney.groovity.Groovity;
import com.disney.groovity.GroovityBuilder;
(...)
File myGroovySources = new File('groovy-sources');
GroovityBuilder builder = new GroovityBuilder().setSourceLocations(myGroovySources.toURI());
Groovity groovity = builder.build();
There are numerous other configuration options on the GroovityBuilder - here is a more fully configured GroovityBuilder
Groovity groovity = new GroovityBuilder()
.setSourceLocations(myGroovySources.toURI()) //one or more source folder URIs, can reference file or http schemes
.setSourcePhase("STARTUP,RUNTIME") // automatically compile changed sources at both startup and runtime
.setJarDirectory(new File("groovy-classes")) // specify a directory for jar files made by the maven plugin or runtime
.setJarPhase("STARTUP,RUNTIME") // create jar files of compiled classes at runtime and use them to speed startup
.setSourcePollSeconds(30) // scan source locations for changes every 30 seconds for RUNTIME auto-compile
.setDefaultBinding((Map) myContext) // pass in a map of objects that should be added to the binding for all scripts
.setAsyncThreads(256) //control number of threads available for background processing
.setScriptBaseClass((String) myCustomClassName) // have Groovity produce your own subclass of groovy Script
.setParentClassLoader(classLoader) //set a parent classLoader for all scripts in this groovity
.setCaseSensitive(false) //treat load() and run() paths as case-insensitive
.setMaxHttpConnTotal(512) //max number of HTTP client connections
.setMaxHttpConnPerRoute(64) //max HTTP Client connections per target HOST
.build(); //return an initialized Groovity ready to use
A common concern for real-world applications is being able to control configuration externally, so that environmental details don't leak into code. Groovity supports a simple declarative syntax for defining configuration keys and default values, and will automatically override the values if a system property or environment variable is found with the same name. It also offers a "Configurator" api that can be used to wire in a custom configuration loader, for example to centralize configuration in a shared database. Configurators are provided to make it easy to load properties from files, folders, resources or urls.
This example shows two example configuration properties with hard-coded defaults; at runtime the template will print out the effective values, which might be the hard-coded value, might come from a system property, environment variable, or properties file entry of the same name, or might come from a custom Configurator. Custom Configurators have the ability to apply values globally or to specific source folders or scripts, so for example an application with 10 different modules, each requiring a database connection, can be configured with one global connection string or with separate connection strings for each module, as needed.
static conf = [
'java.io.tmpdir':'/temp',
'JDBC_CONNECTION_STRING':'jdbc:mysql://localhost:3306/mydb?user=root'
]
<~ My temp dir is ${conf['java.io.tmpdir']} and my database URL is ${conf.JDBC_CONNECTION_STRING} ~>
To target a folder or individual script for configuration, if using system properties you simply prefix the configuration key with the script path. For example, imagine two scripts
/* /script1.grvt */
static conf=[
ttl: 120
]
and
/* /script2.grvt */
static conf=[
ttl: 60
]
If you run the environment with these java environment variables
-Dttl=300 -D/script1/ttl=600
script1 will be configured with a ttl of 600, and script2 and any other scripts declaring ttl as a conf value will be set to 300.
Default values may be strings, numbers or booleans, or a String, Boolean or Number class; if a configurator overrides a default value, for example from a system property, the new value is coerced to the class of the default. If a class is used to define the desired coercion, and no configuration value is supplied by the environment, at runtime that variable will not be present in the conf map; i.e. checking the map for that value will return null.
static conf=[
featureEnabled: false,
retries: 5,
message: String.class
]
write(value:conf)
The above script without any environment configuration will output
{"retries":5,"featureEnabled":false}
Executing the script with the following java environment variables set
-DfeatureEnabled=true -Dretries=10 -Dmessage="Here we go" -Dfoo="bar"
would print out
{"retries":10,"featureEnabled":true,"message":"Here we go"}
Groovity will automatically invoke static init(), start() and destroy() methods you add to your scripts or inner classes. The init() function is called on application startup and/or when a groovy script is (re)compiled. The start() method is called after ALL scripts at startup or compile time have gone through init(), and is useful for deferring certain startup activities until you know all scripts have been initialized. The destroy() method is called when a groovity script is deleted, replaced with a newly compiled version, or during a clean application shutdown. These static methods have access to the default binding and can call other groovy scripts; the framework will alert you if it detects circular references.
Example from the sample app: a script that acts as a factory for a SQL datasource, requiring that the default binding is populated with a variable 'jdbcURL'.
// Define Java/Groovy imports
import groovy.sql.Sql;
import org.h2.jdbcx.JdbcDataSource;
// Define static (i.e. class-level) fields
static Sql sql
// static init: setup the database
public static init(){
def ds = new JdbcDataSource();
ds.setURL(binding.jdbcURL);
sql = new Sql(ds);
}
// static destroy: tear down DB
public static void destroy(){
sql.execute("SHUTDOWN");
sql.dataSource.close(0);
}
//run behavior: return the Sql object
sql
In programming it is typical for an application to expect certain inputs to function correctly; for example a method typically has a signature defining the name, order and type of arguments. All inputs to a groovy script must be placed in the Binding that represents the execution variable scope; as such groovy offers no core way for a script to declare an expected variable name or enforce the presence or validity of argument, rather a script will typically incorporate manual logic to check what was passed in, or will simply attempt to access variables without checks, and deal with the ensuing MissingPropertyException. The problems with this approach are that it gives a caller no visibility into the expected inputs of a script, requiring trial and error or deep code-reading to determine what to pass in; it also makes it all too easy for missing properties to cause a script to fail partway through processing. And it requires a lot of fairly boilerplate code to check for variables and provide default values and type coercion.
Groovity offers a declarative syntax for scripts to define the expected parameters in the binding, providing for default values, type coercion and data validation. This approach resolves ambiguity for script callers, and the Groovity engine will enforce the presence and validity of args in the binding before your script begins execution, ensuring fast failure. A static field named "args" should contain a map of parameter names to one of the following values:
-
a hard-corded default value which will be added to the binding IF the argument is missing; otherwise the class of the default value is used to coerce the argument.
-
A Class which will be used to coerce a required argument, throwing an exception if the value is missing
-
Null, indicating an optional parameter whose argument should default to null if missing
-
A closure; it will be called with any already bound value for the variable name, and whatever it returns will be bound to that variable name before the script is loaded, or it can throw an exception if a value is unacceptable.
static args=[
id: long.class, // id MUST be non-null in the binding already and WILL be converted to LONG
sectionId: { it?.toLong() }, // use closure for null-safe type transformations
page:1, // IF page is missing in the binding it will be SET to 1, otherwise converted to INT
ref:null, // IF ref is missing in the binding it will be bound to null
limit: { // custom validation/transformation/default via closure
if(it){
it = it as int;
if(it in 1..100){
return it;
}
throw new IllegalArgumentException("${it} is not between 1-100");
}
10 //default
}
]
@Field myObject = load('/myService').getObject(id,page,limit)
To load an instance of a Script, make use of the load() method. Two arguments are required, the path to the script source from the root source directory, and a groovy Binding which will act as the variable scope. Loading always returns the same instance of a script for a given Binding, what you might consider a request-scoped singleton.
Binding binding = new Binding();
binding.setVariable('out',System.out);
Script script = groovity.load('/path/to/myScript',binding);
Inside a groovity script, load takes only a single parameter, the path of the script to load; the binding is always shared from the calling script to the callee.
def myScriptInstance = load('/path/to/myScript');
To facilitate fast-failure, Groovity supports load-time dependency chaining and input validation, so that an exception can be thrown before the caller gets the Script object and tries to run it; this can be useful for example to prevent partial output, especially in a web context where you can't set an error code once part of the response body has been written.
The simplest form of load-time logic is to use field initializers to load dependencies and validate that required values are present in the binding.
@Field id = binding.id; // will throw MissingPropertyException if id is not found in binding
load('/util'); // loads another script into a field named 'util', will throw exception if script is missing or it's load fails
You can also perform more complex load-time validation by creating a "load()" method in your script; Groovity will automatically execute this function after running field initializers but before returning a Script object.
def load(){
assert id.isLong()
}
Once loaded, a script can be executed by calling script.run(), which is exactly the same as regular Groovy. However Groovity also offers a convenient run method on the factory which performs a normal load(), runs() the script, and if it returns a Writable object like a streaming template (see below) automatically streams it to the Writer bound to 'out'. This is very handy for treating a script as either a single function or a simple template. Here's a simple contrived example:
//currentTime.grvt - just returns a new date
new Date()
//formatTime.grvt - simple template to output formatted time from 'date' in binding
<~<g:formatDate format="HH:mm" date="${date}"/>~>
//controller.grvt
date = run('/currentTime')
run('/formatTime')
//java application code
Binding binding = new Binding();
binding.setVariable('out',System.out);
groovity.run('/controller',binding)
running this java sample with these three groovity files will print the current time to System.out, e.g.
10:24
The most important language addition to Groovity are streaming templates, a syntax for scripting output that could be used for command line applications, web applications, data transformations, etc. A streaming template bears a passing resemblance to a Groovy GString, but instead of using quotes uses the special <~ ~> notation:
<~Hello ${userName}~>
The comparable GString would be:
"""Hello ${userName}"""
Just like a GString, a streaming template is a Writable that can be assigned to a variable or called on to render its contents to a Writer. However there is a key difference in behavior; a GString evaluates all inline expressions at the time it is constructed and buffers each result as a string, and then replays the strings when it is asked to write out. A Streaming template on the other hand evaluates expressions at the time of writing, and instead of converting all inline expressions to strings can allow references to other templates or writables to be streamed directly instead of buffering to a string first, offering maximum performance potential. Because of this design a streaming template can be defined once and applied again and again with different binding values, since the output is not frozen at template creation time like it is with GStrings. Consider this example:
def name = "Bob"
def gstring = "Hello ${name}"
def template = <~Hello ${name}~>
println gstring
println template
name = "Alice"
println gstring
println template
This will output:
Hello Bob
Hello Bob
Hello Bob
Hello Alice
Coding languages typically have reserved special characters that need to be escaped to be treated as a literal. Groovity is unusual in that there are no special individual characters inside a streaming template; there are 4 character sequences that do however have special meaning within a streaming template. A streaming template is always opened with the character sequence <~, but that sequence is not considered to have any special meaning if it occurs again in the body of the template.
- ${ - a dollar sign followed by a curly brace indicates the beginning of an inline expression; the expression is considered closed when a balanced closing curly brace is found. To output this character sequence as a literal string you can escape it with a single backslash, ${
- ~> - a tilde followed by a greater than sign marks the end of a streaming template. To output this character sequence as a literal string you can escape it with a single backslash, ~>
- <g: - a less than sign followed by the letter 'g' and a colon indicates the opening of a functional tag call; to output this as a literal string you can use an inline expression ${'<g:'}
- </g: - a less than sign followed by a forward slash, the letter 'g' and a colon indicates the closing of a functional tag call; to output this as a literal string you can use an inline expression ${'</g:'}
Streaming template also allow you to make use of built-in or custom Tags to handle conditionals, loops and other logic inside a template.
<~
<ul>
<g:each var="item" in="${items}" pos="i">
<li class="${i%2==0?'even':'odd'}">
<g:write value="${item}" escape="xml" />
</li>
</g:each>
</ul>
~>
By default, if a Groovity script returns a Writable such as a Streaming template or GString, and is executed using the Groovity run command, the writable will automatically be rendered to the Writer currently bound to 'out', if any.
Consider two scripts:
//myTemplate.grvt
<~
The ${subject} ${predicate} over the ${object}
~>
and
//myController.grvt
out = System.out
subject = "quick brown fox"
predicate = "jumped"
object = "lazy dog"
run('/myTemplate');
calling groovity.run('/myController', new Binding()) will cause the following to be written to System out:
The quick brown fox jumped over the lazy dog
You can also explictly render a template using the built in write tag, for example if you want to generate some output but return some other value from your script.
//greetAndGet.grvt
name = load('/nameService').getName()
write{<~Hello ${name}~>}
name
Groovity provides a built-in log tag to support global logging capabilities; you don't have to worry about scope issues in static or async code blocks, and isLogEnabled checks are handled under the hood to minimize logging boilerplate. The framework automatically registers groovity code classes and method names for logging. Four logging levels are supported: info, warn, error and debug.
log(debug:"This is a normal groovy string with ${someVariable}")
<~
<g:set var="httpUrl" value="http://invalid_url"/>
<g:catch var="httpError">
<g:http url="${httpUrl}"/>
</g:catch>
<g:if test="${httpError}">
<g:log error="Error performing HTTP call to ${httpUrl}" thrown="${httpError}" />
</g:if>
~>
Groovity automatically runs scripts in the background if they have a static schedule declaration; the schedule should be specified as a string formatted as a number followed by one of the following strings to indicate the time unit: 'ms' for milliseconds, 's' for seconds, 'm' for minutes, 'd' for days. You may also omit the unit for raw milliseconds.
static schedule="5m"
log(info: "Here I am again")
The specified delay will elapse before the first run; after each run completes the delay will elapse in full again before the next run. Each run is initiated with a new binding, so loose state variables will not persist between executions; static fields may be used to share state from one execution to the next.
There may be times when you want to dynamically discover other scripts, for example to scan for classes with particular annotations or static variables. While each groovity script has its own independent classloader, the classloader offers a special method "getScriptClasses()" to retrieve all currently compiled groovity classes to perform this type of dynamic discovery.
Here is some sample code to produce a list of all compiled scripts that have declared a static field named "foo".
getClass().getClassLoader().scriptClasses.findAll{
try{
java.lang.reflect.Field fooField = it.getDeclaredField("foo");
if(java.lang.reflect.Modifier.isStatic(fooField.getModifiers())){
return true;
}
}
catch(Exception e){
}
return false;
}