Groovy script

Use a groovy script to derive a target property value.

Category: Groovy

Parameter:

Source:
Target:

Function ID: eu.esdihumboldt.cst.functions.groovy

Using the Groovy script function you can control the transformation of source properties to a specific target property through a Groovy script. Basic knowledge of the Groovy language is recommended when using this function. The source properties you choose as variables are available as variables in the script.

The Groovy script is called for each set of source property values to transform, and is responsible to create a corresponding target instance or value. The source variables may or may not be set, depending on the occurrence in the source instance. A source variable that is not set has a null value.

By default the variables contain the value directly associated to a property. You can configure the function to instead use the property instances with their complete structure as variable values. There is a dedicated API to access the properties of any such instance - it is explained in the following chapter.

Groovy script (greedy)

The greedy version of the Groovy script is called for all available source property values at once. Each source variable is bound to a list of values which may be empty.

Access properties in instance variables

Assuming you have an instance variable called instance available in your script, then you can access the instance value with instance.value . With instance.p or instance.properties on the other hand, you can create a properties accessor. This also works for lists of instances in the case of a greedy Groovy script.

With a properties accessor you can navigate through the properties of an instance, as well as their sub-properties and their sub-properties and so on. Once you reached the property you are interested in, you have to decide how to retrieve it. There are several options:

Whether you get instances or values with first() , list() or each{} depends on whether the corresponding property has sub-properties of its own. If it has sub-properties according to the schema, instances will be returned, otherwise directly values.

Let's take a look at the following example structure of an instance we assume is stored in the variable instance :

To retrieve the value of the id property we can use the property accessor like this:

instance.p.id.value()

You can store a value in a variable for later use:

def id = instance.p.id.value()

All names of the instance can be retrieved like this:

def names = instance.p.name.values()

In this case only the direct values associated to the name property are returned.
To access both name and language for all names we can use each :

instance.p.name.each {
  nameInstance ->
	
  // retrieve name
  def name = nameInstance.value
  // retrieve language
  def language = nameInstance.p.language.value()
}

As you see above, you can easily access the value of an instance with .value .

UI assistance

The script editing page offers the possibility to open a tray showing the source variables structure. You can use it to browse the properties and sub-properties. If you select an element, sample code for accessing the property is shown in the text field below, like in this example:

Troubleshooting

It may happen that there are multiple properties with the same name - to explicitly reference a specific property you can provide the namespace of the property like this:

instance.p.name('http://my.namespace.com').value()

The function result

The result of your function can be a plain value for the target property, a structured instance, or both, depending on the type of the target property.

A plain value for the target property can be returned by making it the last statement in the script, or by explicitly exiting the script with the return command.

Build a target instance

To create an instance as result of the script, you have to use the so-called builder API. You have to define a closure that describes how the instance is structured, which properties should have which values and so on, and add it using the _target variable.
The most simple of structures - an empty instance - can be created like this:

_target {

}

The builder by default creates the instance based on the structure defined in the schema. Thus using properties that do not exist in the type definition will fail. To get into more detail on how the builder API works, let's assume the following structure as the schema of our target instance to be created:

The structure is quite complex, but let's start with something simple: There is a type property which can have a string value - we can add a type property with the value test to our instance like this:

_target {
  type('test')
}

The type property may occur multiple times, we can easily add the property more than once:

_target {
  type 'test1'
  type 'test2'
  type 'test3'
}

This creates three type properties in the instance, each with a different value. As an alternative to before, here we use a notation without brackets.

The builder calls can be mixed with programming constructs, for instance could we achieve the same as above using a simple loop:

_target {
  for (i in 1..3) {
    type('test' + i)
  }
}

Creating complex structures

The type structure also contains a complex name property with several sub-properties on multiple levels. Such a nested structure can for example be created like this:

_target {
  name {
    GeographicalName {
      language 'en'
      spelling {
        SpellingOfName {
          text 'some name'
        }
      }
    }
  }
}

UI assistance

The script editing page offers the possibility to open a tray showing the target instance structure. You can use it to browse the properties and sub-properties. If you select an element, sample code for creating an instance with that property is shown in the text field below. Select all the properties you want to populate to generate a template for the instance creation. To use it just copy the sample code to the editor.

In addition there is support for content assistance when building an instance, it can be triggered with Ctrl+Space in the Groovy editor. It allows selecting applicable properties to build at the current position from a list.

Troubleshooting

In case there are multiple properties with the same name you have to reference a specific property explicitly by specifying its namespace. This is done through a named parameter namespace like in the example below:

_target {
  type('test', namespace: 'http://my.namespace.com')
}

Another problem that may arise is that property names may be conflicting with variables, reserved keywords or other identifiers. You can solve this by explicitly calling the builder, which is available as the variable _b in the script:

def type = 'test'
_target {
  _b.type type
}

If you need to use a property name that is a reserved keyword in Groovy, e.g. class, then you need to quote it. For example:

_target {
  'class'('test')
}

Multiple results

If the target property can occur multiple times, you can return multiple results, that will each be represented as an individual instance of the target property.

If the result is created as complex instance, you can create multiple results by simply calling _target multiple times. For each call a result is created, and you can even integrate this with programming logic like loops. For example:

for (num in 1..3) {
  _target {
    id ( "Feature_$num" )
  }
}

If the results are not instances, but values, multiple results can be created by multiple calls to _target as well:

for (num in 1..3) {
  _target("Value $num")
}

Alternatively, if you have an array or collection where each entry should be a result value, you can convert it to a MultiValue and return it:

return ['Value 1', 'Value 2', 'Value 3'] as MultiValue

Putting both together

Now that we know how accessing properties and building instances works, here a small example related to the above structures that makes use of both to create a target structure populated with values from a source instance:

_target {
  instance.p.name.each { name ->
    def lang = name.p.language.value()
    GeographicalName {
      if (lang) {
        language lang
      }
      spelling {
        SpellingOfName {
          text name.value
        }
      }
    }
  }
}

Helper Functions

hale studio provides the possibility to extend it with helper functions that can be conveniently called from Groovy scripts. An overview on the available functions can be found in the functions tray (see below). Select an individual function to get detailed information on:

The functions are accessible through the _ binding in the script, and are organized in categories/packages.

Generally, if a function supports multiple parameters, you have to use the named parameters notation of Groovy. For Example:

_.geom.buffer(geometry: g, distance: 10)

Above the function buffer in the package geom is called with two parameters, the variable g as the geometry and 10 as the distance.

There is auto-completion available for helper functions as well. Auto-completion can be triggered with Ctrl + Space. Make sure to start with _. , you may have to specify a start character to have a valid script for the completion processor to work.

Additional binding content

Now we know already that the binding allows accessing _target, _b and, depending on which Groovy function you are using either _index, _source or the source properties. But there are further variables you can access.

Note: When using one of the transformation contexts that allow you to share data between script executions in different places, keep in mind that usually no order in which instances are transformed can be guaranteed. The only way to influence transformation order is setting cell priorities on type relations.

Collectors

A collector is a helper object that allows you to easily collect information.

To create a new collector instantiate one like this:

def c = new Collector()

A collector often is useful for collecting (shared) information in a transformation run. Thus a helper method is provided as part of the helper functions, that retrieves or creates a collector associated to a context map. For example:

withTransformationContext {
  def c = _.context.collector(it)
}

Store information

In a collector, information is stored based on keys. Most often a key is a string, but you can also use other objects as keys.

The following statement adds a value to the key named identifiers :

c.identifiers << 'ID1'

Keys can be used with an arbitrary number of levels:

c.hydro.rivers.identifiers << 'ID1'

Non-string keys (for example numbers or lists) or variables can be used as keys by using the squared brackets notation:

def key = ['foo', 12]
c[key] << 'bar'
c.hydro.rivers.source[12] << 'ID1'

When you know that you deal with a single value instead of accumulating values, you can use the assign operator:

def key = 'identifier'
c[key] = 'ID1'

There is no need to create keys, the corresponding child collectors are created automatically when a key is accessed.

Retrieve information

To retrieve information from a collector, access is also done using the respective keys. By just specifying the keys you get the respective child collector. To retrieve values from a collector you can call the following methods:

Both of the above mentioned methods ignore any child collectors and only return the values of the addressed level.

Additionally a collector provides methods to iterate over its values and child collectors. To iterate over a collector's values use each or consume with one argument:

c.identifiers.each { value ->
  ...
}

The difference between each and consume is that when using consume , the corresponding list of values is reset.

When using variable keys it may be desired to be able to iterate over all keys (or child collectors) in a collector. For this the eachCollector method can be used. If only one argument is provided, the child collector is passed in, if two arguments are provided the key and the respective child collector are passed in:

c.eachCollector { key, child ->
  ...
}

If you are interested in the keys and corresponding child values of a collector, you can use each or consume with two arguments to iterate over all present keys and the respective value lists. For example:

c.each { key, values ->
  values.each { value ->
    ...
  }
}