During the last year, I've been working on a project developing a transformation between UML models and custom XML. The platform of choice was IBM Rational Software Architect (RSA) which provides very good support for creating model to model transformations. Although there is a pretty a good tutorial in the RSA Help, there are quite a few things that are tricky. Thus I thought that sharing some of the experience I made might help others facing similar challenges. So let's get down to business, here's what I'll cover in the blog:
- Getting setup and getting help
- Tips for structuring model-to-model mapping projects
- How to write custom mapping rules
- Pattern for managing the transformation execution lifecycle
- Aborting an executing transformation
- Pattern for reporting transformation problems
- Model markers and decorators
Before you get started on extending RSA, you should make sure to install the extensibility components. Tricky enough, this does not get installed by default, so fire up IBM Installation Manager, hit Modify and make sure to check the "Extensibility" feature. When this is done, you'll have access to "everything" you need to extend this already incredibly capable tool. One funny(?) quirk is that most of the RSA help is available online from IBM's InfoCenters for the various flavors of RSA. However, the help for the extensibility - including APIs, programmers reference etc is only available from the local installed help. I would suspect that this is due to the on-line help being generated from the features installed by default...
No matter what, it is convenient to use the on-line help for everything else. So here's a couple of links:
Rational Software Architect v8 on-line help
Rational Software Architect RealTime Edition v8 on-line help
Now that you have the help figured out, start out by working through the tutorial on model to model mapping transformations. This will save you a lot of time. Also, when working with the mapping model editor, make sure that you have the Outline view open - otherwise you'll have a tough time navigating through your mapping model...
2. Tips for structuring model-to-model mapping projects
The source the for the model to model mapping project is the mapping model. Typically, this is kept in a "model" folder in your model-to-model mapping project. From it, you then generate the transformation source code. In addition to this, you will typically need to write custom code. Here's where the structuring comes in handy - using a simple but clever Java package scheme, you can easily separate custom (handwritten) code from the generated code. This will allow you to simply delete all generated code to ensure that it is completely regenerated. Typical cases when this is useful, is when you remove map levels in the mapping model. Here's an example of a package structure:
In this example, you can safely delete all classes in the mymappingdemo.transforms package, except... Yes there is an exception. If you have modified the generated code, you will need to be careful and make sure to preserve any modified classes. In this case, you must avoid deleting any classes containing "@generated NOT" tags.
3. How to write custom mapping rules
There's two ways of writing custom rules for mapping a model element from the input model to the output model:
4. Pattern for managing the transformation execution lifecycle
In many cases it is useful to be able to perform operations before and after the actual transformation. In order to do this I came up with the following approach:
In the generated transformation code, there's a class MainTransform in which you can modify the constructor:
Here I've added a call to register my "LifeCycleManager", a class which allows operations for controlling the lifecycle of "things". "Things" can be various caches etc which needs to be cleared before invoking the transformation.
The key reason for introducing this type of "life cycle management" was when I realized that the transformation is only instantiated once - it is a plugin! This if you depend on things being performed at invokation or at the end of a transformation, you need a way to trigger them.
Here's how my LifeCycleManager looks like:
5. Aborting an executing transformation
If we for some reason wants to abort the execution of a transformation, we can add a method cancelTransform() to the class LifecycleManager, which has access to the transformation context and can cancel it. This can be very useful if you detect problems during the execution of a transformation. To implement this we need to modify the class LifecycleManager. First we need to add a method to support cancellation:
Unfortunately this will warn you about access restrictions, so you may want to suppress them:
Now you are ready to cancel the execution when you've detected a problem with:
6. Pattern for reporting transformation problems
Typically, you're transformation will detect various problems when processing the input and generating the output. This can be of the typical severities info, warning and error and thus it fits right into creating problem markers. More on that in the next sections.
However, we need to deal with the fact that transformation may often be executed in headless mode and thus we do not have access to the marker framework. For this, we can use a trick described by Chris Aniszczyk (ref #1):
Next we wrap this up so that we can report problems without having to think about whether we're headless or not ;-)
Here's a sniplet of code that just uses regular "syserr" to print out a message in headless mode, while it calls the problem marker utility when the UI is available:
7. Model markers and decorators
In order to give the user feedback on problems detected during the transformation, we can use the IMarker framework. There's a good starting point in the article on "Mark My Words" (ref #2). However, a short entry here will not give this justice, so I'll followup with a separate post on the topic.
Happy transforming!
No matter what, it is convenient to use the on-line help for everything else. So here's a couple of links:
Rational Software Architect v8 on-line help
Rational Software Architect RealTime Edition v8 on-line help
Now that you have the help figured out, start out by working through the tutorial on model to model mapping transformations. This will save you a lot of time. Also, when working with the mapping model editor, make sure that you have the Outline view open - otherwise you'll have a tough time navigating through your mapping model...
2. Tips for structuring model-to-model mapping projects
The source the for the model to model mapping project is the mapping model. Typically, this is kept in a "model" folder in your model-to-model mapping project. From it, you then generate the transformation source code. In addition to this, you will typically need to write custom code. Here's where the structuring comes in handy - using a simple but clever Java package scheme, you can easily separate custom (handwritten) code from the generated code. This will allow you to simply delete all generated code to ensure that it is completely regenerated. Typical cases when this is useful, is when you remove map levels in the mapping model. Here's an example of a package structure:
mymappingdemo - Core classes for the transformation plug-in
mymappingdemo.custom - Here's your handwritten custom code
mymappingdemo.conditions - You may want to keep your conditions separate
mymappingdemo.l10n - Generated message strings
mymappingdemo.transforms - Generated transformation source code
In this example, you can safely delete all classes in the mymappingdemo.transforms package, except... Yes there is an exception. If you have modified the generated code, you will need to be careful and make sure to preserve any modified classes. In this case, you must avoid deleting any classes containing "@generated NOT" tags.
3. How to write custom mapping rules
There's two ways of writing custom rules for mapping a model element from the input model to the output model:
- Using the inline editor
- Creating a custom rule in an external Java class
4. Pattern for managing the transformation execution lifecycle
In many cases it is useful to be able to perform operations before and after the actual transformation. In order to do this I came up with the following approach:
In the generated transformation code, there's a class MainTransform in which you can modify the constructor:
public MainTransform() {
super(TRANSFORM, Umlrt2ifmMessages.mainTransform);
LifecycleManager.register();
...
}
Here I've added a call to register my "LifeCycleManager", a class which allows operations for controlling the lifecycle of "things". "Things" can be various caches etc which needs to be cleared before invoking the transformation.
The key reason for introducing this type of "life cycle management" was when I realized that the transformation is only instantiated once - it is a plugin! This if you depend on things being performed at invokation or at the end of a transformation, you need a way to trigger them.
Here's how my LifeCycleManager looks like:
package mymappingdemo.custom;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import com.ibm.xtools.transform.core.IRunTransformationListener;
import com.ibm.xtools.transform.core.ITransformContext;
import com.ibm.xtools.transform.core.RunTransformationEventManager;
import com.ibm.xtools.transform.core.extension.AbstractTransformExtensionHelper;
import com.ibm.xtools.transform.core.internal.engine.TransformRunHelper;
/**
* Implements lifecycle management for the transformation<br>
* The transformation is instantiated once per RSA session, thus classes that
* persist objects which should not be cached/reused between invocations of
* the transformation must be reset (cleared). This can be done by invoking
* reset code in the {@link #onTransformationStart(ITransformContext)} method.
*
*/
public class LifecycleManager extends AbstractTransformExtensionHelper implements IRunTransformationListener {
// singleton
private static LifecycleManager instance;
private ITransformContext transformContext;
private LifecycleManager() {
}
public static LifecycleManager getDefault() {
if (instance == null) {
instance = new LifecycleManager();
RunTransformationEventManager.getInstance().addListener(instance);
}
return instance;
}
@Override
public void onTransformationStart(ITransformContext itransformcontext) {
this.transformContext = itransformcontext;
//TODO: add things to perform at the start of a transformation here:
}
@Override
public void onTransformationEnd(ITransformContext itransformcontext, IStatus istatus) {
this.transformContext = null; // nothing to cancel, transf. done
//TODO: add things to perform at the end of a transformation here:
}
/**
* Invoke this method to register the manager as a listener
* with the transformation framework
*
*/
public static void register() {
getDefault();
}
}
5. Aborting an executing transformation
If we for some reason wants to abort the execution of a transformation, we can add a method cancelTransform() to the class LifecycleManager, which has access to the transformation context and can cancel it. This can be very useful if you detect problems during the execution of a transformation. To implement this we need to modify the class LifecycleManager. First we need to add a method to support cancellation:
/**
* Cancels the transformation execution
*
*/
public void cancelTransform() {
if (this.transformContext == null) {
return; // nothing to cancel
}
IProgressMonitor monitor =
TransformRunHelper.getProgressMonitor(this.transformContext);
monitor.setCanceled(true);
}
Unfortunately this will warn you about access restrictions, so you may want to suppress them:
@SuppressWarnings("restriction") // Necessary because of: {@link LifecycleManager#cancelTransform()}
public class LifecycleManager extends AbstractTransformExtensionHelper implements IRunTransformationListener {
...
Now you are ready to cancel the execution when you've detected a problem with:
LifecycleManager.getDefault().cancelTransform();
6. Pattern for reporting transformation problems
Typically, you're transformation will detect various problems when processing the input and generating the output. This can be of the typical severities info, warning and error and thus it fits right into creating problem markers. More on that in the next sections.
However, we need to deal with the fact that transformation may often be executed in headless mode and thus we do not have access to the marker framework. For this, we can use a trick described by Chris Aniszczyk (ref #1):
/** * Detects whether running headless or not * @return */ private static boolean isHeadless() { // source: http://aniszczyk.org/2007/07/24/am-i-headless/ boolean headless = false; Bundle b = Platform.getBundle("org.eclipse.ui"); if (b==null || b.getState() != Bundle.ACTIVE) { headless = true; } return headless; }
Next we wrap this up so that we can report problems without having to think about whether we're headless or not ;-)
Here's a sniplet of code that just uses regular "syserr" to print out a message in headless mode, while it calls the problem marker utility when the UI is available:
public class ProblemReporter { private static boolean isHeadless() {} ... return headless; } /** * Reports an error message for the given model element<br> * Prints to stderr in headless mode, else logs to the platform log * * @param modelElement * @param message */ public static void reportError(NamedElement modelElement, String message) { String location = modelElement.getQualifiedName(); if (isHeadless()) { System.err.println("ERROR: location=" + location + ", message=" + message); } else { TransformationMarkerHelper.getDefault() .createErrorMarker(modelElement, message); } }
In order to give the user feedback on problems detected during the transformation, we can use the IMarker framework. There's a good starting point in the article on "Mark My Words" (ref #2). However, a short entry here will not give this justice, so I'll followup with a separate post on the topic.
Happy transforming!
References: