Notice: This Wiki is now read only and edits are no longer possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.
Wire EMF Databinding RCP
THIS IS AN ARTICLE WORK IN PROGRESS (Bug 195163)
Contents
- 1 Abstract/Requirements Definition
- 2 Setup the Toolchain
- 3 Application Design
- 3.1 Plugin-Design
- 3.1.1 at.bestsolution.addressbook
- 3.1.2 at.bestsolution.addressbook.ui
- 3.1.3 at.bestsolution.addressbook.ui.theme
- 3.1.4 at.bestsolution.addressbook.core
- 3.1.5 at.bestsolution.addressbook.core.model
- 3.1.6 at.bestsolution.addressbook.core.datasource
- 3.1.7 at.bestsolution.addressbook.core.datasource.xmi
- 3.1.8 at.bestsolution.addressbook.core.datasource.iBatis
- 3.2 Plugin Overview
- 3.3 Domain Model
- 3.1 Plugin-Design
- 4 Hands on practice or „Let's begin our journey“
- 5 Implementing at.bestsolution.addressbook.core.datasource
Abstract/Requirements Definition
I'm trying to implement a fairly simple address book application on top of RCP using frameworks provided by Eclipse in 3.3. There's also focus on how to structure and design an RCP to be as extensible and as flexible as possible, including theming, internationalization, datastorage independency, ... .
Setup the Toolchain
- Eclipse 3.3 for RCP-Developers (available from http://www.eclipse.org/downloads)
- EMF using the Update-Manager (Help>Software Updates>Find and Install)
- Derby-Plugins (available from http://db.apache.org/derby/derby_downloads.html)
To install:- Stop your eclipse (if running)
- Unzip „derby.zip“s (as of this writing derby_core_plugin_10.2.2.485682.zip and derby_ui_plugin_1.1.0.zip) into your %ECLIPSE_HOME/plugin directory
- Start up eclipse
- iBatis (available from http://ibatis.apache.org)
- Fetch application icons (e.g. available from http://websvn.kde.org/trunk/KDE/kdelibs/pics/oxygen/ using your favorite SVN-Client)
- (Optional) Visual-Database Design
- Install Clay for Eclipse (available from http://www.azzurri.jp/en/software/clay/)
- Download Clay-Utils used for creation of DDL and documentation (available from http://publicsvn.bestsolution.at/repos/java/clayUtils/release/at.bestsolution.clayutils-nodeps-0.0.7.jar)
- DDL-Utils to create database DDL for any database you want (available from http://db.apache.org/ddlutils)
- (Optional) Subclipse for Version-Control (available from http://subclipse.tigris.org)
Application Design
Plugin-Design
At the beginning of every project is the package design or in RCP/OSGI Environment this breaks down to the design of the various plugins and their responsibilities. One of most important things when it comes to plugin-design is to split all your plugins into UI and NONE-UI (with the business logic) parts this makes automated testing of the business logic using JUnit tests easier.
at.bestsolution.addressbook
The main RCP-Application
at.bestsolution.addressbook.ui
This plugin provides the UI bits (ViewPart, ...) for the addressbook application.
at.bestsolution.addressbook.ui.theme
This plugin addresses the themeability of the application by providing a plugable theme-API. It will by default provide a standard theme.
at.bestsolution.addressbook.core
This plugin provides the none GUI bits for the application like Command-Definitions, handlers, ...
at.bestsolution.addressbook.core.model
This plugin provides the model implementation created using EMF
at.bestsolution.addressbook.core.datasource
This plugin will provide the API to provide plugable datasources
at.bestsolution.addressbook.core.datasource.xmi
This plugin provides a datasource implementation on top of XMI (directly supported by EMF)
at.bestsolution.addressbook.core.datasource.iBatis
This plugin provides a datasource implementation on top of iBatis
Plugin Overview
Domain Model
The domain model is fairly simple and can be represented by 2 classes as shown in the diagram below. The only interesting thing is that there's a bidirectional relationship between Person(Attribute: primaryAddress) and Address(Attribute: person).
Hands on practice or „Let's begin our journey“
Implementing at.bestsolution.addressbook.core.model
Create an EMF-Project
- Open the "New Project Wizard"
- Select Eclipse Modeling Framework
- Name the project "at.bestsolution.addressbook.core.model"
- The resulting workspace looks like this
Create the Ecore-Model
Create the Ecore file
- Select Example EMF Model Creation Wizards > Ecore Model
- Name the model "addressbook.ecore"
- Open the Properties-View (Window > Show View > Others ...)
- Select the root node currently shown in the editor as null
- Editing the properties in the property view
Create the Classes and Attributes
Now we have to add our Domain-Objects Person
and Address
to the Ecore-Model:
- Right click on the
addressbook
-package you have created above and select "New Child > EClass" - Set the following properties (in the Properties View)
Name:Person
- Right click the
Person
and select "New Child > EAttribute" - Set the following properties (in the Properties View)
Name:surname
EType:EString
- Right click the
Person
and select "New Child > EAttribute" - Set the following properties (in the Properties View)
Name:givenname
EType:EString
- Right click the
Person
and select "New Child > EAttribute" - Set the following properties (in the Properties View)
Name:birthday
EType:EDate
- Right click on the
addressbook
-package you have created above and select "New Child > EClass" - Set the following properties (in the Properties View)
Name:Address
- Right click the
Address
and select "New Child > EAttribute" - Set the following properties (in the Properties View)
Name:street
EType:EString
- Right click the
Address
and select "New Child > EAttribute" - Set the following properties (in the Properties View)
Name:zip
EType:EString
- Right click the
Address
and select "New Child > EAttribute" - Set the following properties (in the Properties View)
Name:city
EType:EString
- Right click the
Address
and select "New Child > EAttribute" - Set the following properties (in the Properties View)
Name:country
EType:EString
You'll notice that the types used for the attributes are not the standard classes provided by the JDK but wrappers defined by EMF. EMF provides wrappers for the most import JDK classes.
After having done this your model should look like the following:
EMF holds META-Informations about your model and that's why all classes part of an Ecore-model have to be known to EMF. Those META-Informations are used by EMF to do fancy things but can also be of use for you when you want to get informations about an your model (or the model of someone different).
There are different ways to get EMF to recognize classes:
- You create a new
EClass
in your model - You define a new
EData Type
to wrap an existing class e.g. provided by the JDK
EMF in detail:
- EClass: This represents a Class in EMF terminology TODO more information about EClass
- EAttribute: This represents an Attribute in EMF terminology TODO more information about EAttribute You'll notice that the types used for the attributes are not the standard classes provided by the JDK but wrappers defined by EMF. EMF provides wrappers for the most import JDK classes and want to use another classes coming from the JDK you'll have to add an
EData Type
to your Ecore-model. We'll see this later on.
Model the (bidirectional) relation ship between Person and Address
Next thing we have to model is the bidirectional relation between Person and Address like we defined it in our UML-Class Diagramm. In EMF such a relation can be expressed as an EReference in conjunction with an EOpposite:
- Right click the Person and select "New Child > EReference"
- Set the following properties (in the Properties View)
Name:primaryAddress
EType:Address
Containment:true
- Right click the Address and select "New Child > EReference"
- Set the following properties (in the Properties View)
Name:person
EType:Person
EOpposite:primaryAddress
Transient:true
The Ecore model should look like this now:
EMF in detail:
- EReference: TODO explain EReference
- Containment: TODO explain Containment
- EOpposite: TODO explain EOpposite
- Transient: TODO explain Transient
Model PropertyChangeSupport for Databinding
At this point we have implemented our original Domain-Model in Ecore but we are not finished because when we are working with JFace' Databinding Framework that ships with 3.3 we need to follow the JavaBean specification which means our domain objects have to implement java.bean.PropertyChangeSupport
.
The best way to model this is that all our Domain-Model-Objects inherit from a super-class named BaseObject.
Because there are no wrapper for the Classes and Interfaces needed to implement PropertyChangeSupport
and friends we need to create them our own by defining "EData Types
":
- Right click on the addressbook-package you have created above and select "New Child > EData Type"
- Set the following properties (in the Properties View)
Name:PropertyChangeSupport
Instance Class Name:java.beans.PropertyChangeSupport
- Right click on the addressbook-package you have created above and select „New Child > EData Type“
- Set the following properties (in the Properties View)
Name:PropertyChangeListener
Instance Class Name:java.beans.PropertyChangeListener
- Right click on the addressbook-package you have created above and select „New Child > EData Type“
- Set the following properties (in the Properties View)
Name:PropertyChangeEvent
Instance Class Name:java.beans.PropertyChangeEvent
Now we are able to create our BaseObject
:
- Right click on the
addressbook
-package you have created above and select "New Child > EClass" - Set the following properties (in the Properties View)
Name:BaseObject
- Right click the
BaseObject
and select „New Child > EAttribute“ - Set the following properties (in the Properties View)
Name:id
Etype:EInt
- Right click the
BaseObject
and select „New Child > EAttribute“ - Set the following properties (in the Properties View)
Name:propertyChangeSupport
Etype:PropertyChangeSupport
Changeable:false
- Right click the
BaseObject
and select „New Child > EOperation“ - Set the following properties (in the Properties View)
Name:addPropertyChangeListener
- Right click the
addPropertyChangeListene
r and select „New Child > EParameter“ - Set the following properties (in the Properties View)
Name:listener
Etype:PropertyChangeListener
- Right click the
BaseObject
and select „New Child > EOperation“ - Set the following properties (in the Properties View)
Name:removePropertyChangeListener
- Right click the
removePropertyChangeListene
r and select „New Child > EParameter“ - Set the following properties (in the Properties View)
Name:listener
Etype:PropertyChangeListener
- Select Person-Class and set the ESuperTypes-Attribute to BaseObject
- Select Address-Class and set the ESuperTypes-Attribute to BaseObject
The final Ecore-Diagramm looks like this:
EMF in detail:
- EData Type: TODO Explain EData Type
- EOperation: TODO Explain EOperation
- EParameter: TODO Explain EParameter
Create the Java-Code from the Ecore-model
Instead of writing the Java-Model-Objects our own we use EMFs codegeneration features do this boring task for us. EMF creates Stub-Objects for us which we are going to customize and implement the stub methods.
Generate Stub-Objects
- Select the addressbook.ecore in the Project-Explorer
- Right Click
- Select New > Other ...
- Select EMF Model
- Click Next until you reach this window where you press "Load"
- Click "Next" and afterwards "Finish"
We have no created a so called genmodel which gives us control over how EMF generates Java-Code. E.g. we could define to use Java5 generics, ... . We will make some minor changes like e.g. the name of the base package and suppressing of EMF Types in our public API.
- Free public API from EMF Types
- Select the 1st Addressbook in the Tree
- Scroll in "Properties View" to the section "Model Feature Defaults"
- Change "Suppress EMF Types" to
true
- Modify Base package name
- Select the 2nd Addressbook in the Tree
- Change the „base package“ to „at.bestsolution.core.model“
Time for code generation:
- Select the 1st Addressbook in the Ǵenmodel-Editor
- Right Click
- Select „Generate Model Code“
In the end your project looks like this:
Analyze the generated Java-Code
General
Let's start at the top-level EMF has created 3 packages:
- at.bestsolution.core.model.addressbook: This package holds an interface for every class defined in our ecore-model. Outside of the model-plugin people should only use these interfaces and not the real implementations. We are going to hide them from the user using OSGI access restrictions
- at.bestsolution.core.model.addressbook.impl: This package holds the implemenation for all interfaces from the afore mentionned package.
- at.bestsolution.core.model.addressbook.util: This package holds utility classes useful when working with our model objects
EMF in detail:
You might have noticed that EMF has create 2 more classes we haven't defined in our ecore-Model. Theses classes are useful if you want to use find out informations about your model or interact with it e.g. creating instances of your interfaces.
- AddressbookFactory: Provides methods to instantiate your model classes.TODO More informations?
- AddressbookPackage: Provides informations about the whole package (Informations about all classes, attributes, methods, ...). TODO More informations?
When EMF generated the code for our project it also modified your MANIFEST.MF (added dependencies, ...). A thing we are going to fix now is that EMF added at.bestsolution.core.model.addressbook.impl
to the exported packages list. As we discovered before all Classes
found in the impl
-package have a corresponding Interfaces
in at.bestsolution.core.model.addressbook
. As it is always in software and even more API development you start restrictive and open things if you have a use case. For us this means that we are removing at.bestsolution.core.model.addressbook.impl
from the list like this:
- Open your
META-INF/MANIFEST.MF
- Navigate to the Runtime-Tab
- Remove
at.bestsolution.core.model.addressbook.impl
from the list
Having done this nobody from the outside can access the real implemention classes any more but has to work with their interface representation. Because the real implementations are not visible any more to the consumer of our plugin he/she won't be able to create instances of them directly but instead has to use our AddressbookFactory.
Person p = AddressbookFactory.eINSTANCE.createPerson()
Codepart Analyzation
Let's now take a closer look at the generated Model-Classes:
- Fields generated by EMF have an accompanying static default value field
- All Elements generated by EMF have an
@generated
in their JavaDoc, if you customize a method you need to remove this@generated
else your modifications are overwritten the next time you generate your model code. eSet
can be used to set an attribute valuePerson p = AddressbookFactory.eINSTANCE.createPerson(); p.setSurname("Schindl"); // direct setting of attribute p.eSet(AddressbookPackage.Literals.PERSON__SURNAME, "Schindl"); // indirect setting of attribute
eGet
can be used to get an attribute valuePerson p = AddressbookFactory.eINSTANCE.createPerson(); p.getSurname(); // direct getting of attribute p.eGet(AddressbookPackage.Literals.PERSON__SURNAME); // indirect getting of attribute
PersonImpl#setSurname(String)
: There's more code you thought you find in this method, right? If you used Standard Java Beans before your code looked like this:public void setSurname(String newSurname) { String oldSurname = surname; surname = newSurname; propertyChangeSupport.firePropertyChange("surname",oldSurname,newSurname); }
EMF has its own more mature notification system than the not very feature rich PropertyChangeSupport-API so the code translates into:
public void setSurname(String newSurname) { String oldSurname = surname; surname = newSurname; if (eNotificationRequired()) eNotify(new ENotificationImpl(this, Notification.SET, AddressbookPackage.PERSON__SURNAME, oldSurname, surname)); }
This is a problem because the Databinding-Framework shiped with Eclipse 3.3 can only deal with JavaBeans and not with EMF-Objects (Work is done by the EMF-Group to overcome this limitation in Bug 75625). The translation between the EMFs ChangeNotification-System and the standard PropertyChangeSupport is a task we are going to solve later. A big advantage of EMF generated code is that you can't forget about the notification code like it happened when you handcrafted your model-classes which was boring task.
PersonImpl#setPrimaryAddress(Address)
: Once more you'll find out that there's more code.public void setPrimaryAddress(Address newPrimaryAddress) { if (newPrimaryAddress != primaryAddress) { NotificationChain msgs = null; if (primaryAddress != null) msgs = ((InternalEObject)primaryAddress).eInverseRemove(this, AddressbookPackage.ADDRESS__PERSON, Address.class, msgs); if (newPrimaryAddress != null) msgs = ((InternalEObject)newPrimaryAddress).eInverseAdd(this, AddressbookPackage.ADDRESS__PERSON, Address.class, msgs); msgs = basicSetPrimaryAddress(newPrimaryAddress, msgs); if (msgs != null) msgs.dispatch(); } else if (eNotificationRequired()) eNotify(new ENotificationImpl(this, Notification.SET, AddressbookPackage.PERSON__PRIMARY_ADDRESS, newPrimaryAddress, newPrimaryAddress));
What you might have expected to find is the code in
basicSetPrimaryAddress
. The reason whysetPrimaryAddress
holds more code is that we modeledPerson#primaryAddress
andAddress#person
asEOpposite
in our ecore model. Which means that when ever we setPerson#primaryAddress
Address#person
has to be synced (and the other way round). Once more you would have done the same your own when you handcrafted model classes in former days but there was a great likelyhood that you made a mistake which was very hard to track down.
Writing JUnit-Test PropertyChangeSupport
- Generate the JUnit-Test-Code
- Open the .genmodel
- Right click on the Addressbook and select 'Generate Test Code'
- In your workspace you find a new project called 'at.bestsolution.core.model.tests'</li
- Open '
at.bestsolution.core.model.addressbook.tests.BaseObjectTest
' - Navigate to
testAddPropertyChangeListener__PropertyChangeListener()
- remove
@generated
from the JavaDoc - Write the Test-Case for the
addPropertyChangeListener
:- Check that Listener is added
- Check that adding the listener is not triggering a change event
- Check that setting an attribute triggers a change event
public void testAddPropertyChangeListener__PropertyChangeListener() { BaseObject o = AddressbookFactory.eINSTANCE.createBaseObject(); final int[] val = new int[] { 0 }; assertNotNull(o); PropertyChangeListener l = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { assertEquals("id",evt.getPropertyName()); assertEquals(0, ((Number)evt.getOldValue()).intValue()); assertEquals(1,((Number)evt.getNewValue()).intValue()); val[0]++; } }; o.addPropertyChangeListener(l); PropertyChangeListener[] ls = o.getPropertyChangeSupport().getPropertyChangeListeners(); assertEquals("The listener doesn't seem to be added.", ls.length, 1); boolean found = false; for( PropertyChangeListener t: ls ) { if( t == l ) { found = true; } } assertEquals("The listener has not been added", true, found); o.addPropertyChangeListener(new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { } }); o.setId(1); assertEquals("The PropertyChangeListener was not informed", 1, val[0]); }
- Navigate to
testRemovePropertyChangeListener__PropertyChangeListener()
- remove
@generated
from the JavaDoc - Write the Test-Case for the
removePropertyChangeListener
:- Check that listener is removed
- Check that no change event is generated when listener is removed
- Check that when an attribute is modified the removed listener is not notified any more
public void testRemovePropertyChangeListener__PropertyChangeListener() { BaseObject o = AddressbookFactory.eINSTANCE.createBaseObject(); assertNotNull(o); PropertyChangeListener l = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { fail("This listener should not be notified"); } }; o.addPropertyChangeListener(l); o.removePropertyChangeListener(l); assertEquals("Listener is not removed", 0, o.getPropertyChangeSupport().getPropertyChangeListeners().length); o.setId(1); }
As you see we are following the TestDrivenDevelopment idea which means that we are writing the TestCase before we implement the methods so the tests are supposed to fail at the moment. We'll implement the methods in next section.
Implement PropertyChangeSupport
As you have seen above EMF uses a different system for change-tracking and notification of interested parties. Before we can start to think about how to translate form EMF-Notification-System to PropertyChangeSupport we need to find out how the EMF-System works.
As you have seen above whenever you modify an attribute EMF is sending out a notification by calling eNotify(Notification)
public void setSurname(String newSurname) { String oldSurname = surname; surname = newSurname; if (eNotificationRequired()) eNotify(new ENotificationImpl(this, Notification.SET, AddressbookPackage.PERSON__SURNAME, oldSurname, surname)); }
You see that that the information passed by EMF holds all necessary informations about the change and to get one of parties who is informed about a change we need to register as a so called org.eclipse.emf.common.notify.Adapter
.
We now have enough informations to implement the translation between EMF-Notification and PropertyChangeSupport:
- Open BaseObjectImpl
- Navigate to the constructor and remove
@generated
from the JavaDoc. This ensures that when we regenerate the class using EMF our changes are not lost - Create an anonymous instance of
org.eclipse.emf.common.notify.Adapter
and add it to the adapters of the object/** * <!-- begin-user-doc --> * <!-- end-user-doc --> */ protected BaseObjectImpl() { super(); this.propertyChangeSupport = new PropertyChangeSupport(this); // register ourselves as adapters to receive events about modifications eAdapters().add(new Adapter() { public Notifier getTarget() { return null; } public boolean isAdapterForType(Object type) { return false; } public void notifyChanged(Notification notification) { } public void setTarget(Notifier newTarget) { } }); }
- Implement
notifyChanged(Notification)
public void notifyChanged(Notification notification) { if( ! notification.isTouch() && notification.getFeature() instanceof EStructuralFeature ) { propertyChangeSupport.firePropertyChange( ((EStructuralFeature)notification.getFeature()).getName(), // The modified attribute notification.getOldValue(), // the old value notification.getNewValue() // the new value ); } }
- Implement
addPropertyChangeListener(PropertyChangeListener)
public void addPropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.addPropertyChangeListener(listener); }
- Implement
removePropertyChangeListener(PropertyChangeListener)
public void removePropertyChangeListener(PropertyChangeListener listener) { propertyChangeSupport.removePropertyChangeListener(listener); }
- Run the JUnit-Test case for BaseObjectImpl. All tests should succeed else please check if you code looks like the one presented above. If you don't want to search for the bug you can get the code at this stage from SVN-Freeze_01
Implementing at.bestsolution.addressbook.core.datasource
This plugin acts as the interface between our application and the datastorage backend implementations and hides it form the application code. This plugin provides a DatasourceManager who manages all Datasources who are contributed by specialized plugins using the Extension Point mechanism. If you don't want to depend on Eclipse but only on OSGI-Functions you could as well achieve the same using OSGI-Services (see Article for more informations)
Create a NONE-UI Project
- Open the "New Project Wizard"
- Select the "Plug-in Project"
- Name the project: "at.bestsolution.addressbook.core.datasource"
- Uncheck "This plug-in will make contributions to the UI"
- Click Finish
Creating the "datasource" extension-point
- Open the MANIFEST.MF
- Click the Extension Points-Tab
- Click the Add...-Button
- Fill in the following values in the opened Window:
- Extension Point ID: datasource
- Extension Point Name: Datasource Provider
- Click the Definition-Tab
- Right Click the extension-Element in the Tree
- Select: New > Element
- In the "Element Details"-Sections fill in the following fields:
- Name: datasource
- Right Click the datasource-Element in the Tree
- Select: New > Attribute
- In the "Attribute Details"-Section fill in the following fields:
- Name: id
- Use: required
- Right Click the datasource-Element in the Tree
- Select: New > Attribute
- In the "Attribute Details"-Section fill in the following fields:
- Name: label
- Use: required
- Translateable: true
- Right Click the datasource-Element in the Tree
- Select: New > Attribute
- In the "Attribute Details"-Section fill in the following fields:
- Name: class
- Use: required
- Type: java
- Extends: at.bestsolution.addressbook.core.datasource.Datasource