null
IAdaptable is a
key interface in the Eclipse arsenal. For old hands, its use trips off
the fingers like exception handling or abstract classes. For the
uninitiated, it may be quite scary. This article looks at what the IAdaptable
interface is, and how it can be used to extend existing functionality
within Eclipse.
Since Java is a strongly typed language, each instance has a type associated with it. There are actually two sorts of type; the declared type and the runtime type (also known as the static type and dynamic type respectively). Weakly typed languages like Python are often called "untyped", but that's not strictly true; every instance has a runtime type -- you just don't need to declare it before use.
Back to Java; in order to invoke a method on an object, it needs to be visible at the declared type. In other words, you can only invoke methods that are defined in the parent type, even if the instance is of a known subtype:
List list = new ArrayList();
list.add("data"); // this is OK, list is valid
list.ensureCapacity(4); // this is not, ensureCapacity() is ArrayList only
If we need to invoke methods on the actual type, we first need to
cast it to the appropriate type. In this case, we can cast ArrayList
to List, because the ArrayList type implements
the List interface. You can even test for this dynamically
at runtime using list instanceof ArrayList.
Unfortunately, a class doesn't always implement the interface you need. It might not implement it because there are only some cases where it is valid, or because it is a type in an unrelated library, or because the interface was developed after the original class was written.
This is where IAdaptable comes in. You can think of IAdaptable
as a way of performing casts dynamically, rather than statically. So
instead of using direct casting:
Object o = new ArrayList(); List list = (List)o;
we could do something like:
IAdaptable adaptable = new ArrayList(); List list = (List)adaptable.getAdapter(java.util.List.class);
You can read this as a dynamic cast; what we're trying to do is
convert adaptable into a List instance.
So why bother with the extra step of getAdapter() when
you can just use the cast directly? Well, this mechanism allows us to
cast even when the target class doesn't implement the interface. For
example, we might want to be able to use a HashMap as a List,
despite the two otherwise incompatible classes. We could have:
IAdaptable adaptable = new HashMap(); List list = (List)adaptable.getAdapter(java.util.List.class);
Most implementations of IAdaptable look like a nested if
statement for supported classes. If we were to implement getAdapter()
for our HashMap class, it might look like:
public class HashMap implements IAdaptable {
public Object getAdapter(Class clazz) {
if (clazz == java.util.List.class) {
List list = new ArrayList(this.size());
list.addAll(this.values());
return list;
}
return null;
}
// ...
}
What we're doing is returning an adapter to ourselves (or more
specifically in this case, a copy), rather than doing the cast directly
to the type. If the request is for a non-supported class, the convention
is to return null to indiciate failure, rather than
throwing an exception. (Thus, when using adapters, it's not generally
safe to assume they will return non-null value.)
Of course, it would be a pain to have to edit the class when you want
to add a new 'adaptable' type; and in any case, if you've got the type,
why not modify the interface? Well, there may be good reasons why you
don't want to modify the class (it's less easy to support backwardly
compatible changes if you're using interfaces) or change its type (a HashMap
is not a List, but can be conveted into one).
To solve this problem, there's an abstract class that is used by most
parts of Eclipse called PlatformObject.
This implements the IAdaptable interface on your behalf,
without you needing to know about it. Fine, so it implements this
interface, but what good is it on its own?
It turns out that the PlatformObject delegates all of
its requests to getAdapter() to something called IAdapterManager.
One is provided by default for the Platform, and is accesed by calling Platform.getAdapterManager().
You can think of this as a giant Map that associates
classes with appropriate adapters, and the PlatformObject's
getAdapter() method as a lookup into this Map.
The net effect is that it's possible for any PlatformObject
to have a new adapter type dynamically associated with it with no
recompilation necessary. This is used in many places to support
extensions in the Eclipse workspace.
Let's say that we want to introduce a way of converting a List
of Strings into an XML node. We'd like the XML to look
like:
<List> <Entry>First String</Entry> <Entry>Second String</Entry> <Entry>Third String</Entry> </List>
We can't use the List's toString method,
since it may be used for other purposes. Instead, we can attach a
factory to the List so that when a request for an XML node
is requested, a Node is automatically generated and
returned.
So, how do we attach the factory? We need to do three things:
Node from a List (the
factory)We use the IAdapterFactory
to wrap our conversion mechanism:
import nu.xom.*;
public class NodeListFactory implements IAdapterFactory {
/** The supported types that we can adapt to */
private static final Class[] types = {
Node.class,
};
public Class[] getAdapterList() {
return types;
}
/** The actual conversion to a Node */
public Object getAdapter(Object list, Class clazz) {
if (clazz == Node.class && list instanceof List) {
Element root = new Element("List");
Iterator it = list.iterator();
while(it.hasNext()) {
Element item = new Element("Entry");
item.appendChild(it.next().toString());
root.appendChild(item);
}
return root;
} else {
return null;
}
}
}
Platform's AdapterManagerWe need to pass our factory into the adapter manager, so that when we
ask any List instance for a Node, it knows to
use our factory. The Platform looks after the IAdapterManager
for us, and it's a relatively simple registration call:
Platform.getAdapterManager().registerAdapters( new NodeListFactory(), List.class );
This asks the platform manager to associate the NodeListFactory
with the List type. When we ask for an adapter from List
instances, it will consult the factory. It knows that it can obtain a Node
using this factory, because that's what we've defined on the return type
in the getAdapterList() method. In Eclipse, this step is
normally performed explicitly at plugin startup, but it can also be done
implicitly via the org.eclipse.core.runtime.adapters
extension point.
List for a NodeThis is simply a case of asking the adapter to give us a Node
object:
Node getNodeFrom(IAdaptable list) {
Object adaptable = list.getAdapter(Node.class);
if (adaptable != null) {
Node node = (Node)adaptable;
return node;
}
return null;
}
If you want to be able to add functionality to an existing class at
run-time, all we need to do is define a factory that performs the
translation on an instance, and then register that factory with the Platform's
AdapterManager. This functionality is great for registering
UI-specific components with non-UI components whilst maintaining a clean
seperation between the two, such as used by org.rcpapps.rcpnews.ui
and org.rcpapps.rcpnews
plugins. In these examples, IPropertySource
in the UI plugin needs to be associated with the data objects in the
non-UI plugin. When the UI plugin is initialised, it registers the IPropertySource
with the Platform, so that when a data object is selected
in the navigator, the correct properties are displayed in the properties
view.
Obviously, java.util.List doesn't extend PlatformObject,
so if you want the examples to compile here, you'll need to create a
subtype of List to achieve the effect. However, extending PlatformObject
isn't a requirement:
public class AdaptableList implements IAdaptable, List {
public Object getAdapter(Class adapter) {
return Platform.getAdapterManager().getAdapter(this, adapter);
}
private List delegate = new ArrayList();
public int size() {
return delegate.size();
}
// ...
}
The code sample uses the XOM libraries for generating the XML.