Previous Page
Next Page

17.3. Code Behind an Extension Point

After the extension point has been defined, you must write the code behind it that builds Favorites item types and Favorites objects based on the information declared in extensions of the extension point. Following the Eclipse theme of lazy initialization, you want to keep the memory footprint down, so each Favorites item type and plug-in containing it must be loaded only if necessary. To achieve this, refactor portions of FavoriteItemType (see Section 17.2.3, Extension point elements and attributes, on page 601) into a new FavoriteItemFactory and then reorganize FavoriteItemType to build types from extension information. This is followed by recasting the Favorites item type constants as extensions to the new Favorites extension point.

17.3.1. Parsing extension information

The first modification to the FavoriteItemType involves building instances of this class from the extension information rather than hard-coding the information in the class as constants. Rename the TYPES array to cachedTypes to more accurately represent the purpose of this static field. Modify the getTypes() method to build a new instance of FavoriteItemType for each extension found.

private static final String TAG_ITEMTYPE = "itemType";

private static FavoriteItemType[] cachedTypes;

public static FavoriteItemType[] getTypes() {
   if (cachedTypes != null)
      return cachedTypes;
   IExtension[] extensions = Platform.getExtensionRegistry()
      .getExtensionPoint(FavoritesPlugin.ID, "favorites")
      .getExtensions();
   List found = new ArrayList(20);
   found.add(UNKNOWN);
   for (int i = 0; i < extensions.length; i++) {
      IConfigurationElement[] configElements =
         extensions[i].getConfigurationElements();
      for (int j = 0; j < configElements.length; j++) {
         FavoriteItemType proxy =
            parseType(configElements[j], found.size());
         if (proxy != null)
            found.add(proxy);
      }
   }
   cachedTypes =
      (FavoriteItemType[]) found.toArray(
         new FavoriteItemType[found.size()]);
   return cachedTypes;
}
private static FavoriteItemType parseType(
   IConfigurationElement configElement, int ordinal
)  {
   if (!configElement.getName().equals(TAG_ITEMTYPE))
      return null;
   try {
      return new FavoriteItemType(configElement, ordinal);
   }
   catch (Exception e) {
      String name = configElement.getAttribute(ATT_NAME);
      if (name == null)
         name = "[missing name attribute]";
      String msg =
         "Failed to load itemType named "
            + name
            + " in "
            + configElement.getDeclaringExtension().getNamespace();
            // Eclipse 3.2 - replace getNamespace()
            // with getContributor().getName()
      FavoritesLog.logError(msg, e);
      return null;
   }
}

Tip

As always, proper exception handling is necessary, especially when handling loosely coupled code via extension points. In this case, the instance creation is wrapped in an exception handler so that an improperly declared extension will not cause this method to fail, but instead will generate a log entry containing enough information for the culprit to be tracked down and corrected.


17.3.2. Constructing proxies

Next, you modify the FavoriteItemType constructor to extract the basic information from the extension without loading the plug-in that declared the extension. This instance stands in as a proxy for the factory contained in the declaring plug-in. If a required attribute is missing, then an IllegalArgumentException is thrown, to be caught in the exception handler of the parseType() method described earlier.

 private static final String ATT_ID = "id";
 private static final String ATT_NAME = "name";
 private static final String ATT_CLASS = "class";
 private static final String ATT_TARGETCLASS = "targetClass";
 private static final String ATT_ICON = "icon";

 private final IConfigurationElement configElement;
 private final int ordinal;
 private final String id;
 private final String name;
 private final String targetClassName;
 private FavoriteItemFactory factory;
 private ImageDescriptor imageDescriptor;

 public FavoriteItemType(
    IConfigurationElement configElem, int ordinal
 ) {
    this.configElement = configElem;
    this.ordinal = ordinal;
    id = getAttribute(configElem, ATT_ID, null);
    name = getAttribute(configElem, ATT_NAME, id);
    targetClassName =
       getAttribute(configElem, ATT_TARGETCLASS, null);

    // Make sure that class is defined,
    // but don't load it.
    getAttribute(configElem, ATT_CLASS, null);
 }

   private static String getAttribute(
       IConfigurationElement configElem,
       String name,
       String defaultValue
   )  {
      String value = configElem.getAttribute(name);
      if (value != null)
         return value;
      if (defaultValue != null)
         return defaultValue;
      throw new IllegalArgumentException(
        "Missing " + name + " attribute");
       }

Tip

How do you determine what information to load from an extension immediately versus what should be deferred via lazy initialization to an accessor method? Methods that load extension attribute values, such as IConfigurationElement.getAttribute (String), are very quick to execute because they return already cached information. Other methods, such as IConfigurationElement.createExecutableExtension(String), are quite slow because they will load the declaring plug-in into memory if it has not been loaded already. Our philosophy is to cache and validate attribute values up-front, providing immediate validation and "fast fail" for much of the extension information, but to defer via lazy initialization anything that would cause the declaring plug-in to be loaded.


Potentially, every extension could be invalid and you could end up with no valid instances of FavoriteItemType returned by getTypes(). To alleviate this problem, hard-code a single FavoriteItemType named UNKNOWN and add this as the first object in the collection returned by getTypes().

public static final FavoriteItemType UNKNOWN =
    new FavoriteItemType()
{
    public IFavoriteItem newFavorite(Object obj) {
       return null;
    }
    public IFavoriteItem loadFavorite(String info) {
       return null;
    }
};

