Skip to main content

skip to main content

developerWorks  >  XML | Open source  >

Working XML: Define and load extension points

Extend Eclipse plug-ins to make them more versatile

developerWorks
Document options

Document options requiring JavaScript are not displayed

Discuss


New site feature

Check out our new article design and features. Tell us what you think.


Rate this page

Help us improve this content


Level: Advanced

Benoit Marchal (bmarchal@pineapplesoft.com), Consultant, Pineapplesoft

03 Feb 2005

In this article, Benoît takes integration between XM, the simple content-management solution, and Eclipse one step further. Publishing a Web site requires you to work with many file types in addition to XML, so it makes sense to design a publishing system around an extensible core. Eclipse plug-ins fit the bill nicely. Benoît shows how to make the XM plug-in extensible to accommodate multiple file types. Share your thoughts on this article with the author and other readers in the accompanying discussion forum. (You can also click Discuss at the top or bottom of the article to access the forum.)

I devoted the previous two columns in the Working XML series to an old friend: XM, my easy-to-use document-publishing solution. XM builds on XML and XSLT to manage Web sites and print (PDF) publishing. In the course of the series, I've updated most of the core publishing engine to make it more flexible and to integrate it with the Eclipse IDE to achieve smarter builds and better error reporting. So far, the XM plug-in extends the platform with a fixed set of features -- the ability to process XML and XSLT files. Now I'll show you how to make the XM plug-in itself extensible so that it can process any file.

Although XM relies primarily on XML, it needs to work with other document types, such as images, office documents, and PDF files. From the outset I've organized XM around an extensible core. One interface (originally Mover, now Batch) groups all the knowledge about a file format. To support a new format (such as PDF), you only need to implement a new Batch, register the Batch with the engine, and recompile. That works great if you're willing to mess around with the source code, but you don't have to. With Eclipse, you can physically uncouple the Batch functionality from the main engine. The new implementation can reside in another plug-in, adding tremendous flexibility.

Extension points

This architecture involves two or more plug-ins working together through an extension point (see Importing versus extension points). If you've been following this series, you're already somewhat familiar with extension points. I implemented a few of them in previous installments (most notably, in Building a project with Eclipse and XM and Take advantage of lessons learned by refactoring XM, where I developed the builder extension as a key part of the XM plug-in).

Importing versus extension points

Eclipse plug-ins don't need extension points to cooperate. A plug-in can also simply import classes from another plug-in (through the requires tag in plugin.xml). With importing, developers can only import services they know about at compile time; with extension points, the developer can specify services before they are available or even written -- a far more flexible approach. With importing, the service provider defines the API; with an extension point, the service user defines it.

Before going any further, I should clarify the difference between an extension point and an extension. An extension point is the definition of a port -- an entry point for other plug-ins to offer services. The closest thing in the Java language is an interface. Like an interface, an extension point defines a contract between the user and the service provider.

The extension implementation is the actual service, packaged in a way that makes it callable through an extension point. If you think of an extension point as an interface, you can think of an extension as a class that implements that interface. A plug-in can both implement extensions (offer services to other plug-ins) and define extension points (request services from other plug-ins).

Although the Eclipse platform ships with many standard plug-ins and their corresponding standard extension points, the extension-point mechanism is completely open. Eclipse-provided plug-ins use no secret backdoor or special service; they are just regular plug-ins. The very same extension option available to standard plug-ins is available to any other plug-in.

Define extension points

Like most things in Eclipse, extension points start in the plugin.xml file. A plug-in describes the extension points it supports through the extension-point tag, as in the definition of the Batch extension point, shown here:

<extension-point id="batch" name="Batch" schema="schema/batch.exsd"/>

The extension-point tag requires three parameters:

  • id is the extension point identifier. Eclipse concatenates it with the plug-in id to make a platform-wide unique identifier.
  • name is a user-friendly name.
  • schema points to an XML Schema that describes the markup for the extension. The extension implementer uses the schema in the plugin.xml file for his or her plug-in.
Most extension points also provide one or more Java interfaces for the extension to implement.

Benefits of the Eclipse plug-in architecture

The Java language has always supported dynamic loading of classes. Eclipse plug-ins build on Java dynamic loading but add two important benefits.

First, the platform is a matchmaker for plug-ins. To be of any use, extension points and extension implementations must find each other -- without recompiling. Eclipse manages an extension registry to make this happen.

Second, a plug-in includes a wealth of descriptive information in its plugin.xml file -- and you can add to it through a schema! So in many cases, the extension point uses the markup to decide whether it's worth loading the extension.

Don't underestimate the second benefit. Many applications based on plug-ins seem to take forever to load because they initialize and load all of their plug-ins at startup. (Adobe® Photoshop® is a notorious example.) The Eclipse IDE is built entirely out of plug-ins, so loading them all at startup would be prohibitive. For example, suppose the user has installed plug-ins for Java, C++, and XML development but is currently working on a Java project. Loading the C++ and XML plug-ins is unnecessary and would only slow things down. Eclipse delays loading necessary plug-ins until the last moment.

