Modules. How to start

Modules

UniData runs on a simple module system. New functionality can be added to the platform by implementing a new module. A module is a piece of code, packaged into a jar file and implementing the interface org.unidata.mdm.system.type.module.Module. Also, a module has to have a special entry in its MANIFEST.MF file, denoting the implementing class, like the following:

Unidata-Module-Class: org.unidata.mdm.data.module.DataModule

The interface org.unidata.mdm.system.type.module.Module* has several mandatory methods for implementation.

/**
* Gets module ID, i. e. 'org.unidata.mdm.core'.
* @return ID
*/
String getId();
/**
* Gets module version, consisting of major.minor.rev, i. e. '5.4.3'.
* @return version
*/
String getVersion();
/**
* Gets module localized name, 'Unidata Core'.
* @return name
*/
String getName();
/**
* Gets module localized description, i. e. 'This outstanding module is for all the good things on earth...'.
* @return description
*/
String getDescription();

If the module has dependencies to other modules, it has to implement also the method:

/**
* Returns the dependencies of this module.
*/
Collection<Dependency> getDependencies()

Module service will check and load dependencies prior to start of the module.

Special note about the module ID. Module ID must match the root package of the module, if the module uses Spring and injects beans from other modules. The reason is simple - root package of the module will be scanned by Spring to discover classes, annotated with Spring stereotypes. The class, implementing org.unidata.mdm.system.type.module.Module must not be itself a bean, although @Autowire or JSR330 @inject can be used inside the implementing class.

Regardless of how module interface is implemented - using Spring or not - the @ModuleRef annotation can be used to inject module instance at place of interest, like this:

@ModuleRef
private DataModule dataModule;
// or
@ModuleRef("org.unidata.mdm.data")
private Module dataModule;

There are also several other important methods, one would probably want to implement:

/**
* Runs module's install/upgrade procedure.
* Can be used to init / mgirate DB schema or other similar tasks.
*/
default void install() {
    // Override
}
/**
* Runs module's uninstall procedure.
* Can be used to drop schema or similar tasks.
*/
default void uninstall() {
    // Override
}
/**
* Runs module's start procedure.
* Happens upon each application startup.
* Should be used for initialization.
*/
default void start() {
    // Override
}
/**
* Runs module's stop procedure.
* Happens upon each application shutdown.
* Should be used for cleanup.
*/
default void stop() {
    // Override
}

Common practice for modules writing is:

  • to use separate DB schema per module, if the module uses database

  • to have separate i18n resources

  • to have and use own ExceptionIds

Pipelines

Execution pipelines is a way to configure and run series of operations on a request context object, which inherits from org.unidata.mdm.system.context.PipelineExecutionContext. Such a member operation is called a “segment” and can be (at the time of writing) of type Start, Point, Connector and Finish. A properly configured Pipeline must have a Start segment, can have any number of Point and Connector segments, and must be closed with a Finish segment.

Pipelines can be configured programmaticaly and then either used for direct calls or persisted for caching and future use. Below is an example of such a configuration and subsequent call for saving data:

Pipeline p = Pipeline.start(pipelineService.start(RecordUpsertStartExecutor.SEGMENT_ID))
    .with(pipelineService.point(RecordUpsertValidateExecutor.SEGMENT_ID))
    .with(pipelineService.point(RecordUpsertSecurityExecutor.SEGMENT_ID))
    .with(pipelineService.point(RecordUpsertPeriodCheckExecutor.SEGMENT_ID))
    .with(pipelineService.point(RecordUpsertResolveCodePointersExecutor.SEGMENT_ID))
    .with(pipelineService.point(RecordUpsertMeasuredAttributesExecutor.SEGMENT_ID))
    .with(pipelineService.point(RecordUpsertModboxExecutor.SEGMENT_ID)) // <- Modbox create
    .with(pipelineService.point(RecordUpsertLobSubmitExecutor.SEGMENT_ID))
    .with(pipelineService.point(RecordUpsertMergeTimelineExecutor.SEGMENT_ID))
    .with(pipelineService.point(RecordUpsertIndexingExecutor.SEGMENT_ID))
    .with(pipelineService.point(RecordUpsertPersistenceExecutor.SEGMENT_ID))
    .end(pipelineService.finish(RecordUpsertFinishExecutor.SEGMENT_ID));

UpsertRecordDTO result = executionService.execute(p, ctx);

Pipelines can be saved using the org.unidata.mdm.system.service.PipelineService.savePipeline(…) and then retrieved using the org.unidata.mdm.system.service.PipelineService.getPipeline(…). Pipelines are executed via org.unidata.mdm.system.service.ExecutionService.execute(…) calls, returning a subclass of org.unidata.mdm.system.dto.PipelineExecutionResult. Simplified, the pipeline segment types hierarchie can be shown like this:

Pipeline segment types hierarchie

Figure. Pipeline segment types hierarchie