Mapping between domains

This section explains how to implement mapping between elements from a source domain to elements of the UML domain.

Creating structured references to represent domain elements

Before implementing a mapping from a source domain to a target domain, the domain provider must implement an IStructuredReferenceProvider to create references that uniquely identify the elements from the source domain. These references are called structured references. To create a structured reference for a domain element, the domain provider must identify the key properties of the source element required to reference it.

Creating structured references using modifiers

The StructuredReference object has a private constructor. So, how does one go about creating a structured reference? Structured references must be created using an IStructuredReferenceModifier. While the accessor (get) properties on structured references are public, the constructor and modification (set) methods on structured references must be invoked through a modifier. This is by design. Each modifier controls the creation and modification of structured references of one or more given types. A modifier can only create and modify structured references of certain types.

To distinguish between different types of structured references, StructuredReference objects have the concept of a provider id. For example, while an Eclipse plugin could be uniquely identified by an id and a version, the same could be said for the domain element for the firmware of a CD player. To avoid confusing one type of structured reference with another, structured references of different types must have unique provider ids. Structured reference providers of a given id are passed modifiers that can create structured references only of the same id. This prevents one provider from creating or modifying the references of another provider.

IStructuredReferenceProvider and the StructuredReferenceService

To obtain an IStructuredReferenceModifier, a class must first implement the IStructuredReferenceProvider interface. When the structured reference provider is instantiated by the StructuredReferenceService, the setStructuredReferenceModifier() method will be called and an instance of an IStructuredReferenceModifier will be passed into the method. The structured reference provider thus receives a modifier that can create and modify the types of structured references for which the provider provides.

Since the modifier is passed to the IStructuredReferenceProvider, the provider must also be the primary point for creating and modifying structured references. Apart from implementing the method to accept the IStructuredReferenceModifier, the provider must implement methods to return a structured reference given a source element or a string representation of a structured reference. Also, the provider must implement a method to return a source element when given a structured reference. (There is also one more method to return information about a structured reference, which is not as important for typical domain providers.)

Activation

Activation enables the target elements to become synchronized with changes in the source elements. It is not necessary to activate the target element if we are not interested in on demand synchronization of the source element. We will learn more about activation later on in this section.

Implementing a structured reference provider for IPluginBase

As explained in the last section, the two key properties required to identify a plugin in the PDE Plugin Model are the plugin identifier and the plugin version. Hence, the structured reference for the PDE plugin will have two properties, id and version. The provider id of structured references representing a PDE plugin will be pde.IPluginBase.

Implementing a structured reference provider for IPluginBase requires the provider to implement the IStructuredReferenceProvider interface. Clients can extend their StructuredReferenceProvider from abstract implementation AbstractCachingStructuredReferenceProvider. This abstract implementation of IStructuredReferenceProvider is optimized to cache StructuredReference for domain element. This means, the same instance of the structured reference will be used to represent the domain element.

Providers extending AbstractCachingStructuredReferenceProvider must implement the following methods.

  1. constructStructuredReference(Object referencingContext, Object sourceElement)
  2. This method creates a structured reference for the given domain element. The StructuredReferenceModifier is used to create the structured reference, and the provider can obtain the modifier through AbstractCachingStructuredReferenceProvider.getModifier().

    To create a structured reference for a plugin,

    1. Create a new Map and populate it with property name value pairs for the IPlugin
    2. Use getModifier().createStructuredReference, passing in the pde.IPluginBase provider id and the properties created above.

    The following code creates the structured reference for the given IPluginBase domain element.

    protected StructuredReference constructStructuredReference(Object referencingContext, Object sourceElement) {
        if (sourceElement instanceof IPluginBase) {
            IPluginBase plugin = (IPluginBase) sourceElement;
            Map properties = new HashMap();
            properties.put("id", plugin.getId());
            properties.put("version", plugin.getVersion());
            return getModifier().createStructuredReference("pde.IPluginBase", properties, null);
        }		
    }
    
  3. resolveToDomainElement(Object referencingContext, StructuredReference sRef)
  4. The method obtains the properties required to uniquely identify the domain element from the structured reference. Properties are obtained using the StructuredReference.getProperty() method. Then, the domain element is obtained using the PDE specific findPlugin() API.

    The following code shows how to retrieve the IPluginBase domain element for the given structured reference.

    public Object resolveToDomainElement(Object referencingContext,
        StructuredReference sRef) {
    
        if ("pde.IPluginBase".equals(sRef.getProviderId())) {
            String id = sRef.getProperty("id");
            String version = sRef.getProperty("version");
    
            IPluginModelBase pluginModel =
            PDECore.getDefault().getModelManager().findPlugin(id, version,
                IMatchRules.NONE);
            if(pluginModel != null)
                return pluginModel.getPluginBase();
    
            return null;
        }
    }
    
  5. getInfo(Object referencingContext, StructuredReference sRef, String infoName)
  6. This method will return an Object for the given information. The scope and usage of this implementation is left up to the provider. It will not affect the creation or modification of structured references.

    public Object getInfo(Object referencingContext, StructuredReference vr,
        String infoName) {
    
        if (StructuredReferenceInfoConstants.NAME.equals(infoName)) {
            return vr.getProperty(PLUGIN_ID);
        }
        return null;
    }
    

Registering an IStructuredReferenceProvider

The following XML registers the structured reference provider for IPluginBase.

<extension
    id="pdeumlstructuredreferencehandler"
    name="StructuredRefHandler for PDE constructs"
    point="com.ibm.xtools.mmi.core.StructuredReferenceProviders">
    <StructuredReferenceProvider
        class="com.ibm.xtools.umlviz.ui.examples.pde.internal.handlers.PdeStructuredReferenceHandler">
        <StructuredReferenceProviderId id="pde.IPluginBase"/>
        <DomainElementType class="org.eclipse.pde.core.plugin.IPluginBase"/>
    </StructuredReferenceProvider>
</extension>

In the XML above, the id of the structured reference provider is specified as pde.IPluginBase. Therefore, the modifier dispensed to the provider will only create structured references with a provider id equal to pde.IPluginBase. The domain element object type is specified as IPluginBase, so this provider is only invoked when the source element is an instance of IPluginBase.

Using the StructuredReferenceService

We learnt how to implement a provider for the ModelMappingService. Now, we will look at using the service to obtain a structured reference for a plugin.

Suppose we have obtained a source element, IPluginBase, in the following manner.

IPluginBase base = null;
IPluginModelBase pluginModel =
    PDECore.getDefault().getModelManager().findPlugin("myPlugin", version,
    	IMatchRules.NONE);
if (pluginModel != null)
    base = pluginModel.getPluginBase();

Now, the following line of code will return the structured reference.

StructuredReference sRef =
	StructuredReferenceService.getInstance().getStructuredReference(editingDomain, base);

In the above code, it is up to the individual provider to cache (or not to cache) the returned structured reference. In our example, since we subclassed AbstractCachingStructuredReferenceProvider, the returned structured reference would have been cached, so invoking the same line of code repeatedly would return the same instance of the structured reference.

Mapping a domain model to UML

Source elements from a given domain can be mapped to UML elements. Mapped elements must implement MMI's ITarget interface. The ITarget interface defines methods for synchronization, a concept that we will look at in a later section.

The DevOps Modeling Platform provides an implementation of the UML2 metamodel whose classes also implement ITarget. For example, its UML2 State object will implement both the original org.eclipse.uml2.uml.State interface and the ITarget interface.

Any instance of an EObject can be used as a mapping target if it implements the ITarget interface.

Creating target UML elements

As noted above, UML elements typically belong inside a model. We use the MMICoreUtil.create() API to create floating target model elements. We can think of MMICoreUtil.create() as a factory method for creating floating ITarget objects. To generalize, the MMICoreUtil.create(UMLPackage.eINSTANCE.getUMLType()) will return an EObject of the given UMLType that implements the MMI ITarget interface.

Once we have our UML model element (which is an ITarget), we can create contained objects using the UML model element's create methods, because it is an ITarget and has knowledge of how to create objects that conform to both the domain model and the MMI contracts.

It is important not to create the target model elements using create methods from the target domain factory. Otherwise, you will end up with EObjects that do not implement the ITarget interface. Recall that the mapping targets must implement the ITarget interface. In the context of our example, we must not make use of UMLFactory.eINSTANCE.createArtifact() or UMLFactory.eINSTANCE.createUsage() to create UML Artifact or UML Usage objects.

Implementing an IModelMappingProvider

A model mapping provider maps the domain elements of one domain to another. The provider must implement the IModelMappingProvider interface, which defines an adapt() method among other things. At its simplest form, the adapt() method is reponsible for returning a mapped EObject as previously described. Typically, however, the adapt() method also at a minimum caches the returned ITarget.

Adapting the IPluginBase to a UML Artifact

Typically, an implementation of the adapt() method follows a certain path.

  1. It creates a StructuredReference for the domain element.
  2. Using a caching key based on the structured reference, it determines if the EObject is in a cache.
  3. If the EObject is in the cache, it returns the cached EObject.
  4. If the EObject is not in the cache, it caches the EObject and returns it.

Here are the steps to create the artifact for the IPluginBase.

  1. Create the StructuredReference for the given IPluginBase.
  2. Create the caching key for the structured reference.
  3. Try and find the element from the MMIResourceCache. If the returned element is null, it was not in the cache. Otherwise, it has previously been adapted and cached, so we can simply return the cached element.
  4. For UML elements, elements are typically created in a UML model. If a model doesn't exist, we create one and add it to the MMI Resource.
  5. Create the target UML Artifact.
  6. Activate the UML Artifact to enable synchronization and to associate it with the structured reference.
  7. Cache the element in the MMIResourceCache.
  8. Set the name of the UML Artifact.
  9. Since we have synchronized the name of the UML Artifact, mark the name structural feature as being synchronized (i.e. does not need to be resynchronized until it becomes dirty). However, if you plan on synchronizing the feature later on in your ISourceSynchronizationProvider, you do not need to mark the feature clean. We'll learn more about ISourceSynchronizationProvider and synchronization in the next section.

Implementing adapt()

The following code implements the above nine steps.

Model artifactModel = null;

public EObject adapt(TransactionalEditingDomain editingDomain, Object object,
    EClass langKind) {

    if(object instanceof IPluginBase) {
        StructuredReference sRef = PdeStructuredReferenceHandler.
            getInstance().getStructuredReference(editingDomain, plugin);

        //first try to retrieve from the cache
        ITarget element = (ITarget) MMIResourceCache.getCachedElement(editingDomain,
            new StructuredReferenceKey(sRef, UMLPackage.eINSTANCE.getArtifact()));
		
        //if it has not been cached, create it
        if (element == null) {
            if (artifactModel == null) {
                artifactModel = (Model) MMICoreUtil.create(
                    UMLPackage.eINSTANCE.getModel());
            }

            EObject aElement = artifactModel
                .createPackagedElement(null, UMLPackage.eINSTANCE
                    .getArtifact());

            element = (ITarget) aElement;

            EObjectUtil.setName(element, plugin.getId());

            element.activate(this, sRef);
            MMIResourceCache.cache(editingDomain, element);
            element.setClean(UMLPackage.eINSTANCE.getNamedElement_Name());
        }
        return element;
    }
}

Summary of adapt()

First, we verify if the object is an IPluginBase, i.e. the domain element we are providing for. If it is, we obtain the structured reference that represents the specified instance of the plugin. From the StructuredReference, we construct a key which is used as a key to the resource cache. If the element was found in the cache, we simply return the element. If the element was not found in the cache, we need create the element in the model (using createPackagedElement()), and furthermore if the model did not already exist, we need to create the model (using MMICoreUtil.create()).

Now that the target has been created, we set its properties of the ITarget. The type of properties we are interested in will be specific to the domain element and its intended purpose. In this case, we only care about the plugin's name. After setting the name of the ITarget, we can think of that property, called a feature, as being synchronized with the name of the actual IPluginBase. We activate the target element, enabling it to be further synchronized with changes in the source element (we will discuss synchronization in detail in a later section). Finally, we add it to the resource cache, and since the name feature has been synchronized, we indicate this by marking it clean.

Resolving the structured reference for the IPluginBase into a UML Artifact

We mentioned in a previous section that the process of mapping from a source element to a target element is known as adapting, while the process of mapping from the structured reference to the target element is known as resolving. Now, we will look at how resolution works.

Earlier, when we looked at the adapt() method, we learnt how we could cache elements in the MMIResourceCache. If the element is already in the resource cache, we can simply retrieve it and return it. In our plugin artifact example, this single line of code would suffice if the element was cached.

return MMIResourceCache.getCachedElement(editingDomain,
    new StructuredReferenceKey(sRef, UMLPackage.eINSTANCE.getArtifact()));

However, the element might not already be in the cache. Let's break down the steps for both the adapt() and the resolve() methods and compare them.

It's easiest to think about the process like this.

Adapt: Source Element -> Target Element (ITarget)
Resolve: StructuredReference -> Source Element -> Target Element (ITarget)

Or, more simply

Resolve: StructuredReference -> Source Element, and then run adapt process.

If we are able to derive the source domain element from the structured reference, we can run the adapt process that we learnt how to implement on the derived element.

Implementing resolve()

To continue on with our example, we will implement the resolve() method for our implementation of the IModelMappingProvider.

public EObject resolve(TransactionalEditingDomain editingDomain,
    StructuredReference sRef, EClass eClass) {
	
    String providerID = sRef.getProviderId();
    if("pde.IPluginBase".equals(providerID) &&
        UMLPackage.eINSTANCE.getArtifact().equals(eClass)) {
	
        EObject obj = MMIResourceCache.getCachedElement(editingDomain,
            new StructuredReferenceKey(sRef, eClass));
        if (obj != null) return obj;
	
        IPluginBase plugin = null;
			
        String id = sRef.getProperty(PLUGIN_ID);
        String version = sRef.getProperty(PLUGIN_VERSION);

        IPluginModelBase pluginModel =
            PDECore.getDefault().getModelManager().findPlugin(id, version,
                IMatchRules.NONE);
        if(pluginModel != null)
            plugin = pluginModel.getPluginBase();			
		
        if (plugin != null) {
            return adapt(editingDomain, plugin, eClass);
        }
        return null;			
    }
    return null;
	
}

Summary of resolve()

First, we obtained the provider ID of the structured reference using the getProviderId() method. This is a very useful check that is used commonly, and particularly in resolve() methods, when you need to distinguish between the type of structured reference. We verify the provider ID and the EClass are as expected.

Second, we try to obtain the element from the cache. If we can find it, we return it. If not, we obtain properties from the structured reference so we can uniquely dereference the source domain element from it. This is done using the getProperty() method. In our example, we just need to know the plugin ID and the plugin version. We pass in the ID and version into the findPlugin() method, which again is specific to this particular example. Now, we have obtained our source domain element and can call adapt() on it to return the ITarget EObject.

Registering an IModelMappingProvider

The XML code below registers a model mapping provider. According to the AdaptRequest element below, the model mapping provider can adapt an IPluginBase to an ITarget having an EClass of uml.Artifact. Also, the ResolveRequest element below indicates the provider can resolve structured references having an id of pde.IPluginBase into an uml.Artifact ITarget.

<extension
    id="pdeumlvizprovider"
    name="UML Visualization Provider for PDE constructs"
    point="com.ibm.xtools.mmi.core.ModelMappingProviders">
        <ModelMappingProvider
            class="com.ibm.xtools.umlviz.ui.examples.pde.internal.providers.PdeUmlVisualizationProvider">
            <Priority name="Medium"/>
            <AdaptRequest
                DomainElementType="org.eclipse.pde.core.plugin.IPluginBase"
                TargetKind="uml.Artifact"/>
            <ResolveRequest
                TargetKind="uml.Artifact"
                StructuredReferenceProviderId="pde.IPluginBase"/>
    </ModelMappingProvider>
</extension>

Using the ModelMappingService

We learnt how to implement a provider for the ModelMappingService. Now, we will look at using the service to obtain a target element for the specified source element.

Suppose we have obtained a source element, IPluginBase, in the usual manner.

IPluginBase base = null;
IPluginModelBase pluginModel =
    PDECore.getDefault().getModelManager().findPlugin("myPlugin", version,
    	IMatchRules.NONE);
if (pluginModel != null)
    base = pluginModel.getPluginBase();

Now, the following line of code will give the target element, which is a UML artifact, for the IPluginBase.

EObject artifact = ModelMappingService.getInstance().adapt(editingDomain, base,
	UMLPackage.eINSTANCE.getArtifact());

Mapping plugin dependencies

In the above example, the IPluginBase source element was mapped to a UML Artifact. The IPluginImport, which represents a dependency between two plugins, can be mapped to a UML Usage. The adapt() and resolve() methods for PDE's IPluginImport can be found in the model mapping provider implementation of com.ibm.xtools.umlviz.ui.examples.pde. In the example plugin, the provider id for structured references representing the IPluginImport is pde.IPluginImport.

Synchronizing target elements on demand

By default, an ITarget does not synchronize itself with changes to the source element automatically. It needs to be activated first using the ITarget.activate() method. The activate() method is typically invoked right after the model element has been created and its properties have been set. This is so the ITarget can handle any changes in the source element right away. This method requires an ITargetSynchronizer and a StructuredReference. The ITargetSynchronizer should be implemented to synchronize the feature of the specified ITarget.

A typical synchronizeFeature() method from the ITargetSynchronizer interface could look like this.

public boolean synchronizeFeature(EObject element, EStructuralFeature slotId,
    Object hint) {

    if (element instanceof Artifact && element instanceof ITarget) {
        Artifact artifact = (Artifact) element;
        if (slotId == UMLPackage.eINSTANCE.getNamedElement_Feature()) {
            //synchronize the ITarget here
            ...
        }
        return true;
    }
    return false;
}

