Chapter 32. Core Workspace — Resources

In Eclipse the file system rules. An Eclipse workspace is mapped to the file system directly and there is no intermediate repository. Users can either change a resource on the file system directly or from inside Eclipse. The resources plug-in provides support for managing a workspace and its resources (see Figure 32.1).

Core Workspace

Figure 32.1. Core Workspace

Accessing File-System Resources—Proxy and Bridge

Clients need a way to track a resource in the file system. Resources change during their lifetime: they are created, their contents change, they are replaced with another version, they are deleted, and sometimes recreated. The information about a resource changes over its lifetime, but its identity doesn't. Clients need a simple way to refer to a resource independent of the resource's state in the workspace. We don't want to hold onto stale state, for example, when a file is deleted.

Eclipse addresses this problem by only giving clients access to a handle for a resource, not the full resource. This design is best described as the conjunction of the two structural patterns: Proxy and Bridge. Neither of the patterns alone captures the full intent of the design. The Proxy pattern tells us how to control access to an object and the Bridge pattern tells us how to separate an interface from its implementation. To quote their intent:

  • Proxy—“Provide a surrogate or place holder for another object to control access to it”

  • Bridge (also known as Handle/Body)—“Decouple an abstraction from its implementation so that the two can vary independently”

The relevant aspect from Proxy is controlling the access to an object. This is the key to avoid clients holding on to stale state. The relevant aspect from Bridge is the strong separation between an interface and its implementation. Both patterns address their problems by introducing a level of indirection. Applied to file-system resources this gives us:

  • The handle, which acts like a key for a resource.

  • An info object storing the representation of the file's state. There is only one implementor for each handle and it is therefore an example of a degenerate Bridge.

The handles are defined as interfaces IFile, IFolder, IProject, and IWorkspaceRoot. Figure 32.2 shows their interface hierarchy.

IResource Type Hierarchy

Figure 32.2. IResource Type Hierarchy

None of the resource interfaces are intended to be implemented by clients. Interfaces are used to define the API, but not so clients can implement the interfaces in different ways.

Let's verify the handle separation with the Spider (Figure 32.3) and explore the file TestFailure.java as presented in the Navigator (Figure 32.4).

IResource Type Hierarchy
IFile as Proxy

Figure 32.4. IFile as Proxy

From the diagram we can see that a File handle knows only the path to the resource and its containing workspace. It acts like a key to access the file. Here are some interesting characteristics of the resource handles:

  • They are Value Objects[1] —. small objects whose equality isn't based on identity. Once created, none of their fields ever change. This enables clients to store them in hashed data structures like a Map. More than one handle can refer to the same resource. Always use IResource.equal() when comparing handles.

  • Handles define the behavior of a resource, but they don't keep any resource state information.

  • A handle can refer to non-existing resources.

  • Some operations can be implemented from information stored in the handle only (handle operations). A resource need not exist to successfully execute such an operation. Examples of handle operations are: getFullPath(), getParent(), and getProject(). The existence of a resource can be tested with exists(). Operations that depend on the existence of the resource throw a CoreException when the resource doesn't exist.

  • Handles are created from a parent handle:

    IProject project;
    IFolder folder= project.getFolder("someFolder");
    
  • Handles are used to create the underlying resource:

    folder.create(...);
    

    The fixture class we used in Circle Two and shown in full in Appendix B illustrates how to use the handle-based API to create resources.

  • Since a handle is independent of the state in the file system, there is no way for the client to keep a reference to stale state, for example state about a deleted file. Every time you want state for the handle you have to fetch it anew.

Figure 32.5 illustrates the handle/body separation used for resources. In the following explanations we will use UML diagrams to illustrate the pattern stories. We annotate the UML with shaded boxes identifying the pattern being applied.

IResource Is a Proxy and a Bridge

Figure 32.5. IResource Is a Proxy and a Bridge

The handle IResource is API and the ResourceInfo and Resource are internal classes. ResourceInfo is extended to store additional state for projects (ProjectInfo) and the workspace root. This illustrates the strong separation suggested by the Bridge pattern. Eclipse has a lot of freedom when resolving handles to find the corresponding resource state.

When an IResource is created, the resource information is retrieved from the workspace and the path information is stored inside the handle. Figure 32.6 illustrates how the look up is done to find the ProjectInfo for a project:

Retrieving the ProjectInfo

Figure 32.6. Retrieving the ProjectInfo

