Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

Difference between revisions of "Wire EMF Databinding RCP"

(DatasourceManager#getDatasource(DatasourceDescriptor))
(Implementatiation of Java-Classes or let's get all tests <font color="green"><b>succeed</b></font>)
Line 1,177: Line 1,177:
 
//...
 
//...
 
</pre>
 
</pre>
 +
 +
=== Chapter Summary ===

Revision as of 13:28, 16 September 2007

THIS IS AN ARTICLE WORK IN PROGRESS (Bug 195163)

Contents

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

  1. Eclipse 3.3 for RCP-Developers (available from http://www.eclipse.org/downloads)
  2. EMF using the Update-Manager (Help>Software Updates>Find and Install)
  3. Derby-Plugins (available from http://db.apache.org/derby/derby_downloads.html)
    To install:
    1. Stop your eclipse (if running)
    2. 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
    3. Start up eclipse
  4. iBatis (available from http://ibatis.apache.org)
  5. Fetch application icons (e.g. available from http://websvn.kde.org/trunk/KDE/kdelibs/pics/oxygen/ using your favorite SVN-Client)
  6. (Optional) Visual-Database Design
    1. Install Clay for Eclipse (available from http://www.azzurri.jp/en/software/clay/)
    2. 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)
    3. DDL-Utils to create database DDL for any database you want (available from http://db.apache.org/ddlutils)
  7. (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 non-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 pluggable 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 pluggable 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

EMF RCP PluginOverview.png

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

EMF RCP Domain.png

Implementing at.bestsolution.addressbook.core.model

Create an EMF-Project

  1. Open the "New Project Wizard"
  2. Select Eclipse Modeling Framework
    EMF RCP EMF1.png
  3. Name the project "at.bestsolution.addressbook.core.model"
    EMF RCP EMF2.png
  4. The resulting workspace looks like this
    EMF RCP EMF3.png

Create the Ecore-Model

Create the Ecore file

  1. Select Example EMF Model Creation Wizards > Ecore Model
    EMF RCP EMF4.png
  2. Name the model "addressbook.ecore"
    EMF RCP EMF5.png
  3. Open the Properties-View (Window > Show View > Others ...)
    EMF RCP EMF6.png
  4. Select the root node currently shown in the editor as null
    EMF RCP EMF7.png
  5. Editing the properties in the property view
    EMF RCP EMF8.png

Create the Classes and Attributes

Now we have to add our Domain-Objects Person and Address to the Ecore-Model:

  1. Right click on the addressbook-package you have created above and select "New Child > EClass"
  2. Set the following properties (in the Properties View)
    Name: Person
  3. Right click the Person and select "New Child > EAttribute"
  4. Set the following properties (in the Properties View)
    Name: surname
    EType: EString
  5. Right click the Person and select "New Child > EAttribute"
  6. Set the following properties (in the Properties View)
    Name: givenname
    EType: EString
  7. Right click the Person and select "New Child > EAttribute"
  8. Set the following properties (in the Properties View)
    Name: birthday
    EType: EDate
  9. Right click on the addressbook-package you have created above and select "New Child > EClass"
  10. Set the following properties (in the Properties View)
    Name: Address
  11. Right click the Address and select "New Child > EAttribute"
  12. Set the following properties (in the Properties View)
    Name: street
    EType: EString
  13. Right click the Address and select "New Child > EAttribute"
  14. Set the following properties (in the Properties View)
    Name: zip
    EType: EString
  15. Right click the Address and select "New Child > EAttribute"
  16. Set the following properties (in the Properties View)
    Name: city
    EType: EString
  17. Right click the Address and select "New Child > EAttribute"
  18. 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 RCP EMF9.png

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:

  1. You create a new EClass in your model
  2. You define a new EData Type to wrap an existing class e.g. provided by the JDK

EMF in detail:

  1. EClass: This represents a Class in EMF terminology TODO more information about EClass
  2. 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:

  1. Right click the Person and select "New Child > EReference"
  2. Set the following properties (in the Properties View)
    Name: primaryAddress
    EType: Address
    Containment: true
  3. Right click the Address and select "New Child > EReference"
  4. 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 RCP EMF10.png

EMF in detail:

  1. EReference: TODO explain EReference
  2. Containment: TODO explain Containment
  3. EOpposite: TODO explain EOpposite
  4. Transient: Marking an attribute transient means that it is not persisted in the (e.g. in XMI). There are multiple reasons why an attribute might be marked transient. Here we mark the attribute transient because the person attribute is part of containment and the person-instance referenced here is already persisted (It is the container the Address-Instance is persisted into). Another reason could be that the intance value of the attribute is not serializable e.g. if you inherit from an already existing DataType you only wrap in EMF (see below for an example of this reason)

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":

  1. Right click on the addressbook-package you have created above and select "New Child > EData Type"
  2. Set the following properties (in the Properties View)
    Name: PropertyChangeSupport
    Instance Class Name: java.beans.PropertyChangeSupport
    Serializable: false
  3. Right click on the addressbook-package you have created above and select „New Child > EData Type“
  4. Set the following properties (in the Properties View)
    Name: PropertyChangeListener
    Instance Class Name: java.beans.PropertyChangeListener
    Serializable: false
  5. Right click on the addressbook-package you have created above and select „New Child > EData Type“
  6. Set the following properties (in the Properties View)
    Name: PropertyChangeEvent
    Instance Class Name: java.beans.PropertyChangeEvent
    Serializable: false

Now we are able to create our BaseObject:

  1. Right click on the addressbook-package you have created above and select "New Child > EClass"
  2. Set the following properties (in the Properties View)
    Name: BaseObject
  3. Right click the BaseObject and select „New Child > EAttribute“
  4. Set the following properties (in the Properties View)
    Name: id
    Etype: EInt
  5. Right click the BaseObject and select „New Child > EAttribute“
  6. Set the following properties (in the Properties View)
    Name: propertyChangeSupport
    Etype: PropertyChangeSupport
    Changeable: false
    Transient: true
  7. Right click the BaseObject and select „New Child > EOperation“
  8. Set the following properties (in the Properties View)
    Name: addPropertyChangeListener
  9. Right click the addPropertyChangeListener and select „New Child > EParameter“
  10. Set the following properties (in the Properties View)
    Name: listener
    Etype: PropertyChangeListener
  11. Right click the BaseObject and select „New Child > EOperation“
  12. Set the following properties (in the Properties View)
    Name: removePropertyChangeListener
  13. Right click the removePropertyChangeListener and select „New Child > EParameter“
  14. Set the following properties (in the Properties View)
    Name: listener
    Etype: PropertyChangeListener
  15. Select Person-Class and set the ESuperTypes-Attribute to BaseObject
  16. Select Address-Class and set the ESuperTypes-Attribute to BaseObject

The final Ecore-Diagramm looks like this:

EMF RCP EMF11.png

EMF in detail:

  1. EData Type: TODO Explain EData Type
  2. EOperation: TODO Explain EOperation
  3. 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

  1. Select the addressbook.ecore in the Project-Explorer
  2. Right Click
  3. Select New > Other ...
  4. Select EMF Model
    EMF RCP EMF12.png
  5. Click Next until you reach this window where you press "Load"
    File:EMF RCP EMF13.png
  6. 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.

  1. Free public API from EMF Types
    1. Select the 1st Addressbook in the Tree
    2. Scroll in "Properties View" to the section "Model Feature Defaults"
    3. Change "Suppress EMF Types" to true
  2. Modify Base package name
    1. Select the 2nd Addressbook in the Tree
    2. Change the "Base Package" to "at.bestsolution.addressbook.core.model"

Time for code generation:

  1. Select the 1st Addressbook in the Ǵenmodel-Editor
  2. Right Click
  3. Select „Generate Model Code“

In the end your project looks like this:
EMF RCP EMF14.png

Analyze the generated Java-Code

General

Let's start at the top-level EMF has created 3 packages:

  • at.bestsolution.addressbook.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.addressbook.core.model.addressbook.impl: This package holds the implemenation for all interfaces from the afore mentionned package.
  • at.bestsolution.addressbook.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.addressbook.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.addressbook.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.addressbook.core.model.addressbook.impl from the list like this:

  1. Open your META-INF/MANIFEST.MF
  2. Navigate to the Runtime-Tab
  3. Remove at.bestsolution.addressbook.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 value
    Person 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 value
    Person 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 why setPrimaryAddress holds more code is that we modeled Person#primaryAddress and Address#person as EOpposite in our ecore model. Which means that when ever we set Person#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

  1. Generate the JUnit-Test-Code
    1. Open the .genmodel
    2. Right click on the Addressbook and select "Generate Test Code"
    3. In your workspace you find a new project called at.bestsolution.addressbook.core.model.addressbook.tests
  2. Open "at.bestsolution.addressbook.core.model.addressbook.tests.BaseObjectTest"
  3. Navigate to testAddPropertyChangeListener__PropertyChangeListener()
  4. remove @generated from the JavaDoc
  5. Write the Test-Case for the addPropertyChangeListener:
    1. Check that Listener is added
    2. Check that adding the listener is not triggering a change event
    3. 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]);
    }
    
  6. Navigate to testRemovePropertyChangeListener__PropertyChangeListener()
  7. remove @generated from the JavaDoc
  8. Write the Test-Case for the removePropertyChangeListener:
    1. Check that listener is removed
    2. Check that no change event is generated when listener is removed
    3. 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:

  1. Open BaseObjectImpl
  2. 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
  3. 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) {
            }
    
         });
    }
    
  4. 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
            );
        }
    }
    
  5. Implement addPropertyChangeListener(PropertyChangeListener) (remove the @generated!)
    public void addPropertyChangeListener(PropertyChangeListener listener) {
        propertyChangeSupport.addPropertyChangeListener(listener);
    }
    
  6. Implement removePropertyChangeListener(PropertyChangeListener) (remove the @generated!)
    public void removePropertyChangeListener(PropertyChangeListener listener) {
        propertyChangeSupport.removePropertyChangeListener(listener);
    }
    
  7. Run the JUnit-Test case for BaseObjectImpl. All tests should succeed else please check if you code looks like the one presented above.

EMF in detail:

  • EStructuralFeature: TODO Explain EStructuralFeature
  • Adapter: TODO Explain Adapter

The final result

The final model as a diagram

EMF RCP EMF15.png

The final code can be fetched from SVN-Freeze_01

Chapter Summary

In this chapter we learned step by step how to transform our UML model into real java-code using EMF and its source generating possibilities. We wrote a JUnit-Test to test the bits we implemented our own (from the rest we expect EMF is doing the correct thing e.g. automatic updating of parent-child relationships). EMF has much more to offer than simply creation of code and some of those possibilities we are going to explore late on.

Implementing at.bestsolution.addressbook.core.datasource

This plugin acts as the interface between our application and the datastorage implementations and hides them 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 achieve the same using OSGI-Services (see Article for more informations)

Create a NONE-UI Project

  1. Open the "New Project Wizard"
  2. Select the "Plug-in Project"
    EMF RCP DATASOURCE1.png
  3. Name the project: "at.bestsolution.addressbook.core.datasource"
    EMF RCP DATASOURCE2.png
  4. Uncheck "This plug-in will make contributions to the UI"
    EMF RCP DATASOURCE3.png
  5. Click Finish
  6. Open the MANIFEST.MF
  7. Open Runtime-Tab
  8. Click "Add..."
  9. Select "at.bestsolution.addressbook.core.datasource"
  10. Click "OK"
    EMF RCP DATASOURCE9.png
  11. Open Dependencies-Tab
  12. Click Add ... in the "Required Plug-ins"-Section
  13. Select "org.eclipse.emf.ecore.change"
  14. Click OK

