Eclipse 3.1 - Support Logical Resources - Adaptables

Solution 1: Support Logical Resources - Adaptables

This document outlines a possible solution to the adaptables problem outlined in the Support Logical Resources plan item problem description document. Interested parties should review this document and verify that their use cases are reflected in the requirements then later that the solution provided satisfies their needs. Feedback is strongly encouraged and may be provided on the platform-team-rev mailing list or in the bug report for this plan item

IlModelElement

This solution involves adding an interface to the Resources plugin that maps logical elements to physical ones. The interface is purposely simple with logical model manipulations omitted. A client can't use this interface to display logical models or gain any interesting additional knowledge about it.

Current plugins that add object contributions to IResource (CVS, Search, Compare) would have to also add them to IModelElement and honour the bindings provided by the logical resource. Moreover, the plugins that adapted to IResource to these object contributions shown in the context menu should no longer adapt the IResource and instead implement IModelElement (this could be simplified by adding a tag to the objectConribution XML that indicates that it can be used with IResource or IModelElement).

/**
* This interface defines an element of an application that models the
* data/processes of a particular problem domain. This purpose of this interface
* is to support the transformation of the application model into its underlying
* file system resources for the purposes of reconciling the model with versions
* of the model stored in a repository. Hence, this interface provides the
* bridge between a logical element and the physical resource(s) into which it
* is stored but does not provide more comprehensive model access or
* manipulations.
*
* @see IResource
*/
public interface IModelElement {

/**
* Returns the project which contains this model element. This is a resource
* handle operation; neither the element nor the resulting project need
* exist. Model elements may not span multiple projects. In other words, all
* physical resources that constitute a logical resource are located in the
* same project.
*
* @return the project handle
*/
public IProject getProject();

/**
* Returns one or more traversals that can be used to access all the
* physical resources that constitute the logical resource. A traversal is
* simply a set of resources and the depth to which they are to be
* traversed. This method returns an array of traversals in order to provide
* flexibility in describing the traversals that constitute a model element.
* A depth is included to allow the clients of this interface (most likely
* repository providers) an opportunity to optimize the operation and also
* ensure that resources that were or have become members of the model
* element are included in the operation.
*
* Implementors of this interface should ensure, as much as possible, that
* all resources that are or may be members of the model element are
* included. For instance, a model element should return the same list of
* resources regardless of the existance of the files on the file system.
* For example, if a logical resource called "form" maps to "/p1/form.xml"
* and "/p1/form.java" then whether form.xml or form.java existed, they
* should be returned by this method.
*
* In some cases, it may not be possible for a model element to know all the
* resources that may consitite the element without accessing the state of
* the model element in another location (e.g. a repository). This method is
* provided with a context which, when provided, gives access to
* the members of correcponding remote containers and the contenst of
* corresponding remote files. This gives the model element the opportunity
* to deduce what additional resources should be included in the traversal.
*
* @param context gives access to the state of
* remote resources that correspond to local resources for the
* purpose of determining traversals that adequately cover the
* model element resources given the state of the model element
* in another location. A null may be provided, in
* which case the implementor can assume that only the local
* resources are of interest to the client.
* @param monitor a progress monitor
* @return a set of traversals that cover the resources that constitute the
* model element
*/
public ITraversal[] getTraversals(IModelContext context,
IProgressMonitor monitor) throws CoreException;
}

ITraversal

A logical model element can be made up of one or more traversals that define the physical resources that make up the model element. Traversals are used to allow the clients of the traversal to optimize their operations based on the depth of the resources being traversed.

/** 
* A model element traversal is simply a set of resources and
* the depth to which each are to be traversed. A set of traversals
* is used to describe the resources that constitute a model element.
*
* @see org.eclipse.core.resources.IResource
* @see org.eclipse.core.resources.model.IModelElement
*/
public interface ITraversal {

/**
* Returns the file system resource(s) for this traversal.
* The returned resources must be contained within the same project
* and need not exist in the local file system.
*
* The traversal of the returned resources should be done considering
* the flag returned by getDepth. If a resource returned by a
* traversal is a file, it should always be visited. If a resource of a traversal
* is a folder then files contained in the folder can only be visited if the folder is
* IResource.DEPTH_ONE or IResource.DEPTH_INFINITE.
* Child folders should only be visited if the depth is IResource.DEPTH_INFINITE.
*/
public IResource[] getResources();

/**
* Return the depth to which the resources should be traversed.
* @return the depth to which the physical resources are to be traversed
* (one of IResource.DEPTH_ZERO, IResource.DEPTH_ONE or IResource.DEPTH_INFINITE)
*/
public int getDepth();
}

IModelContext

One of the advantages of a logical model traversal API is that it allows plug-ins to implement any operations they desire in terms of logical resources (e.g. CVS update, CVS commit, CVS tag, dirty decoration, etc.). However, the context of the operation may imply that resources that don't exist locally should be included in the operation. This is not a problem for some logical models. For instance, a java package is a container visited to a depth of one. Also, the plugin example knows all the files that vould potentially be involved, even if they don't exist locally. Given this, a repository provider can easily determine that outgoing deletions should be included when committing or that incoming additions should be included when updating.

However, properly determining the set of traversals that adequately cover a model element gets more complicated if the resources that constitute the element depend of the contents of a manifest file (or some other similar mechanism) and may change over time. In this case, it is possible that the model element could access the contents of the manifest file as contained in the remote location. Given this ability, the model element provider is provided with enough context about the operation to the logical model element so a set of traversal can be returned that adequately cover all resources that constitute the model element.

This has led to the definition of the following IModelContext interface.

/**
* A model context provides a model element with a view of the remote state
* of a local resource as it relates to a repository operation that is in
* progress. A repository provider can pass an instance of this interface to a
* model element when obtaining a set of traversals for a model element. This
* allows the model element to query the remote state of a resource in order to
* determine if there are resources that exist remotely but do not exist locally
* that should be included in the traversal.
*/
public interface IModelContext {

/**
* Return whether the contents of the corresponding remote differs from the
* content of the local file. This can be used by clients to determine if
* they nee to fetch the remote contents in order to determine if the
* resources that constitute the model element are different in another
* location.
*
* @param file the local file
* @param monitor a progress monitor
* @return whether the contents of the corresponding remote differs from the
* content of the local file
* @throws CoreException if this method fails. Reasons include:
* - The corresponding remote resource does not exist (status
* code will be IResourceStatus.REMOTE_DOES_NOT_EXIST).
* - The corresponding remote resource is not a container
* (status code will be IResourceStatus.REMOTE_WRONG_TYPE).
* - The server communications failed (status code will be
* IResourceStatus.REMOTE_COMMUNICATION_FAILURE).
*/
public boolean contentDiffers(IFile file, IProgressMonitor monitor)
throws CoreException;

/**
* Returns an instance of IStorage in order to allow the
* caller to access the contents of the remote that corresponds to the given
* local resource. The provided local file handle need not exist locally. A
* exception is thrown if the corresponding remote resource does not exist
* or is not a file.
*
* This method may be long running as a server may need to be contacted to
* obtain the contents of the file.
*
* @param file the local file
* @param monitor a progress monitor
* @return a storage that provides access to the contents of the local
* resource's corresponding remote resource
* @throws CoreException if this method fails. Reasons include:
* - The corresponding remote resource does not exist (status
* code will beIResourceStatus.REMOTE_DOES_NOT_EXIST).
* - The corresponding remote resource is not a file (status
* code will be IResourceStatus.REMOTE_WRONG_TYPE).
* - The server communications failed (status code will be
* IResourceStatus.REMOTE_COMMUNICATION_FAILURE).
*/
public IStorage fetchContents(IFile file, IProgressMonitor monitor)
throws CoreException;

/**
* Returns the list of member resources whose corresponding remote resources
* are members of the corresponding remote resource of the given local
* container. The container need not exist locally and the result may
* include entries that do not exist locally and may not include all local
* children. An empty list is returned if the remote resource which
* corresponds to the container is empty. An exception is thrown if the
* corresponding remote does not exist or is not capable of having members.
*
* This method may be long running as a server may need to be contacted to
* obtain the members of the containers corresponding remote resource.
*
* @param container the local container
* @param monitor a progress monitor
* @return a list of member resources whose corresponding remote resources
* are members of the remote counterpart of the given container
* @throws CoreException if this method fails. Reasons include:
* - The corresponding remote resource does not exist (status
* code will be IResourceStatus.REMOTE_DOES_NOT_EXIST).
* - The corresponding remote resource is not a container
* (status code will be IResourceStatus.REMOTE_WRONG_TYPE).
* - The server communications failed (status code will be
* IResourceStatus.REMOTE_COMMUNICATION_FAILURE).
*/
public IResource[] fetchMembers(IContainer container,
IProgressMonitor monitor) throws CoreException;
}

Although this can be thought of as a repository provider API, it bypasses the complexity by allowing the repository provider to implement the interface in a maner specific to the operation being performed. For instance, for a commit, the repository provider would provide the contents of files that define the base of a manifest file. In other words, the contents of the file would match the contents before the user modified the model element. This would allow for the inclusion of any outgoing deletions. For an update, the repository provider would provide access to the contents of the file in the repository.

How this will affect existing plug-ins

There are two types of plug-ins this will affect, those that adapt elements in the workbench to IResource to get menu contributions and the plug-in that contributes the contributions. Here is the list of extension points with "adaptable" attributes:

org.eclipse.ui.decorators
org.eclipse.ui.popupMenus
org.eclipse.ui.propertyPages

All extension points document that "adaptable=true" means the object must be adaptable to IResource. And here is the list of references to these extension points in the SDK:

org.eclipse.ui.decorators
 org.eclipse.pde.ui
   objectClass=IProject
 org.eclipse.team.cvs.ui
   objectClass=IResource
 org.eclipse.ui.ide
   objectClass=IResource
 
org.eclipse.ui.propertyPages
  org.eclipse.team.cvs.ui
    objectClass=IFile
    objectClass=IFolder
    objectClass=IProject
  org.eclipse.ui.externaltools
    objectClass=IProject
  org.eclipse.ui.ide
    objectClass=IProject
    objectClass=IResource
  org.eclipse.update.ui

org.eclipse.ui.popupMenus: objectContribution
  org.eclipse.compare
    objectClass=IFile
    objectClass=IContainer
    objectClass=IResource
  org.eclipse.pde.ui
    objectClass=IProject
  org.eclipse.team.cvs.ui
    objectClass=IFile
    objectClass=IResource
    objectClass=IProject
    objectClass=IContainer
    objectClass=IFolder
  org.eclipse.team.ui
    objectClass=IResource
    objectClass=IProject
  org.eclipse.ui.editors  

For the plug-ins contributing extensions to adaptable extension points they will have to make two changes to support the new model element APIs:
  1. Update their plugin.xml files and add IModelElement in the objectClass of their adaptable contributions.
  2. Update their actions to work on ITraversals instead of IResource and respect the depth constraints provided in the traversals.
  3. (Optional) provide a IModelContext if they manage remote resources
And for plugi-ins that define UI elements with adaptable elements to IResource, they will have to change the IAdaptable#getAdapter() methods to return IModelElement instead of IResource. Here are some examples:

For some model elements, simple traversals can be used to describe the element. For instance, here is an example traversal that represents a Java package.

/**
* An example traversal for a Java package
*/
public class JavaPackageTraversal implements ITraversal {
IContainer javaPackage;
public JavaPackageTraversal(IContainer javaPackage) {
this.javaPackage = javaPackage;
}
public IResource[] getResources() {
return new IResource[] { javaPackage };
}
public int getDepth() {
return IResource.DEPTH_ONE;
}
}

The next example class shows a traversal for a fixed set of files. The chosen example is for the data files that define the properties of a plugin in Eclipse. The same files are always returned even if they don't exist locally. It is up to the client of the interface in this case to decide how to handle the files that don't exist locally. If the client is a repository and the operation is an update to fetch the latest contents from the server, then the repository provider would fetch the files from the server if they now esist there or ignore them if they don't exist either locally or remotely.

/**
* An example of a fixed traversal that always includes
* a certain set of files even if they don't exist
* locally
*/
public class PluginDataTraversal implements ITraversal {
IProject pluginProject;
public PluginDataTraversal(IProject project) {
this.pluginProject = project;
}
public IResource[] getResources() {
return new IResource[] {
pluginProject.getFile(new Path("plugin.xml")),
pluginProject.getFile(new Path("plugin.properties")),
pluginProject.getFile(new Path("build.properties")),
pluginProject.getFile(new Path("META-INF/MANIFEST.MF"))
};
}
public int getDepth() {
return IResource.DEPTH_ZERO;
}
}

Assuming that the plugin data consists of the above mentioned files and the contents of the schema directory, the model element class for a plugin's data model could be the following:

/**
* A plugin model element
*/
public class PluginModel implements IModelElement {
IProject pluginProject;
public IProject getProject() {
return pluginProject;
}
public ITraversal[] getTraversals(ITraversalContext context,
IProgressMonitor monitor) throws CoreException {
return new ITraversal[] {
new PluginDataTraversal(pluginProject),
new ITraversal() {
public IResource[] getResources() {
return new IResource[] {
pluginProject.getFolder("schema")
};
}
// Traverse the folder deeply to include all children
public int getDepth() {
return IResource.DEPTH_INFINITE;
}
}
};
}
}

Eclipse Platform Issues

The following issues would need to be addressed for this solutions

  1. ObjectContributions have to be duplicated in the plugins that would like to provide both contributions for IResource and ILogicalResource.
  2. (Bug 53217) Logical classes (e.g. java model, UML) have to implement the interface instead of adapting to it. This is a limitation of the current objectContribution story in the workbench.
  3. Plugins that add object contributions will have to modify their actions to handle logical resources. There should be some standard UI components for showing the mappings. For instance you could show the logical model at the top and in a details part the physical files/folders that will be affected by the operation.
  4. Would it be possible to add a workbench adapter to specific implementations IModelElement in order to get an image and label for a model element?