内容


使用 XML

Eclipse 任务列表

以及 Eclipse 插件的其他问题

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 使用 XML

敬请期待该系列的后续内容。

此内容是该系列的一部分:使用 XML

敬请期待该系列的后续内容。

在这个新的系列中,我将重温 使用 XML 专栏的一位老朋友:XSLT Make 或 XM。它也可以算做使用 Eclipse 插件的一个理由。2001 年 7 月,我在 使用 XML 专栏中介绍的第一个项目就是 XM。它是一种轻量级的、价格低廉的、使用 XML 和 XSLT 发布文档的工具。

2002 年 10 月,我决定为 XM 工具添加图形用户界面。我没有从头开发整个界面,而是求助于刚刚出现的一种 IDE:Eclipse。之所以选择 Eclipse,是因为它是可扩展的、用 Java 编写的,并且提供了一个神奇的小部件库。

重温 XM,使我有机会从两个方面改进 Eclipse 集成:我将修正一个讨厌的用户界面限制(本文中),并重新编写核心 XM 引擎,以便与 Eclipse 更好地集成。通过改进,我还将提高 Eclipse 的扩展性和功能。关于 XM 引擎的工作计划,将在后面的两篇文章中阐述。

简要的历史回顾

XM 已经存在一段时间了。从我的咨询经验和读者的反映来看,它在很多项目中证明了自身的价值。比如,我使用 XM 作为一种教学工具,为客户管理 Web 站点,并以 HTML 和 PDF 格式发表了大量的文档。关于该项目的文章,请参阅 参考资料

XM 的优点

开发 XM 的最初原因是使 XML 和 XSLT 的使用更方便。我需要一种简单而有效地解决方案,依靠小型或中等大小的团队维护 Web 站点。我知道 XML 和 XSLT 提供了一个很好的基础,但当时我没有找到合适的工具。最后我卷起袖子自己做了一个这样的工具。2001 年那时出现的工具不是太简单(只能用于单个文件,而不是整个网站),就是太复杂(以大型团队为目标)。

XM 的功能非常强大(我曾经将其用于包含数千页面的项目),但有足够简单,能适应中小型团队的需要。

XM 有两个最重要的特性:

  • 它是开箱即用的,不需要准备复杂的脚本,也不需要编写高级的配置文件。使用 XM,只要将文档放在一个目录中,把样式表放在另一个目录中, 好了,这样就可以发布文档了。
  • 它生成静态的站点,同时又提供了动态站点的大多数管理优势。比方说,修改站点的布局只需要编辑一个样式表即可 。

第二点可能更容易引起争议,但根据我的经验,维护静态站点的工作量更小,效率也更高。一些站点需要结合静态和动态网页,但是以静态方式为主维护站点可以避免很多问题:使用的软件包更少,因而减少了失效的机会。此外,因为可以使用更成熟的缓冲技术,站点的响应速度也更快。关于 XM 的独到之处,我建议您阅读一下原文(请参阅 参考资料)。

从 XM 的角度看

两年之中情况发生了很多变化。现在,有大量的开源项目能够满足您的需要(请参阅 参考资料)。我曾经用过其中的一些项目,虽然不敢说有广泛的经验,但确实发现其中一些项目的功能比 XM 更强大,但没有一种像 XM 那样易于使用。

Eclipse 平台也发生了根本的变化。现在,Eclipse 是最受人瞩目的开源 IDE 之一,拥有上千种插件。更重要的是,文档得到了更新,提供了更多的例子。我还记得当时和源代码与调试器搏斗以便获得特定效果的情景,因为当时还没有文档,那种情形不复存在了。

从技术上说,Eclipse 项目从 2.0 发展到了 3.0。新的 API 预计将为今后的很长时间奠定基础。所幸的是,不同的版本在很大程度上都是兼容的(事实上为 Eclipse 2.0 编写的 XM 插件在 3.0 中也能很好地工作),但有些变化不是向后兼容的。一个好的办法是清理代码,尽可能地使用新的 API。

