Actions initiated by the user or automatic background tasks can make changes (edits) to a model. These
changes often have ripple effects on other components of an application: the user interface's rendering of
model elements, related models, or even external data stores. Notification of changes is a capability of all
EMF-based models, including UML, and is implemented by the
Notification
API. The most basic means of obtaining notifications from a model is by attaching
Adapter
s
to model elements by adding them to the
eAdapters
list of the elements of interest. However, the DevOps Modeling Platform provides a more powerful mechanism for
receiving notifications, which accounts for the transactional editing environment.
In an EMF-based model, there are three different categories of
Notifier
that commonly produce notifications:
EObject
s
representing the model dataResource
s
that load and save the model dataResourceSet
s
that manage multiple interconnected resources in a single context
The next few sections show how to attach listeners to the UML Modeler's editing domain to observe these different kinds of objects.
Following that is an example of how to declare a listener in the plugin.xml
, to ensure correct
timing of listener registration. Finally, the transactional listener API is contrasted with the use of
adapters to receive notifications of changes as soon as they occur. This is EMF's
lowest-level listener mechanism.
The most important changes that an application will observe in an EMF-based model are changes to the model contents:
the data that the user cares about. To define a listener that will be notified of any change to any EObject
in a model, define an implementation of the
org.eclipse.emf.transaction.ResourceSetListener
interface and attach
it (by calling org.eclipse.emf.transaction.TransactionalEditingDomain.addResourceSetListener()
) to the
TransactionalEditingDomain
:
public void plugletmain(String[] args) { UMLModeler.getEditingDomain().addResourceSetListener(new ResourceSetListenerImpl() { public void resourceSetChanged(ResourceSetChangeEvent event) { for (Iterator iter = event.getNotifications().iterator(); iter.hasNext();) { Notification notification = (Notification) iter.next(); Object notifier = notification.getNotifier(); if (notifier instanceof EObject) { EObject eObject = (EObject) notifier; // only respond to changes to structural features of the object if (notification.getFeature() instanceof EStructuralFeature) { EStructuralFeature feature = (EStructuralFeature) notification.getFeature(); // get the name of the changed feature and the qualified name of // the object, substituting <type> for any element that has no name out.println("The " + feature.getName() + " of the object \"" + EMFCoreUtil.getQualifiedName(eObject, true) + "\" has changed."); } } } }}); }
This example illustrates the most basic implementation of a listener in the transactional API, processing a list
of notifications for all of the changes that occurred during the execution of a read/write transaction. This
listener is invoked after the transaction has committed its changes, and is therefore sometimes
known as a "post-commit listener." There are two primary advantages to this transactional kind of listener
over the basic EMF adapter. Firstly, notifications are sent as a batch after all changes are completed,
so that they can be analyzed efficiently as a group. Secondly, because the ResourceSetChangeEvent
is sent only after a transaction commits, it includes notifications only for those changes that were committed
by the transaction; any changes that were rolled back are not included, so that the listener need never know that
they had occurred and then been reverted. Also, because these notifications are delivered to the listener only after
all of the model changes in the transaction are complete, the listener can optimize its processing using the knowledge
that the model will not change again in the future. For example, when processing a notification, if the notifier EObject
that
sent it is not at that moment attached to the model, then it can be considered as having been permanently deleted, so
that the notification can be ignored (presumably a later notification will be a Notification.REMOVE
that
indicates its removal from the model).
It is also important to note that the editing domain invokes its listeners always in a read-only transaction. This
has two benefits: it ensures that a listener can safely read the model (inspecting the modified elements and related
elements) without having to invoke
org.eclipse.emf.transaction.TransactionalEditingDomain.runExclusive()
.
Also, it ensures that listeners cannot make modifications to the model, as read/write transactions cannot
be started within a read-only transaction. Thus, listeners are assured that any decisions they make will not be
invalidated by model changes performed by some other listener in response to the same events.
The following diagram illustrates the life-cycle of a transaction, in particular the timing of the
ResourceSetChangeEvent
s that are sent to listeners:
In the execution of a command, the TransactionalEditingDomain
first starts a transaction, then
executes the command. After the command has completed its changes, the editing domain invokes pre-commit listeners
(or "trigger listeners") to notify them of the changes that were performed. If any of these pre-commit listeners
throws a org.eclipse.emf.transaction.RollbackException
then the transaction is rolled back (its changes undone) and closed. Otherwise, if the pre-commit listeners
provided any trigger commands, then these are executed in a nested transaction by recursively invoking the same
process. For more information about pre-commit listeners, see the Constraining Models
topic.
Following the execution of trigger commands, if any, the transaction commits. If the transaction is a root transaction (for example, not one that executed triggers), then this only requires closing it. Otherwise, the transaction is a root transaction, and requires validation. This step checks all of the changes that were performed by the original root transaction and any nested transactions against the available constraints (see Constraining Models for information about how to define constraints). If any constraint is violated that results in an Error severity, then the entire transaction is rolled back. Otherwise, post-commit listeners (such as the example above) are called and the transaction is closed.
Listeners can define filters to efficiently select the objects from which they are interested in receiving events and/or
the kinds of events to receive. In this example, we restrict the notifications to those coming from UML Classifiers,
by creating a filter on the notifier type specified as an
EClass
.
There are a few pre-defined filters in org.eclipse.emf.transaction.NotificationFilter
, such as ANY
(matching any notification) and NOT_TOUCH
which is the default filter, matching notifications that are not "touch" events (touch notifications indicate events that
do not actually change the model data). The NotificationFilter
class defines also a number of factory methods
for common filters and for boolean combinations:
public void plugletmain(String[] args) { UMLModeler.getEditingDomain().addResourceSetListener(new DemultiplexingListener( NotificationFilter.createNotifierTypeFilter(UMLPackage.Literals.CLASSIFIER)) { protected void handleNotification( TransactionalEditingDomain domain, Notification notification) { // because the listener filters for notifications from classifiers, it is // certain that the notifier is a classifier Classifier classifier = (Classifier) notification.getNotifier(); // only respond to changes to structural features of the classifier if (notification.getFeature() instanceof EStructuralFeature) { EStructuralFeature feature = (EStructuralFeature) notification.getFeature(); out.println("The " + feature.getName() + " of the classifier \"" + classifier.getQualifiedName() + "\" has changed."); } }}); }
This example also illustrates one of the convenient abstract implementations of the ResourceSetListener
interface, which invokes a call-back method to process each individual Notification
. The class diagram
below depicts the listener types, as follows:
org.eclipse.emf.transaction.ResourceSetListener
:
The base listener interface, defining two call-back methods: resourceSetChanged
and
transactionAboutToCommit
. Both of these methods accept a
org.eclipse.emf.transaction.ResourceSetChangeEvent
which provides a list of all Notification
s generated during a transaction, in the order in which
they occurred, that match the listener's filter. This allows for analysis of the complete set of changes that
were committed, instead of one at a time via an Adapter
.org.eclipse.emf.transaction.ResourceSetListenerImpl
:
Simply provides default implementations of the listener interface methods, including empty implementations of
the resourceSetChanged
and transactionAboutToCommit
methods. Subclasses need only
override what is of interest to them.org.eclipse.emf.transaction.DemultiplexingListener
:
Implements the resourceSetChanged
method to unravel the bundle of notifications, processing them
one at a time by invoking a subclass's implementation of the handleNotification
method. This is
convenient for those applications where it is not necessary to analyze the notifications as a group.org.eclipse.emf.transaction.TriggerListener
:
Implements the transactionAboutToCommit
method to unravel the bundle of notifications, processing them
one at a time by invoking a subclass's implementation of the trigger
method. This is
discussed at greater length in Constraining Models.
As is evident in the examples above, a Notification
describes a discrete change to
one feature of one notifier. This is a very low-level description of model changes.
What about more abstract kinds of changes, that are about an object as a whole and not some
particular feature of it? These must be derived from notifications sent by some particular feature
of some related object. The following table provides mappings for some of the more common cases:
Abstract Change | Related Notifier | Feature | Notification Type(s) | Notification Value(s) |
---|---|---|---|---|
Object Created | eResource | contents |
ADD , ADD_MANY , SET |
The new object is in the newValue property of the notification, which is
a collection in the case of ADD_MANY . Note, however, that the object should
only really be considered as "created" if it was not previously removed from some other
resource or object in the model (in which case, there would also be a corresponding remove
notification).
|
eContainer | any containment reference | |||
Object Deleted | eResource | contents |
REMOVE , REMOVE_MANY , SET |
The deleted object is in the oldValue property of the notification, which is
a collection in the case of REMOVE_MANY . Note, however, that the object should
only really be considered as "deleted" if it is unattached at the end of the transaction
when the listener is invoked (i.e., its eResource() is null ).
If it is still attached, then the object has simply been moved to another container or
resource (and there should be a corresponding add notification). The
NotificationUtil.getDeletedObjects
method computes the list of objects deleted during the transaction.
|
eContainer | any containment reference |
As with any listener, resource set listeners should process notifications as quickly as possible
so that the thread executing the transaction (which may often be the UI thread) remains responsive.
Sometimes, this may require starting work asynchronously on a background thread (or scheduling a job)
or, in the case of UI updates, posting work via Display.asyncExec(Runnable)
. However,
the following considerations are important when asynchronously responding to events:
TransactionalEditingDomain.runExclusive(Runnable)
API if it needs to read the modelResourceSetChangeEvent
cannot be accessed asynchronously; the
event object is only valid in the context of the listener call-back method. Thus, information
such as the list of notifications obtained by calling
ResourceSetChangeEvent.getNotifications()
must be copied before posting the asynchronous handlerNotification
s that the listener received may be obsolete by the time the
asynchronous code runs, as another read/write transaction may have intervened to change
the model state again. A read transaction will ensure that this will not occur concurrently
with the asynchronous handler, but it must be prepared to re-read the model to verify that
the model is still in an appropriate state to do what it intends
Resource
s
are notifiers, so changes to them can be observed, too.
public void plugletmain(String[] args) { UMLModeler.getEditingDomain().addResourceSetListener(new ResourceSetListenerImpl( NotificationFilter.createFeatureFilter(EcorePackage.Literals.ERESOURCE, Resource.RESOURCE__CONTENTS).and( NotificationFilter.createResourceContentTypeFilter("com.ibm.xtools.uml.msl.umlModelContentType").or( NotificationFilter.createResourceContentTypeFilter("com.ibm.xtools.uml.msl.umlFragmentContentType")))) { public boolean isPostcommitOnly() { return true; } public void resourceSetChanged(ResourceSetChangeEvent event) { for (Iterator iter = event.getNotifications().iterator(); iter.hasNext();) { Notification next = (Notification) iter.next(); // because the listener filters for notifications from the resource contents // feature, it is certain that the notifier is a resource Resource resource = (Resource) next.getNotifier(); URI uri = resource.getURI(); switch (next.getEventType()) { case Notification.ADD: out.println("Added to resource " + uri + ": " + ((EObject) next.getNewValue()).eClass().getName()); break; case Notification.ADD_MANY: for (Iterator jter = ((Collection) next.getNewValue()).iterator(); jter.hasNext();) { out.println("Added to resource " + uri + ": " + ((EObject) jter.next()).eClass().getName()); } break; case Notification.REMOVE: out.println("Removed from resource " + uri + ": " + ((EObject) next.getOldValue()).eClass().getName()); break; case Notification.REMOVE_MANY: for (Iterator jter = ((Collection) next.getOldValue()).iterator(); jter.hasNext();) { out.println("Removed from resource " + uri + ": " + ((EObject) jter.next()).eClass().getName()); } break; case Notification.SET: EObject oldValue = (EObject) next.getOldValue(); EObject newValue = (EObject) next.getNewValue(); // resources cannot contain nulls in their contents list out.println("Replaced in resource " + uri + ": " + oldValue.eClass().getName() + " with: " + newValue.eClass().getName()); break; } } }}); }
This example illustrates a listener that responds to changes in the
contents
of a Resource
(the EResource
data type is the Ecore model for resources)
that contains UML model content (stored in *.emx and *.efx files). Note that this listener indicates
to the editing domain that it only wants to receive post-commit events (the resourceSetChanged
call-back).
The ResourceSetChangeEvent
provides not only the list of notifications, but also a reference to the transaction object that was
committed. This is useful for obtaining the status of the transaction, in case it committed with
warnings (the DemultiplexingListener
class does not provide this):
public void plugletmain(String[] args) { UMLModeler.getEditingDomain().addResourceSetListener(new ResourceSetListenerImpl() { public boolean isPostcommitOnly() { return true; } public void resourceSetChanged(ResourceSetChangeEvent event) { Transaction tx = event.getTransaction(); // there may be no transaction in the case of, for example, proxy resolution // occurring when a model was read without any transaction context (not // even a read-only transaction) if (tx != null) { final IStatus status = tx.getStatus(); if (!status.isOK()) { Runnable runnable = new Runnable() { public void run() { ErrorDialog.openError(null, "Transaction warnings", "Transaction committed with warnings", status); }}; // transactions may be created and committed on any thread. // Appropriate synchronization is required for UI updates if (Display.getCurrent() == null) { Display.getDefault().asyncExec(runnable); } else { runnable.run(); } } } }}); }
Note that, because model changes may be performed on any thread, updating the UI needs to be synchronized
appropriately with the display thread. This should usually be done asynchronously to ensure that the
dispatching of ResourceSetChangeEvent
s to listeners is not held up by waiting for a synchronous
return from the display thread. In such cases, it may also be necessary for the asynchronous runnable to
obtain its own read access to the model via TransactionalEditingDomain.runExclusive()
when it
runs on the display thread.
Tip:Display.syncExec()
is a common cause of deadlocks when working with the EMF Transaction API. It should be avoided (preferring insteadDisplay.asyncExec()
) while the current thread has a transaction open, because if thesyncExec
'd runnable should happen to invoke code that attempts to start a transaction (e.g., for reading the model), then deadlock will occur as the invoking thread will have the transaction lock that the runnable is waiting for, but is also waiting for the runnable to finish.
The only feature of ResourceSet
s that provides notifications of change is the
resources
list. It will notify when resources are added to and removed from the resource set, but not
when they are loaded or unloaded (the resources, themselves, notify of these changes).
public void plugletmain(String[] args) { UMLModeler.getEditingDomain().addResourceSetListener(new DemultiplexingListener( NotificationFilter.createFeatureFilter(EcorePackage.Literals.ERESOURCE, Resource.RESOURCE__IS_LOADED).or( NotificationFilter.createFeatureFilter(EcorePackage.Literals.ERESOURCE_SET, ResourceSet.RESOURCE_SET__RESOURCES))) { protected void handleNotification(TransactionalEditingDomain domain, Notification notification) { if (notification.getNotifier() instanceof ResourceSet) { // only the 'resources' feature notifies of changes switch (notification.getEventType()) { case Notification.ADD: out.println("Resource added: " + ((Resource) notification.getNewValue()).getURI()); break; case Notification.REMOVE: out.println("Resource removed: " + ((Resource) notification.getOldValue()).getURI()); break; // other cases omitted for brevity } } else { Resource resource = (Resource) notification.getNotifier(); URI uri = resource.getURI(); // the scalar boolean-valued 'isLoaded' feature can only be set or unset switch (notification.getEventType()) { case Notification.SET: case Notification.UNSET: if (notification.getNewBooleanValue()) { out.println("Resource loaded: " + uri); } else { out.println("Resource unloaded: " + uri); } break; } } }}); }
The examples above illustrate how to add listeners to the UML Modeler's editing domain in code. However, this presupposes that the client of the editing domain is already loaded. What if a plug-in depending on the editing domain is not yet loaded, and it misses some critical changes in the model?
The solution to this problem is to register the listener (in XML) on the org.eclipse.emf.transaction.listeners
extension point. This only works when the editing domain in question is also registered, on the
org.eclipse.emf.transaction.editingDomains
point (which the UML Modeler's editing domain is). An extension would look like:
<extension point="org.eclipse.emf.transaction.listeners"> <listener class="com.example.MyListener"> <!-- The UML Modeler editing domain --> <editingDomain id="org.eclipse.gmf.runtime.emf.core.compatibility.MSLEditingDomain"/> </listener> </extension>
and the listener class (doing the same as the previous example, above) would look like:
public class MyListener extends DemultiplexingListener { public MyListener() { super(NotificationFilter.createFeatureFilter(EcorePackage.Literals.ERESOURCE, Resource.RESOURCE__IS_LOADED).or( NotificationFilter.createFeatureFilter(EcorePackage.Literals.ERESOURCE_SET, ResourceSet.RESOURCE_SET__RESOURCES))); } protected void handleNotification(TransactionalEditingDomain domain, Notification notification) { if (notification.getNotifier() instanceof ResourceSet) { // as above } else { // as above } }});
Note that the filtering criteria cannot be expressed in the XML. As soon as the editing domain is created,
the listener class declared on the extension is loaded and instantiated, and the listener attached to the editing domain.
If a listener is
associated with multiple editing domains, then only a single instance of the listener is created, and it is attached to the
editing domains as they are created. If multiple listener instances are required, then they can be defined in separate
<listener>
elements.
Tip: Because there is no declarative filter mechanism in the extension XML (such as the<enablement>
conditions on actions), creation of the editing domain can cause the bundle defining the listener to start. If necessary, this can be mitigated by adding the listener's package to the exceptions list in theEclipse-LazyStart
header of the bundle manifest. Then, the bundle will only start if and when the listener invokes code (in response to notifications) that cause it to start.
Adapter
:
public void plugletmain(String[] args) { UMLModeler.getEditingDomain().getResourceSet().eAdapters().add(new EContentAdapter() { public void notifyChanged(Notification notification) { super.notifyChanged(notification); Object notifier = notification.getNotifier(); if (notifier instanceof EObject) { EObject eObject = (EObject) notifier; // only respond to changes to structural features of the object if (notification.getFeature() instanceof EStructuralFeature) { EStructuralFeature feature = (EStructuralFeature) notification.getFeature(); // get the name of the changed feature and the qualified name of // the object, substituting <type> for any element that has no name out.println("The " + feature.getName() + " of the object \"" + EMFCoreUtil.getQualifiedName(eObject, true) + "\" has changed."); } } }}); }This is very similar to the first example of a
ResourceSetListenerImpl
subclass, and achieves much the same result. The
EContentAdapter
is a specialized adapter that automatically attaches itself to the entire contents of the original
target (in this case, the resource set). Thus, this adapter receives all of the same notifications that
the resource set listener receives. However, there are several very considerable differences that point
to the advantages of using resource set listeners: