Menu

JumblEclipse

Tom Swain
Attachments
PickWorkspaceDialog.java (22525 bytes)
notification.pdf (80173 bytes)

Notes on the ESTA' Development Project

This page is an accumulation of notes on implementation of ESTA', using the Eclipse Modeling Framework (EMF).

Git Repositories

ESTA' consists of four Eclipse plugins,each with its own Git repository:

Support for Non-EMF Resources (e.g., TML and MML files)

Resource Definition and Code Generation

  1. In the Model section of the jemf.genmodel properties editor, set Resource Type to Basic.
  2. Regenerate code. This step creates EmfResourceFactoryImpl.java and EmfResourceImpl.java in package edu.utk.sqrl.emf.util.
  3. Copy the two files created in 2 and rename the copies to replace "Emf" with the desired non-EMF resource file extension - denoted by <ext> below.
  4. In <ext>ResourceFactoryImpl.java, replace all references to EmfResourceImpl with <ext>ResourceImpl.
  5. In <ext>ResourceImpl.java, override ResourceImpl.doSave(...) and ResourceImpl.doLoad(...) to serialize and deserialize a <ext> file as a ModelEMF.
  6. In plugin.xml,
    a. add the following extension point with "tml" replaced by <ext>:
    <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>
  1. In plugin.properties, add <ext> to the comma delimited list for _UI_EmfEditorFilenameExtensions. For example,
    _UI_EmfEditorFilenameExtensions = emf,tml

Saving JUMBL Objects as EMF Resources

Do the following in doSave(OutputStream output, Map options):

SomeJumblOutputFilter filter = new SomeJumblOutputFilter(output);
SomeEMF objEMF = (SomeEMF) getContents().get(0);
filter.writeObject(objEMF.getJumblObj());

Where the Data Is

JUMBL Usage Model Import

When a JUMBL usage model file (e.g., TML) is opened as an emf resource,

  1. A JUMBL Model object is read using a traditional JUMBL I/O filter.
  2. A ModelEMF is instantiated, and the ModelEMF.setModel() method is used to reference the imported Model as the ModelEMF model attribute.
  3. In addition to setting the model attribute, StateEMF, ModelReferenceEMF, and ArcEMF objects are created to wrap each State, ModelReference, and Arc, respectively, in the Model.
  4. The wrapped State, ModelReference, and Arc objects are attached to the ModelEMF according to the EMF model.

Importing Models with Model References

For each ModelReference encountered during model import,

  1. Use ModelReference.getModel() to obtain the submodel name without file extension.
  2. Search the current ResourceSet for a Resource with a matching URI.
    a. If a matching Resource is found, load the first object from its contents and set ModelReference.model to that object.
    b. If no matching Resource is found, create a new ModelEMF and set ModelReference.model to the new ModelEMF.

Notes on Addition and Removal of Children via the EMF Model Editor

  1. When StateEMF objects are added/removed, code in 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.
  2. To add/remove an ArcEMF, code in 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.

Properties

Limiting Choices on a Selection List

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:

  • Replace createItemPropertyDescriptor with new ItemPropertyDescriptor. (The argument list is unchanged.)
  • Override 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; 
                } 
    }
);
}

Single vs. Multiline Text Editors

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));

Using ChangeCommand for Complex Changes

General EMF Command Framework Benefits

  1. Undo/Redo support is built in
  2. Changed objects in the Editor are automatically marked as dirty and therefore marked as needing to be Saved.

See Sections 3 and 19 in the Eclipse Modeling Framework book for more about using the Common Command framework in general.

ChangeCommand

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:

  1. Subclass ChangeCommand as a new class to perform the changes required.
  2. 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");
    }
    
  3. Next override the ChangeCommand doExecute() method with code to perform the desired changes.

  4. 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();
        }
    }
    
  5. To use the new command, first instantiate it in the execute() method of an associated Handler object.

  6. The command should be executed via the CommandStack. The CommandStack is accessed via the EditingDomain, which is most easily accessed via the active Editor obtained from a static HandlerUtil method. For example,
    //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);
    

Notification

The attachment, notification.pdf, traces the notification chain for changing the sink or source flag in a StateEMF object.

Opening a New Editor Tab under Program Control

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) {
...
}

Opening a File in a Browser Viewer

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.");
}

Modifying <model>ModelWizard.java to Predefine the Root Object

  1. Remove the field protected Combo initialObjectField; and all statements with references to it.
  2. Modify <model>ModelWizard.<model>ModelWizardInitialObjectCreationPage.getInitialObjectName() to return the class name of the root object. For example,

    public String getInitialObjectName() {
    return "ModelEMF";
    }
    
  3. 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);

  4. Also in <model>ModelWizard.<model>ModelWizardInitialObjectCreationPage.createControl(...), create any controls needed to gather initialization data for the root object.

  5. In the 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);
    }
    ...
    

Implementing Workspace Switching

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.

  1. In the product definition (.product) file, add -data \@noDefault to the list of launcher arguments for all platforms.
  2. Subclass org.eclipse.jface.dialogs.TitleAreaDialog to create class PickWorkspaceDialog for chosing a workspace. See the attachment PickWorkspaceDialog.java for an example.
  3. 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(); 
    } 
    }
    
  4. 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();
    }
    

    }

  5. 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());
    

Adding a Progress Monitor Dialog

The following is based on this article.

  1. 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();
    }
    }
    }
    
  2. 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();
    }
    }
    
  3. 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");
    }
    

MongoDB Logo MongoDB