Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Open Source

Factoring for Eclipse


November, 2004: Factoring for Eclipse

Marcus is a senior software engineer for SlickEdit. He can be contacted at [email protected].


Developing a plug-in for Eclipse presents special challenges that traditional application development does not. Applications are generally monolithic, despite any internal object-oriented structure. They have their own set of resources that are allocated and deallocated as needed, and are managed solely by the application.

The architecture of Eclipse, like its development, is community oriented. A plug-in is a unit of functionality in a neighborhood of other plug-ins. Like a real community, one bad neighbor affects all of its residents. Specifically, as a good citizen, you are required to minimize the amount of resources you take up in the environment. This includes memory and computational cycles. Only use what is necessary for the functionality that Eclipse users are engaging. Ideally, if your plug-in is not in use, it does not consume any resources.

The Eclipse plug-in model provides excellent ways of dealing with these concerns. Provided you follow some general guidelines and factor your plug-in correctly, you can supply great functionality with minimal cost to the community.

It is easy to think of your plug-in as a monolith. It provides a specific set of features that are all interrelated. Despite this similarity of function, your plug-in should be designed as a community of smaller plug-ins that comprises the total functionality. By breaking-up your plug-in, you will have a set of smaller plug-ins that can be loaded when invoked.

Without proper factoring, however, this strategy will backfire. If you have four small plug-ins that are not properly decoupled, you will end up continually loading all four plug-ins, thereby increasing overhead.

There are straightforward approaches that you can apply to the design of your plug-in that simplify the decomposition of your plug-in. In this article, I introduce two of them:

  • Factor by Flow breaks apart plug-ins based on how users invoke them.
  • Factor by Dependency looks at the dependency graph to determine the decomposition. I will also demonstrate how to use Extension Points as a decoupling mechanism.

Before you can factor your plug-in, it is important to understand the basic mechanisms that Eclipse employs in its runtime. These mechanisms control the start behavior for plug-ins and give you the power to change that behavior.

The Eclipse Class Loader

To understand how a plug-in is instantiated and started, you have to understand how the Eclipse class loader works. The class loader (org.eclipse.core.runtime.adapter.EclipseClassLoader) maps the association of a class to a plug-in. This includes any class in the plug-in's jar files. These jars are specified in the plugin.xml of a plug-in.

In the EclipseClassLoader class (available electronically; see "Resource Center," page 5), the use of the term Bundle indicates a particular plug-in's definition. This definition is composed of the plugin.xml, and now in Eclipse 3.0 the Open Services Gateway Initiative (OSGI) bundle manifest. Both of these files determine when a plug-in is loaded and started.

The method shouldActivateFor(String className) applies the loading rules to determine whether a class that is being loaded should force a plug-in to start. This determination is based on the plugin.xml and OSGI bundle manifest.

Without an OSGI manifest, a plug-in is loaded when any of the classes in its jar file are loaded, provided that all of the plug-in dependencies defined in its plugin.xml file are present in the environment. If all of the dependencies are present, Eclipse instantiates the owning plug-in and calls its start method. Otherwise, the plug-in is unavailable along with all of the classes that it contains.

This loading policy requires that plug-in developers ensure that interplug-in dependencies are carefully managed. You do not want to force your plug-in to load because another plug-in is using a utility class contained in its jar file. You also do not want your plug-in to force the loading of other plug-ins you are dependent on, if they are not needed.

Third-party developers might abuse your plug-in. Although you might have managed your dependencies, any third party can use one of your classes, which may not have been intended as an API class, and force the loading of your plug-in.

OSGI Bundle Manifest

Undoubtedly, the developers of Eclipse realized the potential for abuse and inefficiency. Eclipse 3.0 introduces the OSGI bundle manifest, which gives you greater control over the loading policy. The file is named Manifest.mf and located in a META-INF subdirectory of the plug-in folder.

Bundle manifests give you three new capabilities to control the plug-in startup behavior:

  • The ability to turn off autostart. This tells Eclipse not to manage the starting of your plug-in. Classes can be loaded from your plug-in without it ever being instantiated and started. This is accomplished by setting Eclipse-AutoStart to False.
  • The ability to add exceptions to the default autostart policy. If the autostart policy is set to True, then you can optionally export packages that do not require the plug-in to be loaded. This is an excellent benefit that promotes reuse. You can have exported packages for utility and base classes that do not force the plug-in to load. This statement in the Manifest.mf file exports a utility package from the sample metrics plug-in: "Eclipse-AutoStart=true;exceptions=com.kestler.metrics.util".
  • Control over what is exported from your plug-in. This lets you explicitly control what portions of your plug-in can be used by other plug-ins. Each package that is visible is added with a Provide-Package statement followed by the package name. Provide-Package:com.myplugin.api exposes an API package from a plug-in.

You must list all of the packages you want to export if you use an OSGI bundle manifest. By default, none of the packages in your plug-in are visible to other plug-ins. This can be tricky because the classes are visible at compile time, but not at runtime. Using the manifest overcomes all of the previous issues with only using a plugin.xml file to describe your plug-in. A manifest is created for you if you select the "Create an OSGI bundle manifest for the plug-in" in the New Plug-in Project Wizard. You can manually add one by creating a META-INF directory and adding a MANIFEST.MF file to it. Once it is created, all of the options can be controlled on the Runtime tab of the plugin.xml editor page. This page has a MANIFEST.MF tab if you have a bundle manifest to allow you to edit the file directly.

While the OSGI bundle manifest might mitigate a lot of the problems with third parties, you still have to carefully plan your plug-in dependencies and start-up behavior.

Putting It to Use

The Metrics Plug-in collects statistics about the workspace in real time and provides a view for displaying them. It collects information about Java and C/C++ projects and general workspace information. The metrics are based on the IMetric interface so that I can treat them uniformly (see the EclipseClassLoader class).

Factor by Flow

The Factor by Flow approach factors the plug-in by user interaction or flow. This approach considers how users interact with your plug-in.

You should determine ahead of time how users invoke specific features and portions of you application. Try to divide up the structure of the plug-in so that each user path, or flow, can be invoked without any extra overhead.

If your plug-in provides two perspectives, for instance, break it up into separate plug-ins. If users are working in one perspective, there is no need to introduce the overhead of the second perspective.

If you have related views that depend on each other and other views that are independent, break the independent view out into a separate plug-in. Keep the related views together in another plug-in.

Since the Metrics plug-in only has one view, I pull it out into a separate plug-in. Separating the user interface gives you added flexibility. If well designed, you can reuse user interface pieces to construct other views without dragging the underlying model with it.

Factor by Dependency

Consider the dependency graph for the plug-in so far (Figure 1). The plug-in has dependencies on the Eclipse Runtime, Resource Plug-in, JDT (Java Development Tools), and CDT (C/C++ Development Tools). The JDT and CDT dependencies need to be optional because these plug-ins might not be present (remember, there is now a platform-only version of Eclipse that does not contain JDT).

If you use this design, you are creating instances of metrics for plug-ins that might not be available. You can mitigate this by writing code to detect the presence of the optional plug-in. Fortunately, you do not have to do this.

Using the Factor by Dependency approach, you focus on the dependency graph of the plug-ins. Start by splitting out plug-ins that provide functionality based on a dependent, optional plug-in. Using this approach, any optional dependencies signal a need to refactor. You should rely on Eclipse to deal with the existence or nonexistence of a plug-in using its plug-in loading model. If the metrics that are dependent on JDT and CDT are moved to separate plug-ins, I can make the dependency required. This tells Eclipse not to load the plug-in if CDT or JDT are not present.

Now, I have three plug-ins. There is a core plug-in called "com.kestler.metrics" that provides infrastructure and workbench metrics; com.kestler.metrics.c for CDT- based metrics; and com.kestler.metrics.java for JDT-based metrics.

