Advanced Collaborative Use Cases

1. Define your own lock strategies

It is possible to define custom Lock Strategies for determining the elements to lock when a modification is made.

Sirius provides a default lock strategy based on common sense :

  • When a user modifies a semantic element, it is immediately locked;

  • When a user creates or deletes a semantic element A inside a semantic element B, B is immediately locked;

  • When adding a reference from an element A to an element B, the element A has to be locked, or both elements if this reference is bidirectional;

  • …​

This default lock strategy allows avoiding any conflict. However, you may want to define your own lock strategy, specific to your models and business rules. Note that it is your responsibility to ensure that your strategy cannot lead to conflicts or that it does whatever is needed to resolve them. In any case, using a lock strategy that locks fewer elements than the default one is out of scope, and the behavior in such a case is undefined. Implement it at your own risks. See org.eclipse.emf.cdo.transaction.CDOTransaction.Options.setConflictResolvers() to use your own CDOConflictResolver to have conflicts resolved, by default CDOMergingConflictResolver is used.

1.1. Create your own SiriusCDOLockStrategy

To provide your own lock strategy, you must define an implementation of the SiriusCDOLockStrategy interface :

  • Collection<? extends CDOObject> getElementsToLock(Notification notification)

Returns the list of elements to lock, according to the given modification.

  • boolean isRelevantNotification(Notification notification)

Indicates if the Lock Strategy can be applied considering the given notification

If you want to define a custom lock strategy that will lock all Cars contained in a Garage any time the name of a Car is modified, you can create the following lock strategy :

// We extends the Vewpoint default Lock strategy
public class MyCustomLockStrategy extends DefaultLockStrategy implements SiriusCDOLockStrategy {

  public boolean isRelevantNotification(Notification notification) {
        boolean isRelevantNotification = false;
        if (notification.getNotifier() instanceof EObject) {
            Option<CDOObject> notifier = CDOSiriusUtil.getCDOObject((EObject) notification.getNotifier());
            if (notifier.some()) {
            	// We only want to consider EObjects that are related
            	// to a certain Package (here my 'CarsPackage').
               isRelevantNotification = CarsPackage.eINSTANCE.equals(notifier.get().eClass().getEPackage());
            }
        }
        return isRelevantNotification;
    }


	public Collection<? extends CDOObject> getElementsToLock(Notification notification) {
		// Here we already know that the notifier is related
		// to the CarsPackage
		 Collection<CDOObject> elementsToLock = Sets.newHashSet();
		 Option<CDOObject> notifier = CDOSiriusUtil.getCDOObject((EObject)notification.getNotifier());
		 // If we just modified the name of a car
		 boolean isConcerningACar = notification.getNotifier() instanceof Car;
		 boolean isConcerningTheNameFeature = (notification.getEventType() == Notification.SET) &amp;&amp; (notification.getFeatureID() == CarsPackage.CAR__NAME);
		 if (isConcerningACar &amp;&amp; isConcerningTheNameFeature){
		 	// then we lock all the cars of contained in this car's garage.
		 	elementsToLock.addAll(((Car)notification.getNotifier()).getGarage().getCars());
		 } else {
		 	// Otherwise, we delegate the calculation of elements to lock
		 	// to the Sirius default lock strategy
		 	super.getElementsToLock(notification);
		 }
	}
}

1.2. Registering a custom Lock Strategy inside Sirius

Once you have defined a new custom lock strategy, you will have to register it through the fr.obeo.dsl.viewpoint.collab.lockstrategy extension point.

  • strategy: the qualified name of the contributed lock strategy

  • strategyID: the identifier of the contributed lock strategy (can be used to override an existing lock strategy)

  • overrideDefaultBehavior: defines how this lock strategy should be used :

    • if ALWAYS: no other lock strategies are considered ;

    • if WHEN_CAN_APPLY: when the contributed lock strategy can be applied (i.e., if isRelevantNotification() returns true), no other lock strategies are considered. Otherwise, default lock strategies are applied ;

    • if NEVER: default strategies are applied, and if the contributed lock strategy defines elements to lock that are not defined by the default lock strategies, the elements are also locked

    • if OVERRIDE_EXISTING_STRATEGY: this lock Strategy will override the Strategy having the given ID

  • overiddenStrategyID: the id of the strategy to override (only makes sense if the strategy OVERRIDE_EXISTING_STRATEGY).

In our example, we want the lock strategy we defined to be applied instead of all other lock strategies when isRelevantNotification() returns true :

<extension point="fr.obeo.dsl.viewpoint.collab.lockstrategy">
	<lockStrategy	overrideDefaultStrategy="WHEN_CAN_APPLY"
             	strategy="com.my.plugin.lock.MyCustomLockStrategy"
             strategyID="com.my.plugin.lock.customLockStrategy">
   </lockStrategy>
</extension>

2. Customize Authentication

Sirius Collaborative API allows you to customize authentication

To do so, you have to implement your own fr.obeo.dsl.viewpoint.collab.api.CDOAuthenticationManager and register it through the CDOAuthenticationManagerRegistry:

// Registering my authentication manager for the CDO Repository located at some IP Adress
CDOAuthenticationManagerRegistry.registerAuthenticationManager("191.XXX.YY.ZZZ", new CustomAuthenticationProvider());

// Registering my authentication manager for all CDO Repositories
CDOAuthenticationManagerRegistry.registerAuthenticationManager("*", new CustomAuthenticationProvider());

To implement your own CDOAuthenticationManage, you can check the fr.obeo.dsl.viewpoint.collab.ui.credentials.DefaultCredentialsProvider, that stores the user login and password inside Eclipse’s secure storage, and open dialogs allowing the user to enter these informations.

3. Customize Commit History View

Sirius Collaborative API allows you to filter element changes in the "Commit History" view.

To do so, you have to implement your own fr.obeo.dsl.viewpoint.collab.ui.api.views.CommitHistoryTreeViewerFiltersProvider and register it through the fr.obeo.dsl.viewpoint.collab.ui.filterProviders extension point.

<extension point="fr.obeo.dsl.viewpoint.collab.ui.filterProviders">
   <filterProvider
         class="com.my.plugin.provider.CustomFilterProvider">
	</filterProvider>
</extension>

To implement your own CommitHistoryTreeViewerFiltersProvider, you can use, for example, same filters than the ProjectExplorer view.

4. The Collaborative Test Framework

The fr.dsl.viewpoint.tests.collab.support and fr.dsl.viewpoint.tests.collab plugins provide API for writing collaborative tests.

4.1. Proving correctness with a Collaborative Test Case

As we cannot directly test collaborative behavior with two Eclipses, each feature must be tested twice :

  1. Remote to Local: ensure that Sirius reacts correctly to repository modifications.

  2. Local to Remote: ensure that Sirius makes the correct modification on the repository.

For example, if we have to test the lock acquiring mechanism :

  1. Remote to Local: Ensure that when CDOObjects are locked (Repository)

    • they cannot be modified with the Local Sirius ;

    • they are correctly displayed (lock decorations…​) inside the Local Sirius.

  2. Local to Remote: Ensure that when modifying a graphical element (or a semantic element) with the Local Sirius :

    • the expected elements are locked in the Repository.

    • modified elements are correctly displayed (lock decorations…​) inside the Local Sirius

4.2. Test framework Architecture

Our collaborative Test Framework :

  1. Allows doing everything you used to do in JUnit and SWTBots tests (simulation of the local user behavior).

  2. Allows checking properties on the CDO Repository (to write Local To Remote tests).

  3. Simulates the behavior of a Remote User (to write Remote to Local tests).

You can define your own test cases by extending :

  • fr.obeo.dsl.viewpoint.tests.collab.swtbot.utils.AbstractCollaborativeSWTBotGefTestCase if you want to write an SWTBot test.

  • fr.obeo.dsl.viewpoint.tests.collab.utils.AbstractDiagramCollaborativeTest if you want to write a JUnit test related to diagrams.

  • fr.obeo.dsl.viewpoint.tests.collab.utils.AbstractTableCollaborativeTest if you want to write a JUnit test related to tables.

  • fr.obeo.dsl.viewpoint.tests.collab.utilsAbstractTreeCollaborativeTest if you want to write a JUnit test related to trees.

All this abstract test cases actually delegates all the collaborative simulation to the CollaborativeTestCase, that you can also extend. However, the CollaborativeTestCase does not provide API to manipulate local session.

If you have to do assertions concerning the CDO Repository state (like checking that an element has been deleted from the repository), standard Collaborative APIs like the CDORepositoryManager should be used.

Any CollaborativeTestCase is associated to a IRemoteUser, which purpose is to simulate the behavior of an end-user using Sirius on a remote computer. The IRemoteUser interface provides APIs needed for simulating this behavior (lock acquiring, representation creation…). It uses a different CDOSession than the one used by the local user, so that all the modifications he makes are considered as remote.

4.3. Test Servers

4.3.1. Local Server

All Collaborative tests need a CDO Repository set up. We have defined the fr.obeo.dsl.viewpoint.tests.collab.server plugin, which contains all logic needed for launching and disposing a CDO Server and Repository.

Closing and reopening this Test Server between each test seems quite a burden while not adding any benefit.

That is why the Activator of the test server plugin launches the server when loaded, and sets it down when disposed. The Test server is therefore loaded once, and closed when all tests are finished. Notice that you can also simulate network failures by stopping this server.

4.3.2. Remote Server

All the collaborative tests can also be launched on any CDO Repository located on a remote computer. To do so, add the following VM arguments to your Test Suite :

-Dserver_location=191.XXX.YY.ZZZ@2036@repo1

You can therefore indicate the address of the repository to launch the tests on. If the variable is not defined in the launch config, then the local server will be automatically started.

4.4. Test data life cycle

At each setUp of any collaborative test :

  • Local files are imported using standard API;

  • Database is then initialized from localFiles using collaborative API (for example uploading on the repository a semantic model located in the test plugin);

  • On tearDown, created CDOResources are deleted.

4.5. Setting up a collaborative test

You can very easily set up a test that will upload a set of models on the repository and create a collaborative session referencing the uploaded resources.

Check the fr.obeo.dsl.viewpoint.tests.collab.utils.AbstractDiagramCollaborativeTest.genericSetUp() methods to get additional information about setting up a Collaborative TestCase.

4.6. Setting up the connection type

As the collaborative framework handles connections to the CDO server using the TCP protocol or the SSL protocol, collaborative tests can run using one of these protocols. By default, the TCP protocol will be used. To launch tests using the SSL protocol, the following arguments need to be added to the launch configuration:

-DconnectionType=SSL
-Dorg.eclipse.net4j.tcp.ssl.passphrase=secret
-Dorg.eclipse.net4j.tcp.ssl.key=file:///${PATH_TO_SERVER_CERTIFICATE}/server.ks
-Dorg.eclipse.net4j.tcp.ssl.trust=file:///${PATH_TO_Client_CERTIFICATE}/trusted.ks

To run a test with an SSL connection, the server and the client require each a certificate. The certificates used in the Sirius Test suite are available in the SSL folder of the project fr.obeo.dsl.viewpoint.tests.collab.support. The server and client certificates are respectively named server.ks and trusted.ks.

4.7. RemoteUser API description

The fr.obeo.dsl.viewpoint.tests.collab.support.api.remoteuser.IRemoteUser interface provides APIs allowing to easily describe the behavior of a Remote end-User. It is associated to a CDOSession and CDOTransaction.

The API is divided in two parts :

  • Fundamental methods allowing to do complex checks and actions

  • A set of methods providing facilities for the most current actions

4.7.1. Key points of the API

In this first part, we will determine the fundamental feature provided by the test API.

CDO facilities: getActiveTransaction() and dispose()

  • CDOTransaction getActiveTransaction(): returns the CDOTransaction to use for manipulating data on the Remote User side.

  • void terminateSession(): disposes up the RemoteUser by closing the CDOTransaction and the CDOSessions.

4.7.2. Asynchronous execution of any Remote User action

All actions made by Remote User are executed in a separated Thread, so that remote and local end-users can do action simultaneously, and all loaded CDOResources and CDOObjects do not interfere with the local ones.

RemoteUserActions

The AbstractRemoteUserAction class implements Runnable. It is taking a RemoteUser in parameter.

Any RemoteUserAction have an execute() method, called inside its run() method. The actions made by the remote user will be described inside this method.

When the RemoteUserAction is finished, the method isFinished() returns true (false otherwise). Notice that if an error occurs during the execution of the RemoteUserAction, a RemoteUserException will be thrown.

Executing RemoteUserActions

The IRemoteUser interface allows executing any RemoteUserAction.

void doAction(boolean shouldWait, RemoteUserAction remoteUserAction)

If shouldWait is true, then the doAction method will wait until the remoteUserAction is finished.

End User’s data storage

Let us consider the following example :

