Skip to main content

Expander Tags

Expanders 6.4.0 introduced a system of Tags. These tags are designed to simplify conditions, group related Expanders and Features and allow for better traceability.

Tags as conditions

Any Expander, Feature or ExpansionStep can define one or more tags. They will only be included if all tags are present on the target element.

<expander xmlns="http://nsx.normalizedsystems.org/20204/expander" name="DeleteMultipleActionExpander">
<packageName>net.democritus.expander.dataElement.bulk</packageName>
<layerType name="CONTROL_LAYER"/>
<technology name="STRUTS2"/>
<sourceType name="JAVA"/>
<tags>
<tag>#cruds.bulk.delete</tag>
</tags>
<elementTypeName>DataElement</elementTypeName>
<artifactModifiers/>
<artifactName>$dataElement.name$DeleteMultipleAction.java</artifactName>
<artifactPath>$componentRoot.directory$/$artifactSubFolders$/$dataElement.packageName;format="toPath"$/action</artifactPath>
<isApplicable>true</isApplicable>
<phase>expansion</phase>
</expander>

Defining Tags

Tags can be defined by adding Tag rules. These rules are defined in an ElementTagRule DataResource.

Each rule defines a premise and a consequence as <consequence> :- <premise>.

  • The premise defines what conditions need to be fulfilled to trigger the rule. If it does not contain any conditions, it is always true.
  • The consequence contains all tags that are enabled if the premise is true.
<dataResource type="expansionControl::ElementTagRule">
<rule name="Example Tag Rule">
#tag1, #tag2 :- condition1, condition2, condition3
</rule>
</dataResource>

In this example, #tag1 and #tag2 are enabled if condition1 AND condition2 AND condition3 are all true.

Tags apply to specific Elements

Tags are always resolved against a target element. They are not necessarily shared along the model hierarchy. E.g. the tag #cruds.bulk.delete could apply to a DataElement, but not to the Fields or Finders of that DataElement. Rules always need to be interpreted in the context of the target element. In order to share tags with other parts of the model, rules need to be defined to cascade the tags. (See below)

Multiple rules defining the same tag are disjunct

If 2 or more rules define the same tag as a consequence, either rule can be used to resolve the tag. As a result, multiple rules can be used to define different ways to enable a tag.

Adding Tags through Options

The following rule enables the #cruds.bulk.delete for all elements that have the option cruds.bulk.delete.

data/cruds/rules.xml
<rule name="Bulk Delete Option">
#cruds.bulk.delete :- option(cruds.bulk.delete)
</rule>
note

This rule is only valid for a DataElement if the option is set directly to that DataElement. However, when combined with the cascading flag on the OptionType, it can also work when set on the Component or Application.

Sometimes, a feature relies on other features to work. E.g. the 'Bulk Delete' feature mentioned earlier relies on the 'Multi-select' feature in the view layer to be able to select and delete multiple instances.

We can use tags to represent this reliance. This causes the 'Multi-select' feature to be automatically enabled in case 'Bulk Delete' was activated.

<rule name="Bulk Delete requires multi-select">
#view.list.multiselect :- #cruds.bulk.delete
</rule>

Other Predicates

In addition to the option(type) predicate, there are a handful of other predicates. Predicates are similar to functions.

  • not($tag) inverts a tag. It only resolves if the given tag does not resolve.
  • instanceOf($elementType) checks whether the element is an instance of DataElement, TaskElement etc.
  • where($reference, $tag) attempts to resolve the given tag on a linked element.
  • existsIn($collection, $tag) attempts to resolve the given tag to one of the items in a collection.
<rule name="Enable Typed Finders unless disabled">
#typedFinders :- not(#legacy.typedFinders.disable)
</rule>
<rule name="Enable IO Import on DataElements with CSV Import">
#io.import :- instanceOf(elements::DataElement), #csv.import
</rule>
<rule name="Cascade useAccount to ApplicationInstance">
#security.useAccount :- where(application, #security.useAccount)
</rule>
<rule name="Expand DataState if DataElement has a status field">
#common.dataState :- existsIn(fields, #workflow.statusField)
</rule>
Cascading Down

The where() predicate can be used to cascade tags down the model hierarchy. Here the #security.useAccount is added to the ApplicationInstance if it present on the Application.

<rule name="Cascade useAccount to ApplicationInstance">
#security.useAccount :- where(application, #security.useAccount)
</rule>
Cascading Up

In reverse, the existsIn() predicate can be used to cascade up. With this rule, we enable the DataState artifact if one of the fields of a DataElement is a status field.