The dependency graph for this approach is still problematic (Figure 2). In this scenario, com.kestler.metrics is optionally dependent on the Java and C metrics plug-ins. This approach is problematic as the Java and C Metrics plug-ins use resources from the main metrics plug-in. This introduces a circular dependency. There is also the problem of the plug-ins still not being available when CDT or JDT are not present. I need to break the dependency from the main plug-in to the C/C++ Metrics and Java Metrics plug-ins to remove the optional dependency and possible circular dependency.

This problem is easily solved by introducing an Extension Point.

Extension Points for Decoupling

You can't do much with Eclipse without using Extension Points. You have probably used them by extension, but they are excellent decoupling mechanisms if you implement them in the right places.

Defining an Extension Point is a simple process. They are defined by a schema and declared in your plugin.xml as an Extension Point. Once defined, any plug-in can provide extensions to your plug-in.

I have defined com.kestler.metrics.Metric extension point (see Listing One). This enables other plug-ins to provide metrics by using the extension point. The idea is that the core plug-in will dispatch events out to a set of Metrics. The Metric element defines the integration. It is defined as requiring a class that implements the IMetric interface, and it has other values for determining how it is categorized and a filter to determine the events it receives.

A typical metric extension would be interested in events about files. The filter is set to org.eclipse.core.resource.IFile. For a simple file counter, I can just keep track of all of the files that come through the event.

There is also a Category element for defining new categories of organization, and an AggregateMetric element, which lets you define metrics that base their values on other Metrics. Any metric used by the MetricAggregate is defined as a MetricParticipant element. The MetricAggregate is notified when the value of any of the Metrics defined by the MetricParticipant changes. This allows for easy collection of statistics, like averages.

At runtime, the Metrics Plug-in gets an instance of IExtensionRegistry. This instance is aware of all of the plug-ins that have extended the Metrics extension point. By calling getExtensionPoint(String extensionPointID), I have access to all of the defined extensions by calling getConfigurationElements().

The result of this call returns only the plug-in extensions that can be loaded. If CDT, for instance, is not installed, then I will not get Metric extensions from the C Metrics plug-in. Since the C Metrics plug-in is dependent on CDT, and CDT is not available, Eclipse ignores the plug-ins Extensions. The resulting set of Extensions does not include the C Metric Extensions. Without any special code, the C metrics plug-in is not available and does not consume resources. Listing Two presents the extension processing code.

The Metrics Plug-in does not need to know anything about the other plug-ins. In effect, the dependency it had on the Java and C metrics plug-ins is reversed, and there are no more optional dependencies in the graph (Figure 3). This creates a form of inversion of control.

Fine Tuning

Now that my house is in order, I have to make sure that I keep the neighbors in line. To ensure that my plug-in is not abused by third parties, I have also set up the OSGI bundle manifest to only export the root package. This package contains the plug-in class and all of the API classes an extending plug-in would need. Everything else is located in the internal package, which is not exported by the OSGI bundle manifest.

None of the other plug-ins export any packages because they are extensions and do not provide useful functionality to the platform.

It is a good practice to start a plug-in project with a single export directory. This reduces your confusion when developing your plug-in and prevents abuse by third parties.

If you have utility classes, put them in one package that is an AutoStart exception in the OSGI bundle manifest. This package must also be exported. If a third party should use it, however, it will not force your plug-in to load.

Listing Three is the final OSGI bundle manifest for the core Metrics plug-in.

Wrapping Up

The complete source code for the set of metrics plug-ins (available electronically; see "Resource Center," page 5) contains useful examples of common processing in Eclipse. Along with Extension Point processing, there are examples of using the CDT and JDT code models to inspect code structure.

Applying the Factor by Flow and Factor by Dependency approaches to decomposing your plug-ins produces efficient, community-friendly plug-ins. While no approach covers all use cases, these two approaches will give you a base.

