In this new series, I will revisit an old friend of the Working XML column: XSLT Make, or XM. This will serve as an excuse to work on Eclipse plug-ins. I first introduced XM in July 2001 as the first project in the Working XML column. It is a lightweight and affordable tool for publishing documents with XML and XSLT.
In October 2002, I decided to add a graphical user interface to the XM tools. Instead of developing a whole interface from scratch, I turned to a nascent IDE: Eclipse. I chose Eclipse because it was extensible, written in the Java language, and offered a fantastic widget library.
Revisiting XM gives me a chance to improve the Eclipse integration in two areas: I will fix an annoying user interface limitation (in this article) and rewrite the core XM engine to better integrate with Eclipse. In the process, I will also make it more extensible and powerful. Work on the core XM engine is scheduled for the next two articles.
XM has been around for a while. It has proven its worth in numerous projects, both in my consulting practice and for readers. For example, I use XM as a teaching tool, to manage Web sites for my customers, and to publish large volumes of documentation in HTML and PDF. See Resources for articles on the project.
The rationale behind the initial development of XM was to make XML and XSLT more approachable. I needed a simple-yet-effective solution for maintaining Web sites with small to medium-sized teams. I knew XML and XSLT offered a great basis, but at the time I could not find a suitable tool. Finally, I rolled up my sleeves and made my own. The tools available in 2001 were either too simple (they worked on individual files, not a whole site) or too complex (being aimed at large teams).
XM is powerful (I have used it in projects with thousands of pages), but it also is simple enough for small and medium-sized teams.
Two of XM's most important features are that:
- It works right out of the box. You don't need to prepare complex scripts or write advanced configuration files. With XM, it suffices to place your documents in one directory, your stylesheets in another, and, voila, you're ready to publish.
- It produces a static site while offering most of the management benefits of dynamic sites. For instance, you only have to edit one stylesheet to change the layout of the site.
The second point may be more controversial, but it has been my experience that hosting a static site requires less work and is more efficient. Some sites require a combination of static and dynamic pages, but hosting much of the site in static mode helps prevent problems: It uses fewer software packages, which reduces the chances of failure. In addition, the site is more responsive because more sophisticated caching is possible. I encourage you to review the original articles (see Resources) for more information on XM's unique features.
Things change in a couple of years. Today, a handful of open-source projects could address my needs (see Resources). I have worked with some of those projects and, while I don't claim to have extensive experience with them, I have found that some of them are more powerful than XM, but none is as simple to use.
The Eclipse platform has also seen radical changes. Today, Eclipse is one of the most visible open-source IDEs, with thousands of plug-ins available. More important, the documentation has been upgraded and more samples are available. I remember struggling with the source code and debugger to figure out how to achieve certain effects because the documentation was nonexistent. Those days are gone.
Technically, the Eclipse project has moved from version 2.0 to version 3.0. The new API is expected to lay the foundation for many years to come. Fortunately, versions are mostly compatible (and indeed the XM plug-in, written for Eclipse 2.0, works well on Eclipse 3.0), but some changes are not backwards compatible. It's a good idea to clean up the code and adopt the new APIs where possible.
This new series has two goals:
- To improve the Eclipse integration. Although functional, the original plug-in has some rough edges. By integrating more closely with Eclipse resource management, I hope to smooth them down a bit.
- To rewrite the core engine. I have used XM in many projects. In some projects, I have met the limits in my original design of the core engine and have had to implement workarounds. It is time to fold these changes into the project.
As I have said many times in past columns, Eclipse is more than an IDE. It is best described as a platform for building an IDE. Eclipse boils down to a system that manages plug-ins. It offers services such as loading plug-ins, managing relationships and dependencies among plug-ins, managing the interfaces between plug-ins (through extension points), and more.
Obviously some of the plug-ins offer services needed by every application, so they are part of the core. SWT, the widget library, is one of these. Other plug-ins, such as the XM plug-in, are more specific and are installed by the user on an ad hoc basis.
One of the core services is the resource
management that's available through the org.eclipse.core.resources
plug-in. For Eclipse, everything underneath the workspace is a resource. The
primary interface for resources is IResource
(pretty obvious). The most commonly used descendants are IFile,
IFolder, and IProject, which
represent files, folders, and projects, respectively.
While related, IResource and JDK's
File objects really are different
beasts. A JDK File represents an entry on the
file system, while an Eclipse IResource adds several
layers of abstraction on top of the file system. First, resources have
properties, which represent information on the resource that helps
plug-ins process the resource. For example, plug-ins can cache the content
of the <?xml-stylesheet?> processing
instruction as a property. Caching the data as a property saves having to
parse the file whenever the plug-in is run. Properties can be stored in
memory (they are lost when the user quits the editor) or persisted to the
file system.
Also, when a resource is added, removed, or edited,
it becomes desynchronized from the file system. IResource
records the state of resources and offers methods for synchronizing with the
file system. Most important, Eclipse can notify a plug-in of changes to
the resources and the file system. When resources are synchronized
with the file system, Eclipse passes the plug-in a delta, the list of
changes since the last synchronization. Obviously, this enables intelligent
builds where only those resources that were modified are recompiled.
From the user's point of view, Eclipse support has two problems: XM has its own logic for rebuilding projects and its own logic for reporting errors. Ultimately, both issues arise because XM ignores Eclipse resource management.
I plan to address the build process in the next two articles in the series. For now, I'll concentrate on error reporting.
Eclipse offers a task list and a problem list for builders and compilers to report errors, as shown in Figure 1. When the user double-clicks an entry, the editor opens with the offending file. Unfortunately, when I was writing the first version of the plug-in, I could not find documentation on how to add entries to the lists, so I skipped it. Instead, the plug-in has its own console, but it does not support double-clicking.
Figure 1. Task and problem lists
As it turns out, it is not difficult to append messages to the standard lists, but it takes an indirection. I originally looked for a task list object, but I could not see how to append an entry to the list. It turns out that you don't -- or at least you don't add them directly to the list. To append an error message, you need to create a marker (interface IMarker) on a resource. To remove a message from the list, you remove the marker from the resource. The lists automatically update to reflect the markers.
The createMarker()
method is used to create markers. It takes the marker ID as a parameter.
The platform defines a few standard marker IDs:
org.eclipse.core.resources.marker-- the root of the marker hierarchyorg.eclipse.core.resources.problemmarker-- represents problems and error messages; they appear in the problem listorg.eclipse.core.resources.taskmarker-- represents to-do items; they appear in the task listorg.eclipse.core.resources.bookmark-- represents a file such as the result of a searchorg.eclipse.core.resources.textmarker-- represents a position in a file such as the location of an error
It is a good idea to define plug-in-specific markers. The IDs for the new markers
are declared in the plugin.xml file (like every
other declaration in Eclipse). Listing 1 shows a marker declaration, defining
an extension on the marker ID (org.eclipse.core.resources.markers).
It also declares that the new marker inherits from problemmarker (to show up
in the problem list) and from textmarker (to be able to record a line
number). Making the marker permanent means that it will be stored between
sessions.
Listing 1. Marker declaration
<extension id="marker"
name="XM Message"
point="org.eclipse.core.resources.markers">
<super type="org.eclipse.core.resources.problemmarker"/>
<super type="org.eclipse.core.resources.textmarker"/>
<persistent value="true"/>
</extension>
|
The user can filter messages according to a number of criteria, such as the type of problem (warning, error), the priority, and the marker ID. Defining a plug-in-specific marker helps the user to apply specific filtering rules to the plug-in messages.
A word of warning: It is possible for Eclipse to filter out messages from a plug-in. If you don't see any error message from XM, make sure that a filter did not remove it. To change the filters, click the filter icon in the task list and in the problem list. Make sure that XM markers are selected.
Once you know this trick, it is not difficult to integrate it into XM. From the
very beginning, the user interface has been abstracted through the Messenger
interface. Messenger defines the methods that the core needs to report
errors and progress information. To support the problem list, you just need to
write a new implementation of Messenger that creates the appropriate
markers, as shown in Listing 2. Note that the begin() method
removes all the markers to clean the problem list before a build.
Listing 2. Messenger for markers
package org.ananas.xm.eclipse;
import java.text.MessageFormat;
import org.eclipse.ui.IWorkbench;
import org.ananas.xm.core.Filename;
import org.ananas.xm.core.Location;
import org.ananas.xm.core.Messenger;
import org.eclipse.ui.IWorkbenchPage;
import org.ananas.xm.core.XMException;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.ui.views.markers.MarkerViewUtil;
public class MessengerTaskList
implements Messenger, EclipseConstants
{
private IProject project = null;
private IWorkbench workbench = null;
private boolean noMarkerSoFar = true;
private static class ShowMarkerView
implements Runnable
{
private IWorkbench workbench;
private IMarker marker;
public ShowMarkerView(IWorkbench workbench,IMarker marker)
{
this.workbench = workbench;
this.marker = marker;
}
public void run()
{
IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
if(window == null)
{
IWorkbenchWindow[] windows = workbench.getWorkbenchWindows();
if(windows != null && windows.length > 0)
window = windows[0];
else
return;
}
IWorkbenchPage page = window.getActivePage();
if(page != null)
MarkerViewUtil.showMarker(page,marker,true);
}
}
public MessengerTaskList(IWorkbench workbench,IProject project)
{
if(null == project || null == workbench)
throw new NullPointerException("null argument in TaskListMessenger constructor");
this.project = project;
this.workbench = workbench;
}
protected void addMarker(String msg,Location location,int severity,int priority)
throws XMException
{
IResource resource = null;
if(null == location || location.equals(Location.UNKNOWN))
resource = project;
else
resource = (IResource)location.getFilename().asPlatformSpecific();
try
{
IMarker marker = resource.createMarker(MARKER_ID);
if(null != location && Location.UNKNOWN_POSITION != location.getLine())
marker.setAttribute(IMarker.LINE_NUMBER,location.getLine());
if(null != msg)
marker.setAttribute(IMarker.MESSAGE,msg);
marker.setAttribute(IMarker.SEVERITY,severity);
marker.setAttribute(IMarker.PRIORITY,priority);
if(noMarkerSoFar)
showMarkerView(marker);
else
noMarkerSoFar = false;
}
catch(CoreException e)
{
throw new XMException(e,location);
}
}
public void error(XMException x)
throws XMException
{
addMarker(x.getMessage(),x.getLocation(),IMarker.SEVERITY_ERROR,IMarker.PRIORITY_NORMAL);
}
public void fatal(XMException x)
throws XMException
{
addMarker(x.getMessage(),x.getLocation(),IMarker.SEVERITY_ERROR,IMarker.PRIORITY_HIGH);
}
public void warning(XMException x)
throws XMException
{
addMarker(x.getMessage(),x.getLocation(),IMarker.SEVERITY_WARNING,IMarker.PRIORITY_LOW);
}
public boolean progress(Filename sourceFile,Filename resultFile)
{
return true;
}
public void info(String msg,Location location)
throws XMException
{
addMarker(msg,location,IMarker.SEVERITY_INFO,IMarker.PRIORITY_NORMAL);
}
public void info(String pattern,Object[] arguments,Location location)
throws XMException
{
info(MessageFormat.format(pattern,arguments),location);
}
public void begin(String source,String target)
throws XMException
{
try
{
project.deleteMarkers(MARKER_ID,true,IResource.DEPTH_INFINITE);
noMarkerSoFar = true;
}
catch(CoreException e)
{
throw new XMException(e);
}
}
public void end()
{
}
protected void showMarkerView(IMarker marker)
{
Display display = Display.getCurrent();
if(display == null)
display = Display.getDefault();
ShowMarkerView showMarkerView = new ShowMarkerView(workbench,marker);
display.syncExec(showMarkerView);
}
}
|
XM has always been organized around two components: the core, which is
independent of Eclipse and offers a command-line interface, and the Eclipse
plug-in itself. To port XM to other user interfaces, implement the interfaces such as Messenger (see the previous section) that abstract the user interface. For some projects, I have defined a servlet user interface over Eclipse.
Although I plan to integrate XM more tightly with Eclipse, I also want to retain the command-line option. Both interfaces offer value. For day-to-day operations, I mostly work with the Eclipse environment, but the command-line version is handy for crontab (a UNIX utility for scheduling job execution). To support both modes, I have abstracted resources and files from the XM core engine.
The original XM worked with the JDK's File object,
which is the root of most integration problems because, as you have seen, Eclipse does not use File objects. Instead, it uses its own IResource interface.
Furthermore, experience has taught me that relying on File is
limiting. Eclipse is not the only package that does not use files -- SAX uses
InputSource, and JAXP uses Source.
What do you do when your code needs to interface with several different libraries? You abstract the libraries using the proxy pattern (see Resources). In the proxy pattern, one (or more) object offers a common interface to the underlying libraries. The object can be instantiated to forward a request to any of the libraries. The beauty of this pattern is that the calling code need not worry about the library the proxy is forwarding to.
XM introduces the Filename interface to
abstract the notion of a file or resource. The Filename
interface has been implemented over Eclipse IResource
(for use within Eclipse) and over the JDK File
object (for use in the command-line version). Listing 3 is the declaration
of Filename. The Eclipse-specific version is
available with the source code (see Resources).
Listing 3. An abstraction for files and resources
package org.ananas.xm.core;
import java.io.File;
import org.xml.sax.InputSource;
import org.ananas.xm.core.XMException;
public interface Filename
extends CoreConstants
{
public boolean isRoot()
throws XMException;
public boolean isFile();
public boolean isFolder()
throws XMException;
public boolean exists()
throws XMException;
public String getName()
throws XMException;
public String getShortName()
throws XMException;
public String getSuffix()
throws XMException;
public String getProjectPath()
throws XMException;
public Filename getParent()
throws XMException;
public Filename[] getChildren()
throws XMException;
public void setPersistentMetadata(String key,String value)
throws XMException;
public void setPersistentMetadata(String key,String[] values)
throws XMException;
public void setTransientMetadata(String key,Object value)
throws XMException;
public Object getMetadata(String key)
throws XMException;
public String getMetadataAsString(String key)
throws XMException, ClassCastException;
public String[] getMetadataAsArray(String key)
throws XMException, ClassCastException;
public File asFile()
throws XMException;
public InputSource asInputSource()
throws XMException;
public Object asPlatformSpecific()
throws XMException;
public boolean hasSamePath(Filename document)
throws XMException;
public boolean isDescendantOf(Filename document)
throws XMException;
public boolean remove()
throws XMException;
}
|
All the classes in the XM core, such as Messenger,
have been rewritten to use Filename.
In the course of two years, Eclipse has become the de facto standard open-source IDE for the Java platform, so it makes sense to strengthen Eclipse support for XM.
One of the benefits of the broader adoption of Eclipse is that more documentation is available, making it easier to write plug-ins. Still, while I value Eclipse, I also believe it pays to abstract the core aspects of your plug-in. For XM, I chose to abstract the user interface and the resource manager. In the next installment, I'll start to focus on the the other major flaw in the XM user interface: the Eclipse build.
- Participate in the discussion forum.
- Download the source code used in this article.
- Find out more about the Eclipse project, an advanced,
extensible, open-source IDE.
- Check out the Forrest Project, a sophisticated solution for publishing documents with XSLT.
- Download Altova's StyleVision, an IDE for XSLT.
- Read
Design Patterns (Addison-Wesley, 1995) by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. This book introduces the proxy pattern. Incidentally, Mr. Gamma is one of the architects of Eclipse.
- Review the previous installments of Benoît Marchal's Working XML column, including
"Using XSLT for content management"
(developerWorks, July 2001) which introduced XM. "Use
Eclipse to build a user interface for XM" (developerWorks,
October 2002) and the articles that followed discussed the Eclipse plug-in
architecture.
- Find hundreds more XML resources on the
developerWorks XML zone.
- Learn how you can become an IBM Certified Developer in XML and related technologies.
- Browse for books on these and other technical topics.

Benoît Marchal is a Belgian consultant. He is the author of XML by Example, Second Edition and other XML books. Benoît is available to help you with XML projects. You can contact him at bmarchal@pineapplesoft.com or through his personal site at marchal.com.




