Skip to main content

skip to main content

developerWorks  >  XML  >

Working XML: Building a project with Eclipse and XM

A close look at incremental builders

developerWorks
Document options

Document options requiring JavaScript are not displayed

Discuss


Rate this page

Help us improve this content


Level: Intermediate

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

01 Mar 2003

In his latest installment on Eclipse and XM, Benoît Marchal implements a builder that recompiles the project automatically when a file is added or changed. He also looks into future enhancements for XM.

The Eclipse and XM saga continues. If you missed the previous columns, here is the plot so far: XM is a simple content management solution originally developed for the Working XML column. It was originally designed for batch processing, but after using it to publish several Web sites and receiving plenty of feedback from users, I realized that it needed a friendlier interface.

Rather than design an integrated development environment (IDE) from scratch, I turned to Eclipse. Eclipse is an open-source project initiated by IBM to build an extensible IDE in the Java programming language, making it possible to add new languages and features to the IDE through plug-ins. The goal in this series is to prepare such a plug-in for XM.

Project builder

In the previous installment of this column (see Resources), I added a New Project wizard to the XM plug-in. The wizard initializes an XM project, including the source, rules, and publish directories. It also adds sample XML and XSLT files to the project.

In addition, the wizard registers a project nature as org.ananas.xm.eclipse.xmnature, and the project nature identifies an XM project as such. Next, Eclipse associates menu items and other interface widgets to the project nature, and then the plug-in registers the batch XM process as a builder (essentially a compiler) with the project.

I wasn't able to discuss the builder in the previous column, so I'll introduce it now. Because I've fixed bugs that were in earlier versions, it's best that you download the latest version of the code -- even if you downloaded the builder when it was originally released (see Resources for a link to the latest release).

I must also warn you that I am not totally happy with this implementation of the builder. More specifically, it suffers from a few limitations in the current (batch-centric) implementation of XM. In this article, I'll show you how you can eventually overcome those limitations.

IncrementalProjectBuilder

Like every other aspect of the Eclipse platform, builders are defined through extension points. As you saw in previous columns -- particularly "Use Eclipse to build a user interface for XM" (see Resources) -- you must declare extension points in the plug-in manifest and provide an implementation class. For builders, the extension point to declare is org.eclipse.core.resources.builders, and the implementation class must extend the org.eclipse.core.resources.IncrementalProjectBuilder class.

The name of the class highlights one of the most important features Eclipse expects from its builders: They should be able to rebuild a project incrementally. Incremental building means that only those files that have been modified since the builder was last called will be processed. The obvious goal is to speed things up.

Some IDEs only build the project when the developer requests it -- such as when he clicks the Run or Debug buttons. In those environments, it seems to take forever to launch a test version of the project. Eclipse works differently. It is constantly rebuilding the project in the background in an attempt to spread the load over the entire development session. It marginally slows things down only when saving a file, but it's very fast when you actually click Run or Debug.

Perhaps more importantly, Eclipse helps catch typos and bugs early. When saving a file, users receive feedback immediately on possible errors. If the builder is speedy enough, it's a great feature. (While working on the plug-in, I have found that the instantaneous feedback saves me a lot of time, compared with some other IDEs I have worked with.) Obviously, since speed is critical, the builder should be as fast as possible so as not to slow down the user.

To speed things up, a good builder should only recompile what the user has changed -- not that this is necessarily an easy task. For instance, when a class is changed in Java code, the change may affect other classes (for instance, depending on optimizations, when the value of a final variable changes, the compiler may have to update other classes that reference the variable). Another example is a C/C++ compiler that needs to save precompiled headers to speed up building a new class.

The platform tries to help with the recompiling effort by continuously tracking the project and reporting the changes to the builder that the programmer has made. In theory, the builder should use this information to decide which files to recompile.

There's good and not-so-good news as far as XM goes. The good news is that XM has always offered incremental builds. DirectoryWalker, the class that is responsible for building the site, makes sure that only those files that have changed since the previous run are styled.

