Skip to content

Junit_module_development_tutorial

Antonin Abhervé edited this page Sep 3, 2020 · 1 revision

Module development tutorial

This tutorial will be based on the Modelio JUnit module.

Setting up the Development Environment

The development of a Modelio module will be carried out with the Eclipse tool for code editing, packaging and debugging.

Eclipse is not mandatory to develop Modelio module, other Java IDE like Netbeans can be used as long as they can use maven. Nevertheless they probably require adaptations of the provided template and described procedures that is beyond the scope of this document.

Maven Project

A Maven Archetype for module developement project is available in modelio public repository.

<groupId>org.modelio</groupId>
<artifactId>modelio-module-archetype</artifactId>
<version>3.0.0.00</version>
  1. Install Maven Plugin on Eclipse (m2eclipse eclipse project)

  2. Create a Maven Project based on the modelio-module-archetype Archetype

    • Launch Eclipse

    • Select File –> New –> Project…,

    • Select Maven Project

    • Register the modelio public repository as a new Remote Repository.

              Catalog File : http://repository.modelio.org/archetype-catalog.xml

      image

    • Select modelio-module-archetype Archetype.

    • Specify the project informations.

                Group Id : org.modelio
                Artifact ID : JUnitModule
                Version : 1.0.00
                Package : org.modelio.junitmodule
                Module Name : JUnitModule

      image

    • This is it, finish to create the project!

Creating the module

Basics

As with many objects that constitute a packaged module, module (the object) creation is two part: XML declaration and Java coding.

Module declaration

Module is the root element in the module.xml file.

1    <module licenseRequired="false"
2        uid="794fecf1-cf25-4f3d-8d90-936f1ceda651"
3        name="${project.name}"
4        version="${project.version}.9015"
5        class="org.modelio.junitmodule.impl.JUnitModuleModule"
6        binaryversion="3.0.00.9015">
  • uid value is a unique identifier. It is very important that the value shouldn’t be copied from another object (don’t copy the id from the example), nor modified after module delivery. If you are using a Maven project, this identifier is automatically generated during the module packaging.

  • name value is the internal name of the module.

  • version value is composed of two parts:

    1. module version in the V.R.C form 2.the metamodel version (9015) retrieved as described in section 4.2.

  • binaryversion value is the same as the version value.

  • licenceRequired value is self explicit and set to false for this tutorial

  • class value is the name of the Java class that will represent the module.

The last files to edit are the manifest property files. There is one file per language supported with a default (without country code extension) that should be for English locale.

module.properties file will look like :

ModuleLabel=JUnit module
ModuleDescription=Builds and manages test case for classes based on JUnit framework.

module_fr.properties file for french will look like :

ModuleLabel=Module JUnit
ModuleDescription=Crée et gère les cas de test de classes basés sur JUnit
Module Java classes

A module contains :

  • A JUnitModule class that will represent your module at runtime. This class is called the main class of the Module. By convention it is always suffixed Mdac.

  • A JUnitSession class and an association. This class is in charge of the implementation of the Module Session Services detailed below. The association is a simple delegate from the Module main class to this JUnitSession class.

The UML model detailed diagram for these classes is presented below.

Illustration 6: Module and Session

Illustration 6: Module and Session

The operations defined in JUnitSession are automatically called according to the stat http://forge.modelio.org/projects/modelio3-moduledevelopersmanuals-devguide/wiki/Module_concepts# Runtime-Phases “Module runtime states and events”).

We won’t dive into the gory details of these methods. We will see later (§7.7)that the start method will attach the module event listener to the Modelio event dispatch system. For more information on these methods.

The implementation of session services for our JUnit module is very simple.

 1package org.modelio.junitmodule.impl;
 2
 3...
 4import org.modelio.api.module.DefaultModuleSession;
 5...
 6
 7public class JUnitModuleSession extends DefaultModuleSession {
 8
 9    public JUnitModuleSession(JUnitModuleModule module) {
10        super(module);
11    }
12
13    public boolean start() throws ModuleException {
14        Version moduleVersion = this.module.getVersion();
15
16
17        ILogService logService = Modelio.getInstance().getLogService();
18
19        String message = "Start of " + this.module.getName() + " " + moduleVersion;
20        logService.info(this.module, message);
21        return super.start();
22    }
23
24    public void stop() throws ModuleException {
25        super.stop();
26    }
27
28    public static boolean install(String modelioPath, String mdaPath) throws ModuleException {
29        return DefaultModuleSession.install(modelioPath, mdaPath);
30    }
31
32    public boolean select() throws ModuleException {
33        return super.select();
34    }
35
36    public void unselect() throws ModuleException {
37        super.unselect();
38    }
39
40    public void upgrade(Version oldVersion, Map<String, String> oldParameters) throws ModuleException {
41        super.upgrade(oldVersion, oldParameters);
42    }
43}
44

