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:
- Retrieve the class name from the extension
- Create an instance of the class
- If the instance implements
IExecutableExtension
, call itssetInitializationData()
method with the initialization data from the provided extension (see next paragraph for details on how this is done) - 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
- If so, return the value object retrieved from its
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 itssetInitializationData()
method is called (can be seen as an@PostConstruct
) - only implement
_IExecutableExtensionFactory_
– an instance of the mentionedIExecutableExtensionFactory
is created but the object created viaIExecutableExtensionFactory#create()
is returned and not theIExecutableExtensionFactory
instance (as the name already implies that it is the Factory-Pattern) - both,
_IExecutableExtension_
and_IExecutableExtensionFactory_
, are implemented – which creates an instance of the mentionedIExecutableExtensionFactory,
configures it viaIExecutableExtension#setInitializationData()
and afterwards returns the object created viaIExecutableExtensionFactory#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 theIConfigurationElement#createExecutableExtension(String attributeName)
was called_propertyName_
- the name of the attribute which causes the create (=attributeName
of the methodIConfigurationElement#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”>
</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 codesub-tags of your tag will become the key/value pairs of the provided `Map`.
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.
This also represents more or less the structure of an Eclipse 3.x application.
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:
- the symbolic name of the bundle which holds the class
- the full qualified name of the class to load
- 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:
- OSGi Version < r4v43 where
**PackageAdmin**
isn’t deprecated: retrieve thePackageAdmin
service from the OSGi service registry and callorg.osgi.service.packageadmin.PackageAdmin.getBundles(String symbolicName, String versionRange)
- OSGi Version > r4v43 and < r6 or if you want to be OSGi version independent: use a
BundleTracker
implementation similar to the one inorg.eclipse.e4.ui.internal.workbench.BundleFinder
- 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
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.
- Example:
- in its
setInitializationData()
method retrieve the required information to load the POJO class-
Example: the data object could be a
bundleclass://
-URIURI 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 theorg.eclipse.e4.tools.compat
bundle- Example:
**return** **new** DIHandler<>(pojoClass);
- Hint: The DI*-classes internally use the shown techniques to retrieve the appropriate
IEclipseContext
.
- Example:
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>
-
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:
- the black magic
IExecutableExtensionFactory
- the usage of the black magic
- the entire test application
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 ;-).