public class CreateElementAction extends AbstractRemoteUserAction {

	public CreateElementAction(RemoteUser remoteUser){
		super(remoteUser);
	}

	@Override
	protected void execute() {

		// Step 1 : Getting the resource in which to create the element
	 	CDOResource r1 = actionRemoteUser.getActiveTransaction().getResource("resourcePath");

	 	// Step 2 : Create the element and add it to the CDOResource
	 	CDOObject myCreatedElement = MyFactory.createElement();
	 	r1.getContents().add(myCreatedElement);
	}
}


public class LockingTest extends AbstractDiagramCollaborativeTest() {

	public void testLockRemoteToLocal() {

		// The remote user creates a CDOObject
		remoteUser.doAction(true, new CreateElementAction(remoteUser));

		// Check 1 : as the remoteUser has committed, the
		// created element should be visible for localUser
		checkIsVisibleElement(XXX);

		// The remote user locks the created CDOObject
		remoteUser.doAction(false, new LockElementAction(XXX));
	}

In this example, you can see that the element created in the CreateElementAction has to be accessed by localUser (to check that it is correctly visible) and by remoteUser (to lock it).

As explained previously, we should not directly store the created CDOObject. That is why you can define variables for a Remote User.

  • void setVariable(String key, Object value);

Stores the given value with the given key as identifier. If the value has to be accessed in the main thread, then it should not be a CDOObject (you should store the CDOID instead and use the local transaction to locally load the CDOObject).

  • Object getVariable(String key);

Returns the value associated to the given variable name.

Let us go back to the example presented above, assuming that the CreateElementAction is now built with a String corresponding to the name to use for registering the CDOID of the created element just after Step 2 :

	protected void execute() {
		...
	 // Step 3 : registering created element
	 getRemoteUser().setVariable(this.nameOfElementToCreate, myCreatedElement.cdoID());
	}

Now it can be used for loading the created element inside the main thread (local user) :

public void testLockRemoteToLocal() {

	// The remote user creates a CDOObject
	remoteUser.doAction(true, new CreateElementAction(remoteUser, "idOfCreatedElement"));

	// Loading the created CDOObject
	CDOObject created = activeTransaction.getCDOObject((CDOID) remoteUser.getVariable("idOfCreatedElement"), true);

	// Making any check on this object
	assertTrue(CDOLockManager.isLockedByOthers(created));

	// Using the created element variable to launch the lock operation
	remoteUser.doAction(false, new LockElementAction("idOfCreatedElement"));

The lock action is now defined with a String corresponding to the name of the variable representing the element to lock.

We obviously want to wait for the end of the CreateElementAction before accessing to the created element (hence we call doAction(true,…​)). If the LockElementAction has no direct effect on Local side, we can continue before it is ended(hence we call doAction(false,…​)).

Providing facilities for Remote Checks

You will often need to check that the Repository State from the Remote User viewpoint is consistent as expected. (Local To Remote Tests).

That is why we defined the AbstractRemoteCheck class. Always launched synchronously and returning a boolean, it makes Remote Checks easier to write :

	remoteUser.assertRemote("Message if the test fails", new AbstractRemoteCheck(remoteUser) {
        @Override
        protected boolean checkCondition() {
            CDOObject elementToCheck = (CDOObject) remoteUser.getVariable("myElementToCheck");
            return "expectedResourcePath".equals(elementToCheck.cdoResource().getPath());
        }
	});

4.7.3. Shortcuts

In order to make Collaborative test writing as simple as possible, we provided a set of utility methods. This following list is not complete, we invite you to study the IRemoteUser interface for additional behavior :

  • void assertIsExpectedLockStatus(boolean shouldBeLockedByMe, boolean shouldBeLockedByOthers, String…​ elementNames): checks that the elements corresponding to the given names (that must have been registered through a remoteUser.setVariable(elementName, element) have the expected lock status.

  • void lockElements(String…​ elementsToLock): locks the elements with the given name

  • void unlockElements(String…​ elementsToUnlock): unlocks the elements with the given name

  • void saveSession(): simply commits all changes

5. Representations Lazy Loading

A new Representation Lazy Loading mode allowing lazy loading of representations is available in Sirius Collaborative Mode, for remote project representations. It is not activated by default, but this might be done in your product (check its own documentation or release notes).

This mode is currently experimental in Sirius for a local project. For more details about the Sirius part, please see Representations Lazy Loading (Experimental).

This mode means two things:

  • At the project opening (a.k.a.Collaborative session opening), none of the representation data is loaded. Representations will be loaded on demand. Each time a feature needs to work with the representation content – Open a representation, move a representation, export as image, etc. – the representation will be loaded.

  • To achieve that, representations are persisted in separate resources. The aird resource will only hold the DRepresentationDescriptor that contains the minimum of information (Representation name, Representation Description, Representation path to load it).From a developer point of view, the representation will be loaded as soon as DRepresentationDescriptor.getRepresentation() is called or is retrieved from its CDOID. It is recommended to get the representation by using the DRepresentationDescriptor.getRepresentation() method. Please see the Cross Referencer section for more details about this point.

5.1. Migrate existing shared projects

Existing shared projects will not be automatically migrated toward one resource per representation. Only new created representations will be stored in their own resource. To migrate existing projects, you have to import them locally and export them again to the server (after activation of the mode). See the next section Import / Export of Modeling Project for more details. Note: Even though passing from one mode to the other requires cleaning the database, the application will work properly with a mix of split or not split representations.

5.2. Import / Export of Modeling Project

The CDOImporter and the CDOExporter update the target representations locations according to the active location rule. When the Representation Lazy Loading mode is activated, the default rule for remote representations is one representation per resource. This behavior can be deactivated by changing the CDOSiriusPreferenceKeys.PREF_CREATE_SHARED_REP_IN_SEPARATE_RESOURCE value. See the Activating the Representation Lazy Loading section for more details. As a result, when exporting a project from local to remote, the CDOImporter will automatically separate representations in different resources. When importing a project from remote to local, the representations will be relocated in the aird resource. This is the default behavior if the split of representations is deactivated for local projects.

5.3. Activating the Representation Lazy Loading

Applications that will leverage the Representation Lazy Loading mode are applications that have been improved to load representations only if needed. The mode is disabled by default. To activate it, you need to set CDOSiriusPreferenceKeys.PREF_CREATE_SHARED_REP_IN_SEPARATE_RESOURCE to true. The preference default value can also be set by a product or by the users using the plugin customization mechanism and the following customization: fr.obeo.dsl.viewpoint.collab/PREF_CREATE_SHARED_REP_IN_SEPARATE_RESOURCE=true.

For testing purpose, you may force the behavior (and then ignore the preference value) with the system property forceCreateSharedRepresentationInSeparateResource.

That preference will, in fact, enable the default remote representation location rule provided by the Sirius Collaborative Mode. This rule uses the fr.obeo.dsl.viewpoint.collab.api.preferences.CDOSiriusPreferenceKeys.PREF_CREATE_SHARED_REP_IN_SEPARATE_RESOURCE preference to split or not representations in separate resources. By setting the preference value to false, remote representations will be held in the same aird resource than the DRepresentationDescriptor, as before. This preference allows activating this new behavior without implementing any specific location rule. However, it is possible to implement your own location rule. See the next section Override the Shared Representation location rule for more details.

5.4. Override the Shared Representation location rule

As described in Representations Lazy Loading, the default representation location rule can be overridden by implementing the interface org.eclipse.sirius.business.api.session.danalysis.DRepresentationLocationRule and declaring it in the org.eclipse.sirius.dRepresentationLocationRule extension point. If this rule is specific to shared representations, you have to make sure that the current representation DView is held in a CDOResource:

    @Override
    public boolean providesURI(DRepresentation representation, Resource dViewResource) {
        return CDOProtocolConstants.PROTOCOL_NAME.equals(dViewResource.getURI().scheme());
    }

5.5. Cross Referencer

The Cross Referencer is installed only on loaded resources. That means if you try to retrieve inverse references toward an element of a representation which is not yet loaded, the Inverse Cross Referencer will not return them. In addition, it is crucial to keep in mind that the CrossReferencer will not be installed on representations loaded without using DRepresentationDescriptor.getRepresentation() method. Indeed, this method will automatically install the Sirius Session CrossReferencer on the representation resource if it has not been installed before. Retrieving the representation by using CDOView.getObject(CDOID) will load the representation without installing the CrossReferencer. That will probably cause instability, mainly because of the DRepresentationDescriptor.representation() inverse reference that will be missing. Sirius uses this inverse reference in several places to retrieve the DRepresentationDescriptor of a given DRepresentation.