Search

Dark theme | Light theme

July 13, 2022

Groovy Goodness: Using Macros For Getting More Information About Variables

In a previous post we learned about the macros SV, SVI and SVD that return a string representation of variables with their name and value. Groovy 4 also added the NP and NPL macros that we can use to inspect variables. Instead of returning a GString instance these macros return a NamedValue instance or a list of NamedValue value instances. The NamedValue class is a simple class with a property name, containing the name of the variable, and property val with the value of the variable. The macro NP can be used when we have a single variable and result is a single NamedValue instance. An the macro NVL can be used with multiple variables. The result is a list of NamedValue instances where each instance represent a variable passed as argument to NVL.

In the following example we use NV and NVL:

def languages = ["Java", "Groovy", "Clojure"]

// Use NV macro to create a NamedValue object 
// with name and val properties containing the
// variable name and variable value.
def namedValue = NV(languages)

assert namedValue.name == "languages"
assert namedValue.val == ["Java", "Groovy", "Clojure"]
assert namedValue.class.name == "groovy.lang.NamedValue"


def alias = "mrhaki"
def name = "Hubert"
def age = 49

// We can pass multiple objects with the NVL macro
// and we get a List with multiple NamedValue objects.
def namedValues = NVL(alias, name, age)
assert namedValues.size() == 3
assert namedValues == [new NamedValue("alias", "mrhaki"), new NamedValue("name", "Hubert"), new NamedValue("age", 49)]

// We can use Groovy collection methods.
assert namedValues[0].name == "alias"
assert namedValues[0].val == "mrhaki"
assert namedValues.name == ["alias", "name", "age"]
assert namedValues.val == ["mrhaki", "Hubert", 49]
assert namedValues.find { nv -> nv.name == "age" }.val == 49

Written with Groovy 4.0.3.

Groovy Goodness: Using Macros For Getting String Representation Of Variables

Groovy 4 added some built-in macros that we can use in our code. A macro is code that will create new code. It does this by manipulating the Abstract Syntax Tree (AST) before the code is compiled. So when we use a macro, the macro will change the AST and those changes will be compiled. The three built-in macros SV, SVI and SVD can create a GString instance with the names and values of the variables that are passed as argument. This can be very useful to create some meaningful debugging statements. Normally we would have to type in the variable name ourselves followed by the value. Now with these macros we don't have to type the variable as the macro will add that to the AST for us.

The SV macro will use the toString method of the variable to get the value. When we want to use the inspect method for the value we can use the SVI macro. Finally we can use the SVD macro that uses the dump method to get the variable value.

In the following example we use all three macros with different variables:

def languages = ["Java", "Groovy", "Clojure"]

// With SV the toString method for the object is used.
// The name of the variable is also in our output.
assert SV(languages) == "languages=[Java, Groovy, Clojure]"
assert SV(languages).class.name == "org.codehaus.groovy.runtime.GStringImpl"

// With SVI the inspect method for the object is used.
assert SVI(languages) == "languages=['Java', 'Groovy', 'Clojure']"
assert SVI(languages).class.name == "org.codehaus.groovy.runtime.GStringImpl"

// We cannot assert here as the output contains the object instance representation
// and that changes with each run.
SVD(languages) // Possible output: languages=<java.util.ArrayList@8f636a77 elementData[Java, Groov, Clojure] size=3 modCount=3>
assert SVI(languages).class.name == "org.codehaus.groovy.runtime.GStringImpl"


// We can pass multiple objects to the SV, SVI and SVD macros.
def alias = "mrhaki"
def name = "Hubert"
def age = 49

assert SV(alias, name, age) == "alias=mrhaki, name=Hubert, age=49"
assert SVI(alias, name, age) == "alias='mrhaki', name='Hubert', age=49"
SVD(alias, name, age) // Possible output: alias=<java.lang.String@c077733c value=[109, 114, 104, 97, 107, 105] coder=0 hash=-1065913540 hashIsZero=false>, name=<java.lang.String@817bc072 value=[72, 117, 98, 101, 114, 116] coder=0 hash=-2122596238 hashIsZero=false>, age=<java.lang.Integer@31 value=49>    

