用JavaMail 和XSLT管理ezine系列之一

2002-8-20 15:21:27【作者】 畅享网 【进入论坛】
广告

用JavaMail 和XSLT管理ezine系列之一

--使用XML和XSLT自动生成纯文本和HTML格式的时事通讯


Benoit Marchal (
bmarchal@pineapplesoft.com)

技术顾问,Pineapplesoft

2001 年 3 月

 在本系列的第一篇文章中,Benoit Marchal 演示了如何用 Java 和 XML 实现电子邮件发布的自动化。这个具体的 XML 和 XSLT 应用描述了一个电子邮件时事通讯 ezine 发布应用程序,该程序既输出 HTML 格式的电子邮件消息,又输出纯文本格式的的电子邮件消息。本文中的六个可重用代码样本包括一个简单的以 DocBook 标记的时事通讯、一个用于将 DocBook 样本时事通讯转换成定制的文本输出的 XSL 样式表、一个 Java 文本格式化器(SAX ContentHandler 形式)、两个 SAX 过滤器以及将所有这些集成在多步骤变换中的 Java 代码。(本文的下一部分将讨论 JavaMail API。)

这样,您已经学过了 XML。并且已经逐步掌握了 DTD、XSLT、SAX 和 DOM。您解开了名称空间的秘密,并认为掌握了常人难以理解的技术。祝贺您!那么,现在该做什么呢?

我从开发人员的反馈得知,不只您一个人自问那个妙极了的问题。本文将通过一个实际的应用程序给出一个答案。它演示了如何用 Java 和 XML 自动进行发布。我想,这会给您一些鼓舞。

本文不介绍 XML。我假设您已熟悉 XSLT,并有一些 SAX 语法分析的概念。即使需要有关那些主题的背景知识,您可能仍想通读本文,因为它会激励您学习更多的新知识。但是请务必参考参考资料一节以学习基本的 XML 知识。

XML ... 和电子邮件?

XML 看起来可能不象是天生与电子邮件搭档的技术。但是别急,当看到这种奇异组合的效果时,您可能会感到惊奇的。

正如您可能知道的那样,Eudora、Outlook、Netscape 和其它新式电子邮件客户机允许您发送 HTML 格式的电子邮件。最初的电子邮件消息只限于纯文本,并且不支持粗体、斜体或超链接。而最新的电子邮件客户机可以识别 HTML,因此,现在可以发送纯文本格式的消息,也可以发送具有多种格式的文档。

这种电子邮件格式的选择为电子邮件杂志 (e-zine) 的发布人员提出了一个问题。实际上,这种选择在 e-zine 发布人员开发各种策略来克服其两个最大问题(争取和保留订户)方面起了一定的作用。不幸的是,订户对支持或反对 HTML 电子邮件抱有很强的立场。

更糟的是,一些客户机(包括流行的 AOL 4.0 到 5.0)根本不支持 HTML。除非极其小心,否则使用那些旧式电子邮件客户机的订户只能看到一些无用信息。

一般地,e-zine 发布人员都尽力为读者着想。在纯文本电子邮件时代,精明的发布人员会手工地改变文章的格式。有些人在 HTML 电子邮件时代还继续保持这个好的传统,不遗余力地为每份文档准备两种版本:为旧式电子邮件客户机提供纯文本,以及为新式客户机提供 HTML 版本。听说这点之后,我灵机一动,想到了“XSLT 样式表”。(这可能就是我将成功的确切信号。)

原理

在这个共分为两部分的文章里,您将看到 XML、XSLT 和一些 Java 编程如何简化工作。在这样做的过程中,您将使用各种 XML 技术。在开始之前,让我们全面回顾一下:

当然,首先是 XML 本身。e-zine 将用 XML(更确切地说,是 DocBook)编写。DocBook 是一种用于技术文档的流行 XML 词汇表。