The Eclipse workspace stores the resource info as a complete tree in memory. This tree is referred to as the element tree. The resource info object corresponding to a handle is retrieved by traversing the element tree using the path stored inside the handle. A big advantage of this workspace implementation is that you can navigate the tree of files in almost no time. When you use the file system you have to make many queries to the underlying operating system (OS).

The resources plug-in not only applies handle/body separation for resources, it also applies the same separation for markers. A marker associates attributes with a resource. A marker doesn't store the attributes directly, but only stores a reference to their marker attribute info object. For markers the info object corresponds to the class MarkerInfo. The same coding idioms used for resources are consistently applied to markers:

  • Markers are created from a handle.

  • Accessing a marker attribute can throw a CoreException.

Now that we understand how an individual resource is represented, let's look into the representation of the resource tree.

The Workspace—Composite

The Eclipse workspace provides resources stored in the file system. A workspace consists of projects containing folders containing files, as shown in Figure 32.7.

The Workspace—Composite

You access the Singleton workspace instance from the static accessor ResourcesPlugin.getWorkspace(). An IWorkspaceRoot represents the top of the resource hierarchy in a workspace.

The workspace is a hierarchical structure and it therefore matches the intent of the Composite pattern well: “Compose object into tree structures to represent part/whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.” Figure 32.8 shows how the implementation of a workspace maps to Composite:

IWorkspace Is a Composite of IContainers and IFiles

Figure 32.8. IWorkspace Is a Composite of IContainers and IFiles

Some observations on the workspace implementation:

  • IResource provides access to its parent. The getParent() method is a handle-only operation that can derive the parent from the path stored in the handle.

  • IContainer is the common base interface for the different composite classes. It provides a method members() that returns its children as a typed IResource array.

You can traverse a resource tree using the members() method provided by IContainer, but there is a better way.

Traversing the Resource Tree—Visitor

Traversing a resource tree manually using the members() method results in a lot of control-flow code in clients. The control flow to traverse a resource tree can be extracted with a visitor. When we check the intent of Visitor we find, “Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.” This is all correct. However, the main purpose here is to extract the common control flow and make it generally reusable.

IResourceVisitor is the visitor interface, which is accepted by IResource (see Figure 32.9).

IResourceVisitor Visits IResources

Figure 32.9. IResourceVisitor Visits IResources

The accept() method implements the resource traversal and calls back the visitor for each resource. The IResourceVisitor interface isn't type-specific; there are no separate methods for visiting a file or a folder. If you need to distinguish between the visited resource types, you can do this inside the visit() method using the getType() method. The code snippet from the Eclipse resource implementation illustrates the use of visitor.

Example . org.eclipse.core.internal.resources/ResourceTree

private void addToLocalHistory(IResource root, int depth) {
  IResourceVisitor visitor = new IResourceVisitor() {
    public boolean visit(IResource resource) throws CoreException {
      if (resource.getType() == IResource.FILE)
        addToLocalHistory((IFile) resource);
      return true;
    }
  };
  try {
    root.accept(visitor, depth, false);
  } catch (CoreException e) {
  }
}

Returning true from visit() indicates that the children of a resource should be visited. Returning false stops the traversal at the current resource.

While performance tuning, it was discovered that for some common traversals only a subset of the resource information is actually needed by the visitor. IResourceProxyVisitor was introduced to reduce the information fetched from the file system. It doesn't pass an IResource to the visit() method but an IResourceProxy.

Example . org.eclipse.core.resources/IResourceProxyVisitor

public interface IResourceProxyVisitor {
  public boolean visit(IResourceProxy proxy) throws CoreException;
}

IResourceProxy is an example of a virtual proxy. It creates an expensive object on demand. The expensive object is in this case the full workspace path of a resource. The proxy is only valid during the call of the visit() method.

The Eclipse workspace has another advantage over accessing the file system directly—comprehensive support for observing changes.

Tracking Resource Changes—Observer

Resources in the workspace can change either as a result of manipulating them inside Eclipse or from resynchronizing them with the local file system. In both cases, observing clients need precise change information so that they can update themselves efficiently. To observe changes, the workspace provides a resource listener, which is an Observer variation (see Figure 32.10). Observers register with the workspace, which acts as the subject to be notified about changes.

IResourceChangeListener Observes IWorkspace

Figure 32.10. IResourceChangeListener Observes IWorkspace

Internally the notification mechanism is implemented by a NotificationManager (see Figure 32.11).

