Mapping of the Elements Model to the Expander template Model

allowmixing
rectangle "**Composite Model**\n*Composite classes" as model
rectangle "**Expansion Context**\n*Context classes" as context
stack "**Mapping**\n*Mapping.xml" as mapping
rectangle "**Template Model**\nnested simple maps\nMap<String, Object>" as templateModel

model --> mapping
context --> mapping
mapping --> templateModel

Problem

Several expanders require a transformation of the elements as they are defined in the Prime Radiant to a model that represents the configuration of the artefact template.

In most of the current implementations of the expanders, this transformation is implemented in the Java class of the expander. Often this includes the managing of insertions. And dozens of different templates accessed by the class.

The solution to this problem must at least solve the following issues with the current implementation:

  1. The expander should use a single main template that itself may use different sub-templates. This would reduce the lack of oversight that can be experienced if some of the larger expanders call a multiple of templates from within different methods by providing a single entry point. The consequence is that this ‘root’-template has to be provided with all information required by the template to expand its different sub-templates.
  2. The solution should ideally provide a better understanding of how the model is mapped. This also means that the amount of logic in the template itself is reduced to a minimum. Important here is that the use of options and attributes should be clear so that this information is readily available when analyzing the expanders instead of hidden away in the Java code.
  3. Since the problem can be defined as a pure transformation of data, it does not change any state, it seems appropriate to design a purely declarative description of the transformation.

Best Practices

  1. The result of the mapping should contain only basic objects (string, numbers, lists, maps) or simple container wrappers (e.g. net.democritus.descriptor.ClassDescriptor).
  2. Don’t pass *Composite instances to the output; use the mapping to extract and transform only what you need.
  3. When using custom classes, avoid reference cycles
    • cycles will not affect expansion, but complicates debugging and tracing
  4. Add a reasonable toString() implementation to your custom wrappers.

Points 3. and 4. are relevant to debugging only – in case of an error, the system will dump the current variables and may try to dump them recursively.

Mapping descriptor

The data used by the expander templates often consists of some complex data structures. Most of the data structures can be defined by a combination of the following sub-structures:

  • a value element to map simple values
    • <value name="name" eval="dataElement.name"/>
  • a include element to embed additional mappings
    • <include path="SharedExpanderMapping.xml"/>
  • a let element to create local variables inside a mapping
    • <let name="useLocalEjbOnly" eval="false"/>
  • a conditionalValue element to build if-elseif-elseif-…-else chains
  • a list element iterates over a provided collection and transforms every element
    • <list name="fields" eval="dataElement.fields" param="field">...</list>
  • A group element, which is the mapping of some node which only appear conditionally. A good example here is that a Field only has a ‘valueType’ if the field is a ValueField. It requires a name and a condition to represent on which condition the group is added to the template model.

With this in mind, the following xml structure was created to describe the mapping of a dataDescriptor to the model required by the DataRefConverterExpander:

<?xml version="1.0" encoding="UTF-8" ?>
<mapping xmlns="http://nsx.normalizedsystems.org/201806/expanders/mapping">
  <value name="dataElement" eval="dataDescriptor.classNamer"/>
  <value name="component" eval="dataDescriptor.componentName"/>
  <value name="hasFunctionalKey" eval="dataDescriptor.functionalKey.defined"/>
  <group name="functionalKey" if="dataDescriptor.functionalKey.defined">
    <list name="linkedElements" eval="dataDescriptor.functionalKey.fields"
          param="field" filter="field.linkFieldDescriptor" unique="field.classDescriptor">
      <value name="targetClass" eval="field.classDescriptor"/>
    </list>
    <list name="fields" eval="dataDescriptor.functionalKey.fields" param="field">
      <value name="name" eval="field.fieldName"/>
      <value name="index" eval="i1"/>
      <value name="type" eval="field.typeName"/>
      <value name="isString" eval="field.typeName = String"/>
      <group name="valueField" if="field.valueFieldDescriptor">
        <value name="isNumber" eval="field.numberType"/>
      </group>
      <group name="linkField" if="field.linkFieldDescriptor">
        <value name="targetClass" eval="field.classDescriptor"/>
        <value name="isDataRef" eval="field.dataRef"/>
      </group>
    </list>
  </group>
</mapping>

Value Element

It requires a name to be used in the template and an evaluation that describes how to calculate the value.

Note that you cannot refer to these values in the mapping itself; use let for that.

<mapping>
  <value name="elementName" eval="dataElement.name"/>
  <value name="literalString" eval="'NSX'"/>
</mapping>

Example result:

{
  "elementName" : "City",
  "literalString" : "NSX"
}

Let Element