Creating a module profile

Profiles are the parent of the extensions, therefore we need one profile to declare the stereotypes.

A profile is declared in module.xml file. Edit the template module xml file, replace prepared field name and leave the field uid empty.

1    <profile uid="" name="JUnitProfile">
  • uid value is a unique identifier. It is very important that the value shouldn’t be copied from another object (don’t copy the id from the example), nor modified after module delivery. If you are using a Maven project, this identifier is automatically generated during the module packaging.

  • name value is the name of the profile. By convention it should end with Profile

A profile is not associated to a Java class.

For organization concern of extensions, several profiles can be declared in the module but they can’t be nested.

Creating module stereotypes

Stereotypes belong to a profile, therefore you should declare one as described above prior to create a stereotype.

JUnit Module specifications identify two stereotypes:

  • [JUnit] that is used to stereotype test classes that are created by the JUnit Module.

  • [JUnitDependency] that is used to stereotype dependency links that are established by the JUnit Module between the test classes and the model class they are testing.

Stereotypes declaration

Stereotypes are declared in module.xml file. Edit the template module xml file and edit the prepared fields.

 1    <stereotype uid="" name="JUnit" label="JUnit" metaclass="Class" is-hidden="false">
 2        <icons>
 3            <explorer path="" />
 4            <diagram path="" />
 5        </icons>
 6    </stereotype>
 7    <stereotype uid="" name="JUnitDependency" label="JUnitDependency" metaclass="Dependency" is-hidden="false">
 8        <icons>
 9            <explorer path="" />
10            <diagram path="" />
11        </icons>
12    </stereotype>
13
  • uid value is a unique identifier. It is very important that the value shouldn’t be copied from another object (don’t copy the id from the example), nor modified after module delivery. If you are using a Maven project, this identifier is automatically generated during the module packaging.

  • name value is the name of the stereotype.

  • label value is the name of the stereotype that will appear in Modelio gui and diagrams. It follows the standard mechanism of resource management : if it begins with % then it is a resource label that will be retrieved in the manifest property file (module*.properties).

Adding icons to a stereotype

Looking at the stereotype declarations in the previous file excerpt, we can see that stereotypes can be associated with several kind of icons :

  • explorer is used to represent the stereotyped element in the explorer pane (provided the stereotype is the first on the element)

  • diagram is used to replace the element standard representation when an image representation is preferred on diagram.

Explorer icons should have a size of size 16x16 px. Diagram icons can be of any size but a base of 32x32 px is a good average. The recommended format is PNG which allows transparency. Explorer icons are often declared with the same icon file in order to have the same icon representation in explorer and diagrams.

Icon files should be stored in resources/res/icons ; resources directory is mandatory, res/icons is a convention. We can know complete the declaration of JUnit stereotype to add it the following icon : image.

1...
2    <icons>
3        <explorer path="res/icons/junitClass16.png" />
4        <diagram path="" />
5    </icons>
6...

Stereotypes can also define tag types and note types :

1...
2         <notetype uid=""  name=""  label=""  is-hidden="false" />
3...
4         <taggedvalues uid=""  name=""  label=""  is-hidden="false"/>
5...

Creating module commands

In this section we will focus on the creation of commands. The commands behaviors, the Java code that do the job will be addressed in §7.6.

As for module, a command creation is a two step process : a declaration in module.xml and a Java class that extends DefaultMdacContextualCommand and redefines appropriate operations.

This tutorial doesn’t address diagram command that appears in palettes of specialization of existing diagram. For more information on it, refer to

Command declaration

A command is declared within the gui section of the module.xml file.

 1    <gui>
 2        ...
 3        <command name="CreateTestCase"
 4            label="%CreateTestCaseLabel"
 5            tooltip="%CreateTestCaseTooltip"
 6            image="res/icons/createTestCase.png"
 7            group="string"
 8            modify-model="true"
 9            group-image=""