Creating the "datasource" extension-point

  1. Open the MANIFEST.MF
  2. Click the Extension Points-Tab
    EMF RCP DATASOURCE5.png
  3. Click the Add...-Button
  4. Fill in the following values in the opened Window:
    • Extension Point ID: datasource
    • Extension Point Name: Datasource Provider

    EMF RCP DATASOURCE6.png

  5. Click the Definition-Tab
    EMF RCP DATASOURCE7.png
  6. Right Click the extension-Element in the tree
  7. Select: New > Element
  8. In the "Element Details"-Sections fill in the following fields:
    • Name: datasource
  9. Right Click the datasource-Element in the tree
  10. Select: New > Attribute
  11. In the "Attribute Details"-Section fill in the following fields:
    • Name: id
    • Use: required
  12. Right Click the datasource-Element in the tree
  13. Select: New > Attribute
  14. In the "Attribute Details"-Section fill in the following fields:
    • Name: label
    • Use: required
    • Translateable: true
  15. Right Click the datasource-Element in the tree
  16. Select: New > Attribute
  17. In the "Attribute Details"-Section fill in the following fields:
    • Name: class
    • Use: required
    • Type: java
    • Extends: at.bestsolution.addressbook.core.datasource.Datasource
  18. Right Click the extension-Element in the tree
  19. Select: New > Compositor > sequence
  20. Right Click the squence-Element in the tree
  21. Select: New > Reference > datasource
  22. In the "Compositor Details" check the Unbounded checkbox

