This page is an accumulation of notes on implementation of ESTA', using the Eclipse Modeling Framework (EMF).
ESTA' consists of four Eclipse plugins,each with its own Git repository:
<extension point="org.eclipse.emf.ecore.extension_parser">
<parser type="tml" class="edu.utk.sqrl.emf.util.TmlResourceFactoryImpl" />
</extension>
b. Add *<ext\>* to the comma-separated list of extensions in the `<editor>` element of the org.eclipse.ui.editors extension point. For example, tml was added to the following:
<extension point="org.eclipse.ui.editors">
<editor
id="edu.utk.sqrl.emf.presentation.EmfEditorID"
name="%_UI_EmfEditor_label"
icon="icons/full/obj16/EmfModelFile.gif"
extensions="emf,tml"
class="edu.utk.sqrl.emf.presentation.EmfEditor"
contributorClass="edu.utk.sqrl.emf.presentation.EmfActionBarContributor">
</editor>
</extension>
_UI_EmfEditorFilenameExtensions = emf,tml
Do the following in doSave(OutputStream output, Map options):
SomeJumblOutputFilter filter = new SomeJumblOutputFilter(output);
SomeEMF objEMF = (SomeEMF) getContents().get(0);
filter.writeObject(objEMF.getJumblObj());
When a JUMBL usage model file (e.g., TML) is opened as an emf resource,
ModelEMF.setModel() method is used to reference the imported Model as the ModelEMF model attribute.model attribute, StateEMF, ModelReferenceEMF, and ArcEMF objects are created to wrap each State, ModelReference, and Arc, respectively, in the Model.For each ModelReference encountered during model import,
ModelReference.getModel() to obtain the submodel name without file extension.ModelReference.model to that object.ModelReference.model to the new ModelEMF.ModelEMFItemProvider.notifyChanged() updates the Node Map in the JUMBL Model wrapped by the ModelEMF. If a StateEMF is being added, this code creates the JUMBL State and sets it as the State wrapped by the new StateEMF.NodeEMFItemProvider.notifyChanged() updates the list of outgoing Arcs in the JUMBL Node wrapped by the StateEMF or ModelReferenceEMF. If an ArcEMF is being added, this code creates the JUMBL Arc and sets it as the Arc wrapped by the new ArcEMF. When adding an ArcEMF, the target NodeEMF is initialized to the source NodeEMF to avoid a null pointer exception in the JUMBL Arc constructor.To define or limit the choices for a property whose value comes from other objects in the EMF model, modify the addpropPropertyDescriptor() method in objItemProvider, where the class obj owns prop, the property of interest. Modify the EMF-generated code as follows:
createItemPropertyDescriptor with new ItemPropertyDescriptor. (The argument list is unchanged.)ItemPropertyDescriptor.getChoiceOfValues() to return the Collection<?> of allowed values for the property.The example below limits the choice of default out arcs to only those arcs that originate from the current node.
protected void addDefaultOutArcPropertyDescriptor(Object object) {
itemPropertyDescriptors.add
(new ItemPropertyDescriptor
//createItemPropertyDescriptor (generated code replaced)
(((ComposeableAdapterFactory)adapterFactory).getRootAdapterFactory(),
getResourceLocator(),
getString("_UI_ModelReferenceEMF_defaultOutArc_feature"),
getString("_UI_PropertyDescriptor_description", "_UI_ModelReferenceEMF_defaultOutArc_feature", "_UI_ModelReferenceEMF_type"),
EmfPackage.Literals.MODEL_REFERENCE_EMF__DEFAULT_OUT_ARC,
true,
false,
true,
null,
null,
null)
// added to limit choices to arcs associated with this ModelReference
{
public Collection<ArcEMF> getChoiceOfValues(Object object)
{
ModelReferenceEMF selModelRef = (ModelReferenceEMF)object;
List<ArcEMF> result = new ArrayList<ArcEMF>();
result.add(null);
result.addAll(selModelRef.getOutArcs());
return result;
}
}
);
}
A single line text box is generated as the default cell editor for properties of type String. To change the cell editor to a popup with a multiline text editor, modify the generated addpropPropertyDescriptor() method in objItemProvider, where the class <obj> owns <prop>, the property of interest. Change the multiline argument of createItemPropertyDescriptor(...) to true. See below.
protected void addTextPropertyDescriptor(Object object) {
itemPropertyDescriptors.add
(createItemPropertyDescriptor
(((ComposeableAdapterFactory)adapterFactory).getRootAdapterFactory(),
getResourceLocator(),
getString("_UI_LabelEMF_text_feature"),
getString("_UI_PropertyDescriptor_description", "_UI_LabelEMF_text_feature", "_UI_LabelEMF_type"),
EmfPackage.Literals.LABEL_EMF__TEXT,
true,
true, // changed to true to use multiline text editor
false,
ItemPropertyDescriptor.GENERIC_VALUE_IMAGE,
null,
null));
See Sections 3 and 19 in the Eclipse Modeling Framework book for more about using the Common Command framework in general.
Most built-in Command subclasses are intended for atomic changes, such as Set, Add, Remove, etc. These can be combined to enforce simple constraints or dependencies using the CompoundCommand subclass. However for more complex or batch changes, the ChangeCommand subclass is easier to use. It employs a change recording mechanism to support Undo/Redo and avoids the need to build a list of the atomic changes. Using ChangeCommand involves the following:
The subclass constructor should look something like the example below. The key elements are using the ChangeCommand constructor with signature, ChangeCommand(ChangeRecorder cr, Notifier nt) and setting the description and label attributes so they will make sense to the user on menus, etc.
public RecordResultsCommand(TestRecordEMF testRecord) {
super(new ChangeRecorder(), testRecord);
this.testRecord = testRecord;
setDescription("Record Test Results");
setLabel("Record Results");
}
Next override the ChangeCommand doExecute() method with code to perform the desired changes.
The ChangeCommand base class automatically handles Undo/Redo of any changes to EObjects. If the EObject changes have non-EObject side effects, it may be necessary to override undo() and redo() to reverse those side effects. For example,
public void undo() {
super.undo();
if (testRecord != null) {
testRecord.refreshJumblTestRecord();
}
}
To use the new command, first instantiate it in the execute() method of an associated Handler object.
//create the command and get the editing domain
RecordResultsCommand recResultsCmd = new RecordResultsCommand(tr);
TestemfEditor editor = (TestemfEditor) HandlerUtil.getActiveEditor(event);
EditingDomain editingDomain = editor.getEditingDomain();
...
editingDomain.getCommandStack().execute(recResultsCmd);
The attachment, notification.pdf, traces the notification chain for changing the sink or source flag in a StateEMF object.
This may not be the best approach or even the Eclipse way, but it works.
URI uri = ... // get the file URI somehow
IFileStore fileStore = EFS.getLocalFileSystem().getStore(new Path(uri.path()));
IWorkbenchPage page = HandlerUtil.getActiveWorkbenchWindow(event).getActivePage();
try {
IDE.openEditorOnFileStore(page, fileStore);
} catch (PartInitException e) {
...
}
Here is a code snippet that will open a file in a browser viewer. The class edu.utk.sqrl.emf.presentation.BrowserView extends org.eclipse.ui.part.ViewPart and is a wrapper for org.eclipse.swt.browser.Browser.
URI uri = ...
String url = "file:///" + uri.device()+ uri.path();
BrowserView browser = (BrowserView) page.findView(BrowserView.ID);
if (browser != null) {
browser.setFocus();
browser.setURL(url);
page.bringToTop(browser);
} else {
MessageDialog.openInformation(null, "Browser View Unavailable",
"Browser view is unavailable. You can open " + traURI.path()+ "in the OS native browser.");
}
protected Combo initialObjectField; and all statements with references to it.Modify <model>ModelWizard.<model>ModelWizardInitialObjectCreationPage.getInitialObjectName() to return the class name of the root object. For example,
public String getInitialObjectName() {
return "ModelEMF";
}
In <model>ModelWizard.<model>ModelWizardInitialObjectCreationPage.createControl(...), remove the following block of code:
Label containerLabel = new Label(composite, SWT.LEFT);
{
...
}
initialObjectField = new Combo(composite, SWT.BORDER);
{
...
}
...
initialObjectField.addModifyListener(validator);
Also in <model>ModelWizard.<model>ModelWizardInitialObjectCreationPage.createControl(...), create any controls needed to gather initialization data for the root object.
run(...) method of the anonymous IRunnableWithProgress class created in <model>ModelWizard.performFinish(...), add any additional initialization code needed for the root object after its creation. For example,...
// Add the initial model object to the contents.
//
EObject rootObject = createInitialModel();
if (rootObject != null) {
//*** set the name of the ModelEMF to the filename specified without extension
((ModelEMF)rootObject).setName(fileURI.trimFileExtension().lastSegment());
resource.getContents().add(rootObject);
}
...
The following describes implementation of the ability to specify the workspace root path at startup and switch it at any time. This implementation is based on this article with the additional requirement that the user.home property be the same as the workspace root path.
Add the following inner class to the <model>EditorAdvisor class:
public static class ActionSwitchWorkspace extends Action {
public ActionSwitchWorkspace() {
super("Switch Workspace");
}
@Override
public void run() {
PickWorkspaceDialog pwd = new PickWorkspaceDialog(true, null);
int pick = pwd.open();
if (pick == Dialog.CANCEL){
return;
}
MessageDialog.openInformation(Display.getDefault().getActiveShell(), "Switch Workspace", "The client will now restart with the new workspace");
// restart client
PlatformUI.getWorkbench().restart();
}
}
Modify <model>EditorAdvisor.Application.start(...) as shown below:
public Object start(IApplicationContext context) throws Exception {
//*** Delete this line when adding workspace switching
//WorkbenchAdvisor workbenchAdvisor = new TestemfEditorAdvisor();
//*** added for workspace switching
// get the initial workspace location and home directory for debug purposes
Location instanceLoc = Platform.getInstanceLocation();
String homeDir = System.getProperty("user.home");
//***
Display display = PlatformUI.createDisplay();
try {
//*** added for workspace switching
// get what the user last said about remembering the workspace location
boolean remember = PickWorkspaceDialog.isRememberWorkspace();
// get the last used workspace location
String lastUsedWs = PickWorkspaceDialog.getLastSetWorkspaceDirectory();
// if we have a "remember" but no last used workspace, it's not much to remember
if (remember && (lastUsedWs == null || lastUsedWs.length() == 0)) {
remember = false;
}
// check to ensure the workspace location is still OK
if (remember) {
// if there's any problem whatsoever with the workspace, force a dialog which in its turn will tell them what's bad
String ret = PickWorkspaceDialog.checkWorkspaceDirectory(Display.getDefault().getActiveShell(), lastUsedWs, false, false);
if (ret != null) {
remember = false;
}
}
// if we didn't remember the workspace, show the dialog
if (!remember) {
PickWorkspaceDialog pwd = new PickWorkspaceDialog(false, null);
int pick = pwd.open();
// if the user cancelled, we can't do anything as we need a workspace, so in this case, we tell them and exit
if (pick == Window.CANCEL) {
if (pwd.getSelectedWorkspaceLocation() == null) {
MessageDialog.openError(display.getActiveShell(), "Error",
"The application can not start without a workspace root and will now exit.");
try {
PlatformUI.getWorkbench().close();
} catch (Exception err) {
}
System.exit(0);
return IApplication.EXIT_OK;
}
}
else {
// tell Eclipse what the selected location was and continue
String wsLoc = pwd.getSelectedWorkspaceLocation();
instanceLoc.set(new URL("file", null, wsLoc), false);
System.setProperty("user.home", wsLoc);
}
}
else {
// set the last used location and continue
instanceLoc.set(new URL("file", null, lastUsedWs), false);
}
WorkbenchAdvisor workbenchAdvisor = new TestemfEditorAdvisor();
// *** end of workspace switching mods
int returnCode = PlatformUI.createAndRunWorkbench(display, workbenchAdvisor);
if (returnCode == PlatformUI.RETURN_RESTART) {
return IApplication.EXIT_RESTART;
}
else {
return IApplication.EXIT_OK;
}
}
finally {
display.dispose();
}
}
Add the following lines to <model>EditorAdvisor.WindowActionBarAdvisor.createFileMenu(...) to add a Switch Workspace item to the File menu:
addToMenuAndRegister(menu, new ActionSwitchWorkspace());
menu.add(new Separator());
The following is based on this article.
Create a (possibly inner) class with basic elements shown below, where numberRepresenting100percentCompletion = numberRepresentingThisPortionOfProcess + numberRepresentingTheRestOfProcess.
public class DoSomethingWithProgress implements IRunnableWithProgress {
//*** add any private fields needed here
/** ... */
public DoSomethingWithProgress(/* application-specific arguments */) {
/* application-specific assignments, perhaps to private fields */
}
@Override
public void run(IProgressMonitor monitor)
throws InvocationTargetException, InterruptedException {
try {
monitor.beginTask("Doing Something", numberRepresenting100percentCompletion);
doSomeWork(/* app-specific args */, new SubProgressMonitor(monitor, numberRepresentingThisPortionOfProcess));
doSomeMoreWork(/* app-specific args */, new SubProgressMonitor(monitor, numberRepresentingTheRestOfProcess));
} catch (OperationCanceledException e) {
MessageDialog.openInformation(null, "Process Cancelation", "Process canceled. " + e.getMessage());
} finally {
monitor.done();
}
}
}
The processing functions (e.g, doSomeWork(...) and doSomeMoreWork(...) above) should implement an idiom similar to the following:
private void doSomeWork(/* app-specific args */, IProgressMonitor monitor){
...
try {
if (monitor != null) {
monitor.beginTask("Generating Random Test Cases for " + model.getName() + ".", number);
}
for(int i = 0; i < numberOfItemsToProcess; i++) {
/* process item i */
if (monitor != null) {
if(monitor.isCanceled()){
throw new OperationCanceledException("User canceled process after item " + (i + 1));
}
monitor.worked(numberRepresentingThisPortionOfProcess / numberOfItemsToProcess);
}
}
} finally {
monitor.done();
}
}
Use the following idiom where you would normally call doSomeWork(...) if not using a progress monitor:
try {
IRunnableWithProgress op = new DoSomethingWithProgress(/* app-specific args */);
new ProgressMonitorDialog(HandlerUtil.getActiveShell(event)).run(true, true, op);
} catch (InvocationTargetException e) {
// handle exception
MessageDialog.openInformation(null, "Process Invocation Failed", e.getMessage());
return null /* or something appropriate */;
} catch (InterruptedException e) {
// handle cancelation
MessageDialog.openInformation(null, "Process Canceled", "Process canceled");
}