10            modify-model="true">
11
12            <scope metaclass="Class" stereotype="JavaClass" />
13            <handler class="org.tutosland.junitmodule.command.CreateTestCase" />
14            <contribution location="contextualpopup" />
15        </command>
16        ...
17    </gui>

command

  • name: this field defines the logical name of the command. It will not be shown to the end-user and is only used internally as an identifier for the command;

  • label & tooltip: the name of the command as shown in Modelio gui and its associated tooltip. When these fields begin with % then it is a resource label that will be retrieved in the manifest property file (module*.properties);

  • image: a 16x16px image that will be displayed on the gui. It is optional if the command is in a popup menu and more than welcome if it is on toolbar or on property box. The image should be stored in resources/… (see §7.4.2);

  • group: is the group name of which the command belong. On contextual menu, the group will shown has a submenu between the module name and the command. When this field begins with % then it is a resource label that will be retrieved in the manifest property file (module.properties);

  • group-image: a 16x16px optional image that will be displayed in the contextual menu in front of the group name. The image should be stored in resources/… (see §7.4.2);

  • modify-model: if true, it means that the command won’t be available if one of the selected element is read-only (ie CMS locked, the presence of the red padlock in the explorer or locked because an edition operation is in progress). In our case, the value is false because we don’t want to create a test model on a read-only class.

scope

  • metaclass & stereotype: filter the availability of the command. The command will only be displayed if all the selected element types are a direct or indirect heir of metaclass value and have the requested stereotype. metaclass name can be found in Modelio metamodel user guide. In our case, the metaclass is “Class” because we want to create test cases for classes and it should have been stereotyped JavaClass because there is no point in adding a Junit testcase class to a non Java class.

handler

  • class: value is the name of the Java class that will be called when the command is invoked. This class should extend DefaultMdacContextualCommand class to be suitable for command activation.

contribution

  • location: indicates where the command should appear. In our case we decide to make it appear in the contextual menu (of class elements).

At this step of the process, the Create a test case command is created and configured but not ready to run.

Later, the only thing to add will be the implementation of the actionPerformed method on the CreateTestCase class to carry out the proper actions.

Command Java class

In this section, we will continue to create our “Create test case” command on the Java side.

A bit of theory: Contextual command filtering and graying

A contextual command will appear only under certain conditions defined by the parameters set for it in the XML declaration of the command (see §7.5.1).

This is a first level of command filtering, based on static data: kind of object and behavior for read-only objects. Although this checking is performed at runtime for each right-click event on a UML element, it is called static filtering because it is defined statically in module.xml.

A second level of command filtering, which is left completely to the module developer code, is also proposed by module commands. This second level of command filtering is called dynamic filtering as it is evaluated by running the module developer code. In practice, a dedicated accept() method that will be called by Modelio each time the Module command can be proposed has to be provided. The accept() method boolean returned value indicates whether or not the command has to be made available to the end-user.

This gives the module developer a means of dynamically checking conditions that could otherwise not be statically defined.

Commands that have passed both the active and dynamic filtering will be displayed, but can be inactive, grayed out.

Determining the graying of a command is also a two step process. First, static graying rules will be applied, and second, if the isActiveFor() method is defined, dynamic graying occurs.

Static graying rules are typically command declared to modify the model when the selected element is read-only.

Note that the dynamic filtering conditions (the developer’s coded filter) will not be evaluated (no call) if the command does not satisfy the static filtering criteria. The same rule applies for graying.

In short :

  • accept method is called to dynamically check if the command is visible;

  • isActiveFor method is called to dynamically check if the command is actionable or grayed out;

  • The order of check is : static filtering, dynamic filtering (accept), static graying and dynamic graying.

Writing command method

The next thing we have to do now is to complete the methods actionPerformed() and accept() with their implementation code.

The accept() method is called to validate the availability of the command in the current selection context. The current selection context is simply passed as the list of the currently selected elements.

For our example, let’s consider that the command accepts only one selected element.

 1    class CreateTestCase extends DefaultModuleContextualCommand {
 2    ...
 3    public boolean accept( List<MObject> selectedElements, IModule module)
 4    {
 5        // Check that there is only one selected element
 6        return selectedElements.size() == 1;
 7    }
 8    ...
 9    }

