Synchronizing models

This section explains how to update model elements to reflect changes in source domain elements and vice versa.

Synchronizing target domain on changes to source elements

In the previous section, we learnt how to activate an ITarget object so it can synchronize its features when structural features of the target elements are accessed.

MMI domain clients must keep target elements synchronized when the source element changes. This can be done by listening to source element changes and marking target structural features dirty to correspond to source changes. The MMI framework only allows synchronization of the target elements that are activated with a StructuredReference and an ITargetSynchronizer.

The MMI framework invokes the target synchronizer of the target element whenever any structural feature of the target is marked dirty. This ensures the target domain is always synchronized with the source domain.

Synchronizing the UML Artifact when a plugin import is added or removed

When a plugin is modified by the addition or removal of a plugin import, the IPluginModelListener is notified about the change. By registering a listener with PDE Model Manager, we can listen to model import changes. This can be done as shown below.

PDECore.getDefault().getModelManager().addPluginModelListener(pluginModelListener);

In the implementation of IPluginModelChangeListener.modelChanged(), in response to changes to plugin imports we can mark the client dependencies structural feature of the UML Artifact dirty as shown below.

public void modelsChanged(PluginModelDelta delta) {
    //obtain plugin from delta
	...
	
    StructuredReference sRef = StructuredReferenceService.getInstance().
        getStructuredReference(domain, plugin);
        
    EObject element = MMIResourceCache.getCachedElement(domain,
        new StructuredReferenceKey(sRef, UMLPackage.eINSTANCE.getArtifact()));
        
    if (element != null) {
        assert element instanceof Artifact;
        ((ITarget) element).setDirty(
            UMLPackage.eINSTANCE.getNamedElement_ClientDependency(), null);
    }
}

The plugin is obtained from the delta, and the structured reference corresponding to the plugin is obtained from the StructuredReferenceService. This structured reference is then used to retrieve the ITarget from the MMIResourceCache. The setDirty() method is then invoked on the artifact target element to mark the client dependencies structural feature as being dirty.

Synchronizing source elements on changes to UML

Synchronization

When changes are made to elements of target models, the source elements need to be updated, or synchronized, to reflect the changes. For example, you may have mapped a PDE plugin to a UML model element. Let's assume you have also mapped the PDE Plugin dependency in the same way we learnt how to map the PDE plugin in the Mapping Between Domains section. Now, when you modify the dependencies of the mapped plugin, the actual plugin would need to be modified too, by adding the required dependencies. This synchronizes the domain element with the model.

Using the SourceSynchronizationService

Models are synchronized using the SourceSynchronizationService. MMI automatically adds a listener to transactions on the target domain when resolve() or adapt() from the ModelMappingService are invoked. The SourceSynchronizationService's emit() method will be invoked automatically when a change is made within a transaction. The emit occurs in the transactionAboutToCommit() method of the ResourceSetListener interface. The emit() method is also invoked upon validation. This means, there is typically only a need to implement an ISourceSynchronizationProvider to react to changes in the model.

The ModelChangeDelta

A ModelChangeDelta represents changes in the model. Since the ModelChangeDelta describes the change that the emit() method is to handle, ModelChangeDelta objects are unsurprisingly also created automatically when a transaction is about to be committed or upon validation. Model change deltas are created from EMF Notification objects, and as such can be thought of as a wrapper for them.

Creating ModelChangeDelta objects

ModelChangeDelta objects are created internally using the ModelChangeDeltaManager using code like the following.

ModelChangeDeltaManager.getInstance().createModelChangeDelta(notifications)

Typically, you do not need to create a model change delta, but if you wish to, you can implement the IModelChangeDeltaProvider interface and override the createModelChangeDelta() method.

The table below shows the correspondance between the ModelChangeDelta and the EMF Notifier.

ModelChangeDelta property EMF Notification property
type type of notification
feature feature of notification
oldValue or oldIndex old value of notification
newValue new value of notification
index position
collection new value of notification (on add) or old value of notification (on remove)
indices new value of notification (on remove)
owner notifier
participantIDs ID of Structured Reference handler (not a property of an EMF Notification)

