Generic Eclipse 3.x views, editors and handlers with DI – by René Brandstetter

In this blog entry I will show you three independent features provided by Eclipse and OSGi which can work together to create views, editors or handlers via the Eclipse ExtensionRegistry but still use the dependency mechanism of Eclipse 4.

Supersede the ExtensionRegistry creation process of an object (an almost forgotten hidden feature)

The Eclipse ExtensionRegistry allows you as an extension provider to not only contribute your custom implementation but to also be part of the “creation & initialization” process. So whenever an ExtensionPoint needs to create an instance of a specific class (e.g.: <view name="View" class="com.example.legacy3x.View" id="com.example.legacy3x.view"/>) it uses the method IConfigurationElement#createExecutableExtension(String attributeName). This method is responsible for creating an instance of the class via the bundle which provides the extension and has the following workflow:

  1. Retrieve the class name from the extension
  2. Create an instance of the class
  3. If the instance implements IExecutableExtension, call its setInitializationData() method with the initialization data from the provided extension (see next paragraph for details on how this is done)
  4. Now check if the instance implements IExecutableExtensionFactory:
    • If so, return the value object retrieved from its create() method
    • If it doesn’t implement this interface, it just returns the already created instance from step no. 2

With these two interfaces you have the following possibilities:

  • neither implement IExecutableExtension nor IExecutableExtensionFactory – a new instance of the named class is returned (this is more or less the most used scenario)
  • only implement IExecutableExtension – an instance of the named class is created and its setInitializationData() method is called (can be seen as an @PostConstruct)
  • only implement IExecutableExtensionFactory – an instance of the mentioned IExecutableExtensionFactory is created but the object created via IExecutableExtensionFactory#create() is returned and not the IExecutableExtensionFactory instance (as the name already implies that it is the Factory-Pattern)
  • both, IExecutableExtension and IExecutableExtensionFactory, are implemented – which creates an instance of the mentioned IExecutableExtensionFactory, configures it via IExecutableExtension#setInitializationData() and afterwards returns the object created via IExecutableExtensionFactory#create() (can be seen as a configurable factory)

Now that we know how the creation process can be influenced lets take a look at how we can specify the initialization data.