The actionPerformed() command is called when the command is activated by the end-user. Its role is to perform the proper action. Thus, the code of this method is the action itself.

 1class CreateTestCase extends DefaultModuleContextualCommand {
 2    ...
 3    public void actionPerformed( List<MObject> selectedElements, IModule module)
 4    {
 5        // Process the command, in our case display the Create Test wizard dialog
 6        CreateTestCaseWizardDialog dlg = new CreateTestCaseWizardDialog(selectedElements.get(0), module);
 7        dlg.open();
 8    }
 9    ...
10}

That’s it!

Of course, you can see that the proposed actionPerformed() method only delegates the expected behavior to a CreateTestCaseWizardDialog object. This is explained by the fact that we need to pop up quite a rich and complicated GUI to fetch parameter values from the user before performing the intended action. Coding the complete GUI in the actionPerformed() method is not a recommended practice, which is why we choose to use an additional class to implement the “Create a Test Case” wizard feature.

Coding this graphical wizard is outside the scope of this document, as it would produce a lot of code that is more directly related to GUI development than to Module programming. However, note that this code can be standard SWT code without any particular problem.

What is important next for our example is that at some point, the Module will have to perform the real Test Case creation, and modify and update the test model.

Processing commands: model transformation

Step 1: Checking for an already existing test case

The main objective of the “Create a test case” command when applied to a Class object MyClass is to create a MyClassTest test class in the test model and at the proper place in the test model package hierarchy.

However, it may be that a test class already exists for MyClass, in which case the Module is not expected to create an additional test case class nor to delete and replace the existing one, but is rather supposed to update the existing test case class, automatically identifying the required changes.

This advanced behavior cannot be presented here, as it would lead to listing a masterpiece of code which is outside our scope. However, note how the implementation of this advanced behavior is simplified by our design choice of adding stereotyped dependencies between the MyClass class and its MyClassTest counterpart in the test model. By navigating this dependency when it exists, the module developer can easily find MyClassTest from MyClass and perform an update instead of a pure creation.

For now, as a simplification, let’s decide that the command will fail if called on a class that already has a corresponding test class and that it will only display an error by popping an error dialog box to the end-user.

So, first have a look at this first part of the code: detecting an existing test class if one exists. We make the hypothesis that the code is implemented in the CreateTestCaseWizardDialog class and that this class also contains two attributes initialized from its constructor that contain the class to test and the JUnit Module object itself. Other designs for the code are, of course, possible, but this one makes the presentation easier to understand.

 1class CreateTestCaseWizardDialog {
 2    . . .
 3    private IClass  classToTest;
 4    private IMdac junitMdac;
 5    . . .
 6    public boolean CreateTestCaseWizardDialog(IElement element, IMdac mdac) {
 7        classToTest = (IClass)element;
 8        junitMdac = mdac;
 9        // Check that there is no already existing test class
10        StereotypeFilter filter = new StereotypeFilter("JUnit");
11        ObList<IDependency> depList = classToTest.getDependsOnDependency().select(filter);
12        if (depList.size() > 0) {
13            MessageBox box = new MessageBox(
14            Display.getCurrent().getActiveShell(),SWT.ICON_ERROR|SWT.OK);
15            box.setText("Error");
16            box.setMessage("Command cannot be applied: class already has a test case");
17            box.open();
18        }
19    }
20    . . .
21}

The previous code simply gets the dependencies that have their origin on the class for which the test is created and which are stereotyped JUnit. If such a dependency is found, the code pops an error message and returns false.

To get the dependencies, we use the getDependsOnDependency() accessor method that returns the list of these dependencies. If no dependencies exist, the returned list is still valid but empty (no null value returned).

The name of the accessor method to use can be easily found from the metamodel. It is the name of the role of the association that you want to reach, prefixed by get. This kind of accessor is used to read the associated values. This is detailed in the Modelio MDA API wiki.

However, the returned list will contain all the dependencies, even those that

are not [JUnit] ones and consequently of no interest to us. This is why we have to filter the returned list for the [JUnit] stereotyped dependencies only thanks to the help of the select method and StereotypeFilter object.

Once we have checked that no test class already exists for the selected class, we have to:

  1. Create/update the test model package hierarchy in order to have the proper place to put the test class.

  2. Create and name the test class.

  3. Add a [JUnit] stereotype to the test class.

  4. Link the test class to the tested class.

  5. Add the test class features (methods and attributes) as required.

