歡迎來到 使用 XML專欄 ― developerWorks 上的一個新專欄 ― 的第一部分。該專欄的前提是,開發人員最好通過研究代碼來學習,因此我會隨同專欄一起開發一系列 XML 項目,這些項目將在幾篇專欄文章中討論。感謝這種形式,這樣我可以解決更大、更現實的項目,而不是通常可能僅為一篇文章的情景所構思的項目。請注意,您可以在本專欄伙伴站點上找到作為開放源碼項目的演示項目本身(請參閱 參考資料)。我期待著這些項目可以隨著你我的使用而不斷發展,屆時我會在這裡報告那些更改。
這一形式另一有趣的特性是您可以一直跟蹤項目,從初期到完成。我發現,人從錯誤中學到的如果說不比從有效的解決方案中更多的話,那至少也是差不多的。 因為 使用 XML跟蹤的是實際項目的開發,所以比起在只有一種假設情況的一篇一次性文章中,我有更多機會告誡您提防死胡同。我希望將正在進行的開放源碼項目和專欄結合可以讓我們成為更好的開發人員。
XM:窮人的內容管理器
我要完成的第一個項目是一個負擔得起的 Web 發布解決方案。管理一個超過 200 個頁面的網站的艱難經歷是這個項目的靈感來源。我發現有一些非常好的工具可以用來管理或小或大的站點,但我無法找到介於兩者之間的適當解決方案。
如果您的站點有 10 到 20 個頁面,那麼象 HoTMetaL PRO、Dreamweaver 或 FrontPage(請參閱 參考資料)這樣的 HTML 編輯器非常不錯。不過,隨著站點不斷成長,以及工作從創建轉移到相對昂貴的維護上,這些工具就證明不那麼有效了。許多 Html 編輯器都不是很適合於維護不斷增長的站點。例如,它們可能會強迫您手工編輯所有頁面而只是為了重新設計一下導航 - 如果管理數量在二十幾個以下的頁面,這樣做只是麻煩一些,但對於數量更多的站點,就要做太多工作了。
領域的另一端是高端發布解決方案,例如 OpenMarket、Vignette 或 eContent(請參閱 參考資料)。它們在管理具有巨大內容庫的大型站點方面做得很好,但(總是有“但”)門票很貴,貴得令大多數超負荷的網站管理員可能只能夢想擁有這樣的一個系統。
那麼這中間是什麼呢?自己開發的解決方案。許多網站管理員都已轉向腳本(JSP、ASP 或 PHP)和數據庫的組合以幫助他們應付不斷成長的站點。這種方法很有效,但不是沒有缺點。首先,增加了服務器代價,因此頁面可能裝入得更慢。另外,基於腳本的網站更容易出現錯誤,甚至崩潰(當然,我是在說自己;錯誤不會困擾 您的代碼)。最後,搜索引擎往往不太可能對動態生成的站點建立索引。總的來說,我發現雖然腳本和數據庫可以使網站管理員的生活更輕松,但對於訪問者來說,它們遠遠不夠理想。
為了適合有關 XML 的專欄,我提出了在 XML 和 XSLT 上構建的一種替代辦法。准備 DocBook 或另其它 XML 詞匯格式的文檔,然後將它們自動轉換成 Html 確實很容易。 自動在這裡是最重要的詞。目的是加快手工處理,並盡可能地將站點維護自動化。當從小規模轉移到工業規模的網站管理時,我願意考慮它。
至少,在理論上說是這樣。實際上,要在家裡嘗試這些,您最好是程序員。象 Xalan 這樣的 XSLT 處理器使用起來確實還不是那麼友好。存在象 Cocoon 這樣的 XML 發布框架(請參閱 參考資料),但它們還是適合於開發人員的。我對 XM(XSLT Make)期待的目標是讓 XSLT 處理器有張友好的面孔。最終,我希望那些管理中型網站的非開發人員也能使用 XM。
我選擇這個項目作為 使用 XML的第一個項目是因為,從讀者對最近的一篇 developerWorks 文章 - Managing e-zines with JavaMail and XSLT- 反饋的郵件看來,對用於發布的 XSLT 友好應用程序感興趣的人不在少數。
線路圖
圖 1 概述了 XM 是如何工作的。准備和發布網站有三個階段:
創作:使用 XML 編輯器編寫內容,或者從非 XML
來源(例如字處理器)獲取
發布:將內容轉換成 Html
欣賞:在浏覽器中查看內容
圖 1:使用 XM 進行 Web 發布的三個步驟
我要提醒您注意,XM 生成的大多是靜態 HTML 頁面。目標是幫助網站管理員更好管理網站 - 不必犧牲原來的性能。要增強性能,大多數動態生成的網站使用某種形式的高速緩存。我相信靜態 Html 頁面,到目前為止,是最好的高速緩存策略。
您可以發現,這個項目相當煩瑣,我並沒有計劃在一篇(甚至兩篇)專欄文章中實現其全部。和其它任何認真的開發項目一樣, 使用 XML的項目將在幾個月內不斷成熟起來。但為了給這一部分做出結論,我仍然希望指出一些我已認識到的難點和裡程碑。很明顯,我期待著隨著開發進行添加更多內容:
為 XSLT 處理器設計一個易於使用的包裝(包括在本專欄中)。
自動通過 FTP 將文件上載到 Web 服務器。
管理文件和那些文件之間的鏈接(一種解決方案可能是讓樣式表讀取目錄)。
支持例如 PDF(Adobe Acrobat 支持的“可移植文檔格式”)、SVG(用於圖像的“可伸縮向量圖形”)、RSS(用於 Web 門戶的“豐富站點摘要”)等多種發布格式。
在 Web 服務器本身上存放 XM 以支持協作編輯。
內容管理例解
XM 第一版中的主要挑戰之一在於使它可用於非程序員。我不僅僅指帶有友好按鈕的漂亮用戶界面。我相信許多漂亮按鈕並不能使不友好的解決方案看起來友好;它仍然是不友好的應用程序。當然,我已嘗試設計一種易於理解的可操作方式。
我的第一個想法是在“Managing e-zines with JavaMail and XSLT”中的配置文件上進行構建。如果您沒看過那篇文章,我可以告訴您,配置文件控制著應用哪些樣式表,以及結果應該怎樣。有關示例,請參閱 清單 1。我測試過許多變體,但無論我嘗試什麼,都無法找出足夠友好的方案。
清單 1. rules.XML:創建友好的配置文件的許多嘗試之一
<?XML version="1.0"?>
<rules version="1.0" XMLns="http://www.ananas.org/2001/xm/rules">
<!-- apply the style sheet on XML files -->
<rule extension="XML">
<apply-stylesheet file="default.xsl"/>
<copy from="$xslt" to="$target"/>
</rule>
<!-- copy GIF files -->
<rule extension="gif">
<copy from="$source" to="$target"/>
</rule>
<!-- create an XML file with the content of
freebIEs/download, style it -->
<rule directory="freebIEs/download">
<ls dir="$current"/>
<apply-stylesheet file="download.xsl"/>
<copy from="$xslt" to="$target"/>
</rule>
</rules>
然後它使我受到了打擊,這樣一個帶有規則和變量(例如 $source )的配置文件天生難以使用。它與腳本語言類似,我們都知道,腳本語言是不適合初學者的。我偶然發現了一個更容易使用的解決方案:讓用戶創建一個模擬最終網站的目錄,並自動產生輸出。我將它稱為 內容管理例解,這是因為用戶實際上創建了如何組織網站的示例,而且 XM 不需要更多配置就可以將它變成真正的網站。
骨架類
如果您計劃按照示例操作,請造訪 ananas.org,然後下載項目代碼;在繼續閱讀下去之前需要它。我在本月介紹的代碼不過是 XM 的骨架。它只做最基本的事:遞歸地遍歷源目錄,在此過程中將樣式表應用到每個 XML 文件以產生 Html 格式的網站。
到目前為止,這些類包括:
NotImplementedException 是 Java 標准庫中我一直非常懷念的唯一類。在開發過程期間,嘗試調用還未實現的代碼或遇到意外(因此是未實現的)情況並不罕見。我發現,明確說明我還沒有編寫代碼一直以來節省了我調試工作所花的大量時間。
XMException ,當 XM 遇到錯誤時,它拋出這個異常。這個異常可以嵌入其它異常,例如 TransformerException 或 SAXException 。
Resources 只是便利手段。它為我提供了對缺省語言環境中資源的快速訪問。
Messenger 是與抽象錯誤和信息消息的接口。當前版本的 XM 從命令行運行,但我期望構建的是 GUI 或基於 Web 的界面。通過將所有消息經過 Messenger 傳輸,支持不同界面就容易了。
DefaultMessenger 是打印到 Writer 的 Messenger 缺省實現。
DirectoryWalker 執行實際的處理。它遞歸地遍歷一組目錄,在這個過程中應用樣式表。
Console 是 XM 的入口點。就象其名稱所揭示的,它是從命令行運行的。
Messenger
如果您忘了異常,那麼我告訴您,到目前為止 XM 中兩個最有用的類是 Messenger 和 DirectoryWalker 。 清單 2 中的 Messenger 定義了與用戶進行通信的接口。這個類模仿了 TrAX 的 ErrorListener 或 SAX 的 ErrorHandler :
error() 、 fatal() 和 warning() 用於報告錯誤。
progress() 可以用來實現進度欄或另一種向用戶報告進度的機制。
info() 報告其它信息。
因為 XM 主要是一個非交互式過程,所以我沒有定義任何提示用戶,或者另外接受輸入的方法。
清單 2. Messenger.Java: XM 對用戶的接口
package org.ananas.xm;
import Java.io.File;
public interface Messager
{
public void error(XMException e)
throws XMException;
public void fatal(XMException e)
throws XMException;
public void warning(XMException e)
throws XMException;
public void progress(File sourceFile,File resultFile)
throws XMException;
public void info(String msg)
throws XMException;
public void info(String pattern,Object[] arguments)
throws XMException;
}
DirectoryWalker
清單 3 中的 DirectoryWalker 主要是將 XML 文件從源目錄(包括其子目錄中的文件)復制到目標目錄。只有一個竅門:它在復制 XML 文件之前對它們應用了一個樣式表。
清單 3. DirectoryWalker.Java:有趣的所在
package org.ananas.xm;
import Java.io.*;
import Java.util.*;
import Javax.XML.transform.*;
import Javax.XML.transform.stream.*;
import org.ananas.util.NotImplementedException;
public class DirectoryWalker
{
protected Templates templates;
protected long lastModifIEd;
protected String extension;
protected Messager messager = null;
public DirectoryWalker(File stylesheetFile,Messager messager)
throws TransformerConfigurationException
{
lastModified = stylesheetFile.lastModifIEd();
templates = TransformerFactory.newInstance().
newTemplates(new StreamSource(stylesheetFile));
extension = '.' + templates.getOutputPropertIEs().
getProperty(OutputKeys.METHOD);
this.messager = messager;
}
public void walk(String source,String target)
throws IOException, XMException
{
walk(new File(source),new File(target));
}
protected void walk(File source,File target)
throws IOException, XMException
{
if(source.isDirectory())
{
File[] files = source.listFiles(),
dirs = new File[files.length],
docs = new File[files.length];
int idirs = 0,
idocs = 0;
for(int i = 0;i < files.length;i++)
{
if(files[i].isDirectory())
dirs[idirs++] = files[i];
else if(files[i].isFile())
docs[idocs++] = files[i];
else
throw new NotImplementedException("Expecting file or directory");
}
if(!(target.exists() && target.isDirectory()))
if(!target.mkdirs())
messager.fatal(new XMException(
Resources.getString("cannotcreatedirectory"),
new Object[] {target.getAbsolutePath()}));
for(int i = 0;i < idocs;i++)
{
String fname = docs[i].getName();
int pos = fname.lastIndexOf('.');
String extension = pos != -1 ? fname.substring(pos + 1) : "";
if(extension.equals("XML"))
{
File result = style(docs[i],target);
messager.progress(docs[i],result);
}
// else copy file
}
for(int i = 0;i < idirs;i++)
walk(dirs[i],new File(target,dirs[i].getName()));
}
else
messager.fatal(new XMException(
Resources.getString("notdirectory"),
new Object[] {source.getAbsolutePath()}));
}
protected File style(File sourceFile,File targetDir)
throws XMException
{
try {
String resultName = sourceFile.getName();
int pos = resultName.lastIndexOf('.');
if(pos != -1)
resultName = resultName.substring(0,pos);
resultName += extension;
File resultFile = new File(targetDir,resultName);
if(resultFile.exists())
{
if(resultFile.lastModified() >= sourceFile.lastModifIEd() &&
resultFile.lastModified() >= lastModifIEd)
return null;
}
Transformer transformer = templates.newTransformer();
transformer.transform(new StreamSource(sourceFile),
new StreamResult(resultFile));
return resultFile;
}
catch(TransformerException e)
{
throw new XMException(e);
}
}
}
walk() 遍歷目錄,應用樣式表。它是通過列出所有目錄內容,然後分成文件和子目錄開始的。接下來,它對每個文件應用樣式,然後為子目錄遞歸地調用自身。
應用樣式表是 style() 方法的職責。為了可移植性, DirectoryWalker 使用 TrAX(XML 的轉換 API)來與 XSLT 處理器交互。到目前為止,我已用 Xalan 測試了代碼(請參閱 參考資料)。
未完待續……
您已經可以下載和練習 XM 代碼了。入口點是 org.ananas.xm.Console 類。它只使用兩個參數:XML 文件所在的源目錄;以及將由 XM 創建的目標目錄。確保樣式表名為 rules.xsl ,並且它位於當前目錄中。
我承認當前的版本做不了太多事情,只能用它來發布最簡單的網站。下一專欄將會有趣許多,因為我計劃添加一種機制來對文件系統應用樣式表。