The key to knowing when a strategy is working in Eclipse is simple. Just ask the following question: Are you writing code that the platform would handle for you? If so, you should look at one of these approaches.

DDJ



Listing One

package com.kestler.metrics;

import org.eclipse.core.runtime.IAdaptable;

/** author Marcus Kestler. This Interface defines a metric. */
public interface IMetric extends IAdaptable {
    /** The event id for REFRESH. This will be called when the metric is 
     * instanciated. The element passed with this event has not been modified.
     */
    public final static int REFRESH = 100;
    /** return The calcuated value for the metric. */
    public String getValue();
    /** Main event for metrics. param notifyFlag one of the following events
     *              IResourceDelta.REMOVED
     *              IResourceDelta.CHANGED
     *              IResourceDelta.ADDED
     *              IMetric.REFRESH
     * param _adaptable The element that has been changed, removed, etc
     */
    public void calculate(int notifyFlag, IAdaptable _adaptable);
    /** Adds a listener that will be notfied when the value of the metric
     * has been changed. 
     * param _listener The listener
     */
    public void addValueListener(IValueListener _listener);
    /** Cleanup method */
    public void dispose();
    /** This will be called with the id set in the extension point
     * param id The id of the metric defined in its extension point
     */
    public void setId(String id);
    /** return Should return the id set in <code>setId()</code> */
    public String getId();
}
Back to article