XSLT 通常用于将 XML 转换成 HTML。那将解决一半问题(即,准备 e-zine 的 HTML 版本)。

增强文本 XSLT 支持的特殊文本格式化器。事实上,正如您所理解的那样,e-zine 的首要需求是最好的文本格式化。

JavaMail,发送电子邮件的标准 Java API。

图 1 演示了这些组件之间的关系。从左至右,最终目标是用 e-zine 的文本和 HTML 版本准备所谓的多部分电子邮件。

图 1. 解决方案中的组件如何交互

准备电子邮件涉及到两个样式表:一个输出文本,另一个输出 HTML 版本。文本格式化器辅助样式表。JavaMail 获得两个副本并将它们发送到订户。

本系列的第一部分重点讨论文本变换。第二部分将用 JavaMail 发送两种版本的消息。

DocBook 文档

首先是清单 1 中的 article.xml 代码。它用 DocBook 编写,这意味着, 所有 XML 标记(<article>、<title> 和 <para>)都由 DocBook 定义。

清单 1. article.xml

<?xml version="1.0"?>
<article>
<articleinfo>
<title>XSL -- First Step in Learning XML</title>
<author><firstname>Beno&#238;t</firstname>
 <surname>Marchal</surname></author>
</articleinfo>
<sect1><title>The Value of XSL</title>
<para>This is an excerpt from the September 2000 issue of
 Pineapplesoft Link. To subscribe free visit
 <ulink url="
http://www.marchal.com">marchal.com</ulink>.</para>
<para>Where do you start learning XML? Increasingly my answer
 is with XSL. XSL is a very powerful tool with many
 applications. Many XML applications depend on it. Let's take
 two examples.</para>
</sect1>
<sect1>
<title>XSL and Web Publishing</title>
<para>As a webmaster you would benefit from using XSL.</para>
<para>Let's suppose that you decide to support smartphones.
 You will need to redo your web site using WML, the
 <emphasis>wireless markup language</emphasis>, instead of
 HTML. While learning WML is easy, it can take days if not
 months to redo a large web site. Imagine having to edit every
 single page by hand!</para>
<para>In contrast with XSL, it suffices to update one style
 sheet the changes flow across the entire web site.</para>
</sect1>
<sect1>
<title>XSL and Programming</title>
<para>The second facet of XSL is the scripting language. XSL
 has many features of scripting languages including loops,
 function calls, variables and more.</para>
<para>In that respect, XSL is a valuable addition to any
 programmer toolbox. Indeed, as XML popularity keeps growing,
 you will find that you need to manipulate XML documents
 frequently and XSL is the language for so doing.</para>
</sect1>
<sect1>
<title>Conclusion</title>
<para>If you're serious about learning XML, learn XSL. XSL is
 a tool to manipulate XML documents for web publishing or
 programming.</para>
</sect1>
</article>

文本标记语言

现在,我们来看一下如何将 DocBook 转换成文本。XSLT 对文本格式化有一定的支持(形式为 <xsl:output method="text"/>),但就我的经验来看,对于 e-zine 发布来说,这还不够。更确切地说,使用 XSL:

无法在特定长度处折行(这是旧式电子邮件客户机所需的)

难以除去语音 符号(旧式电子邮件客户机的另一限制)

难以除去初始文档中的重复空格

乍看起来,XSLT 可能没什么用,但是只需少许 Java 编程就可以使它发挥作用。其中的窍门是定义一个 XML 词汇表(我称之为文本标记语言)来描述文本文档。

我特别为本文创建了这种文本标记语言,因此它要多简单就有多简单。事实上,它只有两个标记: <txt:root>(文档根)和 <txt:block>(在前后都有折行的段落)。它们都在 http://www.psol.com/xns/xslist/xml2text 名称空间中定义。顺便提一句,请记住名称空间只是一个标识,它看起来类似于 URL,但它不指向任何东西。