Step 2: Creating/updating the test model hierarchy

What we have to do at this point is to create a test class and to place it in the proper test package.

Creating the class is quite obvious because Modelio MDA API provides convenient factory creation methods to create UML elements. However, in the test model, we have to find the proper test package if it already exists or create it if it does not. The objective is to establish a test model hierarchy of packages which is parallel to the initial model.

Let’s see what the code can look like:

 1class CreateTestCaseWizardDialog {
 2    . . .
 3    public IPackage getTestCaseParentPackage (IClass classToTest)
 4    {
 5        . . .
 6        try {
 7        IModelingSession session = Modelio.getInstance().getModelingSession();
 8        IUmlModel model = session.getModel();
 9
10        ITransaction t = session.createTransaction("Create test hierarchy");
11        . . .
12
13        session.commit(t);
14        } catch ( ) {
15        . . .
16        }
17    }
18    . . .
19}
Step 3: Creating the test model class

Once the parent package of the test class to create is known and accessible, the creation of the test case class is easy.

Let’s see what the code can look like:

 1class CreateTestCaseWizardDialog {
 2    . . .
 3    private IClass  classToTest;  // the IClass object for which test has to be created
 4    private IMdac junitMdac;
 5    . . .
 6    public boolean createTestCase( )
 7    {
 8
 9        . . .
10
11        try {
12        IModelingSession session = Modelio.getInstance().getModelingSession();
13        UmlModel model = session.getModel();
14
15        ITransaction t = session.createTransaction("Create a test case");
16
17        // Build the test class name
18        String testClassName = classToTest.getName() + "Test";
19
20        // Get the package parent of the test class
21        // The getTestCaseParentPackage() returns an IPackage
22        // which is created on the fly if necessary (ok as we are in a transaction)
23        IPackage testCaseParentPackage = getTestCaseParentPackage(classToTest);
24
25        // Create the class using the convenient factory method createClass()
26        model.createClass(testClassName, testCaseParentPackage);
27
28        session.commit(t);
29
30        } catch ( ) {
31        . . .
32        }
33
34    }
35    . . .
36}
Step 4: Adding the [JUnit] stereotype to the test model class

In order to add a [JUnit] stereotype to the created test class, we need to find the IStereotype object.

A stereotype is an UML extension brought to the modeling session environment by the deployed Modules (in our case, the JUnit Module).

Let’s have a look at how to do this in the following code fragment:

 1class CreateTestCaseWizardDialog {
 2    . . .
 3    public boolean stereotypeTestCase( IClass testClass )
 4    {
 5        . . .
 6        try {
 7            IModelingSession session = Modelio.getInstance().getModelingSession();
 8            IUmlModel model = session.getModel();
 9
10            ITransaction t = session.createTransaction("Stereotype a test case");
11
12            // Find the &lt;&lt;JUnit&gt;&gt; stereotype
13            IStereotype s;
14
15            try {
16                s = session.getMetamodelExtensions().getStereotype(IClass.class,
17                "JUnit");
18                // Add the stereotype to the class
19                testClass.addExtension( s );
20            }
21            catch (StereotypeNotFoundException e) {
22                MessageBox box = new MessageBox(
23                    Display.getCurrent().getActiveShell(),SWT.ICON_ERROR|SWT.OK);
24                box.setText("Error");
25                box.setMessage("Stereotype JUnit not found, check your installation");
26                box.open();
27                return false;
28            }
29            session.commit(t);
30        } catch ( ) {
31        . . .
32        }
33        . . .
34    }
35}

Note that the getStereotype() method requires both the name of the stereotype to find and the target metaclass. An exception is thrown if no stereotype applicable to the metaclass is found.

Introducing a variant for creating the test class

