Follow Techotopia on Twitter

On-line Guides
All Guides
eBook Store
iOS / Android
Linux for Beginners
Office Productivity
Linux Installation
Linux Security
Linux Utilities
Linux Virtualization
Linux Kernel
System/Network Admin
Programming
Scripting Languages
Development Tools
Web Development
GUI Toolkits/Desktop
Databases
Mail Systems
openSolaris
Eclipse Documentation
Techotopia.com
Virtuatopia.com

How To Guides
Virtualization
General System Admin
Linux Security
Linux Filesystems
Web Servers
Graphics & Desktop
PC Hardware
Windows
Problem Solutions
Privacy Policy

  




 

 

Eclipse EMF Model Transaction Development Guide
Previous Page Home Next Page

Tutorial: EMF Model Transaction Workspace Integration

Contents

Overview

The EMF Model Transaction workspace integration allows one to integrate the use of the transaction layer with the eclipse undoable operations framework. This will help the transaction layer to coexist with other eclipse UI components and share operations that modify transactional editing domains as well as other objects.

[ back to top]

References

This tutorial assumes that the reader has a knowledge of EMF, and the eclipse PDE development environment. It is essential that the reader understands the eclipse undoable operations framework. It assumes that the reader has already read the transaction tutorial and understood those concepts.

For reference, the full example for this tutorial is available.

[ back to top]

Introduction

In order to demonstrate transaction workspace integration we will be making use of the library metamodel. This metamodel is a variant of the standard EMF example metamodel used in many of its tutorials.

The goal of this tutorial is to create a reflective editor that manages a single resource within a transactional editing domain. Whenever a resource is loaded in the editing domain a new editor is opened. When an instance of the editor is opened, it finds its editing domain and loads the resource. Each editor will have its own undo/redo actions that reflect only the changes made to its resource.

Our editor will be extending the EXTLibrary editor generated for that metamodel. Much of the UI work has been done and needs only to be adapted to use the undoable operations framework and transactional editing domains.

[ back to top]

Registering the Editing Domain

As a first step, we will declare a transactional editing domain that will be shared among all of the editor instances. Whenever an editor opens, it will retrieve this editing domain to load and manage its resource.

  <extension
        point="org.eclipse.emf.transaction.editingDomains">
     <editingDomain
           factory="org.eclipse.emf.workspace.examples.extlibrary.domain.EXTLibraryEditingDomainFactory"
           id="org.eclipse.emf.workspace.examples.LibraryEditingDomain"/>
  </extension>

The editing domain factory is slightly different than the one outlined in the transaction tutorial. Instead, it will be constructing a special workspace integrated transactional editing domain. A special exception handler is registered that will present a dialog to the user when errors occur within transactions (e.g. roll backs due to validation failure).

public class EXTLibraryEditingDomainFactory implements TransactionalEditingDomain.Factory {
   public TransactionalEditingDomain createEditingDomain() {
      // create an editing domain with a default resource set implementation
      //    and delegating command execution to the default (workbench)
      //    operation history
      TransactionalEditingDomain result = WorkspaceEditingDomainFactory.INSTANCE.createEditingDomain();
      
      // add an exception handler to the editing domain's command stack
      ((TransactionalCommandStack) result.getCommandStack()).setExceptionHandler(
            new CommandStackExceptionHandler());
      
      return result;
   }

   public TransactionalEditingDomain createEditingDomain(ResourceSet rset) {
      // not used when initializing editing domain from extension point
      return null;
   }

   public TransactionalEditingDomain getEditingDomain(ResourceSet rset) {
      // not used when initializing editing domain from extension point
      return null;
   }
}

[ back to top]

Registering a Resource Load Listener

We want to have our special editor opened whenever a new resource is loaded. Resources may load explicity or implicitly through proxy resolution. The listener should be registered against the listeners extension point so that it will be automatically installed on the editing domain whenever it is constructed.

  <extension
        point="org.eclipse.emf.transaction.listeners">
     <listener class="org.eclipse.emf.workspace.examples.extlibrary.presentation.ResourceLoadedListener">
        <editingDomain id="org.eclipse.emf.workspace.examples.LibraryEditingDomain"/>
     </listener>
  </extension>  

