Module Imports
Module imports lack support in the MicroRadiant. Only use this if you already need a modular program setup.
Creating a metamodel supporting multiple modules can be difficult. It becomes even more difficult when you need a truly
modular setup. For example adding new module ElementClasses to an elements::JeeApplication would require changes to
its ProgramFactory.
To reduce the difficulty and improve modularity we have introduced module imports in parallel to the existing mechanisms. Module imports are defined locally on a module, and define which other modules are required to make a module work.
Completely removing the need for customizations in a ProgramFactory or ModuleFactory requires the following steps:
- Loading program expansions through a
<path>configuration instead of<target>. - All modules must define a
ModuleManifestso they can be referenced in an import. - Loading modules through
<import>s in the program or module elements.
Path based program expansion
When configuring program expansions with a target, the ProgramFactory is asked to convert this to a DataRef. Next this
dataRef is resolved against the modelPath on the configured ProgramType. This is not a trivial process and also opaque
to the model loading mechanism. Alternatively, you may define the exact path of the application model like so:
<expansionSettings>
<expansions>
<expansion>
<programType>elements::JeeApplication</programType>
<path>applications/myapp/model/myapp.xml</path>
</expansion>
</expansions>
</expansionSettings>
Module manifests
Before we can import other modules, we need to know which modules are available. To facilitate this a model resource
can define a ModuleManifest dataResource. This defines an identifier and defines where it is located. See the example
below for the account base component. Its identifier is net.democritus:base-components::account (moduleGroup::moduleId).
<dataResource type="elements::ModuleManifest">
<moduleManifest>
<moduleGroup>net.democritus:base-components</moduleGroup>
<moduleId>account</moduleId>
<packaging>model-resource</packaging>
<elementType>elements::Component</elementType>
<description>Account base component</description>
<path>components/account/model/account.xml</path>
</moduleManifest>
</dataResource>
Modules within a project can be referenced using the local moduleGroup, followed by a full model path as the moduleId.
For example when your project contains a invoicing and orders component, they can be referenced as:
local::components/invoicing/model/invoicing.xml
local::components/orders/model/orders.xml
Modules loaded through a manifest must define the type attribute in their XML file, such that the file can be parsed correctly.
The system for loading ModuleManifests is modular itself and implemented using the java ServiceLoader. Alternative ways
of scanning or resolving ModuleManifests can be provided by implementing the
net.democritus.expansion.dependencies.ModelDependencyResolver interface as a ServiceProvider.
Importing modules
Two things are important to consider:
- Imports use a priority based
scopewhich is used to determine the ModuleType that is used during expansion. - Imports are transitive. If a module is imported multiple times, the scope is resolved as the maximum of the imports.
A few scopes are provided by default:
expansion(default) The imported module must be expanded for this module to work.referenceThe imported module is only used as a reference within the model, but does not need to be expanded on its own.runtimeThe imported module is needed at runtime for full functionality, but is not required for the model or expansion.
To add an import, add an <import> tag at the top of a program or module model file. The following example results in
an application with the account, orders and invoicing components.
- app.xml
- invoicing.xml
- orders.xml
<application type="elements::Application">
<import module="local::components/invoicing/model/invoicing.xml" scope="expansion"/>
<name>app</name>
</application>
<component type="elements::Component">
<import module="net.democritus:base-components::account" scope="expansion"/>
<import module="local::components/orders/model/orders.xml" scope="expansion"/>
<name>invoicing</name>
</component>
<component type="elements::Component">
<name>orders</name>
</component>
The imports previously used a reference named <dependencies>. These will automatically be converted when writing the
model in a newer version.
Using imports in expanders
Imports are not regular references, and since they are transitive they are less trivial to use in an expander. To help
with this the dependencies helper is provided as a mapping variable. It has various methods to adjust the query. Some
example usages are listed below:
Mapping examples for dependency resolution
<mapping>
<!-- list all ComponentComposites loaded on this application (transitive). -->
<let name="components" eval="dependencies.on(application)
.elementType('elements::Component')
.transitive()
.list()"/>
<!-- list FeatureModules directly added as dependency with scope expansion -->
<let name="featureModules" eval="dependencies.on(angularApp)
.elementType('angularProjects::FeatureModule')
.scoped('expansion')
.list()"/>
<!-- list Components loaded as JeeComponent -->
<let name="featureModules" eval="dependencies.on(application)
.moduleType('elements::JeeComponent')
.transitive()
.list()"/>
<!-- list all componentDependencies -->
<let name="componentDependencies" eval="dependencies.on(component)
.elementType('elements::Component')
.scoped('expansion')
.list()"/>
</mapping>
Testing imported modules
When writing a TestSpec you will often need multiple modules as well. The most basic approach is to add a module spec directly in a program or another module. This will implicitly create an expansion import between the parent and child:
Nested module spec
class ExpanderTest {
@TestModel
Spec baseModel() {
// Application[app] -import-> Component[invoicing] -import-> Component[orders]
return application("app",
component("invoicing",
component("orders")
)
);
}
}
It is also possible to create an explicit dependency referencing a manifest:
Reference module spec
class ExpanderTest {
@TestModel
Spec baseModel() {
// Application[app] -import-> Component[invoicing] -import-> Component[orders]
return application("app",
component("orders"),
component("invoicing",
dependency("", dataRef("to", "spec::orders"))
)
);
}
}