Listing Two
<?xml version='1.0' encoding='UTF-8'?>
<!-- Schema file written by PDE -->
<schema targetNamespace="com.kestler.metrics.com.kestler.metrics">
<annotation>
      <appInfo>
         <meta.schema plugin="com.kestler.metrics.com.kestler.metrics" 
                                              id="Metrics" name="Metric"/>
      </appInfo>
      <documentation>
         Adds a new Metric to be displayed by the Metrics Plugin
      </documentation>
   </annotation>
   <element name="extension">
      <annotation>
         <documentation>
            A new metric that implements the IMetric interface.
         </documentation>
      </annotation>
      <complexType>
         <sequence>
            <element ref="Metric" minOccurs="1" maxOccurs="unbounded"/>
            <element ref="category" minOccurs="0" maxOccurs="unbounded"/>
            <element ref="AggregateMetric" 
                                   minOccurs="0" maxOccurs="unbounded"/>
         </sequence>
         <attribute name="point" type="string" use="required">
            <annotation>
               <documentation>
               </documentation>
            </annotation>
         </attribute>
         <attribute name="id" type="string" use="required">
            <annotation>
               <documentation>
               </documentation>
            </annotation>
         </attribute>
         <attribute name="name" type="string">
            <annotation>
               <documentation>
               </documentation>
            </annotation>
         </attribute>
      </complexType>
   </element>

   <element name="Metric">
      <annotation>
         <appInfo>
            <meta.element labelAttribute="label"/>
         </appInfo>
      </annotation>
      <complexType>
         <attribute name="class" type="string" use="required">
            <annotation>
               <documentation>
               </documentation>
               <appInfo>
                  <meta.attribute kind="java" 
                                    basedOn="com.kestler.metrics.IMetric"/>
               </appInfo>
            </annotation>
         </attribute>
         <attribute name="label" type="string" use="required">
            <annotation>
               <documentation>
               </documentation>
            </annotation>
         </attribute>
         <attribute name="categoryId" type="string" use="required">
            <annotation>
               <documentation>
               </documentation>
            </annotation>
         </attribute>
         <attribute name="id" type="string" use="required">
            <annotation>
               <documentation>
               </documentation>
            </annotation>
         </attribute>
         <attribute name="TypeFilter" type="string">
            <annotation>
               <documentation>
               </documentation>
            </annotation>
         </attribute>
         <attribute name="icon" type="string">
            <annotation>
               <documentation>
               </documentation>
               <appInfo>
                  <meta.attribute kind="resource"/>
               </appInfo>
            </annotation>
         </attribute>
      </complexType>
   </element>
   <element name="category">
      <complexType>
         <attribute name="label" type="string" use="required">
            <annotation>
               <documentation>
               </documentation>
            </annotation>
         </attribute>
         <attribute name="icon" type="string">
            <annotation>
               <documentation>
               </documentation>
               <appInfo>
                  <meta.attribute kind="resource"/>
               </appInfo>
            </annotation>
         </attribute>
         <attribute name="id" type="string">
            <annotation>
               <documentation>
               </documentation>
            </annotation>
         </attribute>
      </complexType>
   </element>
   <element name="AggregateMetric">
      <complexType>
         <sequence>
            <element ref="MetricParticipant" minOccurs="1" 
                                                  maxOccurs="unbounded"/>
         </sequence>
         <attribute name="id" type="string" use="required">
            <annotation>
               <documentation>
               </documentation>
            </annotation>
         </attribute>
         <attribute name="categoryId" type="string" use="required">
            <annotation>
               <documentation>
               </documentation>
            </annotation>
         </attribute>
         <attribute name="class" type="string" use="required">
            <annotation>
               <documentation>
               </documentation>
               <appInfo>
                  <meta.attribute kind="java" 
                          basedOn="com.kestler.metrics.IAggregateMetric"/>
               </appInfo>
            </annotation>
         </attribute>
         <attribute name="label" type="string" use="required">
            <annotation>
               <documentation>
               </documentation>
            </annotation>
         </attribute>
         <attribute name="icon" type="string">
            <annotation>
               <documentation>
               </documentation>
               <appInfo>
                  <meta.attribute kind="resource"/>
               </appInfo>
            </annotation>
         </attribute>
      </complexType>
   </element>
   <element name="MetricParticipant">
      <complexType>
         <attribute name="metricId" type="string" use="required">
            <annotation>
               <documentation>
               </documentation>
            </annotation>
         </attribute>
      </complexType>
   </element>
   <annotation>
      <appInfo>
         <meta.section type="since"/>
      </appInfo>
      <documentation>
         1.0.0
      </documentation>
   </annotation>
   <annotation>
      <appInfo>
         <meta.section type="examples"/>
      </appInfo>
      <documentation>
         [Enter extension point usage example here.]
      </documentation>
   </annotation>
   <annotation>
      <appInfo>
         <meta.section type="apiInfo"/>
      </appInfo>
      <documentation>
         [Enter API information here.]
      </documentation>
   </annotation>
   <annotation>
      <appInfo>
         <meta.section type="implementation"/>
      </appInfo>
      <documentation>
         [Enter information about supplied implementation 
                                                   of this extension point.]
      </documentation>
   </annotation>
   <annotation>
      <appInfo>
         <meta.section type="copyright"/>
      </appInfo>
      <documentation>
         
      </documentation>
   </annotation>
</schema>
Back to article


Listing Three
private void processExtensions()
{
    IExtensionRegistry _registry = Platform.getExtensionRegistry();
    IExtensionPoint _ext = _registry.getExtensionPoint(EXT_ID);

    IConfigurationElement[] _cfgs = _ext.getConfigurationElements();
    for (int i = 0; i < _cfgs.length; i++) {

        IDefinition _def = ExtensionDefinitionFactory.getDefinition(_cfgs[i]);
        if (_def instanceof IMetricDefinition) {
            m_metricDefs.put(_def.getId(), _def);

        } else if (_def instanceof ICategoryDefinition) {
            m_categories.put(_def.getId(), _def);
            ICategoryDefinition cDef = (ICategoryDefinition)_def;
            if (cDef.getImage() != null) {
                ImageDescriptor _desc = cDef.getImage();
                m_images.put(_def.getId(), _desc.createImage());
            }
        } else if (_def instanceof IAggregateDefinition) {
            m_aggDefs.put(_def.getId(), _def);
        }
    }
}
Back to article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.