If a resource has been loaded the listener finds the IFile for the resource's URI and requests the workbench to open an editor for that file. Otherwise, if the resource has been unloaded it finds any open editor instance for that file and closes it.

public class ResourceLoadedListener extends DemultiplexingListener {

...
   protected void handleNotification(TransactionalEditingDomain domain, Notification notification) {
      if (ignoredResources.contains(notification.getNotifier())) {
         // skip any resource that we are supposed to ignore
         return;
      }
      
      if (notification.getNewBooleanValue() && !notification.getOldBooleanValue()) {
         // a resource has been loaded that was not loaded before.  Open an editor
         final IFile file = WorkspaceSynchronizer.getFile(
               (Resource) notification.getNotifier());
         
         if (file != null) {
            Display.getDefault().asyncExec(new Runnable() {
               public void run() {
                  try {
                     IWorkbenchPage page = getActivePage();
                     
                     if (page != null) {
                        IEditorPart activeEditor = page.getActiveEditor();
                        
                        page.openEditor(
                              new FileEditorInput(file),
                              "org.eclipse.emf.workspace.examples.extlibrary.presentation.EXTLibraryEditorID",
                              false);
                        
                        // restore the previously active editor to active
                        //    state
                        if (activeEditor != null) {
                           page.activate(activeEditor);
                        }
                     }
                  } catch (PartInitException e) {
                     EXTLibraryEditorPlugin.getPlugin().log(e.getStatus());
                  }
               }});
         }
      } else if (!notification.getNewBooleanValue() && notification.getOldBooleanValue()) {
         // a resource has been unloaded that was  loaded before.  Close
         //    the editor, if any
         final IFile file = WorkspaceSynchronizer.getFile(
               (Resource) notification.getNotifier());
         
         if (file != null) {
            Display.getDefault().asyncExec(new Runnable() {
               public void run() {
                  IWorkbenchPage page = getActivePage();
                  
                  if (page != null) {
                     IEditorReference[] editors = page.findEditors(
                           new FileEditorInput(file),
                           "org.eclipse.emf.workspace.examples.extlibrary.presentation.EXTLibraryEditorID",
                           IWorkbenchPage.MATCH_ID | IWorkbenchPage.MATCH_INPUT);
                     
                     page.closeEditors(editors, false);
                  }
               }});
         }
      }
   }   
...
}

[ back to top]

Synchronizing with Workspace Changes

The user could make changes to the files in their workspace and we must keep up-to-date with those changes if they affect our loaded resources. A workspace synchronizer is used to force the closing of editors whose resources have been deleted, and the reloading of resources that have changed.

public void init(IEditorSite site, IEditorInput editorInput) {
   setSite(site);
   setInputWithNotify(editorInput);
   setPartName(editorInput.getName());
   site.setSelectionProvider(this);
   site.getPage().addPartListener(partListener);
   
   workspaceSynchronizer = new WorkspaceSynchronizer(
         (TransactionalEditingDomain) editingDomain,
         createSynchronizationDelegate());
}

...

private WorkspaceSynchronizer.Delegate createSynchronizationDelegate() {
   return new WorkspaceSynchronizer.Delegate() {
      public boolean handleResourceDeleted(Resource resource) {
         if ((resource == getResource()) && !isDirty()) {
            // just close now without prompt
            getSite().getShell().getDisplay().asyncExec
               (new Runnable() {
                   public void run() {
                      getSite().getPage().closeEditor(EXTLibraryEditor.this, false);
                      EXTLibraryEditor.this.dispose();
                   }
                });
         } else {
            removedResources.add(resource);
         }
         
         return true;
      }
      
      public boolean handleResourceChanged(Resource resource) {
         changedResources.add(resource);
         
         return true;
      }
      
      public boolean handleResourceMoved(Resource resource, URI newURI) {
         movedResources.put(resource, newURI);
         
         return true;
      }
      
      public void dispose() {
         removedResources.clear();
         changedResources.clear();
         movedResources.clear();
      }};
}

[ back to top]

Every Read and Write Must be in a Transaction

Because of the multi-threaded nature of transactional editing domains, every read and write must be performed in a transaction. EMF has already made use of a command stack to allow it to manage undo and redo of changes. If changes could be made using the reflective editor that weren't implemented in terms of an EMF command that was executed on their command stack then they would not have been able to ensure the sanity of their undo/redo actions. It is therefore reasonable to assume that all changes that can be made using the reflective editor will be executed as EMF commands on the command stack.