<txt:root> 有一个 lineWidth 属性用于 ...,对了,用于行宽。<txt:root> 有一个 linesAfter 属性指明段落之后的折行数。

接下来,编写一个 Java 应用程序将文本标记语言转换成纯文本。例如,下面的文档(输入)将变成其后的文档(输出)。请注意,折行发生在 65 个字符之后,这由 lineWidth 属性指定:

输入

<?xml version="1.0" encoding="UTF-8"?>
<txt:root lineWidth="65"
 xmlns:txt="
http://www.psol.com/xns/xslist/xml2text">
<txt:block linesAfter="1">This is an excerpt from the September
2000 issue of Pineapplesoft Link. To subscribe free visit
marchal.com.</txt:block>
<txt:root>

输出

This is an excerpt from the September 2000 issue of Pineapplesoft Link. To subscribe free visit marchal.com.

 为了从 XML 文档转换到文本标记语言,我当然将使用 XSLT。顺便提一句,为什么要使用文本标记语言呢?如果要编写 Java 代码,为什么不直接处理 DocBook?简而言之,因为这样做更容易。例如:

无需处理 DocBook 中的所有标记,只需处理文本标记语言中的两个标记即可。

要更改文本输出,只需编辑样式表即可,并且,因为 XSLT 是一种脚本语言,所以它比 Java 更容易。

最后一点(但不是最无关紧要的一点),文本标记语言和 XSLT 的组合可用于 DocBook 和其它 XML 词汇表。

如果您熟悉 XSL,这种文本标记语言就如同使用 FO 创建 PDF 文件一样容易。

样式表

将 DocBook 转换成文本标记语言的样式表是清单 2 中的 text.xsl。请注意 <xsl:output method="xml"/> 标记:这个样式表将 XML (DocBook) 转换成 XML(文本标记语言)-- 而不是 HTML。

清单 2. text.xsl

<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
 xmlns:xsl="
http://www.w3.org/1999/XSL/Transform"
 xmlns:txt="
http://www.psol.com/xns/xslist/xml2text">

<xsl:output method="xml"/>

<xsl:template match="/">
<txt:root lineWidth="65">
 <xsl:apply-templates/>
</txt:root>
</xsl:template>

<xsl:template match="articleinfo">
 <txt:block linesAfter="0">&gt; <xsl:value-of
 select="title"/> &lt;</txt:block>
 <txt:block linesAfter="2">
 by <xsl:value-of select="author/firstname"/>
 <xsl:value-of select="author/surname"/>
 </txt:block>
</xsl:template>

<xsl:template match="sect1/title">
 <txt:block linesAfter="1">* <xsl:apply-templates/> *</txt:block>
</xsl:template>

<xsl:template match="ulink">
 <xsl:apply-templates/>
 <xsl:text> &lt;</xsl:text>
 <xsl:value-of select="@url"/>
 <xsl:text>&gt;</xsl:text>
</xsl:template>

<xsl:template match="emphasis">
 <xsl:text>*</xsl:text>
 <xsl:apply-templates/>
 <xsl:text>*</xsl:text>
</xsl:template>

<xsl:template match="para">
 <txt:block linesAfter="1"><xsl:apply-templates/></txt:block>
</xsl:template>

</xsl:stylesheet>

 文本格式化器

您可以在清单 3 中看到文本格式化器本身 Xml2Text.java。Xml2Text 是一个 SAX ContentHandler。(如果不熟悉 SAX,请参阅侧栏定义的 SAX。)如同 SAX 处理器一样,这个也很简单。在 startElement() 和 characters() 事件中,它缓冲 <txt:block> 的内容。在 endElement() 中,Xml2Text 写入文本并在适当时候插入折行符。

Listing 3. Xml2Text.java

package com.psol.xslist;

import java.io.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;

public class Xml2Text
 extends DefaultHandler
{
 protected static final String
 NAMESPACE_URI = "
http://www.psol.com/xns/xslist/xml2text";
 protected static final int NONE = 0,
 ROOT = 1,
 BLOCK = 2;
 protected StringBuffer buffer;
 protected int state,
 lineWidth,
 linesAfter;
 protected PrintWriter writer = null;

 public Xml2Text(PrintWriter writer)
 {
 this.writer = writer;
 }

 public void startElement(String uri,
 String name,
 String qualifiedName,
 Attributes atts)
 {
 if(!uri.equals(NAMESPACE_URI))
 return;
 if(state == ROOT && name.equals("block"))
 {
 state = BLOCK;
 buffer = new StringBuffer(128);
 try
 {
 linesAfter =
 Integer.parseInt(atts.getValue("linesAfter"));
 }
 catch(NumberFormatException e)
 {
 linesAfter = 0;
 }
 }
 else if(state == NONE && name.equals("root"))
 {
 state = ROOT;
 try
 {
 lineWidth =
 Integer.parseInt(atts.getValue("lineWidth"));
 }
 catch(NumberFormatException e)
 {
 lineWidth = 65;
 }
 }
 }

 public void endElement(String uri,
 String name,
 String qualifiedName)
 {
 if(!uri.equals(NAMESPACE_URI))
 return;
 if(state == BLOCK && name.equals("block"))
 {
 state = ROOT;
 int start = 0,
 current = start,
 lastSpace = start - 1;
 while(current < buffer.length())
 {
 while(current < start + lineWidth &&
 current < buffer.length())
 {
 if(Character.isWhitespace(buffer.charAt(current)))
 lastSpace = current;
 current++;
 }
 if(current < buffer.length() && start < lastSpace)
 {
 for(int i = start;i < lastSpace;i++)
 writer.print(buffer.charAt(i));
 start = lastSpace + 1;
 }
 else
 {
 for(int i = start;i < current;i++)
 writer.print(buffer.charAt(i));
 start = current;
 }
 current = start;
 lastSpace = start - 1;
 writer.println();
 }
 for(int i = 0;i < linesAfter;i++)
 writer.println();
 buffer.delete(0,buffer.length());
 }
 else if(state == ROOT && name.equals("root"))
 state = NONE;
 }

 public void characters(char[] chars,int start,int length)
 {
 if(state == BLOCK)
 buffer.append(chars,start,length);
 }

 public void startDocument()
 {
 state = NONE;
 }

 public void endDocument()
 {
 writer.flush();
 }
}

定义的 SAX

SAX (Simple API for XML) 是处理 XML 文档最有效的解决方案之一。SAX 是基于事件的 API,这意味着,语法分析器会将事件发送到应用程序(而不是将文档的全部节点读入内存)。

最重要的事件是 startElement()、endElement() 和 characters()。

SAX 过滤器是设计用来彼此联系的特殊事件处理器。
本文章系列的第二部分将重述 SAX。
 
两个方便的 SAX 过滤器


Xml2Text 缺乏除去不想要的空格和语音字母的能力。与在 Xml2Text 中实现这些功能相比,用两个 SAX 过滤器来实现它们更为现实。SAX 的妙处在于:可以随意组合它们。

我还能够想出其它几种可以使用这两个过滤器的情况,例如,要在发布 HTML 文档之前作预处理时除去不想要的空格。

清单 4 中的 WhitespaceFilter.java 是除去重复空格的 SAX 过滤器。再说一遍,如果熟悉 SAX 处理器,这个类就非常简单。在 startElement() 和 characters() 中,它缓冲文本。endElement() 除去重复空格。请注意,这段代码是为了清楚起见,而没有考虑效率:它缓冲的内容太多。

这个过滤器还识别标准的 xml:space 属性。您可能已经忘掉了 xml:space,但是它在初始的 XML 标准中定义。它采用两个值:preserve(象 HTML <pre> 一样保留重复空格)和 default(可以除去重复空格。)

清单 4. WhitespaceFilter.java

