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
- Feature
<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>
<feature xmlns="https://schemas.normalizedsystems.org/xsd/prime-core/2024/0/0/feature" name="BulkDelete">
<packageName>net.democritus.expander.dataElement.bulk</packageName>
<condition>true</condition>
<tags>
<tag>#cruds.bulk.delete</tag>
</tags>
<type name="custom"/>
<expanderFeatures>
<!-- ... -->
</expanderFeatures>
</feature>
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 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)
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
.
<rule name="Bulk Delete Option">
#cruds.bulk.delete :- option(cruds.bulk.delete)
</rule>
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.
Activating related Features
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>
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>
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
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>
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.
<value name="enableImport" eval="tags.has('#io.import')"/>
<value name="enableExport" eval="tags.has('#io.export')"/>
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");
}