The not-so-good news is that DirectoryWalker was never designed to integrate with another application. Right now it compiles its own list of changes, which in effect duplicates the project management by Eclipse. The ideal solution, of course, would have been to modify DirectoryWalker to take advantage of the information that the Eclipse platform offers. However, I chose a less elegant solution. I have been thinking about updates to DirectoryWalker for several months now, and I have decided to delay any major changes until I work out all of the features that should be added. I will revisit this topic (including a look at how Eclipse reports changes to the builder) in the Delta management section of this article.

This compromise works reasonably well. Although XM is not as fast as it could be, it's fast enough for most normal operations.

XMBuilder

The builder is implemented in the XMBuilder class, as shown in Listing 1. Obviously, it inherits from IncrementalProjectBuilder. The most important method is build(), which Eclipse calls whenever the project should be rebuilt. The method takes three arguments:

  1. kind -- an integer that takes one of the following values:
    • AUTO_BUILD: If the platform detects that the project has changed, the builder should perform an incremental build.
    • FULL_BUILD: Implemented if the user has requested a full rebuild (for instance, through the menu).
    • INCREMENTAL_BUILD: Implemented if the user has requested an incremental build (though I confess that I have not found out how to get the platform to send this later code).

  2. args -- a set of arguments for the builder

  3. monitor -- a progress monitor for the builder to update the user. The monitor controls a progress bar and a stop button. Since builds can be slow, the builder should test that the user has not cancelled the operation.

You might wonder about the resource changes that I discussed previously. Eclipse has a different interface to report on those, which I discuss later in the Delta management section.

The build() method returns an array of projects. The platform will attempt to track changes in those projects so a builder can register interest in subprojects or any other project it thinks might influence its build process. The platform only attempts to track those projects, though it might not succeed (for example, it may discard changes in order to save memory).


Listing 1. XMBuilder.java
                
package org.ananas.xm.eclipse;

import java.io.*;
import java.util.Map;
import org.ananas.xm.*;
import org.eclipse.ui.*;
import org.eclipse.core.runtime.*;
import org.eclipse.core.resources.*;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.swt.widgets.Display;

public class XMBuilder
   extends IncrementalProjectBuilder
{
   private IWorkbench workbench;
   
   public XMBuilder()
   {
      workbench = PlatformUI.getWorkbench();
   }
	
protected IProject[] build(int kind,Map args,IProgressMonitor _monitor)
   {
      try
      {
         IProject project = getProject();
         if(project != null && project.isAccessible())
         {
            if(_monitor == null)
               _monitor = new NullProgressMonitor();
            saveDirtyEditors();
            MessengerProxy proxy = new MessengerProxy();
            MessengerView console = MessengerView.showConsole(workbench);
            if(console != null)
            {
               console.clean();
               proxy.addMessenger(console);
            }
            MessengerMonitor monitor = 
            new MessengerMonitor(_monitor,"Publish XML documents");
            proxy.addMessenger(monitor);
            IResource publish = runXM(proxy,kind == FULL_BUILD);
            publish.refreshLocal(IResource.DEPTH_INFINITE,_monitor);
            monitor.done();
         }
      }
      catch(Exception x)
      {
  ErrorDialog.openError(workbench.getActiveWorkbenchWindow().getShell(),
                               Resources.getString("eclipse.dialogtitle"),
                               Resources.getString("eclipse.builderror"),
                               PluginTools.makeStatus(x));
      }
      return new IProject[0]; 
   }

   private IResource runXM(Messenger messenger,boolean build)
      throws CoreException, IOException, XMException
   {
      IProject project = getProject();
  String rulesPath = 
  project.getPersistentProperty(PluginConstants.RULES_PROPERTY_NAME),
  sourcePath = 
  project.getPersistentProperty(PluginConstants.SOURCE_PROPERTY_NAME),
  publishPath = 
  project.getPersistentProperty(PluginConstants.PUBLISH_PROPERTY_NAME);
  if(rulesPath == null)
     rulesPath = "rules";
  if(sourcePath == null)
     sourcePath = "src"; 
  if(publishPath == null)
     publishPath = "publish";
  rulesPath = project.getFolder(rulesPath).getLocation().toOSString();
  sourcePath = project.getFolder(sourcePath).getLocation().toOSString();
  IFolder publishFolder = project.getFolder(publishPath);
  publishPath = publishFolder.getLocation().toOSString();
  DirectoryWalker walker = new DirectoryWalker(messenger,rulesPath,build);
  walker.walk(sourcePath,publishPath);
  return publishFolder;
   }

   private void saveDirtyEditors()
   {
      Display display = Display.getCurrent();
      if(display == null)
         display = Display.getDefault();
      display.syncExec(new Runnable()
      {
         public void run()
         {
            IWorkbenchWindow[] windows = workbench.getWorkbenchWindows();
            for(int i = 0;i < windows.length;i++)
            {
               IWorkbenchPage[] pages = windows[i].getPages();
               for(int j = 0;j < pages.length;j++)
                   pages[j].saveAllEditors(false);
            }
         }
      });
   }
}