package com.psol.xslist;

import java.util.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;

public class WhitespaceFilter
 extends XMLFilterImpl
{
 protected Stack stack;

 public WhitespaceFilter()
 {
 super();
 }

 public WhitespaceFilter(XMLReader reader)
 {
 super(reader);
 }

 public void startElement(String uri,
 String name,
 String qualifiedName,
 Attributes atts)
 throws SAXException
 {
 String space = atts.getValue("xml:space");
 if(null != space && space.equals("preserve"))
 stack.push(null);
 else
 stack.push(new StringBuffer());
 super.startElement(uri,name,qualifiedName,atts);
 }

 public void endElement(String uri,
 String name,
 String qualifiedName)
 throws SAXException
 {
 Object object = stack.pop();
 if(object instanceof StringBuffer)
 {
 StringBuffer input = (StringBuffer)object,
 output = new StringBuffer();
 boolean wasWhitespace = false;
 for(int current = 0;current < input.length();current++)
 {
 char c = input.charAt(current);
 if(c == '\n' || c == '\r')
 c = ' ';
 if(Character.isWhitespace(c))
 {
 if(!wasWhitespace)
 output.append(c);
 wasWhitespace = true;
 }
 else
 {
 output.append(c);
 wasWhitespace = false;
 }
 }
 char[] chars = new char[output.length()];
 output.getChars(0,output.length(),chars,0);
 super.characters(chars,0,output.length());
 }
 super.endElement(uri,name,qualifiedName);
 }

 public void characters(char[] chars,int start,int length)
 throws SAXException
 {
 Object object = stack.peek();
 if(object instanceof StringBuffer)
 ((StringBuffer)object).append(chars,start,length);
 else
 super.characters(chars,start,length);
 }

 public void startDocument()
 throws SAXException
 {
 stack = new Stack();
 super.startDocument();
 }
}


第二个过滤器 AsciiFilter.java(请参阅清单 5)除去旧式电子邮件客户机无法识别的语音字符和其它特殊字符。所有处理都在 characters() 中进行。

请注意,AsciiFilter 不过滤属性,并且为了使本示例更简单,它只除去法语中所用的语音符号。您可能想添加更多特殊字符来过滤其它语言。

清单 5. AsciiFilter.java

package com.psol.xslist;

import java.io.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;

public class AsciiFilter
 extends XMLFilterImpl
{
 public AsciiFilter()
 {
 super();
 }

 public AsciiFilter(XMLReader reader)
 {
 super(reader);
 }

 public void characters(char[] chars,int start,int length)
 throws SAXException
 {
 StringBuffer filtered =
 new StringBuffer((int)(length * 1.1));
 int i = start,
 stop = start + length;
 while(i < stop)
 {
 char c = chars[i++];
 switch(c)
 {
 case '?x009C;':
 filtered.append("oe");
 break;
 case '?:
 filtered.append("(c)");
 break;
 case '?:
 case '?:
 filtered.append('a');
 break;
 case '?:
 filtered.append("ae");
 break;
 case '?:
 filtered.append('c');
 break;
 case '?:
 case '?:
 case '?:
 case '?:
 filtered.append('e');
 break;
 case '?:
 case '?:
 filtered.append('i');
 break;
 case '?:
 case '?:
 filtered.append('o');
 break;
 case '?:
 case '?:
 case '?:
 filtered.append('u');
 break;
 // more characters would come here
 default:
 filtered.append(c);
 }
 }
 char[] newChars = new char[filtered.length()];
 filtered.getChars(0,filtered.length(),newChars,0);
 super.characters(newChars,0,filtered.length());
 }
}

运行项目

清单 6 中的 Console.java 合并所有步骤。它应用样式表(通过 Sun 设计的标准 Java API)并通过文本格式化器运行结果。请注意,这确实是一个多步骤的变换:从 DocBook 到文本标记语言,然后再到纯文本。