NotificationManager

Figure 32.11. NotificationManager

Digging into the implementation of NotificationManager, we find that it is very careful in handling the case of modifications to the listener list during notification. By copying the listener list at the beginning of a notification, it ensures that any modifications to the listener list during the notification will have no effect on the ongoing notification. This is also referred to as making a Safe Copy. The internal class ResourceChangeListenerList implements the listener management.

Example . org.eclipse.core.internal.events/ResourceChangeListenerList

public ListenerEntry[] getListeners() {
  if (size == 0)
    return EMPTY_ARRAY;
  ListenerEntry[] result = new ListenerEntry[size];
  System.arraycopy(listeners, 0, result, 0, size);
  return result;
}

The observer pattern asks us to decide how to provide details about a change notification. To quote the Observer pattern: “At one extreme, which we call the push model, the subject sends observers detailed information about the change, whether they want it or not. At the other extreme is the pull model; the subject sends nothing but the most minimal notification, and observers ask for details explicitly thereafter.” Changes in a resource tree can be complex, so Eclipse uses the push model and provides detailed information about a change to all observers. Eclipse calls the change information resource deltas (see Figure 32.12).

IResourceDelta Records a Tree of Changes

Figure 32.12. IResourceDelta Records a Tree of Changes

A resource delta describes the change between two states of the workspace tree. It is itself a tree of nodes. Each delta node describes how a resource has changed and provides delta nodes describing the changes to its children. A delta tree is rooted at the workspace root. Here is the resource delta describing a change to the file FailTest.java (see Figure 32.13 and Figure 32.14).

IResourceDelta Records a Tree of Changes
An IResourceDelta

Figure 32.14. An IResourceDelta

A resource delta has several interesting properties:

  • A resource delta describes a single change and multiple changes using the same structure.

  • A resource delta describes complete change information including information about moved resources and marker changes. The method getMarkerDeltas() returns the changes to markers.

  • It is easy to process a resource delta recursively top-down when updating an observer.

  • Because a resource delta can be expensive, it is only valid during the call of resourceChanged().

  • Because resources are just handles to the real resources, a delta can easily describe deleted resources as well.

You can reuse the traversal logic of a resource delta with an IResourceDeltaVisitor:

Example . org.eclipse.core.resources/IResourceDeltaVisitor

public interface IResourceDeltaVisitor {
  public boolean visit(IResourceDelta delta) throws CoreException;
}

Batching Changes—Execute Around Method

Any system based on change events is vulnerable to being flooded with resource change events. The common practice to avoid this flooding is to batch changes wherever possible. Changes should be grouped together so that only a single notification is sent out at the end of a single logical change. In Eclipse the batching is achieved using an IWorkspaceRunnable that is passed to the workspace for execution. The action specified by the runnable is then run as an atomic workspace operation. The deltas are accumulated during the operation and broadcast at the end. The snippet below illustrates how to create a marker and set its attributes with an IWorkspaceRunnable so that only one instead of two (creation, setting attributes) notifications are sent out:

Example . org.eclipse.ui.texteditor/MarkerUtilities

public static void createMarker(final IResource resource,
    final Map attributes, final String markerType)
    throws CoreException {
  IWorkspaceRunnable r= new IWorkspaceRunnable() {
    public void run(IProgressMonitor monitor) throws CoreException{
      IMarker marker= resource.createMarker(markerType);
      marker.setAttributes(attributes);
    }
  };
  resource.getWorkspace().run(r, null);
}

An IWorkspaceRunnable is an example of the “Execute Around Method” pattern from Smalltalk[2] adapted to Java. IWorkspace.run() is the execute-around method. Before the runnable is executed, it informs the workspace to start batching. Then the runnable is invoked. Finally, when the runnable is done, IWorkspace.run() informs the workspace to end batching. Without an execute-around method, clients need to explicitly invoke the begin and end methods in the right order. This is error prone. Another benefit of an execute around method is that the begin and end batching methods do not have to be published as API.

Another example of an execute-around method in Eclipse is Platform.run(ISafeRunnable runnable). It invokes the runnable in a protected mode and catches exceptions.



[1] M. Fowler, Patterns of Enterprise Application Architecture, Addison-Wesley, Boston, 2003, p. 486.

[2] See “Execute Around Method” in K. Beck, Smalltalk Best Practice Patterns. Prentice Hall PTR, Upper Saddle River, NJ, 1997.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset