.. _template-directives:
Template Directives
===================
This part of the documentation is an introduction explaining the different template directives.
Examples will be provided for both Simple Features and Complex Features.
The syntax of the directives varies slightly between XML based templates and JSON based templates.
The examples will be provided mainly for GeoJSON and GML. However the syntax defined for GeoJSON output, unless otherwise specified, is valid for JSON-LD templates
Template directive summary
--------------------------
The following constitutes a summary of all the template directives and it is meant to be used for quick reference. Each directive is explained in detail in the sections below.
JSON based templates
^^^^^^^^^^^^^^^^^^^^
The following are the directives available in JSON based templates.
.. list-table::
:widths: 30 10 60
* - **Usage**
- **Syntax**
- **Description**
* - property interpolation
- ${property}
- specify it as an attribute value (:code:`"json_attribute":"${property}"`)
* - cql evaluation
- $${cql}
- specify it as an element value (:code:`"json_attribute":"$${cql}"`)
* - setting the evaluation context for child attributes.
- ${source}.
- specify it as the first nested object in arrays (:code:`{"$source":"property"}`) or as an attribute in objects (:code:`"$source":"property"`)
* - filter the array, object, attribute
- $filter
- specify it inside the first nested object in arrays (:code:`{"$filter":"condition"}`) or as an attribute in objects (:code:`"$filter":"condition"`) or in an attribute next to the attribute value separated by a :code:`,` (:code:`"attribute":"$filter{condition}, ${property}"`)
* - defines options to customize the output outside of a feature scope
- $options
- specify it at the top of the JSON template as a JSON object (GeoJSON options: :code:`"$options":{"flat_output":true, "separator":"."}`; JSON-LD options: :code:`"$options":{"@context": "the context json", "encode_as_string": true, "@type":"schema:SpecialAnnouncement", "collection_name":"customCollectionName"}`).
* - allows including a template into another
- $include, $includeFlat
- specify the :code:`$include` option as an attribute value (:code:`"attribute":"$include{subProperty.json}"`) and the :code:`$includeFlat` as an attribute name with the included template path as a value (:code:`"$includeFlat":"included.json"`)
* - allows a template to extend another template
- $merge
- specify the :code:`$merge` directive as an attribute name containing the path to the extended template (:code: `"$merged":"base_template.json"`).
* - allows null values to be encoded. default is not encoded.
- ${property}! or $${expression}!
- ! at the end of a property interpolation or cql directive (:code:`"attribute":"${property}!"` or :code:`"attribute":"$${expression}!"`).
XML based templates
^^^^^^^^^^^^^^^^^^^^
The following are the directives available in XML based templates.
.. list-table::
:widths: 30 10 60
* - **Usage**
- **Syntax**
- **Description**
* - property interpolation
- ${property}
- specify it either as an element value (:code:`${property}`) or as an xml attribute value (:code:``)
* - cql evaluation
- $${cql}
- specify them either as an element value (:code:`$${cql}`) or as an xml attribute value (:code:``)
* - setting the evaluation context for property interpolation and cql evaluation in child elements.
- gft:source
- specify it as an xml attribute (:code:``)
* - filter the element to which is applied based on the defined condition
- gft:filter
- specify it as an XML attribute on the element to be filtered (:code:``)
* - marks the beginning of an XML template.
- gft:Template
- It has to be the root element of an XML template (:code:` Template content`)
* - defines options to customize the output outside of a feature scope
- gft:Options
- specify it as an element at the beginning of the xml document after the :code:`` one (:code:``). GML options: :code:``,:code:``. HTML options: :code:`
-
MeteoStations
-
Code
- $${strConcat('Station_',st:code)}
-
Name
-
Geometry
-
Temperature
-
Pressure
-
Wind Speed
The output of the template will be the following:
.. figure:: images/html-template-result.png
Including other templates
-------------------------
While developing a group of templates, it's possible to notice sections that repeat across
different template instances. Template inclusion allows sharing the common parts, extracting them
in a re-usable building block.
Inclusion can be performed using two directives:
* :code:`include` allows including a separate template as is.
* :code:`includeFlat` allows including a separate template, stripping the top-most container.
As for other directives the syntax varies slightly between JSON based template and XML based ones.
The two directives need to specify a path to the template to be included.
Template names can be plain, as in this example, refer to sub-directories, or be absolute.
Examples of valid template references are:
* ``subProperty.json``
* ``./subProperty.json``
* ``./blocks/aBlock.json``
* ``/templates/test/aBlock.json``
However it's currently not possible to climb up the directory hierarchy using relative references,
so a reference like ``../myParentBlock.json`` will be rejected.
JSON based templates (GeoJSON, JSON-LD)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In this context the two directives can be defined as:
* :code:`$include`.
* :code:`$includeFlat`.
Regarding the :code:`$includeFlat` option is worth mentioning that in a JSON context:
* If a JSON object is included, then its properties are directly included in-place, which makes sense only within another object.
* If instead a JSON array is included, then its values are directly included in-place, which makes sense only within another array.
The following JSON snippet shows the four possible syntax options for template inclusion:
.. code-block:: json
:linenos:
{
"aProperty": "$include{subProperty.json}",
"$includeFlat": "propsInAnObject.json",
"anArray" : [
"$include{arrayElement.json}",
"$includeFlat{subArray.json}"
],
"$includeFlat": "${property}"
}
Notes:
1) The ``subProperty.json`` template (line 2) can be both an object or an array, it will be used as the new value of ``aProperty``
2) The ``propsInAnObject.json`` template (line 3) is required to be a JSON object, its properties will be
directly included in-place where the ``$includeFlat`` directive is
3) The ``arrayElement.json`` template (line 5) can be both an object or an array, the value will be replaced
directly as the new element in ``anArray``. This allows creation of a JSON object as the array
element, or the creation of a nested array.
4) The ``subArray.json`` template (line 6) must be an array itself, the container array will be stripped and
its values directly integrated inside ``anArray``.
In case an includeFlat directive is specified and it's attribute value is a property interpolation directive, if the property name evaluates to a json it gets included flat in the final output e.g
including json:
.. code-block:: json
:linenos:
{
"property":"${property}",
"bProperty":"15",
"cProperty":"30"
}
${property} value:
.. code-block:: json
:linenos:
{
"aProperty": "10",
"bProperty": "20"
}
result:
.. code-block:: json
:linenos:
{
"aProperty":"10",
"bProperty":"20",
"cProperty":"30"
}
The ``${property}`` directive evaluates to a JSON that will be merged with the including one. In case the including JSON as an attribute with the name equal to one of the attributes in the included JSON, the included will override the property with the same name in the including one.
In case an includeFlat directive is specified inside a JSON Array with a Feature property and the property evaluate to a JSON Array, the container array will be stripped and its values included directly inside the container Array:
.. code-block:: json
:linenos:
[
"value1",
"value2",
"value3",
"$includeFlat{${property}}"
]
${property} value:
.. code-block:: json
:linenos:
[
"value4",
"value5"
]
result:
.. code-block:: json
:linenos:
[
"value1",
"value2",
"value3",
"value4",
"value5"
]
XML based templates (GML)
^^^^^^^^^^^^^^^^^^^^^^^^^^
In an XML context the two directives needs to be defined in the following way:
* :code:`path/to/included.xml`.
* :code:`$include{includedTemplate.xml}`.
In the first case the included template will replace the :code:`` element. In the second one the included template will be placed inside the :code:`` element.
Extending other templates via merge (JSON based templates only)
---------------------------------------------------------------
Templates inclusion, described above, allows importing a block into another template, as is.
The ``$merge`` directive instead allows getting an object and use it as a base, that will be
overridden by the properties of the object it is merged into.
For example, let's assume this is a base JSON template:
.. code-block:: json
{
"a": 10,
"b": "${attribute1}",
"c": "${attribute2}",
"array": [1, 2, 3]
}
and this is a template extending it:
.. code-block:: json
{
"$merge": "base.json",
"a": {
"a1": 1,
"a2": 2
},
"b": null,
"d": "${customAttribute}"
}
The template actually being processed would look as follows:
.. code-block:: json
{
"a": {
"a1": 1,
"a2": 2
},
"c": "${attribute2}",
"array": [1, 2, 3]
"d": "${customAttribute}"
}
The general rules for object merging are:
* Overridden simple properties are replaced.
* Properties set to null are removed.
* Nested objects available in both trees are drilled down, being recursively merged.
* Arrays are replaced as-is, with no merging. The eventual top level ``features`` array is the only
exception to this rule.
* While order of the keys is not important in JSON, the merge is processed so that the base
property names are included first in the merged result, and the new ones included in the override
are added after them.
* If in the overalay JSON template, are present attributes with a property interpolation directive or an expression that in turn returns a JSON, the JSON attribute tree will be merged too with the corresponding one in the base JSON tree.
The ``$merge`` directive can be used in any object, making it the root for the merge operation.
This could be used as an alternative to inclusion when local customizations are needed.
Environment parametrization
---------------------------
A template configuration can also be manipulated on the fly, replacing existing attributes, attributes' names and sources using the :code:`env` parameter.
To achieve this the attribute name, the attribute, or the source should be replaced by the env function in the following way :code:`$${env('nameOfTheEnvParameter','defaultValue')}`.
If in the request it is specified an env query parameter :code:`env='nameOfTheEnvParameter':'newValue'`, the default value will be replaced in the final output with the one specified in the request.
The functionality allows also to manipulate dynamically filters and expression. For example it is possible to change Filter arguments: :code:`"$filter":"xpath('gsml:name') = env('nameOfTheEnvParameter','defaultValue')`.
Xpaths can be manipulated as well to be totally or partially replaced: :code:`$${xpath(env('xpath','gsml:ControlledConcept/gsml:name')}` or :code:`$${xpath(strConcat('env('gsml:ControlledConcept',xpath','/gsml:name')))}`.
Dynamic keys
------------
Keys in JSON output can also be fully dependent on feature attributes, for example:
.. code-block:: json
{
"${attributeA}" : "${attributeB}",
"$${strSubstring(attributeC, 0, 3)}": "$${att1 * att2}"
}
Using a key depending on feature attributes has however drawbacks: it won't be possible to use it
for filtering in WFS and for queriables generation in OGC APIs, as it does not have a stable value.
JSON based properties
---------------------
Certain databases have native support for JSON fields. For example, PostgreSQL has both a JSON
and a JSONB type. The JSON templating machinery can recognize these fields and export them
as JSON blocks, for direct substitution in the output.
It is also possible to pick a JSON attribute and use the ``jsonPointer`` function to extract either
a property or a whole JSON subtree from it. See the `JSON Pointer RFC `_
for more details about valid expressions.
Here is an example of using JSON properties:
.. code-block:: json
:linenos:
{
"assets": "${assets}",
"links": [
"$${jsonPointer(others, '/fullLink')}",
{
"href": "$${jsonPointer(others, '/otherLink/href')}",
"rel": "metadata",
"title": "$${jsonPointer(others, '/otherLink/title')}",
"type": "text/xml"
}
]
}
Some references:
- ``Line 1`` uses ``assets``, a property that can contain a JSON tree of any shape, which will be
expanded in place.
- ``Line 4`` inserts a full JSON object in the array. The object is a sub-tree of the ``others`` property,
which is a complex JSON document with several extra properties (could be a generic containers for
properties not fitting the fixed database schema).
- ``Line 6`` and ``Line 8`` extract from the ``others`` property specific string values.
Array based properties (JSON based templates only)
--------------------------------------------------
Along JSON properties, it's not rare to find support for array based attributes in modern databases.
E.g. ``varchar[]`` is a attributes containing an array of strings.
The array properties can be used as-is, and they will be expanded into a JSON array.
Let's assume the ``keywords`` database column contains a list of strings, then the following template:
.. code-block:: json
:linenos:
{
"keywords": "${keywords}"
}
May expand into:
.. code-block:: json
:linenos:
{
"keywords": ["features", "templating"]
}
It is also possible to use an array as the source of iteration, referencing the current
array item using the ``${.}`` XPath. For example:
.. code-block:: json
:linenos:
{
"metadata": [
{
"$source": "keywords"
},
{
"type": "keyword",
"value": "${.}"
}
]
}
The above may expand into:
.. code-block:: json
:linenos:
{
"metadata": [
{
"type": "keyword",
"value": "features"
},
{
"type": "keyword",
"value": "templating"
}
]
}
In case a specific item of an array needs to be retrieved, the ``item`` function can be used,
for example, the following template extracts the second item in an array (would fail if not
present):
.. code-block:: json
:linenos:
{
"second": "$${item(keywords, 1)}"
}
There is currently no explicit support for array based columns in GML templates.
Simplified Property Access
--------------------------
The features-templating plug-in provides the possibility to directly reference domain name when dealing with Complex Features and using property interpolation in a template.
As an example let's use again the meteo stations use case. This is the ER diagram of the Database table involved.
.. figure:: images/meteos-stations-er-diagram.png
The following is a GeoJSON template that directly reference table names and column name, instead of referencing the target Xpath in the AppSchema mappings.
.. code-block:: json
{
"$source":"meteo_stations",
"Identifier":"${id}",
"Name":"${common_name}",
"Code":"$${strConcat('STATION-', xpath('code'))}",
"Location":"$${toWKT(position)}",
"Temperatures":[
{
"$source":"meteo_observations",
"$filter":"propertyPath('->meteo_parameters.param_name') = 'temperature' AND 'yes' = env('showTemperatures','yes')"
},
{
"Timestamp":"${time}",
"Value":"${value}"
}
],
"Pressures":[
{
"$source":"meteo_observations",
"$filter":"propertyPath('->meteo_parameters.param_name') = 'pressure' AND 'yes' = env('showPressures','yes')"
},
{
"Timestamp":"${time}",
"Value":"${value}"
}
],
"Winds speed":[
{
"$source":"meteo_observations",
"$filter":"propertyPath('->meteo_parameters.param_name') = 'wind speed' AND 'yes' = env('showWinds','yes')"
},
{
"Timestamp":"${time}",
"Value":"${value}"
}
]
}
As it is possible to see this template has some differences comparing to the one seen above:
* Property interpolation (``${property}``) and cql evaluation (``$${cql}``) directives are referencing the column name of the attribute that is meant to be included in the final output. The names match the ones of the columns and no namespaces prefix is being used.
* Inside the $${cql} directive instead of using an ``xpath`` function the ``propertyPath`` function is being use. It must be used when the property references domain names inside a ``$${cql}`` directive. Paths in this case are no more separated by a ``/`` but by a ``.`` dot.
* The ``$source`` directive references the table names.
* When a ``column/property`` in a ``table/source`` is referenced from the context of the upper ``table/source``, as in all the filters in the template, the table name needs to be prefixed with a ``->`` symbol, and column name can come next separated by a ``.`` dot. Putting it in another way: the ``->`` signals that the next path part is a table joined to the last source defined.
.. warning:: the :code:`propertyPath('propertyPath')` cql function is meant to be used only in the scope of this plugin. It is not currently possible to reference domain property outside the context of a template file.
This functionality is particularly useful when defining templates on top of Smart Data Loader based Complex Features.
Controlling Attributes With N Cardinality
------------------------------------------
When a property interpolation targets an attribute with multiple cardinality in a Complex Feature, feature templating will output the result as an array. This default behaviour can be controlled and modified with the usage of a set of CQL functions that are available in the plug-in, which allow to control how the list should be encoded in the template.
* ``aggregate``: takes as arguments an expression (a property name or a function) that returns a list of values and a literal with the aggregation type eg. ``aggregate(my.property.name,'MIN')``. The supported aggregation type are the following:
- ``MIN`` will return the minimum value from a list of numeric values.
- ``MAX`` will return the max value from a list of numeric values.
- ``AVG`` will return the average value from a list of numeric values.
- ``UNIQUE`` will remove duplicates values from a list of values.
- ``JOIN`` will concatenate the list of values in a single string. It accepts a parameter to specify the separator that by default is blank space: ``aggregate(my.property.name,'JOIN(,)')`` .
* ``stream``: takes an undefined number of expressions as parameters and chain them so that each expression evaluate on top of the output of the previous expression: eg. ``stream(aPropertyName,aFunction,anotherPropertyName)`` while evaluate the ``aFunction`` on the output of ``aPropertyName`` evaluation and finally ``anotherPropertyName`` will evaluate on top of the result of ``aFunction``.
* ``filter``: takes a literal cql filter as a parameter and evaluates it. Every string literal value in the cql filter must be between double quotes escaped: eg. ``filter('aProperty = \"someValue\"')``.
* ``sort``: sort the list of values in ascending or descending order. It accepts as a parameter the sort order (``ASC``,``DESC``) and optionally a property name to target the property on which the sorting should be executed. If no property name is defined the sorting will be applied on the current node on which the function evaluates: ``sort('DESC',nested.property)``, ``sort('ASC')``.
The above functions can be combined allowing fine grained control over the encoding of list values in a template. Assuming to write a template for the `meteo stations use case <#source-and-filter-complex-feature-example>`__, these are some example of the usage of the functions (simplified property access is used in the example below):
- ``aggregate(stream(->meteo_observations,filter('value > 35')),AVG)`` will compute and return the average value of all the Observation nested feature value attribute.
- ``aggregate(stream(->meteo_observations.->meteo_parameters,sort("ASC",param_name),param_unit),JOIN(,))`` will pick up the ``meteo_parameter`` nested features for each station feature, will sort them in ascending order based on the value of the ``param_name`` and will concatenate the ``param_unit`` values in a single string, comma separated.
Template Validation
-------------------
There are two kind of validation available. The first one is done automatically every time a template is requested for the first time or after modifications occurred. It is done automatically by GeoServer and validates that all the property names being used in the template applies to the Feature Type.
The second type of validation can be issued from the UI (see the configuration section) in case a JSON-LD or a GML output are request. The GML validation will validate the output against the provided ``SchemaLocation`` values. The ``JSON-LD`` validation is detailed below.
JSON-LD Validation
^^^^^^^^^^^^^^^^^^
The plugin provides a validation for the json-ld output against the ``@context`` defined in the template. It is possible to require it by specifying a new query parameter in the request: ``validation=true``.
The validation takes advantage form the json-ld api and performs the following steps:
* the `expansion algorithm `_ is executed against the json-ld output, expanding each features' attribute name to IRIs, removing those with no reference in the ``@context`` and the ``@context`` itself;
* the `compaction algorithm `_ is then executed on the expansion result, putting back the ``@context`` and shortens to the terms the expanded attribute names as in the original output;
* finally the result of the compaction process is compared to the original json-ld and if some attributes are missing it means that they were not referenced in the ``@context``. An exception is thrown with a message pointing to the missing attributes.