Usable as a local variable. Local variables add step of indirection in the mapping and can reduce the readability of the mapping. Thus they should be used primarily to retrieve data not available in the model itself, or data that needs a complex logic construction.

Note: the order of elements is important, you cannot refer to let source before it is defined.

  <let name="targetProjection" eval="expander.findTargetProjection(taskElement, targetDataElement)"/>

  <value name="includePerformOnTarget" eval="... and targetProjection.defined"/>

Example result:

{
  // no targetProjection
  "includePerformOnTarget" : true
}

Include Element

To reduce duplication and avoid inheritance in Expanders, one can include additional mappings with e.g. shared behavior for a group of expanders.

WARNING: Relative paths ("../x/c.xml") are not allowed. Path must be either absolute ("/a/b.xml"), or point to a file in the same directory ("a.xml").

<!-- included file SharedExpanderMapping.xml -->
<mapping>
  <value name="class" eval="classBuilder.from(taskElement)"/>
  <group name="flags">...</group>
</mapping>
<!-- parent file Remote.xml -->
<mapping>
  <let name="useLocalEjbOnly" eval="false"/>
  <include path="SharedExpanderMapping.xml"/>
  <value name="logging" eval="expander.loggingAspect"/>
</mapping>

The behavior of include is the same as if the included elements were placed at the include element’s location:

<!-- Remote.xml -->
<mapping>
  <let name="useLocalEjbOnly" eval="false"/>
  <!-- content from include -->
  <value name="class" eval="classBuilder.from(taskElement)"/>
  <group name="flags">...</group>
  <!-- /content from include -->
  <value name="logging" eval="expander.loggingAspect"/>
</mapping>

Example result:

{
  "useLocalEjbOnly" : false,
  "class" : ClassDescriptor("org.normalizedsystems.nsx.MyTask"),
  "flags" : {
    ...
  },
  "logging" : LoggingAspect(...)
}

Conditional Value

In some cases, the mapping of a value can depend on several conditions. E.g. the targetClass of a task-element can be different for regular tasks, branching tasks or tasks with a specific result type.

For this, a conditionalValue is introduced. The conditional value has a name and a number of options, each with a condition and an evaluation.

The options are tested in the order in which they are declared, and the first matching is used.

If no option matches, null is used.

<?xml version="1.0" encoding="UTF-8" ?>
<mapping xmlns="http://nsx.normalizedsystems.org/201806/expanders/mapping">
<!--...-->
  <conditionalValue name="resultClass">
    <option if="taskElement.option('isBranchingTask').defined" eval="..."/>
    <option if="taskElement.option('hasResultClass').defined" eval="..."/>
    <defaultOption eval="java.lang.Void"/>
  </conditionalValue>
<!--...-->
</mapping>

Example result:

{
  "resultClass" : java.lang.Void
}

Group Element

Group a number of entities together in a nested object. A group can be optional by using if and ifnot

<value name="fieldName" eval="field.name"/>
<group name="flags">
  <value name="isNullable" eval="field.getOption('isNullable').defined"/>
  <value name="hasTranslation" eval="field.getOption('translation').defined"/>
</group>
<group name="linkField" if="field.isLinkField">
  <value name="targetName" eval="field.linkField.targetElement.name"/>
</group>
<group name="valueField" if="field.isValueField">
  ...
</group>

Example result:

{
  "fieldName" : "person",
  "flags" : {
    "isNullable" : true,
    "hasTranslation" : false
  },
  "linkField" : {
    "targetName" : "Person"
  }
  // no valueField
}

List Element

Iterate over a collection and transform every element.

dataElement("City",
  field("name : String"),
  field("nearestCity : City"),
  field("partnerCity : City"))
<list name="fields" eval="dataElement.fields" param="field">
  <value name="name" eval="field.name"/>
  <value name="index0" eval="i0"/>
</list>
{
  "fields" :
  [
    {
      "name" : "name",
      "index0" : "0"
    },
    {
      "name" : "nearestCity",
      "index0" : "1"
    },
    {
      "name" : "partnerCity",
      "index0" : "2"
    }
  ]
}

Use i0 and i1 virtual variables to retrieve the current 0-based or 1-based index of the element.

To filter the collection, use filter to skip some elements, and optionally unique to remove duplicate elements.

NOTE When both options are provided, unique is always applied after filter.

<list name="imports" eval="dataElement.fields" param="field"
      filter="field.isLinkField"
      unique="field.linkField.targetElement">
  <let name="target" eval="field.linkField.targetElement"/>
  <value name="packageName" eval="target.packageName"/>
  <value name="name" eval="target.name"/>
</list>
{
  "imports" :
  [
    {
      "packageName" : "com.example",
      "name" : "City"
    }
  ]
}

References