用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î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">> <xsl:value-of
select="title"/>
<</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>
<</xsl:text>
<xsl:value-of
select="@url"/>
<xsl:text>></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 教程。
关于作者
如果您希望与本文章的作者或其所在机构,进一步交流,请联系:畅享网 姜小姐
jill.jiang@amteam.org | 021-51096826-112 |
在线联系