IExecutableExtension defines the method “void setInitializationData(IConfigurationElement config, String propertyName, Object data) throws CoreException;” which gets:

  • config – the contributed extension on which the IConfigurationElement#createExecutableExtension(String attributeName) was called
  • propertyName – the name of the attribute which causes the create (=attributeName of the method IConfigurationElement#createExecutableExtension(String attributeName))
  • data – the additional initialization data you are able to configure

The additional configured initialization data is an object and can have 2 different types depending on how the initialization data is specified.

  • data instanceof String: the initialization data was directly specified in the attribute by separating the class name and the initialization data with a colon (e.g.: class="com.example.legacy3x.View:hereYouCanSpecifyAllInitializationDataAsAString"; data would be "hereYouCanSpecifyAllInitializationDataAsAString")
  • data instanceof Map: the initialization data was defined as key/value pairs in a separate tag named exactly the same as the attribute name (e.g.:
    <view name="View"
      id="com.example.legacy3x.view">
      <class class="com.example.legacy3x.View">
        <parameter name="key 1" value="value 1" />
        <parameter name="key 2" value="value 2" />
      </class>
    </view>

    ). For the transfer of the attribute name to the tag name there is no UI and you have to do this in the XML source directly. Just remove the attribute, create a tag named like the attribute (in our example this is “class“), and provide an attribute named “class” on it which holds the full qualified name of the real class. All code<parameter> sub-tags of your tag will become the key/value pairs of the provided Map.

    BecomingATag

Get an appropriate IEclipseContext

In the Eclipse ExtensionRegistry or in a legacy 3.x Eclipse which uses the “org.eclipse.e4.tools.compat” bundle you probably won’t have an E4-ApplicationModel object to retrieve an IEclipseContext from it. So how can you retrieve it in these situations?

Keep in mind that the IEclipseContext is organized in a hierarchical structure which reflects more or less the structure of the UI.

e4_IEclipseContext_hierarchy

This also represents more or less the structure of an Eclipse 3.x application.

e3_UI_hierarchy

A closer look at the IWorkbench* – classes above shows that all of them extend the IServiceLocator interface and so we have the possibility to call the IServiceLocator#getService(Class) method on them. The good news about this method is that it will return the IEclipseContext instance associated with the IWorkbench*-object whenever you invoke it with IEclipseContext.class as the argument. This means retrieving the IEclipseContext of the currently active window is done by the following method call:

PlatformUI.getWorkbench().getActiveWorkbenchWindow().getService(IEclipseContext.class)

or retrieving it from the application it would require:

PlatformUI.getWorkbench().getService(IEclipseContext.class)

Load classes from specific bundles

In OSGi it’s a bad habit to load classes yourself but sometimes (hopefully in really rare cases) it happens that you want to provide a functionality which has to load a specific class from another bundle that your code isn’t aware of (means it doesn’t know it and so can’t define an Import-Package entry in the MANIFEST.MF). But as Eclipse 4.x shows there must be a way of doing this. To load a class from a bundle only known at runtime your code has to know the following:

  1. the symbolic name of the bundle which holds the class
  2. the full qualified name of the class to load
  3. this would be nice to have but is sadly always ignored because of easier handling: the version or version range of the bundle which holds the class

How can this information be put into a String format? The best and easiest way is to put it in an URI which has a standardized format and this is how it’s done in e4 with the “bundleclass://“-references. For example “bundleclass://com.example.legacy3x.view/com.example.legacy3x.View“, the host-part of the URI would be the bundle symbolic name (=com.example.legacy3x.view) and the full qualified name of the class is held in the first path element (=com.example.legacy3x.View).

Now that we know how to present this information we can ask the OSGi environment to give us the required bundle and with the help of this bundle we can then load the class. To retrieve the bundle form a pure OSGi environment you have 3 possibilities depending on the OSGi version you’re using:

  1. OSGi Version < r4v43 where PackageAdmin isn’t deprecated: retrieve the PackageAdmin service from the OSGi service registry and call org.osgi.service.packageadmin.PackageAdmin.getBundles(String symbolicName, String versionRange)
  2. OSGi Version > r4v43 and < r6 or if you want to be OSGi version independent: use a BundleTracker implementation similar to the one in org.eclipse.e4.ui.internal.workbench.BundleFinder
  3. OSGi Version >= r6: here a new method exists FrameworkWiring#findProviders(Requirement) which can be used to query the requirement/capabilities framework of OSGi

In the following example, I will show you only the latest way of retrieving a bundle from OSGi via FrameworkWiring#findProviders(Requirement), because the other 2 options are self-explanatory.

// ... unimportant imports and null checks omitted for simplicity ...
import org.osgi.framework.Constants;
import org.osgi.framework.namespace.IdentityNamespace;
import org.osgi.framework.wiring.BundleCapability;
import org.osgi.framework.wiring.FrameworkWiring;
import org.osgi.resource.Namespace;
import org.osgi.resource.Requirement;
import org.osgi.resource.Resource;
// ... other imports ...
 
static <T> Class<T> loadClass(final String bundleName, String className) throws ClassNotFoundException {
      // retrieve the FrameworkWiring: according to the OSGi specification only the system bundle can be adapted to this
      FrameworkWiring frameworkWiring = context.getBundle(Constants.SYSTEM_BUNDLE_ID).adapt(FrameworkWiring.class);
          
      // build FrameworkWiring search criteria to find a specific bundle with a given name
      Requirement requirement = new Requirement() {
            @Override
            public Resource getResource() {
              return null// requirement is synthesized hence null
            }
            
            @Override
            public String getNamespace() {
              return IdentityNamespace.IDENTITY_NAMESPACE;
            }
            
            @Override
            public Map<String, String> getDirectives() {
              return Collections.<String, String> singletonMap(Namespace.REQUIREMENT_FILTER_DIRECTIVE, "(" + IdentityNamespace.IDENTITY_NAMESPACE + "=" + bundleName + ")");
            }
            
            @Override
            public Map<String, Object> getAttributes() {
              return Collections.<String, Object>emptyMap();
            }
      };
      Collection<BundleCapability> capabilities = frameworkWiring.findProviders(requirement);  // query the OSGi environment for a bundle with the given symbolic name
          
      // we didn't specify a version for simplicity so we take the bundle with the highest version
      Bundle bundle = null;
      for( BundleCapability capability : capabilities ){
        Bundle b = capability.getRevision().getBundle();
        if( bundle == null || b.getVersion().compareTo(bundle.getVersion()) > 0 ){
          bundle = b;
        }
      }
      // in case the bundle exists, the bundle with the given symbolic name and the highest version is held in the "bundle" variable
      return (Class<T>)bundle.loadClass(className);
  }

The for-loop over the retrieved capabilities only exists because in the example bundle versions are not taken into account. If you enrich your URI with a version (e.g.: bundleclass://com.example.legacy3x.view/com.example.legacy3x.View?v=1.0.0) you could enrich the filter provided in the directives variable with this information too. For this the filter should look like: "(&(" + IdentityNamespace.IDENTITY_NAMESPACE + "=" + bundleName + ")(" + IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE + "=" + bundleVersion + "))"

Hint: If you stick to Eclipse as the OSGi environment and use the org.eclipse.core.runtime bundle, you could also use the static methods getBundle(String symbolicName) or getBundles(String symbolicName, String version) of org.eclipse.core.runtime.Platform.

Plumbing everything together for a generic DIViewPart

Now that we know how to

  • supersede the ExtensionRegistry class creation mechanism,
  • retrieve an IEclipseContext from different Workbench elements and
  • load a class from another “unknown” bundle

we can use this information to extend the DIViewPart, DIEditorPart and DIHandler classes of the org.eclipse.e4.tools.compat bundle to be more generic than they are now.

For this you have to create a simple bundle which uses the above mentioned hints to:

  • provide a configurable IExecutableExtensionFactory
    • Example: public class DIFactoryForE3Elements implements IExecutableExtension, IExecutableExtensionFactory{ … }
    • Hint: The package of this factory should also be exported so it can be used in other bundles.
  • in its setInitializationData() method retrieve the required information to load the POJO class
    • Example: the data object could be a bundleclass://-URI
      URI bundleClass = new URI((String)data);
      String bundleName = bundleClass.getHost();
      String className = bundleClass.getPath();
  • in its create() method instantiate, depending on the used extension point (handler, view or editor), the appropriate DI*-class of the org.eclipse.e4.tools.compat bundle
    • Example: return new DIHandler<>(pojoClass);
    • Hint: The DI*-classes internally use the shown techniques to retrieve the appropriate IEclipseContext.

After you’ve created this bundle, you can use the created IExecutableExtensionFactory in all bundles which want to provide an “e4-like-POJO-based” view, editor or handler. To do so, you just have to:

  • write a POJO class with the appropriate e4-annotations (@Inject, @PostConstruct, …)
  • import the package which holds your IExecutableExtensionFactory implementation
  • create the appropriate extension entry and use your IExecutableExtensionFactory
    • Example:
      <extension point="org.eclipse.ui.views">
        <view allowMultiple="true"
           class="com.example.e3.with.di.DIFactoryForE3Elements:bundleclass://com.example.e3.with.di.usage/com.example.e3.with.di.usage.PojoView"
           id="com.example.e3.with.di.usage.pojoview"
           name="DI enabled View"
           restorable="true">
        </view>
      <extension>

Final Words

If my explanations are a little bit confusing or if you just want to see it in action, take a look at the following GitHub links:

 

Many thanks to:

Lars Vogel for once again motivating me to write this blog entry

and

Tom Schindl, the mastermind behind the “how to make the DI*-classes generic “ idea, for allowing me to present it here.

And as usual, sorry for being too chatty and for all the typos and errors: comment on them or keep them ;-).

This entry was posted in Eclipse, Lars Vogel and tagged , , , , , . Bookmark the permalink.