Our editing domain factory produces transactional editing domains with a special command stack that will first wrap each EMF command into an EMFCommandOperation on the operation history. It opens a write transaction during the course of the command execution. Writing should be trivially solved due to the transactional editing domain's command stack implementation.

Reading is a more complicated problem because when the reflective editor reads information from the editing domain, it does not assume that any particular command or method call has to be made to allow the read. We are forced to override most of the cases where reading is done by the editor.

First there is the main tree viewer for the editor. It updates its content and labels by reading data from the editing domain. It is overrided so that it uses a transactional adapter factory:

public void createPartControl(Composite parent) {
	// Creates the model from the editor input
	//
	createModel();

	Tree tree = new Tree(parent, SWT.MULTI);
	selectionViewer = new TreeViewer(tree);

	selectionViewer.setContentProvider(
			new TransactionalAdapterFactoryContentProvider(
				(TransactionalEditingDomain) getEditingDomain(), adapterFactory));

	selectionViewer.setLabelProvider(new TransactionalAdapterFactoryLabelProvider(
			(TransactionalEditingDomain) getEditingDomain(), adapterFactory));
	
	// unlike other EMF editors, I edit only a single resource, not a resource set
	selectionViewer.setInput(getResource());

	new AdapterFactoryTreeEditor(selectionViewer.getTree(), adapterFactory);

	createContextMenuFor(selectionViewer);
}

There is also the property sheet that needs to update its contents by performing read operations on the editing domain.

public IPropertySheetPage getPropertySheetPage() {
   if (propertySheetPage == null) {
      propertySheetPage =
         new ExtendedPropertySheetPage(editingDomain) {
            public void setSelectionToViewer(List selection) {
               EXTLibraryEditor.this.setSelectionToViewer(selection);
               EXTLibraryEditor.this.setFocus();
            }

            public void setActionBars(IActionBars actionBars) {
               super.setActionBars(actionBars);
               getActionBarContributor().shareGlobalActions(this, actionBars);
            }
         };
      propertySheetPage.setPropertySourceProvider(
            new TransactionalAdapterFactoryContentProvider(
               (TransactionalEditingDomain) getEditingDomain(), adapterFactory));
   }

   return propertySheetPage;
}

Saving and loading a resource requires appropriate transactions.

public void doSave(IProgressMonitor progressMonitor) {
   WorkspaceModifyOperation operation =
      new WorkspaceModifyOperation() {
         // This is the method that gets invoked when the operation runs.
         //
         public void execute(IProgressMonitor monitor) {
            try {
               ((TransactionalEditingDomain) getEditingDomain())
                  .runExclusive(new Runnable() {
                  public void run() {
                     try {
                        // Save the resource to the file system.
                        //
                        Resource savedResource = getResource();
                        savedResources.add(savedResource);
                        savedResource.save(Collections.EMPTY_MAP);
                     }
                     catch (Exception exception) {
                        EXTLibraryEditorPlugin.INSTANCE.log(exception);
                     }
                  }});
            }
            catch (Exception exception) {
               EXTLibraryEditorPlugin.INSTANCE.log(exception);
            }
         }
      };
   ...
}

[ back to top]

Managing the Undo Context

We would like each editor to maintain its own undo context. From that context it will be determined which operations should show up on our editor's undo/redo menu. Basically, we want to have only those operations that affect the editor's resource have our context applied to them. We do this by creating an operation history listener:

public EXTLibraryEditor() {
   super();

   // Create an adapter factory that yields item providers.
   //
   List factories = new ArrayList();
   factories.add(new ResourceItemProviderAdapterFactory());
   factories.add(new EXTLibraryItemProviderAdapterFactory());
   factories.add(new ReflectiveItemProviderAdapterFactory());

   adapterFactory = new ComposedAdapterFactory(factories);

   // Get the registered workbench editing domain.
   //
   editingDomain = (AdapterFactoryEditingDomain) 
         TransactionalEditingDomain.Registry.INSTANCE.getEditingDomain(
            "org.eclipse.emf.workspace.examples.LibraryEditingDomain");
   undoContext = new ObjectUndoContext(
         this,
         EXTLibraryEditorPlugin.getPlugin()
            .getString("_UI_EXTLibraryEditor_label"));
   getOperationHistory().addOperationHistoryListener(historyListener);
}

