深蓝海域KMPRO

使用XML:将XSLT用于内容管理

2002-08-28 14:30

使用XML:将XSLT用于内容管理
 
--介绍XM,穷人的内容管理器


Beno顃 Marchal (bmarchal@pineapplesoft.com)

顾问,Pineapplesoft

2001 年 7 月

这是使用 XML 专栏的第一部分,该专栏带有相应项目代码,演示了成熟 XML 应用程序的演变。在这一专栏中,作者兼软件顾问 Beno顃 Marchal 介绍了 XM(XSLT Make),一种利用 XML 和 XSLT 的简单的、负担得起的 Web 发布内容管理解决方案。代码样本显示了 XSLT 封装器的开发,使得非程序员也能轻松使用。可以通过链接获得 XM 项目代码。

欢迎来到使用 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,并且它位于当前目录中。

我承认当前的版本做不了太多事情,只能用它来发布最简单的网站。下一专栏将会有趣许多,因为我计划添加一种机制来对文件系统应用样式表。

参考资料



关于作者

authorBeno顃 Marchal 是住在比利时那慕尔的顾问和作家。他是
XML by Example Applied XML SolutionsXML and the Enterprise 的作者。同时是 Gamelan 的专栏作家。www.marchal.com 上有其最新项目的详细信息。可以通过 bmarchal@pineapplesoft.com 与他联系。

相关推荐