Written with Groovy 4.0.3.

July 12, 2022

DataWeave Delight: Define Multi Line Strings

To define a string value in DataWeave we can use both double quotes or single quotes. To define a multi line string we simply define a string value over multiple lines. We don't have to do any strange concatenation, but simply define the value over multiple lines. DataWeave will add an end-of-line character automatically after each line.

In the following example we use different ways to defined single and multi line strings:

Source

%dw 2.0

import lines from dw::core::Strings

output application/json
---
{
    doubleQuotes: "String value defined using double quotes",
    singleQuotes: 'String value defined using single quotes',

    // Multi line strings can be defined as well.
    multiLineDoubleQuotes: "This is a multi line
string value
with double quotes",
    multiLineSingleQuotes: 'This is a multi line
string value with 
single quotes',

    // We can use the lines function to transform
    // each line into an element in an array.
    multiLines: lines("Multiline
string transformed
to array of strings")
}

Output

{
  "doubleQuotes": "String value defined using double quotes",
  "singleQuotes": "String value defined using single quotes",
  "multiLineDoubleQuotes": "This is a multi line\nstring value\nwith double quotes",
  "multiLineSingleQuotes": "This is a multi line\nstring value with \nsingle quotes",
  "multiLines": [
    "Multiline",
    "string transformed",
    "to array of strings"
  ]
}

Written with DataWeave 2.4.

July 11, 2022

DataWeave Delight: Using Literal Types

DataWeave has a nice language feature called literal types. Literal types are types with a single predefined values and can be defined using a String, Number or Boolean value. So the value of a literal type is a fixed value. We can combine multiple literal types into a new type using a union type to define an enumaration in DataWeave. The enumaration can only be one of the literal types used to define it.
Together with overloaded functions literal types are very useful. We can define a function where one of the input arguments is a literal type to define specific behaviour based on the literal type. Then we can overload the function for other literal types with different behaviour. DataWeave will make sure the correct function is called based on the value of the input argument and how it matches to the literal type value.

In the following example we define four new literal types (North, East, South, West), use a union type to define the enumaration Compass and the overloaded function direction:

Source

%dw 2.0

import capitalize from dw::core::Strings

// String based literal types with one value.
type North = "north"
type East = "east"
type South = "south"
type West = "west"

// Using union types we can use several literal
// types to define a new type like an enumeration.
type Compass = North | East | South | West

// Simple function that will be invoked if the input
// argument is either "north" or "south".
fun direction(direction: North | South): String = 
    "You are walking to " ++ 
    // We can still use String functions, 
    // because the base literal type is String.
    capitalize(direction) ++ 
    " pole."

// Overloaded function to "catch" the other values
// for the literal type Compass: "east" and "west".    
fun direction(direction: Compass): String = "Where are you going?"

// Simple data structure with String values.
var data = { up: "NORTH", right: "EAST", down: "SOUTH", left: "WEST" }

output application/json
---
{
    north: direction("north"),
    east: direction("east"),

    // We can coerce a String value into a literal type.
    south: direction(lower(data.down) as Compass),
    west: direction(lower(data.left) as Compass)
}

Output

{
    "north": "You are walking to North pole.",
    "east": "Where are you going?",
    "south": "You are walking to South pole.",
    "west": "Where are you going?"
}

In the following example we use Number and Boolean literal types. Instead of defining them explicitly as types we use the literal type in the function definitions directly:

Source

%dw 2.0

// Overloaded functions with literal types defined 
// directly at the argument level of the function.
// Here we use a Number literal type.
fun displayItems(items: 0) = "You have no items"
fun displayItems(items: 1) = "You have 1 item"
fun displayItems(items: Number) = "You have " ++ items ++ " items"

// Also Boolean literal types are supported.
// We can combine default argument values
// and overloaded functions.
fun message(value: String, debug: true) = "value - " ++ message(value)
fun message(value: String, debug: Boolean = false) = value

output application/json
---
{
    items0: displayItems(0),
    items1: displayItems(1),
    items10: displayItems(10),

    message: message("DataWeave literal types"),
    messageWithDebug: message("DataWeave literal types", true)
}

Output

{
    "items0": "You have no items",
    "items1": "You have 1 item",
    "items10": "You have 10 items",
    "message": "DataWeave literal types",
    "messageWithDebug": "value - DataWeave literal types"
}   

Written with DataWeave 2.4.

July 4, 2022

Groovy Goodness: Closed And Open Ranges

Groovy supports ranges for a long time. But Groovy 4 adds a new feature for ranges and that is the support for open (exclusive) ranges at the beginning of a range. Open means the number that defines the range is not part of the actual range result and we must use the less-than character (<). This is also referred to as exclusive, where the value is excluded from the range. When a range is closed the value is included, also called inclusive. Before Groovy 4 we could already define the end of the range to be exclusive or inclusive, but now we can also define the beginning of the range to be exclusive.

In the following example we use closed and open range definitions from the start or end:

def inclRange = 0..5

assert inclRange == [0, 1, 2, 3, 4, 5]
assert inclRange.from == 0
assert inclRange.to == 5


def exclEndRange = 0..<5

assert exclEndRange == [0, 1, 2, 3, 4]
assert exclEndRange.from == 0
assert exclEndRange.to == 4


// Support for exclusive begin added in Groovy 4.
def exclBeginRange = 0<..5

assert exclBeginRange == [1, 2, 3, 4, 5]
assert exclBeginRange.from == 1
assert exclBeginRange.to == 5


// Support for exclusive begin added in Groovy 4.
def exclRange = 0<..<5

assert exclRange == [1, 2, 3, 4]
assert exclRange.from == 1
assert exclRange.to == 4

Written with Groovy 4.0.3.

Get Absolute Path To An SDK With SDKMAN!

SDKMAN! has a home command that will return the absolute path of any SDK we pass as argument. This can be usefull in scripts where we need the absolute path to a SDK. We can specify the name of the SDK after the home command and the version. To use the current version that is set as default we can use as version current.

In the following example we use the home command for several SDKs:

$ sdk home groovy 4.0.3
/Users/mrhaki/.sdkman/candidates/groovy/4.0.3%
$ sdk home java 17.0.3-tem
/Users/mrhaki/.sdkman/candidates/java/17.0.3-tem%
$ sdk home jbake current
/Users/mrhaki/.sdkman/candidates/jbake/current%
$ export ASCIIDOCTORJ_DIR=`sdk home asciidoctorj current`
$ echo $ASCIIDOCTOR_DIR
/Users/mrhaki/.sdkman/candidates/asciidoctorj/current
$

Written with SDKMAN! 5.15.0.

July 1, 2022

Groovy Goodness: Creating TOML Configuration With TomlBuilder

Groovy 4 introduced support for TOML configuration file. In a previous post we already learned how we can parse TOML content. In this post we will see we can use a builder syntax to create TOML content. We need the class TomlBuilder and then define our structure using a nice builder DSL. The DSL is comparable to create JSON using the JsonBuilder. The names of the nodes in the DSL structure will be the names of the properties. Nodes within nodes will result in concatenated property names with the name of each node separated by a dot (.). We can also use collections as arguments and those will translated to TOML arrays. A collection can optionally be followed by a closure that processes each item in the collection to generate the content for the TOML array.

In the following example we use the builder syntax to create what we want in our TOML content. Using the toString method we get the TOML data as string:

import groovy.toml.TomlBuilder

// Helper record class to store "server" properties.
record Server(String env, String host, int port) {}

// Create Tomlbuilder.
def toml = new TomlBuilder()

// Define structure.
toml {
    // Use closure to group.
    application {
        name "Groovy TOML"
        version "1.0.0"
    }

    // Use closures to define levels.
    users {
        env {
           enabled true
        }
        acc {
            enabled false
        }
    }
         
    // Use maps
    debug(enabled: true)  
    
    // We can use collections
    ports([80, 443]) 
    
    // Convert data with closure applied for each item in collection.
    servers([new Server("dev", "localhost", 8080), 
             new Server("uat", "cloud-acc", 80)], { server -> 
        env server.env 
        host server.host 
        port server.port 
    }) 
}

assert toml.toString() == """\
application.name = 'Groovy TOML'
application.version = '1.0.0'
users.env.enabled = true
users.acc.enabled = false
debug.enabled = true
ports = [80, 443]
servers = [{env = 'dev', host = 'localhost', port = 8080}, {env = 'uat', host = 'cloud-acc', port = 80}]
"""

// In order to write to a writer we could use:
// def sw = new StringWriter()
// toml.writeTo(sw)
// def content = sw.toString()

Instead of using the builder DSL syntax we can also use a Map with all data we want to transform to TOML data:

import groovy.toml.TomlBuilder

def instant = Instant.ofEpochSecond(1656487920)
def clock = Clock.fixed(instant, ZoneOffset.UTC)

def config = [
    application: [
      name: "Groovy TOML",
      version: "1.0.0"
    ],
    users: [
        dev: [enabled: true],
        uat: [enabled: false]
    ],
    ports: [80, 443],
    debug: [
        enabled: false
    ],
    build: [
        jdk: 'openjdk version "17.0.3" 2022-04-19',
        time: ZonedDateTime.now(clock).dateTimeString
   ],
   servers: [
       [env: "dev", host: "localhost", port: 8080],
       [env: "uat", host: "cloud-acc", port: 80]
   ]
]

// Create TomlBuilder
def toml = new TomlBuilder()

// Use data defined in the Map.
toml config

assert toml.toString() == """\
application.name = 'Groovy TOML'
application.version = '1.0.0'
users.dev.enabled = true
users.uat.enabled = false
ports = [80, 443]
debug.enabled = false
build.jdk = 'openjdk version "17.0.3" 2022-04-19'
build.time = '2022-06-29T07:32:00Z'
servers = [{env = 'dev', host = 'localhost', port = 8080}, {env = 'uat', host = 'cloud-acc', port = 80}]
"""

Written with Groovy 4.0.3.

June 29, 2022

Groovy Goodness: Reading TOML Configuration

Since Groovy 4 we can parse TOML configuration data into a Map. Once the TOML data is transformed into the Map we can use all possibilities in Groovy to lookup keys and their values in maps. For example we can use GPath expressions to easily get the value of a (nested) key. To parse TOML configuration data we must use the TomlSlurper class that is in the groovy.toml package. We can use the parse method when we have a file, reader or stream with our configuration. To parse a String value with TOML configuration we use the parseText method.

In the following example we define our configuration using TOML syntax and use the parseText method to transform it into a Map. We use different ways with GPath expressions to read the configuration data:

import groovy.toml.TomlSlurper

// Configuration in TOML syntax.
def config = '''
application.name = "Groovy TOML"
application.version = "1.0.0"
application.ports = [80, 443]

# Set to true for debugging
debug.enabled = false

[build]
jdk = 'openjdk version "17.0.3" 2022-04-19'
time = "2022-06-29T07:32:00Z"

[[servers]]
name = "dev"
host = "localhost"
port = 8080

[[servers]]
name = "uat"
host = "cloud-acc"
port = 80
'''

// We get back a Map with all configuration.
def toml = new TomlSlurper().parseText(config)
// To read data from files, readers or streams we can use overloaded
// versions of the method TomlSlurper#parse.

// We can reference the properties using GPath expressions.
assert toml.application.name == "Groovy TOML"
assert toml.application.version == "1.0.0"
// TOML array is transformed to ArrayList.
assert toml.application.ports == [80, 443]
assert toml.application.ports.class == java.util.ArrayList
assert toml.application == [name: "Groovy TOML", version: "1.0.0", ports: [80, 443]]

// TOML boolean is transformed to boolean.
assert !toml.debug.enabled

assert toml.build.jdk == /openjdk version "17.0.3" 2022-04-19/
assert toml.build.time == "2022-06-29T07:32:00Z"
// Dates are not parsed, but we get them as String value.
assert toml.build.time.class == java.lang.String

// Array of tables in TOML are also supported by the TomlSlurper.
assert toml.servers.size() == 2

def developmentConfig = toml.servers.find { s -> s.name == "dev" }
assert developmentConfig.host == "localhost"
assert developmentConfig.port == 8080

def uatConfig = toml.servers.find { s -> s.name == "uat" }
assert uatConfig.host == "cloud-acc"
assert uatConfig.port == 80

assert toml.servers*.name == ["dev", "uat"]

Written with Groovy 4.0.3.

June 28, 2022

Groovy Goodness: Get Row Number In GINQ Result Set

GINQ (Groovy-INtegrated Query) is added since Groovy 4. With GINQ we can query in-memory collections with SQL like statements. If we want to get the row numbers for each row in the query result set we can use the implicit variable _rn. We must specify _rn in the select expression of our GINQ query. We can even use as to give it a meaningful name in the result set.

In the following example we have a basic GINQ query where use _rn to get the row number of each row in the query result set:

def letters = GQ {
    from letter in ['a', 'b', 'c']
    select _rn, letter
}.collect { item -> [item._rn, item.letter] }

assert letters == [[0, 'a'], [1, 'b'], [2, 'c']]

In the following example we first parse JSON and then use GINQ to query it. We see in the example that the row number is based on the result after the where expression is applied:

import groovy.json.JsonSlurper

def json = new JsonSlurper().parseText('''
[
  {
    "id": 1001,
    "language": "Groovy"
  },
  {
    "id": 1002,
    "language": "Clojure"
  }, 
  {
    "id": 1003,
    "language": "Java"
  }
]
''')

def languages = GQ {
  from l in json
  where l.language == "Groovy"
  // We add 1 to _rn to make it 1-based instead of 0-based. 
  // Also we use as rowNumber to give a meaningful name.
  select _rn + 1 as rowNumber, l.id as id, l.language as name
}.collect { item -> [row: item.rowNumber, name: item.name] }

// Also notice the row number is calculated based
// on the result after applying the where clause.
assert languages.first() == [row: 1, name: "Groovy"]

Written with Groovy 4.0.3.

Groovy Goodness: Using The Switch Expression

In a previous (old) post we learned how Groovy supports different classifiers for a switch case statement. Since Groovy 4 we can use switch also as an expression. This means the switch statement returns a value without having to use return. Instead of using a colon (:) and break we use the -> notation for a case. We specify the value that the switch expressions returns after ->. When we need a code block we simply put the code between curly braces ({...}).

In the following example we use the switch expression with different case statements:

def testSwitch(val) {
    switch (val) {
        case 52 -> 'Number value match'
        case "Groovy 4" -> 'String value match'
        case ~/^Switch.*Groovy$/ -> 'Pattern match'
        case BigInteger -> 'Class isInstance'
        case 60..90 -> 'Range contains'
        case [21, 'test', 9.12] -> 'List contains'
        case 42.056 -> 'Object equals'
        case { it instanceof Integer && it < 50 } -> 'Closure boolean'
        case [groovy: 'Rocks!', version: '1.7.6'] -> "Map contains key '$val'"
        default -> 'Default'
    } 
}

assert testSwitch(52) == 'Number value match'
assert testSwitch("Groovy 4") == 'String value match'
assert testSwitch("Switch to Groovy") == 'Pattern match'
assert testSwitch(42G) == 'Class isInstance'
assert testSwitch(70) == 'Range contains'
assert testSwitch('test') == 'List contains'
assert testSwitch(42.056) == 'Object equals'
assert testSwitch(20) == 'Closure boolean' 
assert testSwitch('groovy') == "Map contains key 'groovy'"
assert testSwitch('default') == 'Default'

Written with Groovy 4.0.3.