The final result should look like this:
File:EMF RCP DATASOURCE4.png

Extension Points in detail:

In the above we created a so called "Extension Point" definition which allows others to contribute implementations and extend the functionality of the application. A possibile use case could be to add an implementation for a completely different datastorage system (e.g. one might use an LDAP-Server as the storage system). The advantage of extension points against programmatic contributions is the fact that the contributed bundle is only loaded (in OSGI terminology activated) when a class is accessed and not when the application is started.

Implementing the Java-Bits

The following UML diagram shows you all needed classes.
EMF RCP DATASOURCE8.png

Initial implementation
class DatasourceException

We create our own specialized exception which wrapping the orginal exception thrown by the datasource. This gives us the possibility to add features later on if we want (e.g. passing an Error-Code, a serverity, ... ).

package at.bestsolution.addressbook.core.datasource;

/**
 * Wraps the original execption which might be datasource specific (e.g. an
 * SQLException, ...)
 *
 * @author Tom Schindl <tom.schindl@bestsolution.at>
 *
 */
public class DatasourceException extends Exception {

	/**
	 *
	 */
	private static final long serialVersionUID = 1L;

	/**
	 * Create a new exception wraping the datasource specific one
	 *
	 * @param message
	 *            the error message
	 * @param the
	 *            wrapped exception
	 */
	public DatasourceException(String message, Throwable throwable) {
		super(message, throwable);
	}

}
class Datasource

This is the abstract definition of the datasource implementation. Concrete implementations are contributed using the "datasource" extension point we just declared above.

package at.bestsolution.addressbook.core.datasource;

import java.util.Collection;
import org.eclipse.emf.ecore.change.ChangeDescription;

/**
 * The abstract definition of the datasource implementation. Implementations are
 * contributed using "at.bestsolution.addressbook.datasource.datasource"
 * extension point
 *
 * @author Tom Schindl <tom.schindl@bestsolution.at>
 *
 */
public abstract class Datasource {

	/**
	 * Executed the specified query and return the result as a collection
	 *
	 * @param query
	 *            the query identifier
	 * @param parameter
	 *            parameter to execute the query (used in the where clause)
	 */
	public abstract Collection<java.lang.Object> find(java.lang.String query, Object parameter) throws DatasourceException;

	/**
	 * Persist the changes made since last persisted
	 *
	 * @param changeDescription
	 *            the modifications
	 */
	public abstract void persist(ChangeDescription changeDescription) throws DatasourceException;

}

You might have asked yourself when we create the plugin why we added the org.eclipse.emf.ecore.change plugin now you know why we need it.

EMF in detail:

EMF provides a full featured change recording mechanism with the org.eclipse.emf.ecore.change. The 2 most important classes in this plugin are:

  • org.eclipse.emf.ecore.change.util.ChangeRecorder: The recorder works like any recorder your know from the real world you can start/stop recording, build summeries, rolling back changes, ...
  • org.eclipse.emf.ecore.change.ChangeDescription: Represent the changes made to objects while the recorder is running
class DatasourceDescriptor

This class is responsible to hold the informations provided by the "datasource" extension point. This information is provided the org.eclipse.core.runtime.IConfigurationElement-Instance.

