-
Notifications
You must be signed in to change notification settings - Fork 0
Getting Started
📦 modicio is developed in Scala and used as a direct JAR dependency by other projects. The future roadmap is to make modicio available as an SBT maven package. The compiled JAR is not explicitly tested with any Java environment, but usage may be possible.
🚀 Modicio provides all required support to be used in Java directly with the Scala language plugin. Check the modicio gradle starter project.
As of right now, modicio can only be build from sources or downloaded as JAR from the CI pipeline.
Directly use the compiled dist package by going to the CI log of the repository here. On this page, click on the top-most successful CI run. The dist package is provided as an artifact that can be downloaded as ZIP. The unpacked archive contains the current modicio build as a JAR that can be used directly. Alternatively, the compiled .class files are available as well.
Compile modicio yourself by cloning this repository. You need SBT (Scala build system) and a recent JDK (we recommend OpenJDK 14+) installed and working on your system. In the top level directory where the build.sbt and README is located, execute sbt package. Afterwards the build artifacts and the compiled JAR is located in the generated target/ folder.
Modicio is maintained in Scala and we recommend Scala as target language for modicio based applications. However, with a few tweaks, the Scala framework can be used in Java applications directly.
By this, you can use modicio in Java nearly the same way as you would in Scala. To point out hints and differences, take a look at the dedicated Java Integration page.
Most of the following functionality is documented as part of the modicio package documentation
Before modicio workflows can be used, certain objects need to be initialized and prepared. In this context, the Registry plays the most central role because it represents a modicio workspace. If SimpleMapRegistry is used (non persistent storage) only one registry object must exist per workspace. It is recommended to store the setup state in a Singleton or static context to make it available during workflow execution.
In this tutorial, Type, ModelElement or Class ModelElement are used as synonyms.
//Here we use the default framework placeholder-verifiers which let everything pass
val modelVerifier = new SimpleModelVerifier()
val definitionVerifier = new SimpleDefinitionVerifier()
//The TypeFactory which is later used to create new types:
val typeFactory = new TypeFactory(definitionVerifier, modelVerifier)
//The InstanceFactory to instantiate types (creating ESI):
val instanceFactory = new InstanceFactory(definitionVerifier, modelVerifier)
//Rules do now generate their own UUIDs:
Rule.enableAutoID() //enabled by default in recent modicio versions
//The registry instance represents the workspace of our runtime type model.
//Multiple registries can be used for parallel runtime type models (views) with different purposes.
//Here, we use the SimpleMapRegistry which simulates persistence but stores only until application shutdown:
val registry = new SimpleMapRegistry(typeFactory, instanceFactory))
//We have to add the registry to our factories:
typeFactory.setRegistry(registry)
instanceFactory.setRegistry(registry)After the above setup, the model and instance store are empty (At least if no persistence-enabled registry extension is used). In some cases, an initial model should be loaded on system start to leverage the modelling workload at runtime. The code below shows how such an initial model is loaded from a file. Some examples for initial model definitions are provided in our example collection. Note that importing is an async operation. While you execute this function, no other model modifications must take place.
In any case, each registry must contain a root element. If not present, no insert operations will be accepted. Therefore, after an empty registry was created, the root element must be inserted first as in the following code snippet. Imported models must not contain root elements as of right now.
for {
root <- typeFactory.newType(ModelElement.ROOT_NAME, ModelElement.REFERENCE_IDENTITY, isTemplate = true, Some(TimeIdentity.create))
_ <- registry.setType(root)
} yield {
//...
}In the full setup context:
def applyInitialModel(/*any arguments*/): Future[Any] = {
//Load the initial model file and print its content to debug:
val source = Source.fromFile("resources/model_file.json")
val fileContents = source.getLines.mkString
println(fileContents)
source.close()
//Parse and transform the initial input to a model representation
//The model contents are added to the registry workspace automatically and are available later on
//The NativeInput can be used as long as the model file matches our DSL specification
//Custom Transformator trait implementations are required otherwise
val initialInput: NativeInput = NativeInputParser.parse(fileContents)
val transformator: NativeInputTransformator = new NativeInputTransformator(registry, definitionVerifier, modelVerifier)
//Complete the async setup. See further doc to assure the right execution order!
for{
//... any other async setup operation
root <- typeFactory.newType(ModelElement.ROOT_NAME, ModelElement.REFERENCE_IDENTITY, isTemplate = true, Some(TimeIdentity.create))
_ <- registry.setType(root)
_ <- transformator.extendModel(initialInput)
} yield Future.successful()
}The last part of the code snippet is a for-comprehension, which basically executes a block of async operations in linear order. An extended example setup is performed in RegistryProvider.scala.
💡 Note that most of the following calls are returning a Future. This means they are processed asynchronously. You must await the Future in your respective context or chain calls to the returning future. See the modicio-insights example project for those code-snippets in context.
Most of the following functionality is documented as part of the modicio core package documentation.
The usage of ConcreteValues and Singleton-Instances are documented on their separate respective wiki pages.
Adding a type i.e. a class ModelElement is done by using the TypeFactory and the newType() factory method. The returned ModelElement represented by its TypeHandle must the be added to the registry and is persisted asynchronously.
val typeName = "Todo"
val isTemplate = false //template := abstract
typeFactory.newType(typeName, ModelElement.REFERENCE_IDENTITY, isTemplate) flatMap (newType =>
registry.setType(newType) map (_ => println("New class type added"))Editing a type with existing instances or editing a type without instances does not make a difference, because the types (Fragment-hierarchies) are owned by the instance and are transcendent in consequence. If a type is edited, always the reference identity of it is target for changes. If an instantiated type (part of an ESI), which provides the same interface, is edited, an exception is thrown. Editing a type corresponds to editing a ModelElement and can be divided in the operations: add attribute; remove attribute; add association; remove association; add parent relation; remove parent relation.
Each operation can be performed by simple method calls using the framework.
val attributeName = "Title"
val datatype = "String"
val nonEmpty = true
//Here, we use not the constructor but a creator method instead:
val newRule = AttributeRule.create(attributeName, datatype, nonEmpty)
//Get the type to edit from the registry
registry.getType("Todo", ModelElement.REFERENCE_IDENTITY) flatMap (typeOption =>
//Unfold the type, not required if no special verification is applied
typeOption.get.unfold() map (typeHandle => {
//Actually add the new Rule
typeHandle.applyRule(newRule)
typeHandle.commit map (_ => println("Rule added successfully")) //optional in SimpleRegistry mode
}))val linkName = "partOf"
val targetName = "Project"
val multiplicity = "*"
//Here, we not use the constructor but a creator method instead:
val newRule = AssociationRule.create(linkName, targetName, multiplicity)
//Get the type to edit from the registry
registry.getType("Todo", ModelElement.REFERENCE_IDENTITY) flatMap (typeOption =>
//Unfold the type, not required if no special verification is applied
typeOption.get.unfold() map (typeHandle => {
typeHandle.applyRule(newRule)
typeHandle.commit map (_ => println("Rule added successfully")) //optional in SimpleRegistry mode
}))Just adding an AssociationRule does not enable associations without adding at least one valid Slot. You can find the detailed documentation of Slots in the modicio DSL doc.
In simple terms, each model element has a variant identifier which has a timestamp. This is either the time of model creation or the time a client has called the registry.incrementVariant() method. The Registry as well as the TypeHandle class provide methods to get their TimeIdentity with all relevant timestamps. Just check the ScalaDoc page linked at the beginning of this page.
A Slot now enables an association with targets of a certain variant or interval of variants given by their timestamps. The following code snipped creates a slot that allows association with every variant, a wildcard so to say:
typeHandle.applySlot(associationRuleId, ">0")
typeHandle.commit map (_ => println("Slot added successfully"))An AssociationRule can have any number of slots and an association will be allowed, if there is a slot that holds true for the given variant.
val parentName = "Collectible"
//Here, we not use the constructor but a creator method instead:
val newRule = ParentRelationRule.create(parentName, ModelElement.REFERENCE_IDENTITY)
//Get the type to edit from the registry
registry.getType("Todo", ModelElement.REFERENCE_IDENTITY) flatMap (typeOption =>
//Unfold the type, not required if no special verification is applied
typeOption.get.unfold() map (typeHandle => {
typeHandle.applyRule(newRule)
typeHandle.commit map (_ => println("Rule added successfully")) //optional in SimpleRegistry mode
}))val ruleId = someRule.id
//Get the type to edit from the registry
registry.getType("Todo", ModelElement.REFERENCE_IDENTITY) flatMap (typeOption => {
//Unfold the type, not required if no special verification is applied
typeOption.get.unfold() map (typeHandle => {
typeHandle.removeRule(ruleId)
typeHandle.commit map (_ => println("Rule removed successfully")) //optional in SimpleRegistry mode
})
})Note that Instance operations also are mostly async. For example the new Instance can only be used after the async newInstance() call has completed.
//Specify the type name of the new Instance
//The whole type-hierarchy will be instantiated
//The ModelElement must hold isTemplate = false
val ofType = "Todo"
//newInstance automatically adds the instance elements to the registry
instanceFactory.newInstance(ofType) map (instance =>
println("Instance created successfully))As of right now, you can only access an Instance by its id as shown in the other examples or get all Instances of a certain type via its name:
val typeName = "Todo"
registry.getAll(typeName) map (deepInstances => {
println("Got a Seq of DeepInstances")
}🚑 Future versions should support filter operations to access Instances.
However, the client could manage an index table with instanceIds in their custom implementation aside from the framework.
A detailed documentation of all Instance operations can be found here. Note that you can click on the method-cards in the ui-viewer.
val instanceId = "SomeInstanceID"
val attributeName = "Title"
val newValue = "Thesis"
registry.get(instanceId) flatMap (instanceOption =>
instanceOption.get.unfold() map (deepInstance => {
deepInstance.assignDeepValue(attributeName, newValue)
deepInstance.commit map (_ => println("Data updated successfully")) //optional in SimpleRegistry mode
}))💡 In the following example, we expect to have only the instanceIDs of both instances to associate given a relation.
val aInstanceId = "InstanceIDofA"
val bInstanceId = "InstanceIDofB"
val associateAsType = "SomePolymorphTypeOfB"
val relation = "SomeAssociationName"
(for {
aInstanceOption <- registry.get(aInstanceId)
bInstanceOption <- registry.get(bInstanceId)
aInstance <- aInstanceOption.get.unfold()
bInstance <- bInstanceOption.get.unfold()
} yield (aInstance, bInstance)).map(res => {
val (aInstance, bInstance) = res
aInstance.associate(bInstance, associateAsType, relation)
aInstance.commit map (_ => println("Data updated successfully")) //optional in SimpleRegistry mode
})
})If both instances are known and unfolded from the start, this can be quite simple instead:
val associateAsType = "SomePolymorphTypeOfB"
val relation = "SomeAssociationName"
aInstance.associate(bInstance, associateAsType, relation)
aInstance.commit map (_ => println("Data updated successfully")) //optional in SimpleRegistry modeHere, we again expect the instance to be known and unfolded.
//associationId is fetched from aInstance beforehand
aInstance.removeAssociation(associationId)
aInstance.commit map (_ => println("Data updated successfully")) //optional in SimpleRegistry mode