This section explains how to implement mapping between elements from a source domain to elements of the UML domain.
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.
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.
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 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.
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.
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,
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); } }
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; } }
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; }
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.
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.
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.
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.
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.
Typically, an implementation of the adapt() method follows a certain path.
Here are the steps to create the artifact for the IPluginBase.
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.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; } }
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.
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.
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; }
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.
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>
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());
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.
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.
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); } } }
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().
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.