New Expander Structure

This text is concerned with File/Artifact expanders that produce File Artifacts.

Expander Model

In $expander.name$.xml.

This file adheres to the elements.Expander Data Element structure and can be interchanged with Prime Radiant.

<expander name="RemoteExpander" xmlns="http://nsx.normalizedsystems.org/20195/expander">
  <packageName>net.democritus.expander.ejb3.taskElement</packageName>
  <layerType name="SHARED_LAYER"/>
  <technology name="EJB3"/>
  <sourceType name="JAVA"/>
  <elementTypeName>TaskElement</elementTypeName>
  <artifactName>$taskElement.name$Remote.java</artifactName>
  <artifactPath>$componentRoot.directory$/$artifactSubFolders$/$taskElement.packageName;format="toPath"$</artifactPath>
  <isApplicable>true</isApplicable>
  <active value="true"/>
  <anchors/>
  <customAnchors/>
</expander>

LayerType

The layerType defines which layer the artifact belongs in. This will affect the artifactSubFolders in the artifactPath. It will also prevent the expander from running if the option no<layerType>Layer is defined (e.g. noDataLayer in case of layerType=DATA_LAYER).

Correct values are:

  • ROOT
  • SHARED_LAYER
  • DATA_LAYER
  • LOGIC_LAYER
  • PROXY_LAYER
  • CONTROL_LAYER
  • VIEW_LAYER
  • CLIENT_LAYER

Technology

The technology defines which technology the artifact is linked to. This will affect the artifactSubFolders in the artifactPath. Also, the expander will only run if the defined technology is applicable.

SourceType

The sourceType defines how the artifact will be added to the build.

Correct values are:

  • JAVA (or SRC)
  • RESOURCE
  • HTML
  • JS
  • JSP
  • IMAGES
  • CSS
  • STYLES
  • WEB-INF
  • LIB
  • JSON
  • DOC
  • ACTION
  • EXP
  • STYLESHEETS
  • BUILD_FILE

ElementTypeName

The elementTypeName defines the target element.

Correct values are:

  • ApplicationInstance
  • Component
  • Finder
  • DataElement
  • DataCommand
  • DataProjection
  • ValueFieldType
  • TaskElement
  • FlowElement
  • ServiceElement
  • DataWaterfall

Artifact Name and Path

The full path of the artifact is defined in the model and is parametrized with the Expander’s metadata and the ExpansionContext including all its parent contexts (e.g. taskElement, taskElementContext, component, componentContext, application, applicationContext).

Elements injected into the path (layerType, taskElement, …) are instances of Composite projections.

The ArtifactPathBuilder will also expose a number of properties to build the artifactPath defined by the ArtifactPathBuilderMapping:

<mapping xmlns="http://nsx.normalizedsystems.org/201806/expanders/mapping">
  <let name="expansionConfigurationExpansionContext"
       eval="@net.democritus.expansion.ExpansionConfigurationExpansionContext@findIn(expansionContext.context)"/>
  <let name="applicationInstanceExpansionContext"
       eval="@net.democritus.elements.ApplicationInstanceExpansionContext@findIn(expansionContext.context)"/>
  <let name="componentExpansionContext"
       eval="@net.democritus.elements.ComponentExpansionContext@findIn(expansionContext.context)"/>

  <let name="buildEngine"
        eval="applicationInstanceExpansionContext.defined ?
                  applicationInstanceExpansionContext.value.applicationInstanceComposite.globalOptionSettings.buildEngine
                  : 'MAVEN'"/>

  <group name="scriptsRoot" if="expansionConfigurationExpansionContext.defined">
    <value name="directory"
           eval="expansionConfigurationExpansionContext.value
                    .expansionConfigurationComposite
                    .applicationExpansionSettings
                    .expansionDirectory
                  + '/scripts'"/>
  </group>

  <group name="applicationRoot" if="applicationInstanceExpansionContext.defined">
    <let name="appExpansionContext" eval="applicationInstanceExpansionContext.value"/>
    <conditionalValue name="directory">
      <option if="usage.equals('HARVEST')"
              eval="appExpansionContext.sourceDirectory + '/harvest'"/>
      <option if="buildEngine.equals('ANT')"
              eval="appExpansionContext.expansionDirectory
                    + '/applications/' + appExpansionContext.applicationInstanceComposite.name"/>
      <defaultOption eval="appExpansionContext.expansionDirectory"/>
    </conditionalValue>
  </group>

  <group name="componentRoot" if="componentExpansionContext.defined">
    <conditionalValue name="directory">
      <option if="usage.equals('HARVEST')"
              eval="componentExpansionContext.value.sourceDirectory + '/harvest'"/>
      <defaultOption eval="componentExpansionContext.value.expansionDirectory"/>
    </conditionalValue>
  </group>

  <value name="layerDir" eval="expander.layerType.subdir"/>

  <conditionalValue name="artifactSubFolders">
    <option if="usage.equals('HARVEST')"
            eval="expander.layerType.subdir + '/' + expander.technology.subdir + '/' + expander.sourceType.subdir"/>
    <option if="buildEngine.equals('ANT')"
            eval="expander.layerType.subdir + '/' + expander.technology.subdir + '/' + expander.sourceType.subdir"/>
    <defaultOption eval="expander.layerType.subdir + '/gen/' + expander.technology.subdir + '/' + expander.sourceType.subdir"/>
  </conditionalValue>
