Wednesday, October 25, 2017

Testing NiFi Expression Language with Groovy

(This post is adapted from a Hortonworks Community Connection article I wrote)
Some NiFi Expression Language (EL) expressions can be fairly complex, or used in a large flow, or both. These can make it difficult to test an EL expression on a running NiFi system. Although an excellent feature of NiFi is being able to adapt the flow while the system is running, it may not be prudent to stop a downstream processor, reroute a connection to something like UpdateAttribute, then list the queue in order to see attributes, content, etc.
To make EL testing easier, I wrote a Groovy script called testEL.groovy that uses the same EL library that NiFi does, so all functions present in the specified NiFi version are available to the test tool. The following is the usage:
  1. usage: groovy testEL.groovy [options] [expressions]
  2. Options:
  3. -D <attribute=value> set value for given attribute
  4. -help print this message
As an example, the following tests an expression that appends "_world" to the "filename" attribute:
  1. > groovy testEL.groovy -D filename=hello '${filename:append("_world")}'
  2. hello_world
Note that it accepts multiple attribute definitions and multiple expressions, so you can test more than one expression using a single set of attributes:
  1. > groovy testEL.groovy -D filename=hello -D size=10 '${filename:append("_world")}' '${filename:prepend("I say "):append(" ${size} times")}'
  2. hello_world
  3. I say hello 10 times
The script is as follows:
@Grab(group='org.apache.nifi', module='nifi-expression-language', version='1.4.0')
import org.apache.nifi.attribute.expression.language.*

def cli = new CliBuilder(usage:'groovy testEL.groovy [options] [expressions]',
                          header:'Options:')'print this message')
cli.D(args:2, valueSeparator:'=', argName:'attribute=value',
       'set value for given attribute')
def options = cli.parse(args)
if(!options.arguments()) {
  return 1

def attrMap = [:]
def currKey = null
options.Ds?.eachWithIndex {o,i ->
  if(i%2==0) {
    currKey = o
  } else {
    attrMap[currKey] = o
options.arguments()?.each {
  def q = Query.compile(it)
  println q.evaluate(attrMap ?: null)

and is also available as a Gist.
Hopefully you find this script helpful, if you try it please let me know how/if it works for you, and as always I welcome any questions, comments and suggestions on how to make things better :)