<rule name="Expand DataState if DataElement has a status field">
#common.dataState :- existsIn(fields, #workflow.statusField)
</rule>

Profiles

Work in progress

Profiles are currently functional, but most expanders have not been migrated yet to leverage this. Hence, the example here presents tags that could be used in the future.

Profiles have been introduced to replace variant elements in the model, such as ApplicationInstance. A profile can be applied to an expansion in the expansionSettings.xml:

<expansionSettings>
<modelDirectory>..</modelDirectory>
<expansionDirectory>../expansions</expansionDirectory>
<expansions>
<expansion>
<type component="elements" name="JeeApplication"/>
<target>spaceApp::1.0.0</target>
<profile name="backend"/>
</expansion>
</expansions>
<expansionResources>...</expansionResources>
</expansionSettings>

A profile can be defined as a DataResource, or in a settings directory (in settings/expansionProfile/{yourProfile}.xml).

<profile name="backend">
<tags>
<tag>#server.tomee</tag>
<tag>#db.postgresql</tag>
<tag>#data.hibernate</tag>
<tag>#logic.ejb3</tag>
<tag>#proxy.rmi</tag>
<tag>#control.struts</tag>
<tag>#view.knockout</tag>
<tag>#jee7</tag>
<tag>#java17</tag>
</tags>
</profile>

The tags of the profile will be applied to the Program, e.g. the Application (not ApplicationInstance!).

In addition, a rule can look at the tags in the profile in their premise. These tags are also known as root tags. This rule enables several JEE specification versions based on the JEE version defined in the profile.

<rule name="JEE 7 specification versions">
#logic.ejb3_1, #control.servlet3_1, #view.jsp2_3, #jee :- $jee7
</rule>
Recursive Tags

Take care when defining rules based on root tags that you do not add a rule that can recurse on itself. This can cause infinite recursion to occur.

<rule name="Infinite recursion">
#recursive.tag :- $recursive.tag
</rule>

This might be fixed in a future release by adding some loop detection.

Other Uses

LayerImplementations

LayerImplementations can also define tags to replace their conditions.

<layerImplementation name="JaxRsController">
<layerType name="CONTROL_LAYER"/>
<technology name="JAXRS"/>
<condition>true</condition>
<tags>
<tag>#control.cruds.jaxrs</tag>
</tags>
<!-- ... -->
</layerImplementation>

Expander Mapping

You can also check for the presence of tags in mapping files.

BeanExpanderMapping.xml
  <value name="enableImport" eval="tags.has('#io.import')"/>
<value name="enableExport" eval="tags.has('#io.export')"/>
Tags on other elements

These tags are resolved against the target element of the Expander or Feature. Sometimes, you will need to check for tags on other elements. They can be accessed with tags.on(target).has(tag)

  <value name="isIoEnabled" eval="tags.on(dataElement).has('#io.page')"/>

Adding Technologies

Expanders, Features etc. are filtered on technologies. These technologies are usually provided based on the ApplicationInstance. It is possible to enable a technology with a Tag rule:

<rule name="EJB3 Technology">
#technology:EJB3 :- #logic.ejb3
</rule>

Testing

expanders-test-utils

The ExpanderTester and FeatureTester classes have specific methods to test Tag resolution:

  @Test
public void test_isApplicable() {
DataElementComposite model = baseModel();
tester.testTagIsMissing("#cruds.bulk.delete", model);
modelBuilder.extendModel(model, option("cruds.bulk.delete"));
tester.testTagsArePresent(model);
}

Tags can be added with the TestTagProvider class:

@Test
public void testBase() {
tester.extendContext(expansionContext -> TestTagProvider.addRootTags(expansionContext, "#java11"));
tester.testBase(baseModel());
}

expander-assert

Use the @TestProfile annotation to define tags for a test. The Assert classes have methods to test for tags.

  @Test
@TestProfile(tags = "#test.tag")
void test_tags(TestExpansion<DataElementComposite> testExpansion) {
testExpansion.assertThatExpander().hasAllTags();
}

@Test
void test_tag_notPresent(TestExpansion<DataElementComposite> testExpansion) {
testExpansion.assertThatExpander().doesNotHaveTag("#sample.feature");
}

You can also test tags directly with assertThatTags():

@Test
@TestProfile(tags = {"#control.struts2"})
void test_tags(TestExpansion<DataElementComposite> expansion) {
expansion.assertThatTags()
.contain("$control.struts2")
.contain("$java11")
.contain("$jee7")
.doesNotContain("#control.struts2");
}