Groovy Scripts - Reusable Code
Let's start with the following Groovy script which reads and prints all the properties from a file.
import java.nio.file.Path
Properties loadProperties() {
Properties properties = new Properties()
Path propertyFilePath = Path.of('my.properties')
propertyFilePath.withInputStream { properties.load(it) }
properties
}
Properties properties = loadProperties()
properties.each { println it }
The script needs 'my.properties' file with the following content.
key1=value1
key2=value2
Suppose we also want a script that accepts the property key as a command-line argument and prints out the property value along with the key. We could write that script as follows.
import java.nio.file.Path
Properties loadProperties() {
Properties properties = new Properties()
Path propertyFilePath = Path.of('my.properties')
propertyFilePath.withInputStream { properties.load(it) }
properties
}
Properties properties = loadProperties()
println properties.find { it.key == args[0] }
You might have already noticed the duplicate code. However, what is prominent here is that the logic to load the properties from the file is represented in multiple places leading to the violation of DRY (Don't Repeat Yourself) principle.
Let's work towards adhering to the DRY principle. It's obvious the logic to load the properties cannot be in one of the two scripts.
One option is to extract the common piece of code into a class as follows.
import java.nio.file.Path
class PropertyUtil {
static Properties loadProperties() {
Properties properties = new Properties()
Path propertyFilePath = Path.of('my.properties')
propertyFilePath.withInputStream { properties.load(it) }
properties
}
}
I have moved the loadProperties
method into a new class PropertyUtil
in the same default package. The scripts would look as follows with our refactoring.
Properties properties = PropertyUtil.loadProperties()
properties.each { println it }
Properties properties = PropertyUtil.loadProperties()
println properties.find { it.key == args[0] }
The above code works as expected. However, since the method loadProperties
is not in a script, it would not have any of the context information of the script.
Let's make a case for using script capabilities in the reusable method loadProperties
. Suppose we don't want to hard-code the name of the property file. Instead, we want to pass the name of the property file as a command-line argument. The code would look as follows.
import java.nio.file.Path
class PropertyUtil {
static Properties loadProperties(String propertyFile) {
Properties properties = new Properties()
Path propertyFilePath = Path.of(propertyFile)
propertyFilePath.withInputStream { properties.load(it) }
properties
}
}
And the script would look as follows.
Properties properties = PropertyUtil.loadProperties(args[0])
properties.each { println it }
For brevity, I am not showing the second script here onwards.
Here, we had to pass the command-line argument holding the name of the property file as an argument to loadProperties
. Instead, you could take the following approach.
Lets make the class PropertyUtil
extend groovy.lang.script
as follows
import java.nio.file.Path
abstract class PropertyUtil extends Script {
Properties loadProperties() {
Properties properties = new Properties()
Path propertyFilePath = Path.of(args[0])
propertyFilePath.withInputStream { properties.load(it) }
properties
}
}
Note how we are not passing the property file name anymore to loadProperties
, and relying upon the script capabilities to get access to command-line arguments.
Our script will be updated as follows.
import groovy.transform.BaseScript
@BaseScript PropertyUtil propertyUtil
Properties properties = loadProperties()
properties.each { println it }
The BaseSCript
AST transformation makes sure that the generated class for our script extends from PropertyUtil
rather than Script
. Hence the method loadProperties
is available to our script.
The above code violates the Liskov Substitution Principle (LSP) which does not make me happy. The composition strategy would be a better design approach here than inheritance.
Let's modify the code to fix the LSP violation.
import java.nio.file.Path
abstract class PropertyUtil extends Script {
protected Properties props
void loadProperties() {
Properties properties = new Properties()
Path propertyFilePath = Path.of(args[0])
propertyFilePath.withInputStream { properties.load(it) }
props = properties
}
abstract void useProperties()
@Override
Object run() {
loadProperties()
useProperties()
}
}
In our script, we need to supply the implementation for the abstract method useProperties
as follows.
import groovy.transform.BaseScript
@BaseScript PropertyUtil propertyUtil
props.each { println it }
Here, whatever code is in the body of the script becomes the implementation for the abstract method defined in the base script. If you are familiar with design patterns, you would have already recognised the template method pattern.
One last thing about the Groovy scripts - if you have a very complex set of command-line arguments I would suggest you opt for libraries like picocli than a conventional Groovy script.