Sunday, November 30, 2014

IAdaptable - GEF4's Interpretation of a Classic

Adaptable Objects as a Core Pattern of Eclipse Core Runtime

The adaptable objects pattern is probably the most important one used by the Eclipse core runtime. Formalized by the org.eclipse.core.runtime.IAdaptable interface, an adaptable object can easily be queried by clients (in a type-safe manner) for additional functionality that is not included within its general contract.
public interface IAdaptable {
  /**
   * Returns an object which is an instance of the given class
   * associated with this object. Returns <code>null</code> if
   * no such object can be found.
   *
   * @param adapter the adapter class to look up
   * @return a object castable to the given class, 
   *    or <code>null</code> if this object does not
   *    have an adapter for the given class
   */
   public Object getAdapter(Class adapter);
}
From another viewpoint, if an adaptable object properly delegates its getAdapter(Classimplementation to an IAdapterManager (most commonly Platform.getAdapterManager()) or provides a respective proprietary mechanism on its own, it can easily be extended with new functionality (even at runtime), without any need for local changes, and adapter creation can flexibly be handled through a set of IAdapterFactory implementations.

Why org.eclipse.core.runtime.IAdaptable is not perfectly suited for GEF4 

As it has proven its usefulness in quite a number of places, I considered the adaptable objects pattern to be quite a good candidate to deal with the configurability and flexibility demands of a graphical editing framework as well. I thus wanted to give it a major role within the next generation API of our model-view-controller framework (GEF4 MVC).

GEF4 MVC is the intended replacement of GEF (MVC) 3.x as the core framework, from which to build up graphical editors and views. As it has a high need for flexibility and configurability it seemed to be the ideal playground for adaptable objects. However, the way the Eclipse core runtime interprets the adaptable objects pattern does not make it a perfect math to fulfill our requirements, because:
  • Only a single adapter of a specific type can be registered at an adaptable. Registering two different Provider implementations at a controller (e.g. one to specify the geometry used to display feedback and one to depict where to layout interaction handles) is for instance not possible.
  • Querying (and registering) adapters for parameterized types is not possible in a type-safe manner. The class-based signature of getAdapter(Classdoes for instance not allow to differentiate between a Provider<IGeometry> and a Provider<IFXAnchor>.
  • IAdaptable only provides an API for retrieving adapters, not for registering them, so (re-) configuration of adapters at runtime is not easily possible. 
  • Direct support for 'binding' an adapter to an adaptable object, i.e. to establish a reference from the adapter to the adaptable object, is not offered (unless the adapter explicitly provides a proprietary mechanism to establish such a back-reference).

Adaptable Objects as Interpreted by GEF4 Common

I thus created my own interpretation of the adaptable objects pattern. It is provided by the GEF4 Common component and can thus be easily used standalone, even for applications that have no use for graphical editors or views (GEF4 common only requires Google Guice and Google Guava to run).

The core concept of the GEF4 Common adaptable objects mechanism of course is the adaptable contract, here formalized as org.eclipse.gef4.common.adapt.IAdaptable. Leaving out the additional syntactic sugar, it basically boils down to the following:
public interface IAdaptable extends IPropertyChangeNotifier {
  ...

  /**
   * Returns an adapter for the given {@link AdapterKey} if one can
   * unambiguously be retrieved, i.e. if there is only a single adapter that
   * 'matches' the given {@link AdapterKey}.
   * <p>
   * An adapter 'matching' the {@link AdapterKey} is an adapter, which is
   * registered with an {@link AdapterKey}, whose {@link TypeToken} key (
   * {@link AdapterKey#getKey()}) refers to the same type or a sub-type of the
   * given {@link AdapterKey}'s {@link TypeToken} key (@see
   * {@link TypeToken#isAssignableFrom(TypeToken)} and whose role (
   * {@link AdapterKey#getRole()})) equals the role of the given
   * {@link AdapterKey}'s role.
   * <p>
   * If there is more than one adapter that 'matches' the given
   * {@link AdapterKey}, or there is no one 'matching' it, <code>null</code>
   * will be returned.
   * 
   * @param key
   *            The {@link AdapterKey} used to retrieve a registered adapter.
   * @return The unambiguously retrievable adapter for the given
   *         {@link AdapterKey} or <code>null</code> if none could be
   *         retrieved.
   */
   public <T> T getAdapter(AdapterKey<? super T> key);

  /**
   * Registers the given adapter under the given {@link AdapterKey}. The
   * adapter has to be compliant to the {@link AdapterKey}, i.e. it has to be
   * of the same type or a sub-type of the {@link AdapterKey}'s type key (
   * {@link AdapterKey#getKey()}).
   * <p>
   * If the given adapter implements {@link IAdaptable.Bound}, the adapter
   * will obtain a back-reference to this {@link IAdaptable} via its
   * {@link IAdaptable.Bound#setAdaptable(IAdaptable)} method.
   * 
   * @param key
   *            The {@link AdapterKey} under which to register the given
   *            adapter.
   * @param adapter
   *            The adapter to register under the given {@link AdapterKey}.
   */
   public <T> void setAdapter(AdapterKey<? super T> key, T adapter);

  /**
   * Unregisters the adapter registered under the exact {@link AdapterKey}
   * given, returning it for convenience.
   * <p>
   * If the given adapter implements {@link IAdaptable.Bound}, the
   * back-reference to this {@link IAdaptable} will be removed via its
   * {@link IAdaptable.Bound#setAdaptable(IAdaptable)} method, passing over
   * <code>null</code>.
   * 
   * @param key
   *            The {@link AdapterKey} for which to remove a registered
   *            adapter.
   * @return The adapter, which has been removed.
   */
   public <T> T unsetAdapter(AdapterKey<? super T> key);
}
However, let's have a look at all the details to understand the mechanism in all detail:


AdapterKey to combine Type with Role

Instead of a simple Class-based type-key, adapters may now be registered by means of an AdapterKey, which combines (a Class- or TypeToken-based) type key (to retrieve the adapter in a type-safe manner) with a String-based role.

The combination of a type key with a role allows to register several adapters of the same type under different roles. Two different Provider implementations can for instance now easily be registered (to provide independent geometric information for selection feedback and selection handles) through:
setAdapter(AdapterKey.get(new TypeToken<Provider<IGeometry>>(){}, "selectionFeedbackGeometryProvider"), selectionFeedbackGeometryProvider)
setAdapter(AdapterKey.get(new TypeToken<Provider<IGeometry>>(){}, "selectionHandlesGeometryProvider"), selectionHandlesGeometryProvider)

TypeToken instead of Class

The second significant difference is that a com.google.common.reflect.TypeToken (provided by Google Guava) is used as a more general concept instead of a Class, which enables parameterized adapters to be registered and retrieved in a type-safe manner as well. A geometry provider can for instance now be easily retrieved through getAdapter(new TypeToken<Provider<IGeometry>>(){}), while an anchor provider can alternatively be retrieved through getAdapter(new TypeToken<Provider<IFXAnchor>>(){}). For convenience, registering and retrieving adapters by means of Class-based type keys is also supported (which will internally be converted to a TypeToken).

IAdaptable as a local adapter registry

In contrast to the Eclipse core runtime interpretation, an org.eclipse.gef4.common.adapt.IAdaptable has the obligation to provide means to not only retrieve adapters (getAdapter()) but also register or unregister them (setAdapter(), unsetAdapter()). This way, the 'configuration' of an adaptable can easily be changed at runtime, even without providing an adapter manager or factory.

Of course this comes at the cost that an org.eclipse.gef4.common.adapt.IAdaptable is itself responsible of maintaining the set of registered adapters. This (and the fact that the interface contains a lot of convenience functions) is balanced by the fact that a base implementation (org.eclipse.gef4.common.adapt.AdaptableSupport) can easily be used as a delegate to realize the IAdaptable interface.

IAdaptable.Bound for back-references

If adapters need to be 'aware' of the adaptable they are registered at, they may implement the IAdaptable.Bound interface, which is used to establish a back reference from the adapter to the adaptable. It is part of the IAdaptable-contract that an adapter implementing the IAdaptable.Bound will be provided with a back-reference during registration (if an adaptable uses org.eclipse.gef4.common.adapt.AdaptableSupport to internally realize the interface, this contract is  of course guaranteed). 

IAdaptables and Dependency Injection

While the possibility to re-configure the registered adapters at runtime is quite helpful, proper support to create an initial adapter configuration during instantiation of an adaptable is also of importance. To properly support this, I integrated the GEF4 Common adaptable objects mechanism with Google Guice. 

That is, the adapters that are to be registered at an adaptable can be configured in a Guice module, using a specific AdapterMap binding (which is based on Guice's multi-bindings). To register an adapter of type VisualBoundsGeometryProvider at a FXGeometricShapePart adaptable can for instance be performed using the following Guice module configuration:
protected void configure() {
  // enable adapter map injection support
  install(new AdapterMapInjectionSupport());
  // obtain map-binder to bind adapters for FXGeometricShapePart instances
  MapBinder<AdapterKey<?>, Object> adapterMapBinder
AdapterMaps.getAdapterMapBinder(binder(), FXGeometricShapePart.class);
  // bind geometry provider for selection handles as adapter on FXGeometricShapePart
  adapterMapBinder.addBinding(
AdapterKey.get(new TypeToken<Provider<IGeometry>>(){},"selectionHandlesGeometryProvider")).
        to(VisualBoundsGeometryProvider.class);
  ...
}
It will not only inject a VisualBoundsGeometryProvider instance as an adapter to all direct instances of FXGeometricShapePart but also to all instances of its sub-types, which may be seen as a sort of 'polymorphic multi-binding'.

Two prerequisites have to be fulfilled in order to make use of adapter-map injections:

  1. Support for adapter-map injections has to be enabled in your Guice module by installing an org.eclipse.gef4.common.inject.AdapterMapInjectionSupport module as outlined in the snippet above.
  2. The adaptable class (here: FXGeometricShapePart.class) or any of its super-classes has to offer a method that is annotated with @Inject (com.google.inject.Inject) and that provides a single parameter of type Map<AdapterKey<?>, Object>, which is annotated with @AdapterMap (org.eclipse.gef4.common.inject.AdapterMap):
@Inject(optional = true)
public void setAdapters(@AdapterMap Map<AdapterKey<?>, Object> adaptersWithKeys) {
  // TODO: implement (probably by delegating to an AdaptableSupport)
}
GEF4 MVC makes use of this mechanism quite intensively for the configuration of adapters (and indeed, within the MVC framework, more or less everything is an adapter). However, similar to the support for adaptable objects itself, the related injection mechanism is easily usable in a standalone scenario. Feel free to do so!