private IOperationHistoryListener historyListener = new IOperationHistoryListener() {
   public void historyNotification(final OperationHistoryEvent event) {
      if (event.getEventType() == OperationHistoryEvent.DONE) {
         Set affectedResources = ResourceUndoContext.getAffectedResources(
               event.getOperation());
         
         if (affectedResources.contains(getResource())) {
            final IUndoableOperation operation = event.getOperation();
            
            // remove the default undo context so that we can have
            //     independent undo/redo of independent resource changes
            operation.removeContext(((IWorkspaceCommandStack)
                  getEditingDomain().getCommandStack()).getDefaultUndoContext());
            
            // add our undo context to populate our undo menu
            operation.addContext(getUndoContext());
            
            getSite().getShell().getDisplay().asyncExec(new Runnable() {
               public void run() {
                  dirty = true;
                  firePropertyChange(IEditorPart.PROP_DIRTY);

                  // Try to select the affected objects.
                  //
                  if (operation instanceof EMFCommandOperation) {
                     Command command = ((EMFCommandOperation) operation).getCommand();
                     
                     if (command != null) {
                        setSelectionToViewer(command
                              .getAffectedObjects());
                     }
                  }
                  
                  if (propertySheetPage != null) {
                     propertySheetPage.refresh();
                  }
               }
            });
         }
      }
   }};

In order for the outside world (including our undo/redo actions) to know about our context we need to provide this information by being adaptable to our own undo context.

public Object getAdapter(Class key) {
	if (key.equals(IContentOutlinePage.class)) {
		return getContentOutlinePage();
	}
	else if (key.equals(IPropertySheetPage.class)) {
		return getPropertySheetPage();
	}
	else if (key.equals(IGotoMarker.class)) {
		return this;
	}
	else if (key.equals(IUndoContext.class)) {
		// used by undo/redo actions to get their undo context
		return undoContext;
	}
	else {
		return super.getAdapter(key);
	}
}

Finally, we must override the default EMF undo/redo actions to use the operation history rather than the command stack. In order to do this we use wrappers for operation history UndoActionHandler/RedoActionHandlers to EMF undo/redo actions in our action bar contributor class:

public void init(IActionBars actionBars) {
	super.init(actionBars);

    ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();
    
	// override the superclass implementation of these actions
    undoAction = new UndoActionWrapper();
    undoAction.setImageDescriptor(
    		sharedImages.getImageDescriptor(ISharedImages.IMG_TOOL_UNDO));
    actionBars.setGlobalActionHandler(
    		ActionFactory.UNDO.getId(), undoAction);

    redoAction = new RedoActionWrapper();
    redoAction.setImageDescriptor(
    		sharedImages.getImageDescriptor(ISharedImages.IMG_TOOL_REDO));
    actionBars.setGlobalActionHandler(ActionFactory.REDO.getId(), redoAction);
}

Each action wrapper is able to determine the editor's undo context by adapting the active editor part to an IUndoContext (see above code for getAdapter()). With this information they can determine which operations should show up in our editor's undo/redo actions.

[ back to top]

Summary

In this tutorial, we did the following:

  1. Registered a workspace integrated transactional editing domain for our editor instances to share
  2. Created and registered a listener responsible for opening new editor instances whenever a resource is loaded
  3. Instantiated a workspace synchronizer to keep our editing domain synchronized with changes in the workspace
  4. Ensured that every read and write that occurs in the editor will be performed inside an appropriate transaction
  5. Looked at various helpful classes provided by the transaction and workspace integration layers to override basic EMF classes and adapt them for use in transactional editing domains
  6. Managed the undo context for our editors so that they show only undo/redo for operations that affect their resource
  7. Overrode an EMF generated reflective editor to integrate it into transactional editing domains and the eclipse operations framework

[ back to top]


Copyright (c) 2006 IBM Corporation and others. All Rights Reserved.


 
 
  Published under the terms of the Eclipse Public License Version 1.0 ("EPL") Design by Interspire