使用 XML

更好地使用 XI

用户界面挑战

Comments

系列内容:

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

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

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

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

XI(代表 XML Import)是将文本文件转换成 XML 的工具。许多应用程序都产生文本文件, 从而可以使用这种工具将大量有用的数据导入 XML 工作流。我最初设计 XI 来检索 XML 格式的地址簿。 但是,XI 使用正则表达式(正如 JDK 1.4 中所引入的那样)来解析输入文件,所以它可以同样出色地用于服务器日志、用逗号分隔的值(CSV)、Excel 文件和许多其它文档。请把 XI 看作将旧数据导入最新 XML 流的一般工具箱。

将正则表达式与 XSLT 结合起来

您可能记得我的上一篇专栏文件“完成 XI”,我选择了实现 XMLReader 接口。软件生成 XML 文档,而不是直接从 XI 解析器写入 XML。在许多方面,XI 的行为象 XML 解析器 ― 但它解析 任何文本文档。

清单 1(同样在 XI 系列的第一篇专栏文章中出现)是一个地址簿。 由电子邮件应用程序将它作为文本文档维护。

清单 1. 原始地址簿
alias "jdoe" jdoe@xmli.com
note "jdoe" 
    <country:US><zip:45202>
    <state:OH><city:Cincinnati>
    <address:34 Fountain Square Plaza>
    <name:John Doe>
alias "jsmith" jsmith@worth-it.com
note "jsmith" 
    <first:Jack><last:Smith>
    <name:Jack Smith>
alias "pdupont" pdupont@pineapples.net
note "pdupont" <name:Pierre Dupont>

两步骤转换

XI 的前提是最好使用两个步骤将文本文档转换成 XML。

  1. XI 解析器将文档转换成粗略的 XML 版本。粗略的 XML 在句法上是正确的,但可能没有使用正确的词汇表。
  2. 这种粗略的 XML 文档是使用一个或多个 XSLT 样式表进行后处理的。 经验显示您常常需要一个成熟的脚本语言来产生一个干净的 XML 文档,而 XSLT 是 适合于该目的的最佳语言之一。

要将地址簿转换成 XML,需要对一些数据重新组织。例如,在文本文档中,电子邮件和邮政地址在不同行上; 在 XML 中,我希望所有内容都在一个标记内。另外,不是每一项都有相同的数据类型(如邮政地址)。最后,在文本文档中有一些 XML 中不需要的字段(姓,名)。 所有这些都用于简单情况。其它情况(如必须计算字段的值)会复杂得多。 在最近的客户项目(使用在概念上与 XI 相似的工具)中,必须对几行数据求和以及求平均值。使用脚本语言确实很方便。

XSLT 链接

在我的上一篇专栏文章结束时,我提供了一些代码,这些代码使用 XIReader 并将它传递到 Java Transformer 来保存结果。样本使用未初始化的 Transformer 来保存解析结果。 实际上,这仅实现转换的第一步。然而,使用 XSLT 样式表初始化 Transformer 以执行第二步也是一件简单的事情。 清单 2 显示了 XSLTLink 类,它封装完整的转换。

清单 2. XSLTLink 类
package org.ananas.xi;
import java.io.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
import javax.xml.transform.*;
import javax.xml.transform.sax.*;
import javax.xml.transform.stream.*;
public class XSLTLink
{
   private String name;
   private File stylesheet,
                output;
   private String suffix;
   private XMLReader xiReader;
   private Transformer transformer;
   public XSLTLink(File stylesheet,File output)
      throws IOException, SAXException,
             TransformerConfigurationException
   {
      this.stylesheet = stylesheet;
      this.output = output;
      name = stylesheet.getName();
      int pos = name.lastIndexOf('.');
      if(pos != -1)
         name = name.substring(0,pos);
      reload();
   }
   public void reload()
      throws IOException, SAXException,
             TransformerConfigurationException
   {
      InputSource input =
         new InputSource(stylesheet.toURI().toString());
      xiReader =
         XMLReaderFactory.createXMLReader("org.ananas.xi.XIReader");
	     xiReader.setProperty(XIReader.RULESETS_URI,input);
      TransformerFactory factory = TransformerFactory.newInstance();
      transformer =
         factory.newTransformer(new StreamSource(stylesheet));
      suffix = transformer.getOutputProperty("method");
   }
   public File applyTo(File source,boolean overwrite)
      throws IOException, SAXException, TransformerException
   {
      if(!source.isFile())
         throw new IOException(source.getPath() + " is not a file");
      InputSource input = new InputSource(source.toURI().toString());
      String name = source.getName();
      int pos = name.lastIndexOf('.');
      String base = pos != -1 ? name.substring(0,pos + 1)
                              : name + '.';
      File result = new File(output,base + suffix);
      int index = 1;
      while(result.exists() && !overwrite)
      {
         result = new File(output,base + index + "." + suffix);
         index++;
      }
      transformer.transform(new SAXSource(xiReader,input),
                            new StreamResult(result));
      return result;
   }
   public String getDisplayName()
   {
      return name;
   }
}

XSLTLink 提供附加服务。首先,它将 XSLT 样式表和正则表达式分组到一个文件中。 这要感谢 XSLT 标准中有远见的条款,它声明处理器必须忽略出现在 xsl:stylesheet 之内但在另一个名称空间定义的元素。

因为 XI 规则(正则表达式)有其自己的名称空间,所以将它们复制到样式表是可能的。XI 处理器已经在文档的任何地方查找规则(它不要求它们出现在文档的根部)。 清单 3是一个样本样式表。请注意,它还包含 XI 规则。

清单 3. Import 地址簿样式表
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:xi="http://ananas.org/2002/xi/rules"
                xmlns:an="http://ananas.org/2002/sample">
<xi:rules version="1.0"
          defaultPrefix="an"
          targetNamespace="http://ananas.org/2002/sample">
<xi:ruleset name="address-book">
   <xi:match name="alias"
             pattern="^alias ([^\s]*) (.*)$">
      <xi:group name="id"/>
      <xi:group name="email"/>
   </xi:match>
   <xi:match name="note"
             pattern='^note ([^\s]*) (.*)$'>
      <xi:group name="id"/>
      <xi:group name="fields"/>
   </xi:match>
   <xi:error message="unknown line type"/>
</xi:ruleset>
<xi:ruleset name="fields">
   <xi:match name="fields"
             pattern="[\s]*<([^<]*)>">
      <xi:group name="field"/>
   </xi:match>
</xi:ruleset>
<xi:ruleset name="field">
   <xi:match name="field"
             pattern="([^:]*):(.*)">
      <xi:group name="key"/>
      <xi:group name="value"/>
   </xi:match>
</xi:ruleset>
</xi:rules>
<xsl:output method="xml"/>
<xsl:template match="an:address-book">
   <sect1><xsl:apply-templates/></sect1>
</xsl:template>
<xsl:template match="an:alias">
<address>
   <xsl:variable name="id" select="an:id"/>
   <xsl:for-each select="/an:address-book/an:note[an:id = $id]">
      <personname><xsl:value-of
         select="an:fields/an:field[an:key='name']/an:value"/>
      </personname>
      <xsl:if test="an:fields/an:field[an:key='country']/an:value">
         <street><xsl:value-of
            select="an:fields/an:field[an:key='address']/an:value"/>
         </street>
         <postcode><xsl:value-of
            select="an:fields/an:field[an:key='zip']/an:value"/>
         </postcode>
         <city><xsl:value-of
            select="an:fields/an:field[an:key='city']/an:value"/>
         </city>
         <state><xsl:value-of
            select="an:fields/an:field[an:key='state']/an:value"/>
         </state>
         <country><xsl:value-of
            select="an:fields/an:field[an:key='country']/an:value"/>
         </country>
      </xsl:if>
   </xsl:for-each>
   <email><xsl:value-of select="an:email"/></email>
</address>
</xsl:template>
<xsl:template match="an:note"/>
</xsl:stylesheet>

XSLTLink 提供一个附加服务:它从输入计算出输出名。根据选项, 它可以创建唯一名称,以便新文件决不会覆盖现有文件。在 UI 应该做什么中, 它的理由将变得更清楚。

调试技巧

我注意到这种两步骤方法中有一个主要优点,即 XSLT 是一个具有许多选项的标准。市场上供应几种 XSLT 处理器;如果一种处理器很一般,可以挑选另一种。开发人员可以使用书籍、编辑器、调试器和更多工具。 然而,有一个缺点:由于每件事情都要通过样式表,所以您决不会看到 XI 生成什么。 如果输出中有什么地方不对,那么它是正则表达式、样式表还是两者的组合?很难回答。

幸运的是,有一个变通方法。诀窍就是使用逐字复制其输入的样式表。这只需要一个模板,如 清单 4 中所示

清单 4. 使用逐字复制其输入的样式表
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:xi="http://ananas.org/2002/xi/rules">
<xi:rules version="1.0"
          defaultPrefix="an"
          targetNamespace="http://ananas.org/2002/sample">
<xi:ruleset name="address-book">
   <xi:match name="alias"
             pattern="^alias ([^\s]*) (.*)$">
      <xi:group name="id"/>
      <xi:group name="email"/>
   </xi:match>
   <xi:match name="note"
             pattern='^note ([^\s]*) (.*)$'>
      <xi:group name="id"/>
      <xi:group name="fields"/>
   </xi:match>
   <xi:error message="unknown line type"/>
</xi:ruleset>
<xi:ruleset name="fields">
   <xi:match name="fields"
             pattern="[\s]*<([^<]*)>">
      <xi:group name="field"/>
   </xi:match>
</xi:ruleset>
<xi:ruleset name="field">
   <xi:match name="field"
             pattern="([^:]*):(.*)">
      <xi:group name="key"/>
      <xi:group name="value"/>
   </xi:match>
</xi:ruleset>
</xi:rules>
<xsl:output method="xml"/>
<xsl:template match="@*|node()">
   <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
   </xsl:copy>
</xsl:template>
</xsl:stylesheet>

当启动新项目时,应该总是复制该模板并调试正则表达式。 当您对正则表达式感到满意时,开始编写样式表本身。调试是一个两步骤过程。

用户界面

现在,只剩下非常重要的用户界面。当我在本专栏文章中发布第一个项目 XM(XSLT Make)时, 我无法包括正确的用户界面(它只有命令行界面)。结果怎样呢?那些喜欢使用 XM 但不喜欢使用命令行的用户还是给我发了电子邮件。 我将在今后的专栏文章中修正 XM 这方面的问题,但直到那时我才可能避免陷入本项目中相同的陷阱。

本专栏文章的大部分工作是为 XI 提供用户界面。这需要许多代码,所以我不打算在本专栏文章中复制整个清单。 此外,正如您将看到的那样,它是直接的 AWT(Abstract Window Toolkit)代码。 当然,您可以从 developerWorks开放源码一部分(请参阅 参考资料)下载整个项目。

UI 决策

首先,我必须在一对选项之间做出选择,如 我需要桌面界面还是基于 Web 的界面? 桌面界面有一组更丰富的控件并且通常可以变得对用户更友好,而基于 Web 的界面可以从任何地方进行访问。 最终,对用户友好取胜,我使用桌面界面。

接下来讨论是使用 AWT 还是使用 Swing。我知道对于大多数 Java 技术开发人员,Swing 使用起来不用动脑筋,因为它有一组更丰富的控件,但我个人不喜欢使用 Swing。 我很热衷于 Swing 的首次发布,但对其结果感到很失望。简而言之,我认为 Swing 没能够很好地仿效 Windows。它感觉完全不一样。

我对 IBM 资助的 Eclipse 项目及其 SWT 工具箱(请参阅 参考资料)感兴趣。SWT 提供了更高级组件的本机版本,如树、工具栏和表。尽管如此, 在本项目中使用 SWT 似乎有点小题大作,所以我决定使用正规的 AWT。实际上,我只是没有发现它具有在框中将按钮作为缺省值的功能,但我可以忍受这一点。

UI 应该做什么

更重要的是如何组织用户界面的问题。我算不上是一名用户界面设计师,所以我希望从现有项目开始并调整它。 在该实例中,我先简单介绍我为我的书籍 XML by Example(请参阅 参考资料)编写的包装 Xalan 的简单封装器。

当时,我详尽地论述了由于我必须收集三个参数(输入 XML 文档、XSLT 样式表和输出文档), 所以我只需要三个输入域、一个“Transform”按钮和用于显示错误消息的大显示区,如 图 1中所示。

图 1. 不太好的用户界面
不太好的界面
不太好的界面

既然我使用了该界面好几个月,我清楚地知道其优点和缺点。 首先,优点是:它起作用并且很容易说明。遗憾的是,该界面还有许多问题:

  • 它使用起来很笨拙。如果您使用过相似的界面,您就知道我的意思了。 如果您必须用一个单独的样式表只处理一个 XML 文档,那么该界面会很好地工作。 实际上,一个界面通常与几个文档和几个样式表一起工作。选择和重新选择相同的文件是一件麻烦的事情。
  • 很容易弄错域,将 XML 文档放置到 XSL 域中,反之亦然。
  • 甚至更容易错误地覆盖文件。

因为我知道我将继续从事用户界面工作,所以我在 Pineapplesoft 工作时研究了如何使用 XSLT 处理器。我发现三种不同的情况:

  • 调试样式表
  • 手工处理文件
  • 运行批处理

调试时,我们重复地将样式表应用于一组固定文件。我们需要明确的错误消息和选择文件的便利方法。 如果输出文件在其自己的目录中,则比较方便。大致在一半情况下,我们要保存所有测试案例。 对于另一半,最后一个测试会擦除前一个测试。

当手工转换文件时,我们通常使用两个或三个经过良好测试的样式表。 选择正确的样式表肯定会特别方便。

当以批处理方式工作时,我们从命令行设置所有参数。我们通常使用在 API 级别上工作的特殊版本的软件。 然而,对于这种情况,我的优先选择不是批处理界面。

简化、简化再简化

总而言之,我判定最佳解决方案是扩展现有界面,以便我可以拖放文件。 我详尽论述了如果我可以将文件放到各种域中,那么在样式表之间循环或选择文件会更方便。

拖放是在 JDK 1.2 中引入的,其使用尤其方便。足够实现 DropTargetListener 界面以将正规组件放入拖放目标。

我快速地测试了一下,发现我从未将文件放在正确位置。 图 2说明了这一点:我错误地将样式表放到两个域中。

图 2. 拖放有帮助吗?
它有帮助吗?
它有帮助吗?

无疑,拖放是一个好的想法,因为它简化文件选择,但三个域中有两个域多余。 同样,对于输出文件,拖放不能如此好地工作,(目前)这会导致产生最后一个界面。

我十分相信尽量简化用户界面,这样更少的选项和更少的控件可以减少混乱。 如 图 3 说明的那样,我除去了所有域。我确实认为下拉列表框能更好地替换样式表域。 该界面列出了 rules 目录中的所有文件, 它对于通过少数样式表循环尤其方便。应用程序自动生成输出文件的名称。 没有文本域,我可以在窗口上的任何位置放下输入文件 ― 比命中小的文本域方便得多。 我还为拖放不方便的情形提供了一个 Open按钮。

图 3. 更好的用户界面
更好的用户界面
更好的用户界面

我无法根据我的意愿广泛地测试该界面,所以希望在邮件列表上共享您的意见(请参阅 参考资料)。 我认为需要下列几个选项:

  • 覆盖或保存输出目录中文件的选项,它对调试有用
  • 为每次调用重新装入样式表的选项(同样,这对更新样式表时的调试有用)
  • 在最后转换之后关闭软件的选项(主要用于批处理方式)

我还提供了命令行参数来设置输入文档、选择样式表和更改选项。

结束语

本文向您演示如何构建将文本文档转换成 XML 的工具。我从过去的经验得知,如果仅用于将旧文档转换成 XML, 那么很容易取得这种工具。您照例可以在 developerWorks 的在线源码资源库中找到应用程序的源代码。 您可以将 XI 作为一个库集成到您项目中(它使用熟悉的 XMLReader API)或变成用户界面以用于临时数据转换。


相关主题

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
  • 通过单击本文顶部或底部的 讨论来参与本文的 论坛
  • 在线源代码资源库下载相应的代码。项目邮件列表在同一地址。
  • 请参考 IBM 发起的 Eclipse,它开发一般的 IDE。特别引人注意的是 SWT, 它是 Java 的一组本机 UI 组件。
  • 请尝试 IBM 发行的另一种正则表达式库 regex for Java。它可以替换 JDK 1.4 正则表达式。
  • 在 XSLT 库中,查找 样本样式表来解析定长的输入。请注意以 XML 形式输入文本文档的诀窍。
  • 请阅读 Benoît Marchal 编写的 XML by Example。 这本有关 XML 的介绍性书籍涵盖了不断发展的 XML 标准的最新特性,包括最终的 XML Schema 建议书和使用 XSL 的最新发展。
  • 请阅读 Jeffrey E.F. Friedl 编写的有关正则表达式的参考书籍 Mastering Regular Expressions
  • 请阅读 Benoît Marchal 的所有 使用 XML文章。
  • 请在 developerWorks XML 技术专区上查找更多 XML 参考资料。
  • 获取 IBM WebSphere Studio Application Developer,这是一个易于使用的集成开发环境,用于构建、测试和部署 J2EE 应用程序,包括从 DTD 和模式生成 XML 文档。
  • 了解如何成为一位 XML 和相关技术的 IBM 认证开发人员

评论

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML
ArticleID=162492
ArticleTitle=使用 XML: 更好地使用 XI
publish-date=12012002