ModelLoader (steps and how to add custom model loading)
All models are loaded by the ModelLoader
class. This class receives an object of the class ModelLoadingParameters
as
input, which defines 3 parameters:
- workDirectory: A temporary work directory that can be used to extract files etc.
- settingsFile: The expansionSettings xml file that defines the expansion
- resourceManagementFactory: An object that can create a ResourceFinder and a ResourceFetcher, these are used to fetch expansionResource jars. By passing this as a parameter, we can switch between strategies.
The ModelLoader loads the model in 7 different steps:
- Read Expansion Settings
- Resolve Resources
- Extract Resources
- Read Model
- Convert Model
- Enrich Model
- Configure Expansion
The loadModel()
method on the ModelLoader class allows you to define up to which step you wish to load the model,
depending on the use case.
1. Read Expansion Settings Step
Reads the expansionSettings file into the ExpansionSettingTree object.
Because there is a new DataElement to represent expansionSettings (changing from ApplicationExpansionSettings, which is strongly coupled to Application), this step also implements conversion to the new representation when reading a older file.
Here is an example of an expansionSettings xml. It contains a type (referencing a programType), a target (referencing the program, or Application in this case) and a variant (referencing a configuration element, in this case the ApplicationInstance).
<expansionSettings>
<modelDirectory>..</modelDirectory>
<expansionDirectory>../expansions</expansionDirectory>
<programExpansions>
<expansion>
<type component="elements" name="JeeApplication"/>
<target>eurent::1.0.0</target>
<variant>eurent_tomee</variant>
</expansion>
</programExpansions>
<settingsDirectorys>
<settingsDirectory>
<directory>../settings</directory>
</settingsDirectory>
</settingsDirectorys>
<expansionResources>
<expansionResource name="net.democritus:nsx-default-stack" version="2022.14.3"/>
</expansionResources>
</expansionSettings>
2. Resolve Resources Step
Fetches all expansion-resources defined in the expansionSettings. Then, the dependencies are resolved by comparing the versions needed of each distinct expansion-resource and taking the highest required version. If an expansion-resource was set to a specific version in the expansionSettings by the user, but a higher version is required by one of the other expansion-resources, then the resolution fails stating a conflict in versions.
Because we resolve all transitive dependencies of an expansion-resource during the build of the expansion-resource and list them in the expansionResource manifest, it is not necessary to recursively fetch and resolve dependencies.
3. Extract Resources Step
Extracts any model-relates resource from the expansion-resource jars:
- Model resources in the jars (e.g. base-components) are extracted to separate directories in the workDirectory. These directories are then added to the modelDirectory defined in the expansionSettings
- Data Resources (e.g. technologies and layerTypes in prime-data) are read and loaded into the DataRegistry.
Data Resources also include the programTypes and moduleTypes xml files, which contain information on how to load the models.
4. Prepare Model Step
Performs some preliminary steps before the model can be read.
- Groups all ModelDirectories in
ModelLoadingContext#modelDirectories
, giving priority to directories of the ModelResources. - Groups all settingsDirectories in
ModelLoadingContext#modelDirectories
. - Loads ModelExtensions and ElementTypes, so that model extensions can be loaded correctly.
5. Read Model Step
Reads the model in the model directories. The ProgramType and ModuleType data resources of the previous step contain information on how to load the models.
Reading the Program
Here is an example of a ProgramType:
<programTypes>
<programType name="JeeApplication">
<component>elements</component>
<rootExpandableElement component="elements" name="Application"/>
<programFactory>net.democritus.elements.JeeApplicationFactory</programFactory>
<modelDirectory>$source.rootDirectory$/applications/$application.shortName$/model</modelDirectory>
<sourceDirectory>$source.rootDirectory$/applications/$application.shortName$</sourceDirectory>
<expansionDirectory>$expansion.rootDirectory$/$application.shortName$</expansionDirectory>
<concernTypes>
<concernType name="TRANSPORT"/>
...
</concernTypes>
<layerTypes>
<layerType name="ROOT"/>
...
</layerTypes>
</programType>
</programTypes>
For the model loading, there are 2 important pieces of information:
- modelDirectory: this defines where to find the xml representing the programs
- programFactory: this points to a class which will provide the implementations of several classes needed to read and load the models
In this step, the factory will provide 3 classes:
- programExpansionConverter: Converts the programExpansion reference of the expansionSettings to a DataRef, which will be used to find the model file
- programReader: Takes a DataRef and reads the corresponding model into a Tree object. Then, based on the variant selected in the programExpansion, it filters out all other variants. (e.g. selects the correct ApplicationInstance and filters out the rest.)
- programModuleRetriever: Retrieves the DataRefs for the modules for this program for each ModuleType
Reading the Modules
To read the Modules, similar to the programType, the ModuleType defines where and how to read the modules:
<moduleTypes>
<moduleType name="JeeComponent">
<component>elements</component>
<programType component="elements" name="JeeApplication"/>
<expandableElement component="elements" name="Component"/>
<moduleFactory>net.democritus.elements.JeeComponentFactory</moduleFactory>
<modelDirectory>$source.rootDirectory$/components/$component.name$/model</modelDirectory>
<sourceDirectory>$source.rootDirectory$/components/$component.name$</sourceDirectory>
<expansionDirectory>$program.expansionDirectory$/components/$component.name$</expansionDirectory>
<concernTypes>
...
</concernTypes>
<layerTypes>
...
</layerTypes>
</moduleType>
</moduleTypes>
The moduleType also defines a modelDirectory
and a moduleFactory
.
To read the module models, the factory provides the ModuleReader
class, which takes the DataRef f the
programModuleRetriever and finds and reads the correct file into a Tree object.
Reading Configuration Data
Configuration data is data which is typically referenced by a configuration element (program variant). The configuration data is kept in xml files in the settings directory (which is defined in the expansionSettings). Examples of this are BusinessLogicSettings, GlobalOptionSettings, TechnologyStack etc.
The last one, TechnologyStack, is a list of technologies and was added to make it easier to manage the technologies for the Application.
Configuration types are defined in xml files in data resources. They list where to find them and how to load them.
<configurationTypes>
<configurationType name="BusinessLogicSettings">
<component>elements</component>
<elementTypeCanonicalName>net.democritus.settings.BusinessLogicSettings</elementTypeCanonicalName>
<configDirectory>$settings.rootDirectory$/businessLogicSettings</configDirectory>
</configurationType>
</configurationTypes>
Reading Extensions
Finally, there are the modelExtensions. ModelExtensions are defined in xml files in data resources. In this file, the modelExtension links to a parent element and an element type name, which is used to load the correct classes. The extensions will be registered to the XmlReader class of the parent before the models are read, so that they can be read along with the other children.
<modelExtensions>
<modelExtension name="Contract">
<parent component="elements" name="Component"/>
<elementTypeCanonicalName>net.palver.contract.Contract</elementTypeCanonicalName>
</modelExtension>
</modelExtensions>
6. Convert Model Step
This step converts the Tree models to Composite objects, thus linking all of the objects. In this step, it is important for referenced objects to be loaded prior to the objects referencing them. Thus, a number of tricks are applied to make this possible:
- Modules are converted before programs, so that the references in the programs can be resolved correctly
- A Tree object with it's children will be converted by first registering all of the objects and then resolving the links. This means that there should never be an issue with links within a single Tree (e.g. within a Component)
- There is room for customization in the conversion mechanism, e.g. sorting the Components on dependencies.
- The DataResources and Configuration data have been loaded beforehand, providing all of the references to types, technologies etc.
The models are converted using ProgramLoader and ModuleLoader classes created with the Program- and ModuleFactories. These will convert the models and wrap them in ProgramComposite and ModuleComposite classes.
These classes hold the actual composites of the elements (e.g. ApplicationComposite and ComponentComposite) and add additional information, such as the Program- or ModuleType, technologies applicable to the program or module and all modules linked to the program.
This information will be useful later when running expansion in a way that is decoupled from the actual elements in question.
7. Enrich Model Step
This step runs some post-processing steps for the Composite transformation. This entails:
- Converting options to ElementOptions, which provide a better API to consult the options
- Adding extra implicit elements, such as the
Details
andInfo
DataProjections - Setting back references from Component to Application
- Setting a reference from the Program to it's selected variant
- Combining the technologyStacks referenced in the Configuration elements an storing the list of Technologies on the ProgramComposite and ModuleComposite
Most of this is custom code.
8. Configure Expansion
This step combines the models and expansionSettings into an ExpansionConfiguration object.
This step also provides an ApplicationExpansionSettings object for backwards compatibility with older expanders. (Only added for expansions with Application as program.)
Adding Custom Model Loading Logic
The ModelLoader allows 2 ways to add custom model loading logic:
- Add a new ProgramType and/or a ModuleType. The factory classes can then be implemented with custom implementation to read and convert the models.
- Add a ModelLoadingListener to attach some logic to the model loading process.