package at.bestsolution.addressbook.core.datasource;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;

/**
 * Describes a datasource contribution made using the
 * "at.bestsolution.addressbook.datasource.datasource" extension point
 *
 * @author Tom Schindl <tom.schindl@bestsolution.at>
 *
 */
public class DatasourceDescriptor {
	private Datasource datasource;

	private IConfigurationElement element;

	private static final String ATT_CLASS = "class";

	private static final String ATT_ID    = "id";

	private static final String ATT_LABEL = "label";

	DatasourceDescriptor(IConfigurationElement element) {
		this.element = element;
	}

	public String getId() {
		return element.getAttribute(ATT_ID);
	}

	Datasource getInstance() throws CoreException {

		if (datasource == null) {
			datasource = (Datasource) element.createExecutableExtension(ATT_CLASS);
		}
		return datasource;
	}

	public String getLabel() {
		return element.getAttribute(ATT_LABEL);
	}
}

You'll notice that the above creates the datasource instance lazily this has the advantage that the plugin is only activated on demand and significantly improves startup performance. A general advice: Whenever possible you should load bundles only on demand and not by default.

class DatasourceManager

The job of this class is to manage all datasource implementations contributed and providing access to them.

package at.bestsolution.addressbook.core.datasource;

import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.CopyOnWriteArrayList;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;


/**
 * This class manages all datasources contributed using the
 * "at.bestsolution.addressbook.datasource.datasource" extension point
 *
 * @author Tom Schindl <tom.schindl@bestsolution.at>
 *
 */
public class DatasourceManager {
	private static DatasourceManager INSTANCE;

	private Collection<DatasourceDescriptor> datasources;

	private DatasourceManager() {

	}

	public static DatasourceManager getInstance() {
            return null;
	}

	public Datasource getDatasource(DatasourceDescriptor descriptor) {
            return null;
	}

	public Collection<DatasourceDescriptor> getDatasourceDescriptors() {
            return null;
	}

}
class Activator

This class is generated automatically by PDE when the plugin is created. It is instantiated the first time a class from this bundle is loaded and afterwards the BundleActivator#start(BundleContext) is called. Currently this class is part of the public API of our plugin but this doesn't make much sense because nobody from the outside should access this class.

Finalizing
  1. Let's create a new package named at.bestsolution.addressbook.core.datasource.internal and move the class there because the internal package is not part of the exported ones nobody from the outside can access this class.
  2. Because ChangeDescription is part of our API all plugins who make use of our Datasource-API need to import the org.eclipse.emf.ecore.change-plugin and to make this more comfortable we can also tell this plugin to reexport this dependency this way all plugins importing this plugin will automatically import org.eclipse.emf.ecore.change.
    1. Open the Dependencies-Tab
    2. Select the org.eclipse.emf.ecore.change
    3. Click the "Properties..."
    4. Check the "Reexport this dependency" checkbox
      File:EMF RCP DATASOURCE10.png

Implementing JUnit-Tests

Setting up the plugin

As we did before we are writing plain JUnit-Testcases. Another possibility which is build above JUnit would be to use TPTP which is really cool project but has the big shortcoming that it doesn't work on OS-X until they solved this Bug and because we'd like to stay as cross-platform as possible this is not a possibility for us which is a sad thing because TPTP provide so many cool features.

The setup process:

  1. File > New > Project
  2. Select "Plug-in Project"
    EMF RCP DATASOURCE11.png
  3. Enter "Project name" "at.bestsolution.addressbook.core.datasource.tests"
    EMF RCP DATASOURCE12.png
  4. Uncheck the "This plug-in will make contributions to the UI" and Select "Would you like to create a rich client application": No
    EMF RCP DATASOURCE13.png
  5. Click Finish
  6. Open the MANIFEST.MF
  7. Click the Dependencies-Tab
  8. Click the "Add..." button in "Required Plug-ins" section
  9. Search for the "org.junit" plugin (we are not using JUnit4 in this article). Click OK
  10. Search for the "at.bestsolution.addressbook.core.datasource" plugin. Click OK
  11. The page looks like the following:
    File:EMF RCP DATASOURCE14.png
Create the Java-Classes I
DummyDatasource

