一年之後,BenoÎt 又回到了 XM (XSLT Make) 項目。他將介紹 Eclipse 平台的變化,並著手對 Eclipse 作一次較大的更新,使其與 XML 更緊密地集成在一起。首先他將考察一種簡單的界面增強,用戶經常提出這類請求,即支持問題和任務列表,更確切地說是支持做標記。正如您將看到的,您需要間接地使用這些列表。他還將考察 Eclipse 自身的資源管理,討論編寫在 Eclipse 和命令行中同樣也能運行的代碼的技術。
在這個新的系列中,我將重溫 使用 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 那樣易於使用。
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(非常明確)。最常用的後代有 IFile、 IFolder 和 IProject,分別代表文件、文件夾和項目。
雖然有一定的關系,但 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 構建。