</mapping>

IsApplicable

IsApplicable should be an OGNL expression that defines when the expander should be run.

E.g. if the (dataElement-)expander should only run on the option enableYellowfeature, then the following condition can be used:

dataElement.getOption('enableYellowfeature').defined

Active

Active can be set to false to exclude it from the build.

Mapping

In $expander.name$Mapping.xml.

Artifact data (e.g. TaskElement instance) retrieved from XML/PrimeRadiant as Composite projection is transformed to a Map injected into a String Template.

<mapping xmlns="http://nsx.normalizedsystems.org/201806/expanders/mapping">
  <value name="class" eval="classBuilder.from(taskElement)"/>
  <value name="targetClass" eval="classBuilder.from(taskElement.targetClass)"/>
</mapping>

See Expander Mapping.

String Template

base() ::= <<
package <class.packageName>;

// <expanderComment>

import <targetClass.qualifiedName>;

public interface <class.className>Remote
  extends TaskPerformer\<Void, <targetClass.className>\> {
  // anchor:custom-methods:start
  // anchor:custom-methods:end
}
>>

The base() template doesn’t need any parameters, all data is injected automatically as specified in the Mapping.

Note expanderComment is injected automatically into the mapped model and doesn’t need to be specified in the Mapping.

Testing

Each expander should have $expander.name$Test class and $expander.name$Test.stg template.

Test Class

public class RemoteExpanderTest {

  ExpanderTester tester = ExpanderTester.forTest(this);

  private TaskElementComposite baseModel() {
    Element.ElementSpec spec = component("testComp",
        set("packageName", "net.democritus.elements"),
        dataElement("Application"),
        taskElement("TaskAccessBuilder",
            set("targetClass", "net.democritus.elements.ApplicationDetails"),
            dataRef("targetElement", "testComp::Application"),
            option("includeDelegation"),
            option("includePerform"),
            option("includeRemoteAccess")));

    return ModelSpecBuilder.buildAndFind(spec, taskElement("testComp::TaskAccessBuilder"));
  }

  @Test
  public void test_base() {
    tester.testBase(baseModel());
  }
}

@TODO: document test matchers (matchesTemplate/containsTemplate, hasBaseContent, hasAnchor)

Test Template

The test StringTemplate should contain code as it will look after expansion.

The delimiters used in Java test templates are $ to make it easier to copy code from actual expansion as well as minimize issues with unescaped angle brackets (<) in the source template.

TEST_() ::= <<
package net.democritus.acl;

// %expanderComment%

import net.democritus.acl.DataAccessQuery;
import demo.booking.Email;

public interface TaskAuthorizationRemote
  extends TaskPerformer<Void, DataAccessQuery> {
  // anchor:custom-methods:start
  // anchor:custom-methods:end
}
>>

hasBaseContent() matcher makes several changes when comparing the results:

  • all anchors are cleared, so we can compare separately the core (base) of the expander and the individual anchors
  • // expanded with version .* is replaced with // %expanderComment% as the value changes every release

@TODO example of anchor test template