In the particular case of creating a class, the modeling session factory proposes an advanced createClass variant that takes the name of an applicable stereotype to the created class. This stereotype is then automatically added by the factory method to the created class. This would give the following code for creating the test class. This code does not require that we get the IStereotype object.

 1public boolean createTestCase( )
 2{
 3 . . .
 4 try {
 5  IModelingSession session = Modelio.getInstance().getModelingSession();
 6  IUmlModel model = session.getModel();
 7  ITransaction t = session.createTransaction("Create a test case");
 8  // Build the test class name
 9  String testClassName = classToTest.getName() + "Test";
10         // Get the package parent of the test class
11  // The getTestCaseParentPackage() returns an IPackage
12                // which is created on the fly if necessary (ok as we are in a transaction)
13  IPackage testCaseParentPackage = getTestCaseParentPackage(classToTest);
14  // Create the class
15  model.createClass(testClassName, testCaseParentPackage, "JUnit");
16  session.commit(t);
17 } catch ( ) {
18  . . .
19 }

In order to add a [JUnit] dependency link to the created test class, we need to create the IDependency object.

A dependency is an UML extension brought to the Modeling Session environment by the deployed Modules (in our case, the JUnit Module).

Let’s have a look at how to do this in the following code fragment:

 1class CreateTestCaseWizardDialog {
 2 . . .
 3 public boolean stereotypeTestCase( IClass testClass )
 4 {
 5  . . .
 6  try {
 7   IModelingSession session = Modelio.getInstance().getModelingSession();
 8   IUmlModel model = session.getModel();
 9   ITransaction t = session.createTransaction("Stereotype a test case");
10   // Create the class
11  IClass testClass = model.createClass(
12            testClassName, testCaseParentPackage, "JUnit");
13                try {
14                        //Create the dependency
15                        IDependency dep = model.createDependency(
16
17                    classToTest,testClass,"JUnitDependency");
18   }
19   catch (StereotypeNotFoundException e) {
20   MessageBox box = new MessageBox(
21      Display.getCurrent().getActiveShell(),SWT.ICON_ERROR|SWT.OK);
22                                box.setText("Error");
23     box.setMessage("Stereotype JUnitDependency not found, check your installation");
24     box.open();
25     Modelio.err.println("Stereotype JUnitDependency not found, check your installation");
26     return false;
27   }
28   session.commit(t);
29  } catch ( ) {
30  . . .
31 }
32. . .
33}
Step 6: Adding the test class features
 1class CreateTestCaseWizardDialog {
 2 . . .
 3 public boolean stereotypeTestCase( IClass testClass )
 4 {
 5  . . .
 6  try {
 7   IModelingSession session = Modelio.getInstance().getModelingSession();
 8   IUmlModel model = session.getModel();
 9   ITransaction t = session.createTransaction("Stereotype a test case");
10   . . .
11try {
12     // Create the class
13     IClass testClass = model.createClass(
14
15            testClassName, testCaseParentPackage, "JUnit");
16
17  ...
18                  //Create the test operation
19
20model.createOperation("testOperation1", testClass);
21   }
22   catch (StereotypeNotFoundException e) {
23   MessageBox box = new MessageBox(Display.getCurrent().getActiveShell(),SWT.ICON_ERROR|SWT.OK);
24                                box.setText("Error");
25     box.setMessage("Stereotype JUnit not found, check your installation");
26     box.open();
27     return false;
28   }
29   session.commit(t);
30  } catch ( ) {
31  . . .
32 }
33. . .
34}

Catching modeling events

When an application class is deleted, the expected behavior is that the JUnit module should automatically remove the test class that corresponds to a deleted class.

In practice, when the end-user deletes a class from his model the module is supposed to:

  • Check whether the deleted class has a corresponding test case class or not.

  • Delete the test case class if it exists.

Technically, this means that our JUnit module has to register itself to receive “Delete Element” events.

Later, when such events occur, the JUnit module is supposed to process these events and clean up the test model accordingly.

A bit of theory: module model events

In some particular circumstances, model events are sent by Modelio to modules that have registered themselves to receive these events.

For model changes, a ModelChangeEvent event is sent when a top-level transaction is committed. The ModelChangeEvent event contains a structured and optimized list of changes that have been applied to the model. By processing these changes, a Module is able to perform any processing it requires.

The structured list of changes contained in a ModelChangeEvent 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 module. For example, no update 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 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.

Event handling or listening

Modelio provides two levels of notification :

  1. handler : handlers are triggered before listener and event handlers are allowed to modify the model

  2. listener : listeners are triggered last and their code can’t modify the model. Listeners are notified of modification events done by handlers.

Implementation example

The best place to register the JUnit to receive ModelChangeEvent is the module session services start() method (remember that the start() is called when a project is opened).

Similarly, we have to unregister the Module when the project is closed, in other words, in the stop() session services method, as no model change events should occur after having closed the project.

Let’s have a look at the modified code for the start and stop methods.

 1class JUnitSession {
 2. . .
 3private JUnitModelChangeHandler modelChangeHandler = null;
 4public boolean start ()throws MdacException
 5{
 6 IModelingSession session = Modelio.getInstance().getModelingSession();
 7
 8 modelChangeHandler = new JUnitModelChangeHandler();
 9 session.addModelHandler(modelChangeHandler);
10 . . .
11}
12public void stop ()throws MdacException
13{
14 IModelingSession session = Modelio.getInstance().getModelingSession();
15 session.removeModelHandler(modelChangeHandler);
16 modelChangeHandler = null;
17 ...
18}

The processing of the model changes is delegated to a JUnitModelChangeHandler object. This delegate is created and registered as a model change handler in the start() session service method. It is removed in the stop() method.

Handler is used rather than listener because, our code will modify the model to remove test case classes on model classes destruction.

The JUnitModelChangeHandler class must implement the IModelChangeHandler interface in order to be registered as a model change handler. This interface has only one method which will be called each time a top level transaction is successfully committed and which receives a ModelChangeEvent object as parameter.

In our case, we are only interested in deleted elements that are classes and that have a [JUnit] stereotype dependency to a test class. For these deleted elements, we wish to delete the existing test class.

Let’s suppose, in this case, that the TestModelUpdater class is a visitor that checks that the tested model is up-to-date (and that deletes elements that have to be deleted). It extends com.modeliosoft.modelio.api.model.utils.DefaultMetamodelVisitor and redefines the operation visitModelTree to remove class stereotyped JUnit without [JunitDependency] dependency.

 1class JUnitModelChangeHandler implements IModelChangeHandler
 2{
 3  void handleModelChange(IModelingSession session, IModelChangeEvent  event) {
 4
 5   TestModelUpdater updater = new TestModelUpdater();
 6   ObList<IElement> elementsToDelete = new ObList<IElement> ();
 7
 8   // Memorize the parents to be updated
 9   for (IElementDeletedEvent deletedElement : event.getDeleteEvents() )
10   {
11    if (!elementsToDelete.contains(deletedElement.getOldParent()))
12     elementsToDelete.add(deletedElement.getOldParent());
13   }
14
15   // Visit the elements to delete
16   Transaction t = Modelio.getInstance().getModelingSession().
17           createTransaction("Update the test hierarchy");
18   boolean modelUpdated = false;
19   for ( Object res : elementsToDelete.map(updater) )
20   {
21    if ( (Boolean)res == true )
22     modelUpdated = true;
23   }
24
25   // If the model has changed, the transaction is commited.
26   if ( modelUpdated )
27    Modelio.getInstance().getModelingSession().commit(t);
28   else
29    Modelio.getInstance().getModelingSession().rollback(t);
30 }
31}

For more information on Modelio events, see Listening to model changes on the wiki.

Creating a module property page

We will now add a property page to the JUnit Module.

In this property page we will simply:

  • add the previously defined “Create a Test case” command

  • display the name of the currently selected element

Property page creation is a two steps process : a declaration in module.xml and a Java class that extends AbstractMdacPropertyPage and redefines appropriate operations.

Property page declaration

A property page is declared within the gui section of the module.xml file.

1    <gui>
2    ...
3    <property-page name="JUnitPropertyPage" label="%JUnitPropertyPage" image=""
4    class="org.tutosland.junitmodule.JUnitPropertyPage" />
5    ...
6    </gui>
  • name: this field defines the “logical” name of the property page. It will not be shown to the end-user and is only used internally as an identifier for the command

  • label: the name of the property page as shown in Modelio gui. When this field begins with % then it is a resource label that will be retrieved in the manifest property file (module*.properties).

  • image: a 16x16px image that will be displayed on the gui. It should be stored in resources/… (see §7.4.2) but it can be empty

  • class: value is the name of the Java class that will be activaed to manage the property page. This class should extend AbstractMdacPropertyPage class to be suitable for command activation.

Commands associated with the property page

To make commands appear in the property page, add or change the location tag. Its contribution attribute should be set as “property”. For more information on commands see §7.5.

In the example below, the create test case command is added to the property page (line XXX)

 1    <command name="CreateTestCase"
 2        label="%CreateTestCaseLabel"
 3        tooltip="%CreateTestCaseTooltip"
 4        image="res/icons/createTestCase.png"
 5        group="string"
 6        group-image=""
 7        modify-model="true">
 8
 9        <scope metaclass="Class" stereotype="JavaClass" />
10        <handler class="org.tutosland.junitmodule.command.CreateTestCase" />
11        <contribution location="contextualpopup" />
12        <contribution location="property" />
13    </command>
Coding the property page behavior

Coding property page behavior is quite simple, only two method implementations have to be provided.

1public void update(ObList<IElement> elements, MdacPropertyTable table)

This method is called by Modelio each time the current selection changes in the tool, therefore potentially requiring an update of the property box contents.

The “elements” parameter contains the list of the newly selected elements.

The “table” parameter is the table that must be filled with the updated contents of the property box before returning.

1public void changeProperty(ObList<IElement> elements, int row, String value)

This method is called by Modelio whenever the end-user changes a value in the property box.

The “elements” parameter contains the list of the currently selected elements.

The “row” parameter is the row number of the modified value.

The “value” parameter is the new value the user has set to the given row.

Typical code in this method consists in implementing a switch on the row number, transforming the string value into the proper internal type and setting the proper attribute of the selected element(s) to the new value.

Remember that in case of model changes, a transaction is required.

Here is an example of the JUnitPropertyPage class contents, which only displays and updates the name of an element:

 1package org.tutosland.junitmodule;
 2class JUnitPropertyPage extends AbstractMdacPropertyPage
 3{
 4  // This method is called when the current selection changes and that the
 5  // property box contents requires an update.
 6  // In this example, simply add one property (the Name of the currently selected element)
 7  public void update(ObList<IElement> elements, IMdacPropertyTable table)
 8  {
 9    IModelElement modelElement = ((IModelElement)elements.get(0));
10    table.addProperty ("Name", modelElement.getName());
11  }
12  // This method is called when a value has been edited
13  // in the property box in the row
14  // Here we simply have to update the currently selected element name
15  // Note the use of a Transaction to modify the model.
16  public void changeProperty(ObList<IElement> elements, int row, String value)
17  {
18    ITransaction t = Modelio.getInstance().getModelingSession().createTransaction("Change a property");
19    for ( IElement element : elements )
20    {
21      if ( element instanceof IModelElement )
22      {
23        IModelElement modelElement = (IModelElement)element;
24        switch (row)
25        {
26          case 0: // Name
27            modelElement.setName (value);
28            break;
29        }
30      }
31    }
32    Modelio.getInstance().getModelingSession().commit(t);
33  }
34}

Packaging

Packaging the module is the operation that produces a deployable jmdac file. It is activated through a dedicated target in the ant buildfile named package.

Packaging the module with Maven

To package the module from Maven Project, follow this procedure :

With Command line

       mvn install

Under Eclipse with m2 plugin

  • Select the pom.xml file at the root of the project

  • On the contextual menu (right-click), select Maven install

  • The resulting jmdac file JunitModule_xx.yy.zzz.jmdac is in the target directory.

image

Packaging the module with Ant

To package the module from the Ant Eclipse project, follow this procedure :

  • select the JUnitModule.xml ant file at the root of the project;

  • on the contextual menu (right-click), select Run As → Ant Build… (second one);
    image

  • Select the package target, then click on apply then run buttons.
    image

  • the resulting jmdac file JunitModule_xx.yy.zzz.jmdac is in the mdac directory.

Updating

Each time a module is delivered to users, its version should be increased. To do this, two files should be edited.

module.xml

Version attribute should be changed. Beware not to change the last digit group (8006) which is the metamodel version.

1    <module uid="f83e4fa4-ad92-11de-80db-0014224f9977"
2        name="JUnitModule"
3        version="0.1.00.9015" binaryversion="3.0.00.8015"
4        licenseRequired="false"
5        class="org.tutosland.junitmodule.impl.JUnitMdac">

JUnitModule.xml ant build file

 1The version number should be reported, without the metamodel version part, in the property module.version value.
 2...
 3    <target name="init">
 4        <echo message="Initializing Ant properties"/>
 5        <property name="module.name" value="JUnit"/>
 6        <property name="module.version" value="0.1.00" />
 7        <property name="generation.path" location="src" />
 8        <property name="compilation.path" location="bin" />
 9        <property name="packaging.path" location="mdac" />
10    </target>
11...

Both files should be kept synchronized in term of version number.

Clone this wiki locally