This is the class we are going to contribute using the extension point.

package at.bestsolution.addressbook.core.datasource.tests;

import java.util.Collection;

import org.eclipse.emf.ecore.change.ChangeDescription;

import at.bestsolution.addressbook.core.datasource.Datasource;
import at.bestsolution.addressbook.core.datasource.DatasourceException;

public class DummyDatasource extends Datasource {

	@Override
	public Collection<Object> find(String query, Object parameter) throws DatasourceException {
		return null;
	}

	@Override
	public void persist(ChangeDescription changeDescription) throws DatasourceException {

	}

}
Contributing the Datasource
  1. Open the MANIFEST.MF
  2. Click the "Extensions"-Tab
  3. Click the "Add..." button
  4. Select the "at.bestsolution.addressbook.core.datasource.datasource" entry
  5. Click OK
  6. Enter the following data into the form:
    id: at.bestsolution.addressbook.core.datasource.tests.datasource1
    label: DummyDatasource1
    class: at.bestsolution.addressbook.core.datasource.tests.DummyDatasource
  7. Right click on "at.bestsolution.addressbook.core.datasource.datasource" and select "datasource"
  8. Enter the following data into the form:
    id: at.bestsolution.addressbook.core.datasource.tests.datasource2
    label: DummyDatasource2
    class: at.bestsolution.addressbook.core.datasource.tests.DummyDatasource

EMF RCP DATASOURCE 15.png

Create the Java-Classes II
DatasourceManagerTestCase

This class implements tests for the API provided by DatasourceManager.

package at.bestsolution.addressbook.core.datasource.tests;

import java.util.Collection;

import at.bestsolution.addressbook.core.datasource.Datasource;
import at.bestsolution.addressbook.core.datasource.DatasourceDescriptor;
import at.bestsolution.addressbook.core.datasource.DatasourceManager;
import junit.framework.TestCase;

public class DatasourceManagerTestCase extends TestCase {

	public void testGetInstance() {
		DatasourceManager mgr1 = DatasourceManager.getInstance();
		assertNotNull("Manager can't be null", mgr1);

		DatasourceManager mgr2 = DatasourceManager.getInstance();
		assertSame("The instance must be always the same",mgr1, mgr2);
	}

	public void testGetDatasourceDescriptors() {
		DatasourceManager mgr = DatasourceManager.getInstance();
		Collection<DatasourceDescriptor> list = mgr.getDatasourceDescriptors();
		assertNotNull("Descriptorlist was null", list);
		assertEquals("Descriptorlist has wrong size", 2, list.size());
	}

	public void testGetDatasource() {
		DatasourceManager mgr = DatasourceManager.getInstance();
		Collection<DatasourceDescriptor> list = mgr.getDatasourceDescriptors();
		DatasourceDescriptor desc = list.iterator().next();

		Datasource datasource1 = mgr.getDatasource(desc);
		assertTrue("Descriptor not instance of DummyDatasource", datasource1 instanceof DummyDatasource);
		assertSame("The datasource instance was not always the same", datasource1, mgr.getDatasource(desc));
	}
}
DatasourceDescriptorTestCase

This class implements test for the API provided by DatasourceDescriptor.

package at.bestsolution.addressbook.core.datasource.tests;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Iterator;

import at.bestsolution.addressbook.core.datasource.Datasource;
import at.bestsolution.addressbook.core.datasource.DatasourceDescriptor;
import at.bestsolution.addressbook.core.datasource.DatasourceManager;
import junit.framework.TestCase;

public class DatasourceDescriptorTestCase extends TestCase {

	public void testGetId() {
		Collection<DatasourceDescriptor> descs = DatasourceManager.getInstance().getDatasourceDescriptors();
		Iterator<DatasourceDescriptor> it = descs.iterator();
		assertEquals("at.bestsolution.addressbook.core.datasource.tests.datasource1", it.next().getId());
		assertEquals("at.bestsolution.addressbook.core.datasource.tests.datasource2", it.next().getId());
	}

