Model services

Modelio v3.6

Modelio API model services are used to manipulate the model of the currently opened project.

Quick outline:

  1. Access and explore model
  2. Modify the model: create/modify/delete model elements
  3. Transactions
  4. React to model changes

Access and explore the model

The Modelio Model API is a part of the Modelio API that is used to access and explore the model. It is a set of Java classes and methods that represent the model elements.

The Model API Java classes and their methods follow some fixed naming rules that help a lot in identifying which object and which methods to use to navigate in the model. So it is worth spending a little time on these naming rules, the alternative would be to browse and learn the complete Model API classes and methods from the Java documentation… more than 250 classes and 3000 methods. Go for the naming rules!

Note: An interesting approach to find out how to navigate a given model, consists in using the semantic model browser. In the semantic model browser model elements are displayed along with metamodel indications about their metaclass, attributes and meta-dependencies. By examining the semantic data shown in the semantic browser one can directly guess which methods to use simply by applying the naming rules described below.

Model API Naming rules

The Model API provides Java classes to represent the model element (metaclasses instances) and methods to access the attributes and relations of these elements.

Metaclass representation

For each metaclass of the Modelio metamodel, a corresponding Java interface is defined. It is simply named from the Modelio metaclass. The metamodel inheritance graph between metaclasses is cloned at the Model level.

The following table provides several examples.

Modelio metaclass nameModelio parent metaclassJava interfaceJava parent interface
OperationBehavioralFeatureOperationBehavioralFeature
ClassGeneralClassClassGeneralClass
UseCaseGeneralClassUseCaseGeneralClass

Associations and Attributes methods

The Model API provides direct access to attributes and associations in the metamodel in the form of methods called accessors.
Accessors that are used to read the model are called getters while accessors that modify the model are called setters.

As for metaclasses, the model API has been designed so that accessors names directly relate to the navigated attributes or associations thereby making it easy to guess the name of the concrete accessors of any attribute or association from a quick look to the metamodel diagrams.

The following table shows the prefixing rules for a association role or an attribute named XXX.

AccessorCardinalityPrefix rule
Getter 1getXXX() or isXXX() for a boolean
Getter *getXXX()
Setter 1setXXX()
Setter *setXXX().add() and setXXX().remove()

Accessing metaclass attributes

Each metaclass attribute has corresponding accessors in the Java class representing it. Naming rules are pretty direct and simple.
For an attribute named att in the metamodel, the accessor methods are (note the capitalization of the attribute name):

  • getAtt()
  • setAtt(value)

Let us apply all this to our root packages in order to get their name (the name of a Package instance is stored in its ‘name’ attribute).

1    // getting the root packages of the UML model in the currently opened project
2    IModelingSession session = MyModule.getInstance().getModuleContext().getModelingSession();
3    for (MObject root : session.getModel().getModelRoots()) {
4        if (root instanceof Project) {
5            Package rootPackage = ((Project) root).getModel();
6
7            // get the name of the root package
8            System.out.println("Root element name is : " + root.getName() );

Warning:

For setXXX() accessors that modify the model, please carefully read the chapter on Transactions, which defines a certain number of major model modification rules.

Accessing associations

Model API provides two different sets of accessors for associations, depending on their cardinality.

Single association

fig1

An association, which has the ‘role’ role and a max cardinality equal to one, has two accessors:

  • B getRole() returns the B object or null
  • void setRole(B b) set the role B object (set to null to remove)

Single association example

Getting the type of an Attribute (see the diagram above).

1    Attribute att1 = ...;
2    
3    // getting the type of the attribute
4    GeneralClass type = att1.getType();
5
6    

line 1 let’s consider that att1 is given
line 4 as the type of an attribute is hold by the role Type (see the diagram above), simply call the getType() method to get the type of the attribute. Similarly, to change the type of att1 use the setType(newType) accessor.

Multiple association

fig2

An association, which has the ‘role’ role and a ‘max’ cardinality greater than one, has only one accessor:

  • EList<B> getRole() the navigation accessor, used to enumerate the values of the association.

When several model elements are to be returned, they are returned as an EList instance which is a Model API specific java.util.List implementation. Modifying an EList directly modifies the model itself.

Please note that removing a model element from an EList does not delete it but only removes the relation between them.

Multiple association example

Getting the list of Elements owned by a package (see the diagram above).

 1    Package root = ...;
 2    
 3    // getting the elements owned by a package
 4    EList<ObModelTree> ownedElements = root.getOwnedElement();
 5    
 6    // Iterate on the owned elements and print their names
 7    for ( ObModelTree e: ownedElements ) {
 8        System.out.println( e.getName() );
 9    }

Warning:

For accessors that modify the model, please carefully read the transactions chapter, which defines some major model modification rules.

See also:

Transactions

Overview

Modelio Transactions:

  • are required to modify the model: no modification of the model can be carried out without being encapsulated within a Transaction.
  • have to be committed for the modification to take place: changes in the model are only effective after a transaction has been successfully committed.
  • can be rolled back, leaving the model unchanged up to its state when starting the transaction.

Any modification of the model made outside any transaction will throw an exception.

Transactions guarantee that the modifications of the model follow the ACID paradigm. ACID is an acronym for Atomic, Consistent, Isolated, and Durable.

Atomicity

A transaction allows for the grouping of one or more changes in the model to form an atomic or indivisible operation. In other words, either all of the changes occur or none of them do. If for any reason the transaction cannot be completed, everything this transaction changed can be restored to the state it was in prior to the start of the transaction, via a rollback operation.

Consistency

Transactions always operate on a correct model and when they end always leave the model in a correct state. The model is said to be correct as long as the Modelio audit system does not return blocking errors for it. During a transaction, the model may be incorrect at some point, however no other transaction will be allowed to see these inconsistencies, and all such inconsistencies will have been eliminated by the time the transaction ends.

Isolation

To a given transaction, it should appear as though it is running all by itself on the model. In Modelio, there is only one active transaction at a time. Such a unique transaction is de facto isolated.

Durability

Once a transaction is committed, its effects become definitive, ie they are definitively applied to the model. As long as the transaction is not committed, the modifications can be rolled back to restore the model to its initial state.

Transactions API

The transaction management API is available from the IModelingSession object.

The modeling session provides the following transaction creation method:

1class IModelingSession {
2    ...
3    public ITransaction createTransaction(String name);
4    ... 
5}

The returned ITransaction object provides the following transaction management methods:

1public interface ITransaction extends AutoCloseable {
2    void commit();
3
4    void rollback();
5}

Golden rules:

  • Never leave a transaction opened in the tool may be with the secret hope that someone will close it for you.
  • When your code creates a transaction, it is responsible for closing it, either by a commit() or by a rollback().

For convenience ITransaction implements AutoCloseable so that it can be used in a try-with-resources statement. If the commit() method has not been called at the end of the statement, the transaction will be automatically rollbacked by its close() implementation.

A simple transaction example

The following code snippet shows a typical use of a transaction to change the name of a Package:

 1IModelingSession session = myModule.getModuleContext().getModelingSession();
 2Package myPackage = ...            // some code to get a Package instance 
 3
 4try (ITransaction t = session.createTransaction("Rename a package");) {
 5    
 6    myPackage.setName("new name");
 7
 8    t.commit();
 9}
  • line 4 – this construction known as try-with-resources will guarantees that the transaction will be properly closed in any circumstances (uncaught or unplanned exceptions occuring)
  • line 8 – if an uncaught exception occurs, this closing brace will automatically rollback and close the transaction created on line 3. Of course, this does not happen when everything is successful and when the transaction is successfully committed.

To ensure proper closing of transactions even when exceptions are thrown during model processing, we so highly recommend the use of the following ‘try with resources’ code pattern that this pattern can be thought as being mandatory.

Remark
Some exceptions may be raised by transaction management methods. This is not detailed here but has to be carefully dealt with by the Module developer in real life.

The proposed code snippet commits the transaction without any check that the resulting model is correct. This could violate the consistency rule of an ACID transaction. This is why, in Modelio, the commit() method will in any case audit the changes before committing them. If the audit succeeds, that is if no blocking errors are reported by the audit, the commit() operation will return true and the transaction will indeed be committed. If the audit fails, that is if there is at least one blocking error reported, the transaction will automatically rollback() and the commit() will fail, throwing an exception.

A more sophisticated transaction example

In the following code fragment a package is renamed . Prior to committing the transaction a check is made that no sibling package has the same name, allowing for deciding to commit() or to rollback() the transaction.

 1IModelingSession session = myModule.getModulecontext().getModelingSession();
 2Package myPackage = ...         ;   // some code to get a Package instance 
 3String newName = "new name";
 4try (ITransaction t = session.createTransaction("Rename a package");) {
 5    myPackage.setName(newName);
 6
 7    int count = 0;
 8    for (ModelTree sibling : myPackage.getOwner().getOwnedElement()) {
 9        if (newName.equals(sibling.getName())) {
10            count++;
11        }
12    }
13
14    if ( count == 1 ) {
15        // Validate the transaction
16        t.commit();
17    } else {
18        // abort the transaction
19        t.rollback();
20    }
21} 

The above code fragment does not care about possibly thrown exceptions.
If not cared of, the exception processing may neglect to ‘close’ the transaction that was previously opened and which is possibly not yet closed (this depends on where the caught exception was thrown).
However, thanks to the ‘try with resource’ pattern (line 4) used here, the transaction is guaranteed to be finally rollbacked and closed should an uncaught exception be thrown. Remember that unclosed transactions are evil…

Note: It remains of course possible to manually catch exceptions either within the processing code or at the ‘try’ level.

Transactions and Audit

Modelio constantly monitors the model for correctness by running an Audit.

Some of the controls carried out by the audit are blocking, ie they cannot fail without breaking the model. These strong inconsistencies are always controlled by Modelio in real-time each time a transaction is committed. This is why committing a transaction can fail, the changes can simply NOT be carried out by the tool without leading to serious troubles (including tool crashing in the worst cases). Modelio will throw an exception at any model modification out of a transaction because it needs this final check at commit time to ensure its own integrity.

Creating and deleting model elements

Creating elements

To create a model element, the modeling session provides a model element creation factory IUmlModel. The model element factory provides many creation methods, listing them all would be counter-productive here so let us just have a look to a code fragment:

1// get the modeling session
2IModelingSession session = myModule.getModuleContext().getModelingSession();
3
4// get the factory
5IUmlModel factory = session.getModel();
6
7// use the factory to create elements, here a simple class
8Class newClass = factory.createClass();

A complete and explained example is visible here.

IMPORTANT WARNINGS

  • Using the factory is the unique operational means of creating new model elements.
  • do not attempt to modify the model outside an opened transaction.
  • do not use the Java new operator to create new elements in the model as this would not work at all and lead to errors.

Deleting elements

In order to delete an element, simply call its delete method.

Example: deleting all the classes under the root package

 1IModelingSession session = myModule.getModuleContext().getModelingSession();
 2
 3try (ITransaction t = session.createTransaction("Delete all root classes");) {
 4    // Get all work model roots
 5    for (MObject rootObj : session.getModel().getModelRoots() ) {
 6        // Looks for a Project instance
 7        if (rootObj instanceof Project) {
 8            // Get the model root package
 9            Package root = ((Project) rootObj).getModel();
10
11            // Get the root children classes
12            List<org.modelio.metamodel.uml.statik.Class> ownedClasses = root.getOwnedElement(org.modelio.metamodel.uml.statik.Class.class);
13
14            // Loop on classes under root
15            for (org.modelio.metamodel.uml.statik.Class clazz : new ArrayList<>(ownedClasses )) {
16                // Delete the class
17                clazz.delete();
18            }
19        }
20      }
21
22    // commit the transaction 
23    t.commit();
24}
25
  • line 3 – this construction known as ‘try with resources’ will guarantees that the transaction will be properly closed in any circumstances (uncaught or unplanned exceptions occuring)
  • line 5, 7 – iterates the Modelio project work models to get the root Project instances.
  • line 12 – the use of the filtered getter accessor getOwnedElement(org.modelio.metamodel.uml.statik.Class.class) will return only the classes under root. A fully namespaced class name is used to avoid any confusion with other classes like java.lang.Class and so on.
  • line 15 – as ‘delete’ modifies the list content, it is necessary to loop on a copy (new ArrayList()).
  • line 17 – the deletion of the class is done on line 17 but will only become effective after the commit on line 23.
  • line 24 – if an uncaught exception occurs, this closing brace will automatically rollback and close the transaction created on line 3. Of course, this does not happen when everything is successful and when the transaction is sucessfully committed (line 23).

Listening to model changes

Adding a model change listener

The modeling session can register so-called ‘model change listeners’. A model listener is any object implementing the IModelChangeListener interface. Registered model listeners are called when a change is committed in the model and a IModelChangeEvent object describing the changes is passed to them. Model change listeners can, for example, be used to update views displaying model elements or any information that depends on model elements.

 1IModelingSession session = myModule.getModuleContext().getModelingSession();
 2
 3// register a IModelChangeListener
 4
 5session.addModelListener(new IModelChangeListener() {
 6    public void modelChanged(IModelingSession session, IModelChangeEvent event) {
 7        System.out.println("model changed");    
 8    }
 9});

The above code fragment registers a IModelChangeListener that simply prints out a message whenever the model is modified (see about using System.out). Of course by exploiting the passed IModelChangeEvent more advanced behaviour can be implemented.

Removing a model change listener

You can remove a model change listener by

1    session.removeModelListener(myListener)


but this supposes that you have previously kept a reference to your listener.

Model change events

For model changes, a IModelChangeEvent event is sent when a top-level transaction is committed. Therefore, the model change event relates to a well-formed modification of the model as the transaction has been successfully committed (see Transactions and Audit).

The IModelChangeEvent event contains a structured and optimized list of changes that have been applied to the model. By processing these changes, a model change listener is able to perform any processing it requires.

The only practical, however fundamental, limitations to what can be carried out by a model change listener when processing a IModelChangeEvent are:

  • the model change listener MUST NOT modify the model
  • the processing time should be as short as possible (otherwise, long processing time will slow down the Modelio application and frustrate the end-user.)

The IModelChangeEvent

The structured list of changes contained in a IModelChangeEvent distinguishes between:

  • Created elements
  • Deleted elements
  • Updated elements
  • Moved elements

These lists are optimized by Modelio in order to reduce the amount of data to be processed by the listener. For example, no update event is reported for a deleted element.

Created elements

A created element is an element that has been added to the model during the transaction.

Optimizations
  • In the case of the creation of a parent and its children, only the parent creation is reported
  • A created element will not appear in either the updated elements list or the moved elements list
  • If an element is created and deleted by the transaction, no creation (and no deletion) is reported

Deleted elements

A deleted element is an element that has been removed from the model during the transaction.

Optimizations
  • In the case of the deletion of a parent and its children, only the parent deletion is reported
  • A deleted element will not appear in either the updated elements list or the moved elements list
  • If an element is created and deleted by the transaction, no deletion (and no creation) is reported

Updated elements

An updated element is an element which is not a created or deleted element and which has been modified by the transaction without being moved. The reordering of the child elements owned by a parent element makes this parent element appear in the updated elements list.

Optimizations
  • No matter how many changes have been made to an element, it is reported only once in the updated list

Moved elements

An element E is considered as a moved element if its “parent” has changed. “Parent” here must be understood as the value of E.getCompositionOwner() .

Optimizations
  • Whatever the number of parent changes that have occurred during the transaction, only one report will be available in the moved elements list. The reported move will contain the initial (before the transaction) and last known (at the end of the transaction) parent.

Implementation example

The very common place for a module to register some model change listeners is the start() method. Remember that the start() method is called when a project is opened, or when a new module is deployed in the project; obviously, no model changes should occur before a project is open!

Similarly, we should take care of unregistering the listeners when the project is closed, in other words, in the stop() session services method, as no model change events should occur after the project has been closed.

Let’s have a look at the modified code for the start and stop methods, registering and unregistering a model change listener. The code is straightforward we simply use the addModelChangeListener() and removeModelChangeListener methods of the IModelingSession.

 1public class MyModuleLifeCycleHandler extends DefaultModuleLifeCycleHandler {
 2
 3    private IModelChangeListener modelChangeListener = null;
 4    
 5    public boolean start ()throws ModuleException 
 6    {
 7        IModelingSession session = this.module.getModuleContext().getModelingSession();
 8        this.modelChangeListener = new MyModelChangeListener();
 9        session.addModelListener(this.modelChangeListener);
10    }
11
12    public void stop ()throws ModuleException 
13    {
14        IModelingSession session = this.module.getModuleContext().getModelingSession();
15        session.removeModelListener(this.modelChangeListener);
16        this.modelChangeListener = null;
17    }
18}

The actual processing of the model changes is carried out by a MyModelChangeListener object. The MyModelChangeListener class must implement the IModelChangeListener interface in order to be registered as a model change listener. This interface has only one method which will be called each time a top level transaction is successfully committed, the method receives a IModelChangeEvent object as parameter.

 1class MyModelChangeListener implements IModelChangeListener 
 2{
 3    public void modelChanged(IModelingSession session, IModelChangeEvent event) {
 4        // process created elements
 5        for (MObject createdElement : event.getCreationEvents()) {
 6            ...
 7            System.out.println("Created element:" + createdElement.getName());
 8        }
 9        
10        // process deleted elements (passed as IElementDeletedEvent)
11        for (IElementDeletedEvent deletedEvent : event.getDeleteEvents()) {
12            ...
13            // The deleted element is still accessible and navigable
14            MObject deletedEl = deletedEvent.getDeletedElement();
15            System.out.println(deletedEl.getName()+ " deleted from:" + deletedEl.getCompositionOwner().getName());
16                
17        }
18        
19        // process updated elements
20        for (MObject updatedElement : event.getUpdateEvents()) {
21            ...
22            System.out.println("Updated element:" + updatedElement.getName());
23        }
24        
25        // process moved elements (passed as IElementMovedEvent)
26        for (IElementMovedEvent moveEvent : event.getMoveEvents()) {
27            ...
28            System.out.println("Moved element: " + moveEvent.getMovedElement().getName());
29            System.out.println("         from: " + moveEvent.getOldParent().getName());
30            System.out.println("           to: " + moveEvent.getNewParent().getName());
31        }   
32    }   
33}

Accessing deleted elements

Deleted elements are still directly accessible and navigable although they have been deleted.

Their owner is available in the IElementDeletedEvent and on the deleted element itself. Deleted elements are available until the project is saved. Saving a project frees deleted elements data in order to free memory. Then, accessing a such freed model object results in a DeadObjectException being thrown.

multiple_association.png (5.66 KB) admin admin, 10 January 2017 18:14

single_association.png (4.23 KB) admin admin, 10 January 2017 18:14