Implementing an ISourceSynchronizationProvider

Source synchronization providers are registered against a particular structured reference provider's ID, in the same manner the resolve of the ModelMappingProvider also specifies a structured reference provider's ID.

Only one method needs to be implemented for the ISourceSynchronizationProvider.

public ICommand emit(ModelChangeDelta delta);

The emit() method is required to return an ICommand. If no source changes are required, you can return a NoOpCommand. If an error has occurred, you can return an UnexecutableCommand. The first case is a perfectly valid scenario, as not all model changes must result in changes to source elements. In the latter case, however, since the command is obviously invalid, an error dialog will appear alerting the user of the error, and model changes will be rolled back. In other cases, the returned ICommand should perform the task of updating the source domain element in its execute() method.

Example

Let us examine what happens when we add a dependency in the model.

Suppose we have two plugins represented as UML artifacts. When we add a plugin dependency between the two model elements representing the plugins, logically, we must be modifying the model. The dependent plugin (artifact) is being modified to contain a dependency to the new plugin. Of course, we are also creating a dependency and setting the dependency's ends to the artifacts representing the plugins.

In terms of code, a CompoundModelChangeDelta is created to represent these changes to the model. The CompoundModelChangeDelta contains 4 separate ModelChangeDelta objects. These deltas correspond to what happens logically when we modify the model.

We need to take all the deltas into account to construct a semantically meaningful operation that modifies the source element. Simply considering one delta does not give enough context to obtain a clear picture about the entire change. Look at the table below to learn what happens when we add a plugin dependency. Now, assume you will change the client of the dependency. In other words, you are changing the dependent plugin to some other plugin. In this second case, one of the ModelChangeDelta objects is a Notification.ADD, where the owner is a Usage, the new value is an artifact, and the feature is the client end of the usage dependency. This delta looks very similar to the third delta shown below that we got when we added the client dependency. If we didn't take the other deltas into account (we would also get Notification.REMOVE deltas when we change the client of the dependency), we would confuse one model change with another.

Delta Owner (Container) New Value Feature Type
1 Model (Package) Usage Packaged element
The package's packaged element
Notification.ADD
2 Artifact (Named element) Usage Client dependency
The named element's client dependency
Notification.ADD
3 Usage (Dependency) Artifact Client
The client end of the usage dependency
Notification.ADD
4 Usage (Dependency) Artifact Supplier
The supplier end of the usage dependency
Notification.ADD

For the sake of simplicity, we will only cover how to synchronize the addition of a plugin dependency here.

Overview

In order to correctly interpret the CompoundModelChangeDelta that contains the above 4 ModelChangeDelta objects, we could have code like this.

public ICommand emit(ModelChangeDelta delta) {
    return convertToCommand(editingDomain, delta, new HashMap());
}

private ICommand convertToCommand(TransactionalEditingDomain editingDomain,
    ModelChangeDelta modelDelta, Map analyzedData) {
		
    if the modelDelta is a CompoundModelChangeDelta
        iterate through each ModelChangeDelta
            recursively call covertToCommand with the analyzedData map
        //code to combine ModelChangeDelta objects goes here
            combine individual commands
    else
        //code to handle third and fourth ModelChangeDelta goes here
        if the ModelChangeDelta describes a feature that has been analyzed
            no need to make a new command

        //code to handle first ModelChangeDelta goes here
        handle first possible ModelChangeDelta

        //code to handle second ModelChangeDelta goes here
        handle second possible ModelChangeDelta
    }

    return command;
}

The convertToCommand() method above works recursively, iterating through individual ModelChangeDelta objects contained in a CompoundModelChangeDelta. It uses a map to keep track of deltas that have been analyzed each time the method is invoked. This map will be read and filled by the code that handles each ModelChangeDelta.

Now, we are ready to look at the code that will be required to handle each ModelChangeDelta individually.

First ModelChangeDelta

This first block of code handles the first ModelChangeDelta.