	public void testGetInstance() throws Exception {
		Collection<DatasourceDescriptor> descs = DatasourceManager.getInstance().getDatasourceDescriptors();
		Iterator<DatasourceDescriptor> it = descs.iterator();
		DatasourceDescriptor desc = it.next();
		Method m = DatasourceDescriptor.class.getDeclaredMethod("getInstance", new Class[0]);
		m.setAccessible(true);
		Datasource ds1 = (Datasource) m.invoke(desc, new Object[0]);
		Datasource ds2 = (Datasource) m.invoke(desc, new Object[0]);

		assertNotNull("The datasource instance can never be null. An exception should have been thrown",ds1);
		assertSame("The instance returned from the descriptor has to be always the same instance", ds1, ds2);

		desc = it.next();
		Datasource ds3 = (Datasource) m.invoke(desc, new Object[0]);
		assertNotSame("The datasource instance have to be different", ds1, ds3);
		assertSame("The datasource class has to be the same", ds1.getClass(), ds3.getClass());
	}

	public void testGetLabel() {
		Collection<DatasourceDescriptor> descs = DatasourceManager.getInstance().getDatasourceDescriptors();
		Iterator<DatasourceDescriptor> it = descs.iterator();
		assertEquals("The label for the first datasource is not correct","DummyDatasource1", it.next().getLabel());
		assertEquals("The label for the second datasource is not correct","DummyDatasource2", it.next().getLabel());
	}

}
DatasourceAllTests

In the end we combine the 2 test classes into a TesSuite.

package at.bestsolution.addressbook.core.datasource.tests;

import junit.framework.Test;
import junit.framework.TestSuite;

public class DatasourceAllTests extends TestSuite {
	public DatasourceAllTests(String name) {
		super(name);
		addTestSuite(DatasourceManagerTestCase.class);
		addTestSuite(DatasourceDescriptorTestCase.class);
	}

	public static Test suite() {
		TestSuite suite = new DatasourceAllTests("Datasource Test-Suite");
		return suite;
	}
}

The only important thing is that we add a public static Test suite() method which is needed to run the suite as a "JUnit Plug-in Test".

Running the JUnit-Tests

Running the JUnit-Tests is trickier than a simple JUnit-Test because to take the extension point mechanism we are using to contribute our DummyDatasources the whole TestSuite has to run as an OSGI (or better Equinox) application. Nonetheless it's not fairly though to make this work because PDE provides the possibility to run the Suite in plugin-mode. The only thing is that the TestSuite-class has to have a method named: suite().

  1. Open context menu of DatasourceAllTests in Package Explorer
  2. Run As > JUnit Plug-in Test

EMF RCP DATASOURCE16.png

Running the suite we'll see all Tests are failing. So the logical next step is to fix the tests step by step.

Implementatiation of Java-Classes or let's get all tests succeed

DatasourceManager#getInstance()

The manager is implemented as a Singleton so the implementation should be fairly straight forward for you.

// ...
	public static DatasourceManager getInstance() {
		if (INSTANCE == null) {
			INSTANCE = new DatasourceManager();
		}

		return INSTANCE;
	}
// ...
DatasourceManager#getDatasourceDescriptors()

Here we see how we get access to the datasources contributed to us using our well defined extension point. In the end we ensure that noone from external can programmatically add a datasources by modifing the returned collection by create an unmodifiable one which is good programming praxis.

// ...
	private DatasourceManager() {
		this.datasources = new CopyOnWriteArrayList<DatasourceDescriptor>();
		for (IConfigurationElement element : Platform.getExtensionRegistry().getConfigurationElementsFor(Activator.PLUGIN_ID, "datasource")) {
			datasources.add(new DatasourceDescriptor(element));
		}
	}
// ...
	public Collection<DatasourceDescriptor> getDatasourceDescriptors() {
		return Collections.unmodifiableCollection(datasources);
	}
// ...
DatasourceManager#getDatasource(DatasourceDescriptor)

Because DatasourceDescriptor#getInstance() is only package-scoped the only possibility to get access to a datasource is to call this method.

//...
	public Datasource getDatasource(DatasourceDescriptor descriptor) {
		try {
			return descriptor.getInstance();
		} catch (CoreException e) {
			Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e));
		}

		return null;
	}
//...

Chapter Summary

Back to the top