It seems logical to map AUTO_BUILD or INCREMENTAL_BUILD to a regular XM build operation (which uses DirectoryWalker's own incremental builder) and map FULL_BUILD to an XM full build.

The build process

The builder retrieves the project to build through the inherited getProject() method, first testing to ensure that the project is accessible before attempting the build.

Next, the builder validates the IProgressMonitor interface it received as a parameter. If the monitor is missing, it creates a NullProgressMonitor object instead. NullProgressMonitor, which has no user interface, ignores the progress reports. It's handy because it saves testing whether the monitor is null before issuing a progress report.

The builder then attempts to save the various editors opened in the IDE. This code is borrowed from the original XM plug-in. I confess that I have not yet decided whether I should leave this in or get rid of it. You could argue that the user does not need that sort of hand guiding. Yet I found it convenient in the original plug-in, so I decided to leave it in there for now. (You can relate your experience in the discussion forum.)

The next step is to initialize various Messenger objects. Messenger is an interface that XM uses to report progress and errors to the user. See the MessengerMonitor and MessengerProxy sections for more details on this topic.

Finally, build() invokes the runXM() method, which is equivalent to calling XM from the command line. The method returns an IResource instance that points to the directory where the new site has been published. The build() method then terminates by refreshing the directory. The method also catches exceptions and displays them in a standard ErrorDialog.

XM invocation

The actual invocation of XM takes place in the runXM() method. The method is very similar to main() in the batch interface. It retrieves the various parameters (source, rules, and publish directories) from the project properties. The parameters were initialized by the project wizard, as discussed in the "Creating the project" column (see Resources). The method then creates and lets loose an instance of DirectoryWalker (the original batch process), which rebuilds the Web site.

MessengerMonitor

The original design of XM called for a separation of user interface and processing. This has proved invaluable when writing plug-ins. The separation is achieved through the Messenger interface, which defines four methods that XM calls to report progress to the user:

  • progress()
  • info()
  • warning()
  • error()

The separation is effective. In "Integrating XM and Eclipse" (see Resources), I wrote a version of Messenger that displays messages in a view on the workspace.

Since the platform passes an IProgressMonitor instance to the builder, it seems logical to forward progress information to it. Listing 2 shows MessengerMonitor, an implementation of Messenger that does exactly that.


Listing 2. MessengerMonitor.java
                
package org.ananas.xm.eclipse;

import java.io.File;
import org.ananas.xm.*;
import org.eclipse.core.runtime.IProgressMonitor;

public class MessengerMonitor
   implements Messenger
{
   private IProgressMonitor monitor;

   public MessengerMonitor(IProgressMonitor monitor,String taskName)
   {
   	  this.monitor = monitor;
      monitor.beginTask(taskName,IProgressMonitor.UNKNOWN);
   }

   public void done()
   {
      monitor.done();
   }

   public boolean progress(File sourceFile,File resultFile)
      throws XMException
   {
   	  monitor.worked(IProgressMonitor.UNKNOWN);
      return !monitor.isCanceled();
   }

   public void error(XMException e)
      throws XMException
   {
   }

   public void fatal(XMException e)
      throws XMException
   {
   }

   public void warning(XMException e)
      throws XMException
   {
   }

   public void info(String msg)
      throws XMException
   {
   }

   public void info(String pattern,Object[] arguments)
      throws XMException
   {
   }
}

The method also tests to see if the user has cancelled the build. DirectoryWalker will stop if progress() returns false.

MessengerProxy

Obviously, MessengerMonitor only does something sensible in the progress() method. What if XM reports an error? There's no provision for errors on IProgressMonitor. One option would be to open an error box. The problem with error boxes, though, is that when you dismiss them, you can no longer see the message. When debugging a style sheet, it's more convenient to retain a list of errors on the screen. The ideal solution, then, is to open the XM console and route error messages to it.

Listing 3 is an excerpt from MessengerProxy. This class offers a convenient solution for routing messages to different Messenger instances. It implements the Messenger interface and forwards any messages it receives to all registered Messengers -- sort of like multicasting AWT events.


Listing 3. Excerpt of MessengerProxy.java
                
public synchronized void error(XMException e)
   throws XMException
{
   Iterator iterator = messengers.iterator();
   XMException caught = null;
   while(iterator.hasNext())
      try
      {
         ((Messenger)iterator.next()).error(e);
      }
      catch(XMException x)
      {
        	caught = x;
      }
   if(caught != null)
      throw new XMException(caught);
}

The builder uses the proxy to send messages from XM to the progress monitor and the console. This solution is particularly flexible and makes it easy to add even more Messengers in the future, such as a Messenger to update the status bar.

Refreshing directories

As you have seen, the platform is continuously monitoring the project to decide whether a rebuild is required and to update the user interface (such as the Eclipse navigator). When a plug-in creates a file, it should use IResource and its descendant. This was illustrated in the wizard introduced in my previous column. When the wizard creates project files and directories, it does so through IFile and IFolder.

This approach works as long as the application was designed specifically for Eclipse. It does not work so well with most Java language libraries, such as the XSLT processor. The libraries typically use java.io instead of their Eclipse counterpart. The solution is to tell Eclipse to read the resource from the local file system. Incidentally, you would use the same technique if the project was calling an external tool (say a native application).

XMRunner

If you have been following the Eclipse series of articles since the beginning, you might remember the XMRunner class. It manages a Run XM entry in the contextual (right-click) menu and was the original plug-in for Eclipse.

XMRunner has been rewritten entirely to take advantage of the incremental builder. It seems a waste of resources to maintain two different classes that invoke XM. Instead, it makes more sense to have one class call the other.

XMRunner still extends the ActionDelegate interface (to be notified when the user selects the menu entry). The run() method has been rewritten to simply request a full build of the project. This is done through the build() method on the IProject interface.

Note the validateProjectNature() method. This method makes sure that the project is registered with the XM nature so as to enable XM-specific menus. To decide whether a project has the right nature, validateProjectNature() calls the hasNature() method on the project object. If the XM nature is missing, the method grabs an array with all the current project natures, copies them in a new array with room for one more entry, adds the XM nature, and registers the larger array with the project. It's important to retain existing project natures because a project could be a combination of, say, PHP code (a popular language for Web sites) and XM code. Therefore, it would have both the PHP and the XM natures.

Admittedly, it is currently impossible to invoke XMRunner if the project does not have the right nature (because the Run XM menu entry is registered based on the project nature), so it's sort of a moot point. Yet I expect to add an option to turn any project into an XM project, which would make it easier to create mixed projects that use both XM and, say, PHP.



Back to top


The future of DirectoryWalker

Although I decided not to rely on Eclipse's own resource management to track changes to the project, I did look into it. In this section, I'll discuss my findings. Furthermore, as I explained previously, I have plans to modify DirectoryWalker to better take advantage of Eclipse.

A new DirectoryWalker

The first step in preparing for a new DirectoryWalker is to analyze the requirements. Based on my own experience with XM and the experience of my colleagues, here are some of the new features that could be added to a new version of DirectoryWalker:

  • Improved support for content generation: In my experience, one of the most useful features of XM is its ability to read a directory and generate an XML document with the files. I have used this feature to generate download pages, tables of contents (when a document is spread across different files), statistics, and much more. Yet XM is not very efficient when processing these files. It always regenerates them, which may slow things down with large sites.

  • Improved link management: Link management works well, but it assumes XSLT 1.0. XSLT 2.0 will add the ability to split an XML document into several documents. Most XSLT processors already have an extension that simulates it. XM link management was not designed to accommodate this feature.

  • Support for XInclude or another mechanism to incorporate one document in another: Again, XM should ideally manage the relationship between two documents so that it publishes the combined document if either of the two change.

  • Ability to ignore hidden files

  • Ability to publish a document if its style sheet has been modified

  • Ability to interface with an external project manager, such as Eclipse.

What's on your list? Please take a moment to share your thoughts on the forum.

Currently, DirectoryWalker runs through the directory and processes files as it finds them. To better manage the relationship between files, I would need to change DirectoryWalker to run over two passes -- a first pass would collect information on all the files (and their relationships), and the second would process the documents and apply the style sheets.

Delta management

To understand better how Eclipse tracks changes to the project, I wrote IncrementalBuilderDemo. This class extends IncrementalProjectBuilder, so you can register it as a builder. Instead of compiling a project, IncrementalProjectBuilder reports on changes to the resources as it receives them from the Eclipse platform.

The report starts in build(), as it did before. If the request is for an incremental build, control flows to incrementalBuild(); otherwise a message is printed to the console.

incrementalBuild() retrieves the status of the project and prints the changes to the console. The platform reports those changes through the IResourceDelta interface. The builder then retrieves an instance of IResourceDelta by calling the getDelta() method.

Surprisingly, the getDelta() method takes the IProject interface as a parameter. A builder can request information on projects other than the one it is currently building, which is particularly handy for subprojects. As we discussed in the XMBuilder section, your builder should register interest in those projects by returning them from the build() method.

The IResourceDelta interface implements the visitor pattern. The visitor pattern is a simple technique to process arbitrary data structures. Here the data structure is IResourceDelta.

In the visitor pattern, the calling method (the builder, in this case) implements a visitor interface (the specific interface to implement is IResourceDeltaVisitor). The data structure accepts the visitor object and will call its visit() method for every entry in the data structure. In this particular case, IResourceDelta calls visit() for every file change.

Listing 4 is the IncrementalBuilderDemo. The visitor is implemented in an inner class (aptly called Visitor). The relevant code is in the visit() method. It takes IResourceDelta as a parameter. Through this class, you can access information on the change, such as whether it's a new file, a deletion, or a file change. The demonstration simply prints data to the console.


Listing 4. IncrementalBuilderDemo.java
                
package org.ananas.xm.eclipse;
import java.io.*;
import java.util.Map;
import org.ananas.xm.*;
import org.eclipse.ui.*;
import org.eclipse.core.runtime.*;
import org.eclipse.core.resources.*;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.swt.widgets.Display;

public class IncrementalBuilderDemo
   extends IncrementalProjectBuilder
{
   private IWorkbench workbench;
   
   private class Visitor
      implements IResourceDeltaVisitor
   {
      Messenger messenger;
      
      public Visitor(Messenger messenger)
      {
         this.messenger = messenger;
      }
      
      public boolean visit(IResourceDelta delta)
         throws CoreException
      {
      	 try
      	 {
            Object[] path = new Object[]
         	{
         delta.getResource().getProjectRelativePath().toOSString(),
      	    };
            switch(delta.getKind())
            {
               case IResourceDelta.ADDED:
                  messenger.info("Resource {0} added.",path);
                  break;
               case IResourceDelta.REMOVED:
                  messenger.info("Resource {0} removed.",path);
                  break;
               case IResourceDelta.CHANGED:
                  messenger.info("Resource {0} changed.",path);
                  break;
               default:
                  messenger.fatal(new XMException("Unknown delta type"));
            }
            return true;
      	 }
      	 catch(XMException x)
      	 {
      	 	throw new CoreException(PluginTools.makeStatus(x)); 
      	 }
      }
   }
   
   public IncrementalBuilderDemo()
   {
      workbench = PlatformUI.getWorkbench();
   }
	
   protected IProject[] build(int kind,Map args,IProgressMonitor _monitor)
   {
      try
      {
         IProject project = getProject();
         if(project != null && project.isAccessible())
         {
            if(_monitor == null)
               _monitor = new NullProgressMonitor();
            MessengerProxy proxy = new MessengerProxy();
            MessengerView console = MessengerView.showConsole(workbench);
            if(console != null)
            {
               console.clean();
               proxy.addMessenger(console);
            }
            MessengerMonitor monitor = 
            new MessengerMonitor(_monitor,"Publish XML documents");
            proxy.addMessenger(monitor);
            switch(kind)
            {
            	case INCREMENTAL_BUILD:
            	   proxy.info("INCREMENTAL_BUILD requested");
                   incrementalBuild(proxy);
                   break;
            	case AUTO_BUILD:
            	   proxy.info("AUTO_BUILD requested");
                   incrementalBuild(proxy);
                   break;
            	case FULL_BUILD:
            	   proxy.info("FULL_BUILD requested");
                   break;
                default:
            	   proxy.fatal(new XMException("unknown build kind requested"));
            }
            monitor.done();
         }
      }
      catch(Exception x)
      {
         ErrorDialog.openError(workbench.getActiveWorkbenchWindow().getShell(),
                               Resources.getString("eclipse.dialogtitle"),
                               Resources.getString("eclipse.builderror"),
                               PluginTools.makeStatus(x));
      }
      return new IProject[0]; 
   }

   private void incrementalBuild(Messenger messenger)
      throws CoreException, IOException, XMException
   {
      IResourceDelta delta = getDelta(getProject());
      if(delta != null)
         delta.accept(new Visitor(messenger));
      else
         messenger.info("Delta is empty (null)");
   }
}

It's easy to test the IncrementalBuilderDemo -- just update plugin.xml so that the class for the builder is set to IncrementalBuilderDemo, as shown in Listing 5. Run Eclipse and try to modify resources in an XM project. The XM console should report on your change.


Listing 5. Excerpt of plugin.xml
                
<extension id="xmbuilder"
           name="XM Builder"
           point="org.eclipse.core.resources.builders">
   <builder>
      <run class="org.ananas.xm.eclipse.IncrementalBuilderDemo"/>
   </builder>
</extension>



Back to top


The verdict on Eclipse

Over the last four columns, I've covered a fair amount of material on how to write Eclipse plug-ins. If I had to summarize my experience writing the plug-in, I would say that Eclipse is a very interesting platform -- and it offers a well-designed API -- but the documentation is still lacking.

Though significant, Eclipse's documentation shortfall is to be expected from a relatively new project. I hope, however, that this series of articles helps to address the problem.



Resources



About the author

Benoît Marchal is a Belgian consultant. He is the author of XML by Example and other XML books. Benoît is available to help you with XML projects. You can contact him 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