private FavoriteItemType() {
   this.id = "Unknown";
   this.ordinal = 0;
   this.name = "Unknown";
   this.configElement = null;
   this.targetClassName = " ";
}

Now, revise the accessors for obtaining information about the item type based on the cached extension information. The icon attribute is assumed to have a path relative to the declaring plug-in, and the image descriptor is constructed accordingly. Images take precious native resources and load comparatively slowly, thus they are lazily initialized on an as-needed basis. Loaded images are cached so that they can be reused and then properly disposed of when the plug-in is shut down (see Section 7.7, Image Caching, on page 315 for ImageCache information).

private static final ImageCache imageCache = new ImageCache();

public String getId() {
   return id;
}
public String getName() {
   return name;
}
public Image getImage() {
   return imageCache.getImage(getImageDescriptor());
}
public ImageDescriptor getImageDescriptor() {
   if (imageDescriptor != null)
      return imageDescriptor;
   String iconName = configElement.getAttribute(ATT_ICON);
   if (iconName == null)
      return null;
   IExtension extension =
      configElement.getDeclaringExtension();
   String extendingPluginId = extension.getNamespace();
   // Eclipse 3.2 - replace getNamespace()
   // with getContributor().getName()
   imageDescriptor =
      AbstractUIPlugin.imageDescriptorFromPlugin(
         extendingPluginId,
         iconName);
   return imageDescriptor;
}

17.3.3. Creating executable extensions

The loadFavorite(String) and newFavorite(Object) methods are redirected to the factory object as specified in the extension. Since instantiating the factory object involves loading the plug-in that contains it, this operation is deferred until needed. The targetClassName is used by the newFavorite(Object) method to determine whether the associated factory can handle the specified object and thus whether the associated factory needs to be loaded. The code that instantiates the factory object is wrapped in an exception handler so that detailed information can be logged concerning the failure that occurred and which plug-in and extension are involved.

public IFavoriteItem newFavorite(Object obj) {
   if (!isTarget(obj)) {
       return null;
   }
   FavoriteItemFactory factory = getFactory();
    if (factory == null) {
       return null;
   }
   return factory.newFavorite(this, obj);
}

private boolean isTarget(Object obj) {
   if (obj == null) {
      return false;
   }
   Class clazz = obj.getClass();
   if (clazz.getName().equals(targetClassName)) {
      return true;
   }
   Class[] interfaces = clazz.getInterfaces();
   for (int i = 0; i < interfaces.length; i++) {
      if (interfaces[i].getName().equals(targetClassName)) {
          return true;
      }
    }
   return false;
}

public IFavoriteItem loadFavorite(String info) {
   FavoriteItemFactory factory = getFactory();
   if (factory == null) {
      return null;
   }
   return factory.loadFavorite(this, info);
}

private FavoriteItemFactory getFactory() {
   if (factory != null) {
      return factory;
   }
   try {
      factory = (FavoriteItemFactory) configElement
         .createExecutableExtension(ATT_CLASS);
   } catch (Exception e) {
      FavoritesLog.logError(
        "Failed to instantiate factory: "
           + configElement.getAttribute(ATT_CLASS)
           + " in type: "
           + id
           + " in plugin: "
          + configElement.getDeclaringExtension().getNamespace(),e);
            // Eclipse 3.2 - replace getNamespace()
            // with getContributor().getName()
    }
    return factory;
}

Tip

Whenever instantiating an object specified in an extension, always use the IConfigurationElement.createExecutable(String) method. This method automatically handles references from extensions in one plug-in's manifest to code located in another plug-in's runtime library as well as various forms of post-instantiation initialization specified in the extension (see Section 20.5, Types Specified in an Extension Point, on page 723). If you use Class.forName(String), then you will only be able to instantiate objects already known to your plug-in because Class.forName(String) uses your plug-in's class loader and thus will only instantiate objects in your plug-in's classpath (see Section 20.9, Plug-in ClassLoaders, on page 742 for more on class loaders).


The new factory type is an abstract base class that must be extended by other plug-ins providing new types of Favorites objects. See the "Tip" in Section 17.2.3, Extension point elements and attributes, on page 601 for a discussion of interface versus abstract base class. The factory type includes a concrete dispose method so that subclasses can perform cleanup if necessary, but are not required to implement this method if cleanup is not needed.

package com.qualityeclipse.favorites.model;

public abstract class FavoriteItemFactory
{
   public abstract IFavoriteItem newFavorite(
      FavoriteItemType type, Object obj);

   public abstract IFavoriteItem loadFavorite(
      FavoriteItemType type, String info);

   public void dispose() {
      // Nothing to do... subclasses may override.
   }
}

17.3.4. Cleanup

When the plug-in shuts down, you must dispose of all cached images and give each of the factory objects an opportunity to clean up. Add the methods, disposeTypes() and dispose(), to the FavoriteItemType. Modify the FavoritesPlugin stop() method to call this new disposeTypes() method.

public static void disposeTypes() {
   if (cachedTypes == null) return;
   for (int i = 0; i < cachedTypes.length; i++)
      cachedTypes[i].dispose();
   imageCache.dispose();
   cachedTypes = null;
}

public void dispose() {
   if (factory == null) return;
   factory.dispose();
   factory = null;
}


Previous Page
Next Page