if(owner == getWorkspacePluginUMLModel(editingDomain)) {
    childCmd1 = UnexecutableCommand.getInstance();
    if(modifiedFeature == UMLPackage.eINSTANCE.getPackage_PackagedElement()) {
        if(deltaType == Notification.ADD) {
            if(newValue instanceof Usage) {
                Map newValueSF = new HashMap();
                analyzedData.put(newValue, newValueSF);
                childCmd1 = getCommandForNewUsage(editingDomain,
                    (Usage) newValue, newValueSF);
            }
        }
    }
}

In the block of code above, we make use of a Map, which we have called analyzedData. This map is used to help keep track of the features we have analyzed after having interpreted each ModelChangeDelta. It stores the new value property of the ModelChangeDelta (in this case, a usage) as its keys. The values of this map are more maps (newValuesSF in the code above). The keys of the second map are the structural features that have been considered. Each value of this second map is an individual ICommand that corresponds to the feature being examined. When executed, each ICommand will perform the task of modifying the source element based on the results of analyzing a ModelChangeDelta. Storing the ICommand in this map will prove to be useful if we plan on modifying a previously generated command when another ModelChangeDelta is analyzed and the previously generated command needs to be augmented to reflect new discoveries during analysis.

In the getCommandForNewUsage() method, we pass in the newValueSF map. In addition to returning a command to handle creating a new usage, the method must also modify the map. The features for the client and supplier of the dependency will become the keys of this map. We'll see why this is important when we handle deltas 3 and 4.

Your implementation of the emit() method need not necessarily contain these maps, nor does it even need to augment a command. Augmenting commands is useful only when you have multiple ModelChangeDelta objects that you wish to take into account individually. In these cases, you may find it easier to generate a general or perhaps even incomplete ICommand when the first ModelChangeDelta is encountered and augment it with specific details when further ModelChangeDelta objects are encountered and a more complete picture of the entire change can be obtained.

Second ModelChangeDelta

This block of code handles the second ModelChangeDelta.

if(owner instanceof Artifact) {
    Map sfAnalyzed = new HashMap();
    analyzedData.put(owner, sfAnalyzed);
    childCmd2 = getCommandForExistingArtifact(
        editingDomain, (Artifact)owner, sfAnalyzed, owner.eContainer() == null);
}

Third and fourth ModelChangeDeltas

Finally, we don't need to do special handling on the third and fourth ModelChangeDelta objects. Observe that the first and second blocks of code will have inserted the relevant entries into the sfAnalyzed map, causing the NoopCommand to be returned.

Map sfAnalyzed = (Map)analyzedData.get(owner);
if(sfAnalyzed != null) {
    // If the owner has been analyzed then is that feature analyzed too.
    ICodeCommand codeCmd = (ICodeCommand)sfAnalyzed.get(modifiedFeature);
    if(codeCmd != null) {
        codeCmd.augmentCommand(modelDelta);
        childCmd = NoopCommand.getInstance();
    }
}

Combining the ModelChangeDelta objects

Finally, we need to combine the individual four child commands together.

ICommand command = new CompositeWorkspaceCommand("Workspace Command");
((CompositeCommand)command).compose(childCmd);

Execute, undo and redo

We learnt that an ICommand should be returned by the emit method, and we saw how the individual commands were combined together into a CompositeCommand. To enable undo and redo, each command needs to return true in the canUndo() and canRedo() methods. This is the default for AbstractCommand. If you are subclassing AbstractCommand as recommended, the main code that modifies the source element will reside in the doExecuteWithResult() method. To handle undo and redo, implement doUndoWithResult() and doRedoWithResult(). doUndoWithResult() should reverse the operations that were done in doExecuteWithResult(), while doRedoWithResult() can typically invoke the doExecuteWithResult() method directly to redo the operation.

Displaying user interfaces

It is possible, but not always necessary, to display a user interface dialog when returning a command in your ISourceSynchronizationProvider. One common reason is to display a checkout dialog if the domain element that will be modified is in a file that's either read only or under source control.

For example, the code below could bring up a dialog to check out the required files.

ResourcesPlugin.getWorkspace().validateEdit(files, context)

Typically, this code would reside in the execute() method of the returned ICommand.


Legal notices