本系列文章有两个目标:

  • 改进 Eclipse 集成。虽然 Eclipse 的功能很齐全,但原来的插件还有一些粗糙之处。通过与 Eclipse 资源管理更紧密地集成在一起,我希望能够稍微缓解一下不足之处。
  • 重写核心引擎。我曾经在很多项目中使用过 XM,在一些项目中遇到了核心引擎最初设计中的一些局限,不得不临时改变实现。现在是时候将这些修改加入到项目中了。

Eclipse 资源管理

在以前的专栏文章中,我曾多次提到,Eclipse 不仅仅是一种 IDE。最好将其看作是构建 IDE 的平台。Eclipse 可以归结为管理插件的一个系统。它提供了诸如加载插件、管理插件之间的联系和依赖性、管理插件之间的接口(通过扩展点)等服务。

显然,一些插件提供的服务是每个应用程序都需要的,所以可以将它们作为核心的一部分。部件库 SWT 就是其中之一。另一些插件,如 XM 插件,具有更强的专用性,则由用户在需要的时候安装。

还有一种核心服务是资源管理,该服务由 org.eclipse.core.resources 插件提供。对于 Eclipse 来说,工作区之下的一切都是资源。资源的基本接口是 IResource(非常明确)。最常用的后代有 IFileIFolderIProject,分别代表文件、文件夹和项目。

虽然有一定的关系,但 IResource 和 JDK 中的 File 对象实际上是两码事。JDK File 代表文件系统中的一个记录,而 Eclipse IResource 在文件系统之上又添加了几层抽象。首先,资源有属性,属性代表关于资源的信息,帮助插件处理资源。比如,插件可以把 <?xml-stylesheet?> 处理指令的内容作为属性来进行缓冲。同时将数据缓冲在属性中,这样就避免了每次运行插件时都需要解析文件。属性可以存储在内存中(用户退出编辑器时将丢失)或者持久存储到文件系统中。

此外,当添加、删除或编辑资源时,资源和文件系统就不再同步。 IResource 记录资源的状态,并提供与文件系统同步的方法。更重要的是,Eclipse 可以通知插件资源和文件系统的变化。当资源与文件系统同步时,Eclipse 将传递给插件一个 delta,即上一次同步之后的变动列表。显然,这样就能够进行智能构建,也就是说仅对修改过的资源进行重新编译。

记号和任务列表

从用户的观点看,Eclipse 支持有两个问题:XM 有自己的项目重建逻辑和错误报告逻辑。最终,这两个问题表现为 XM 忽略了 Eclipse 的资源管理。

我准备在本系列的后两篇文章中讨论构建过程,现在主要解决错误报告的问题。

记号

Eclipse 为构建人员和编译人员提供了任务列表和问题列表来报告错误,如图 1 所示。当用户双击其中的错误项时,编辑器就会打开有问题的文件。不幸的是,编写 XM 插件的第一个版本时,我没有找到如何添加列表项的文档,所以忽略了它。结果,该插件有自己的控制台,但不支持双击。

图 1. 任务和问题列表
任务和问题列表
任务和问题列表

最后发现,向标准列表中添加消息并不难,但是不能直接添加。一开始,我试图寻找一个任务列表对象,但是没有发现添加列表项的方法。最后发现,无法添加或者至少无法 直接添加列表项。要添加错误消息,需要在资源上创建一个 记号(接口 IMarker)。从列表中删除一个消息,也要从资源上去掉记号。列表会自动更新翻译记号的变化。

createMarker() 方法用于创建记号。该方法以记号 ID 作为参数。平台中定义了以几种标准的记号 ID:

  • org.eclipse.core.resources.marker —— 记号层次结构的根。
  • org.eclipse.core.resources.problemmarker —— 表示问题或错误消息,出现在问题列表中。
  • org.eclipse.core.resources.taskmarker —— 表示待办事项,出现在任务列表中。
  • org.eclipse.core.resources.bookmark —— 表示文件,比如搜索结果。
  • org.eclipse.core.resources.textmarker —— 表示文件的位置,比如出现错误的位置。

定义插件专用的记号是一种不错的选择。新记号的 ID 在 plugin.xml 文件(与 Eclipse 中的其他声明一样)重定义。清单 1 显示了一个记号声明,定义了记号 ID( org.eclipse.core.resources.markers)的一个扩展。它还声明了新的记号,这些记号分别从 problemmarker(显示在问题列表中)和 textmarker(为了记录行号)中继承而来。将记号声明为持久的是为了在会话之间保存这些记号。

清单 1. 记号声明
<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>

用户可以根据不同的条件过滤消息,比如问题的类型(警告、错误)、优先级和记号 ID。定义插件专用的记号可以帮助用户对插件消息应用专门的过滤规则。

警告:Eclipse 有可能过滤掉插件消息。如果没有看到 XM 的任何消息,应该看看这些消息是不是过滤掉了。要改变过滤器,请在任务列表或问题列表中单击过滤器图标,一定要选中 XM 记号。

了解其中的窍门之后,集成到 XM 中并不难。从一开始,就通过 Messenger 接口将用户界面抽象化了。Messenger 定义了核心需要报告错误或进程信息的方法。为了支持方法列表,只需要编写新的 Messager 实现来创建适当的记号,如清单 2 所示。注意, begin() 方法将删除所有的记号,以便在构建之前清除问题列表。

清单 2. 记号的 Messenger 实现
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 一直围绕这两个组件来组织:核心,独立于 Eclipse 并提供命令行界面;Eclipse 插件。要将 XM 移植到其他界面,只需要像 Messenger(请参阅 上一节)那样抽象用户界面的接口即可。我曾经为一些项目定义了 Eclipse 之上的 servlet 用户界面。

虽然我计划进一步加强 XM 与 Eclipse 的集成,但是也想保留命令行选项。两种界面各有自己的用途。对于日常操作而言,我多数时间都在用 Eclipse 环境,但是命令行版本对于 crontab(计划工作执行的一种 UNIX 工具)非常方便。为了同时支持两种方式,我抽象了 XM 核心引擎中的资源和文件。

最初的 XM 使用的是 JDK File 对象,以后您会看到它是造成多数集成问题的根源,Eclipse 没有使用 File 对象。相反,它使用了自己的 IResource 接口。此外,经验告诉我,依靠 File 有很大的局限性。Eclipse 不是惟一没有使用文件的软件包,SAX 使用 InputSource,而 JAXP 使用 Source

如果代码需要和几种不同的库进行交互该怎么办?可以使用代理模式(请参阅 参考资料)来抽象各个库。在代理模式中,由一个(或多个)对象为底层的库提供通用的接口。可以实例化该对象,把请求转发给任何一个库。采用这种模式的好处是,调用代码时无需担心代理要转发的库。

XM 引入 Filename 接口来抽象文件或资源的概念。 Filename 已经在 Eclipse IResource(为了在 Eclipse 内使用)和 JDK File 对象(为了在命令行中使用)上得以实现。清单 3 是 Filename 的声明。Eclipse 专用版提供了源代码(请参阅 参考资料)。

清单 3. 文件和资源的抽象
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;
}

XM 核心中的所有类(如 Messenger)都使用 Filename 进行了改写。

结束语

这两年中,Eclipse 已经成为 Java 平台事实上的标准开源 IDE,因此加强 Eclipse 对 XM 的支持非常必要。

更多采用 Eclipse 的好处之一是,能够使现有的更多文档可用,使编写插件更容易。在赞美 Eclipse 的同时,我仍然相信抽象插件的核心是值得的。对于 XM,我选择了抽象用户界面和资源管理。在下一期文章中,我将开始讨论 XM 用户界面的另一个主要问题:Eclipse 构建。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML
ArticleID=49114
ArticleTitle=使用 XML: Eclipse 任务列表
publish-date=11012004