The extension schema

Eclipse delays loading plug-ins until the last minute, using data from the plugin.xml descriptor to determine whether a given plug-in is worth loading (see Benefits of the Eclipse plug-in architecture). But how do you decide if you need to load an extension point? The criteria vary depending on the extension point.

To address this problem, plugin.xml is extensible to let the extension point designer add markup with appropriate information. This includes information that the extension point needs to load the extension, such as the class name, and information it needs not to load the extension, such as criteria for deciding if the plug-in applies. You might recall that the XM builder I wrote in previous columns provided both a class name (to load the plug-in) and a project nature (to decide whether it's worth loading the plug-in) in plugin.xml. And you might have noticed that every extension uses different markup.

The extension point specifies the data in an XML Schema. The schema must declare the extension element (with three attributes: id, name, and point). The Eclipse platform requires this element and its three attributes in order to identify the extension. However, the extension element's content is determined by the developer. Because most file types have a unique extension, I decided on a simple filter based on filenames. Listing 1 contains the Batch extension point's schema definition.


Listing 1. Batch schema
<?xml version='1.0' encoding='UTF-8'?>
<schema targetNamespace="org.ananas.xm.eclipse">
<annotation>
   <appInfo>
      <meta.schema plugin="org.ananas.xm.eclipse" id="batch" name="Batch"/>
   </appInfo>
   <documentation>Adds a file type to XM.</documentation>
</annotation>
<element name="run">
   <annotation>
      <documentation>implementation class</documentation>
   </annotation>
   <complexType>
      <sequence/>
      <attribute name="class" type="string" use="required"/>
   </complexType>
</element>
<element name="target">
   <annotation>
      <documentation>filtering to recognize the file type</documentation>
   </annotation>
   <complexType>
      <sequence/>
      <attribute name="pattern" type="string" use="required"/>
      <attribute name="targetAdded" type="boolean"/>
      <attribute name="targetModified" type="boolean"/>
      <attribute name="targetRemoved" type="boolean"/>
      <attribute name="targetUnchanged" type="boolean"/>
   </complexType>
</element>
<element name="batch">
   <complexType>
      <sequence>
         <element ref="run"/>
         <element ref="target" minOccurs="0"/>
      </sequence>
   </complexType>
</element>

<element name="extension">
   <complexType>
      <sequence><element ref="batch"/></sequence>
      <attribute name="point" type="string" use="required">
         <annotation>
            <documentation>
               should be org.ananas.xm.eclipse.batch
            </documentation>
         </annotation>
      </attribute>
      <attribute name="id" type="string">
         <annotation>
            <documentation>identifier</documentation>
         </annotation>
      </attribute>
      <attribute name="name" type="string">
         <annotation>
            <documentation>name</documentation>
         </annotation>
      </attribute>
   </complexType>
</element>
</schema>

Note that Eclipse supports only a subset of the schema definition. Specifically, you must use global elements (defined directly under the schema element and referred to by the ref attribute). The schema must also start with a special annotation called meta.schema that points to the plug-in.



Back to top


Call the extension point

Now that you've declared the extension point, you need to load it. The trick is to delay the loading until it is absolutely necessary.

Find the extension

The premise behind an extension point is that its implementation isn't available at compile time, so a simple new won't suffice. The Eclipse platform manages a registry of extension implementations. To load an extension, you just need to access the registry (through an instance of IExtensionRegistry) from the platform (through the aptly name Platform object), then inquire for the extension points that the plug-in is interested in. The platform returns an IExtensionPoint object.

IExtensionPoint returns an array of IConfigurationElement objects, which represent the extension tags in plugin.xml. For each plug-in that implements the extension point, you'll receive an IConfigurationElement. IConfigurationElement offers methods such as getChildren() and getAttribute(), to retrieve the data from the XML markup. Last but not least, createExecutableExtension() returns a Java class that implements the extension. It takes the name of the Java class from an attribute in the XML markup.

See the loadExtensions() method in Listing 2 for an example.

The proxy pattern

The best solution for loading extension points is to use the proxy pattern. In this pattern, an object (the proxy) handles all access to another object (which, for lack of a better name, I'll call the real object). In doing so the proxy can monitor requests to the real object and filter or reorganize them if needed. Proxies have many applications, such as wrapping legacy systems, adapting interfaces to libraries, and managing copies of the real object. I'll use a proxy to isolate the loading.

Remember, it's better not to load a plug-in until it is absolutely required. For example, it's useless to load a plug-in to process file types that are not in use. The proxy filters calls based on data from the plugin.xml file (as returned by IConfigurationElement).

My implementation filters files by their filenames, but it isn't convenient to test whether the plug-in is necessary on every call. Instead, it makes sense to include the loading management in a proxy object and let the proxy object load the plug-in if and when it's needed. Listing 2 shows a proxy object for loading the Batch extension point.


Listing 2. Proxy for loading extension points
package org.ananas.xm.eclipse;

import org.ananas.xm.core.Batch;
import org.ananas.xm.core.Location;
import org.ananas.xm.core.Filename;
import org.ananas.xm.core.Messenger;
import org.ananas.xm.core.XMException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IConfigurationElement;

public class ExtensionBatch
   implements Batch
{
   protected BatchExtensionPoint extension;
   protected IConfigurationElement element;
   protected String pattern;
   protected boolean targetAdded,
                     targetModified,
                     targetUnchanged,
                     targetRemoved;
   protected Messenger messenger;
   public ExtensionBatch(IConfigurationElement element)
   {
      this.element = element;
      extension = null;
      messenger = null;
      System.out.println(element.getName());
      IConfigurationElement children[] = element.getChildren("target");
      if(children == null || children.length == 0)
      {
         pattern = null;
         targetAdded = true;
         targetModified = true;
         targetUnchanged = false;
         targetRemoved = false;
      }
      else
      {
         pattern = children[0].getAttribute("pattern");
         String st = children[0].getAttribute("targetAdded");
         targetAdded = st == null ?
            true : Boolean.valueOf(st).booleanValue();
         st = children[0].getAttribute("targetModified");
         targetModified = st == null ?
            true : Boolean.valueOf(st).booleanValue();
         st = children[0].getAttribute("targetUnchanged");
         targetUnchanged = st == null ?
            true : Boolean.valueOf(st).booleanValue();
         st = children[0].getAttribute("targetRemoved");
         targetRemoved = st == null ?
            true : Boolean.valueOf(st).booleanValue();
      }
   }
   public void setMessenger(Messenger messenger)
      throws XMException
   {
      this.messenger = messenger;
      if(extension != null)
         extension.setMessenger(messenger);
   }
   public Messenger getMessenger()
   {
      return messenger;
   }
   public boolean isTargetAdded()
   {
      return targetAdded;
   }
   public boolean isTargetModified()
   {
      return targetModified;
   }
   public boolean isTargetUnchanged()
   {
      return targetUnchanged;
   }
   public boolean isTargetRemoved()
   {
      return targetRemoved;
   }
   public String getName()
   {
      return element.getAttribute("name");
   }
   public int appliesTo(Filename filename)
      throws XMException
   {
      if(filename.nameMatches(pattern))
      {
         loadExtension(filename);
         return extension.confirmAppliesTo(filename);
      }
      return 0;
   }
   public boolean process(Filename publish,Filename file)
      throws XMException
   {
      loadExtension(file);
      return extension.process(publish,file);
   }
   protected void loadExtension(Filename file)
      throws XMException
   {
      try
      {
         Object o = null;
         IConfigurationElement children[] = element.getChildren("run");
         if(children != null && children.length != 0)
            o = children[0].createExecutableExtension("class");
         if(o == null || !(o instanceof BatchExtensionPoint))
            messenger.fatal(
               new XMException(messenger.getResourceString(
                  "eclipse.noextension",element.getAttribute("id")),
                  new Location(file,
                     Location.UNKNOWN_POSITION,
                     Location.UNKNOWN_POSITION)));
         else
         {
            extension = (BatchExtensionPoint)o;
            extension.setMessenger(messenger);
         }
      }
      catch(CoreException x)
      {
         messenger.fatal(new XMException(x));
      }
   }
   static public ExtensionBatch[] loadExtensions(Messenger messenger)
      throws XMException
   {
      IExtensionRegistry registry = Platform.getExtensionRegistry();
      IExtensionPoint extensionPoint =
         registry.getExtensionPoint("org.ananas.xm.eclipse.batch");
      IConfigurationElement points[] =
         extensionPoint.getConfigurationElements();
      ExtensionBatch batches[] = new ExtensionBatch[points.length];
      for(int i = 0;i < points.length;i++)
      {
         batches[i] = new ExtensionBatch(points[i]);
         batches[i].setMessenger(messenger);
      }
      return batches;
   }
}



Back to top


Conclusion

Eclipse is more than an IDE. It includes a sophisticated plug-in solution that turns it into a true development platform. You can use Eclipse for many applications, including a publishing solution, as this series has demonstrated. One of the keys to Eclipse's success is that very little is prewired into the platform. Instead you, the developer, are given flexible tools that you can shape into the best form for a task.



Resources



About the author

Benoît Marchal is a consultant and writer based in Namur, Belgium. He has just released the second edition of XML by Example . He is also the author of Applied XML Solutions and XML and the Enterprise. Details on his latest projects are at marchal.com. You can contact Benoît at bmarchal@pineapplesoft.com.




Rate this page


Please take a moment to complete this form to help us better serve you.



 


 


Not
useful
Extremely
useful
 


Share this....

digg Digg this story del.icio.us del.icio.us Slashdot Slashdot it!



Back to top