清单 6. Console.java

package com.psol.xslist;

import java.io.*;
import javax.xml.transform.*;
import javax.xml.transform.sax.*;
import javax.xml.transform.stream.*;

public class Console
{
 public static void main(String[] args)
 {
 try
 {
 if(args.length < 3)
 {
 System.out.println("java com.psol.xslist.Console " +
 "input.xml stylesheet.xsl output.txt");
 return;
 }
 Xml2Text xml2Text =
 new Xml2Text(new PrintWriter(new FileWriter(args[2])));
 WhitespaceFilter whitespaceFilter = new WhitespaceFilter();
 whitespaceFilter.setContentHandler(xml2Text);
 AsciiFilter asciiFilter = new AsciiFilter();
 asciiFilter.setContentHandler(whitespaceFilter);
 TransformerFactory factory = TransformerFactory.newInstance();
 Transformer transformer =
 factory.newTransformer(new StreamSource(new File(args[1])));
 transformer.transform(new StreamSource(new File(args[0])),
 new SAXResult(asciiFilter));
 }
 catch(IOException e)
 {
 System.err.println(e.getMessage());
 }
 catch(TransformerException e)
 {
 System.err.println(e.getMessage());
 }
 }
}

结束语

看起来,要满足使用老式电子邮件客户机的订户们的需求要做大量工作。为什么劳神那样做?某些人可能建议订户应该升级,但是很多 e-zine 发布人员更愿意多花点力气来尽力满足他们的读者。而且,由于 XML 和 XSLT,该过程的重复部分得以自动完成,从而使这种努力更实际。

在第二部分中,我将演示如何组合文本转换和 JavaMail,以使该操作完全自动化。

参考资料

可以下载本项目的源代码,包括一个 ANT 构建文件。

Ralph Wilson 博士完成了一个电子邮件客户机调查。他报告了用户对 HTML 电子邮件的偏爱。

如果您是 XSLT 新手,请参阅 Michael Kay 所著的 What kind of language is XSLT?

JAXP,Java API for XML,集成了 SAX(XML 语法分析)和 TrAX(XSLT 变换)。

JavaMail 是用于电子邮件的标准 Java API。

如果您喜欢本文对 XML 解决方案的描述,可以在本文作者所著的 Applied XML Solutions 中找到其它八个高质量的示例。

有关 XML 的 Java 编程的介绍,请参阅同名的 developerWorks 教程。

关于作者

Benoit Marchal 是比利时 Namur 的咨询人员和作家。他编写了 XML by ExampleApplied XML Solutions。他是 Gamelan 的专栏作家。
当 Ben 在 1998 年创建了 Pineapplesoft Link 时,他率先学习了 e-zine 发布。可以在 http://www.marchal.com/ 订阅他的 e-zine 并找到有关他最近项目的详细信息。

如果您希望与本文章的作者或其所在机构,进一步交流,请联系:畅享网 姜小姐
jill.jiang@amteam.org | 021-51096826-112 | 在线联系
老孙的IT运维管理之道[原创]用户的BSM用户的IT业务管..

从企业实际的IT运营角度来看,BSM是推动IT与业务融合,实现、改善WCNG司IT管理和治理的最佳实践之一。

吕建伟 专栏和CIO问答软件项目实施管理

现实中很少能按照正规流程来的,所以只能把流程中的各个环节拆开,个个击破,以后就可以见招拆招了。

ITIL实施:CIO时刻准备着

千军易得,一将难求,要推进ITIL实施,CIO扮演的角色不容忽视。吹响集结号,CIO出击的时刻已经来到。

节能与优化IT 企业CIO过冬良策

当前金融危机的影响还在继续漫延,很多企业都在苦寻过冬的良策,在这种情况下,节能与优化技术与产品无疑成为CIO们关注的首要对象,本次选题就是针对节能与优化IT来为CIO们提供过冬的良……