First, we verify the element corresponds to the target model element we expected, and that it is a mapped object (since it implements the ITarget interface). Then, we check if we are synchronizing a feature we are interested in. Now, we need to update the ITarget as appropriate. A specific implementation has been left out, since it will depend on the domain of the object we're trying to synchronize. A typical implementation will involve obtaining the StructuredReference object from the ITarget using the getStructuredReference() method. From the structured reference, we can obtain various properties that will uniquely describe the source element. In the plugin example, we can obtain the plugin id which will uniquely describe the plugin. Since we have identified the source element, it can now be queried for its properties (for example, its dependencies). Then, the ITarget can be synchronized by adding or removing dependencies to match the source element.

Marking features as being synchronized

Earlier on, we used the setName() method to set the name of the UML Artifact and then used the setClean() method to mark the name feature of the artifact as being synchronized. This prevents synchronization from needlessly occurring when accessing the name feature. The other option is to synchronize the name feature on demand in the synchronizeFeature() method. In this case, we would implement the synchronizeFeature() method to verify if the affected slot id corresponded to the name feature, obtain the domain element, and then perform the required change to synchronize the ITarget.

public boolean synchronizeFeature(EObject element, EStructuralFeature slotId,
    Object hint) {

    if (element instanceof Artifact && element instanceof ITarget) {

        if (slotId == UMLPackage.eINSTANCE.getNamedElement_Name()) {
            //synchronize the name
            StructuredReference sRef = ((ITarget) element).getStructuredReference();
    
            Object sourceElement = StructuredReferenceService.resolveToDomainElement(editingDomain, sRef);
    
            //obtain name from source element
            ...

            EObjectUtil.setName(element, name);
        }
    }
}

Merging while synchronizing

The above example was quite straight forward, as only the new name needed to be set and the previous name could be replaced with the new name. However, what if we were modifying the dependencies of the plugin? Suppose our plugin had existing dependencies on PluginA and PluginB. If we removed the dependency on PluginB and added a new dependency on PluginC, we must ensure we kept the dependency on PluginA in addition to adding and removing the dependencies.

private boolean synchronizePluginArtifactDependency(Artifact artifact, IPluginBase plugin) {
    IPluginImport[] pluginImports = plugin.getImports();
    List allReqStructuredReferences = new Vector();

    for (int i = 0; i < pluginImports.length; i++) {
        IPluginImport pluginImport = pluginImports[i];
        if (PluginImportAdapter.getInstance().isResolvableImport(pluginImport)) {
            StructuredReference vr = StructuredReferenceService.getInstance().
                getStructuredReference(editingDomain, pluginImport);
            allReqStructuredReferences.add(vr);

            ModelMappingService.getInstance().adapt(editingDomain, pluginImport,
                UMLPackage.eINSTANCE.getUsage());

        }
    }

    EList clientDependencies = artifact.getClientDependencies();
    for(int i=0; i < clientDependencies.size(); i++) {
        Dependency clientDependency = (Dependency)clientDependencies.get(i);
        assert clientDependency instanceof ITarget;
        StructuredReference dependencyVR = ((ITarget)clientDependency).getStructuredReference();
        if(allReqStructuredReferences.contains(dependencyVR) == false) {
            DestroyElementCommand.destroy(clientDependency);
            clientDependencies.remove(clientDependency);
            --i;
        }

    }
    return true;
}

The above method is intended to be called by the synchronizeFeature() method. In our implementation of synchronizeFeature(), the source element was obtained from the provided target element using the StructuredReferenceService's resolveToDomainElement() method. We pass the returned source element, an IPluginBase, into the above synchronizePluginArtifactDependency() method.

In the first for-loop, we loop through the plugin's dependencies obtained from the actual source element. The plugin dependencies are IPluginImport objects. Using the StructuredReferenceService (or by directly consulting the appropriate IStructuredReferenceProvider), we create a structured reference that represents each valid IPluginImport. We'll use these structured references to keep track of the dependencies that are actually in the source model. Using the ModelMappingService (or by directly consulting the appropriate IModelMappingProvider), we adapt the IPluginImport since the adapt() method creates the ITarget if necessary and adds it to the MMIResourceCache.

In the second for-loop, we loop through the dependencies obtained from the target element. We check if the structured reference of the ITarget corresponds to one of the structured references representing valid plugin dependencies, which was computed in the first for-loop. If there is no correspondence, this indicates the ITarget representing the plugin import is no longer valid and must be destroyed. The destruction is performed using DestroyElementCommand.destroy().

Marking features as being unsynchronized

While setClean() marks the feature on the target element as being synchronized, setDirty() has the opposite effect. Dirty features will be synchronized the next time the dirty feature is accessed. When the dirty feature is accessed, the synchronizeFeature() method is invoked. We'll understand when we should mark features dirty as we learn more about synchronizing UML model elements in the next section.


Legal notices