可扩展标记语言(XML)是过去几年里谈论得最多的技术。刚开始的时候,这个技术的潜能可能被夸大了。但不要怀疑它基于文本格式来表现数据的能力。在这一章里,我们将着眼于两个XML主题,以及考虑这两个主题是如何涉及到Velocity的:从Velocity模板里的DOM对象访问XML数据和使用Anakia来处理XML数据转换。
在Velocity模板里访问XML
你大概知道,Sun正努力为Java开发者提供一个处理XML数据的工具。起初,XML只得到很少的支持,一些如JDOM(一个开源API)、Xerces(Apache软件基金组织提供)的产品被用于向开发者提供处理XML数据的能力。很快,Sun在Java 1.3里提供了支持XML的附加包,在Java 1.4版本里,Sun把XML支持直接构建到语言里。
如果你想在你的Velocity模板里使用XML,你需要在控制器Java类里处理XML文件,并且需要向上下文对象中附加必需的信息。现在,让我们来看一示例。首先看一下Listing 11.1里的XML文件。目的是为了让Velocity控制器能够读取这个XML文件,并且将其解析到一个对象里,以便得到Velocity模板的支持。Listing 11.2显示了控制器处理这个XML文件的示例。
<?xml version="1.0"?>
<cds>
<cd>
<title>2112</title>
<artist>Rush</artist>
</cd>
</cds>
Listing 11.1 An XML document.
import java.util.Vector;
import javax.servlet.http.*;
import org.apache.velocity.Template;
import org.apache.velocity.context.Context;
import org.apache.velocity.servlet.VelocityServlet;
import org.apache.velocity.exception.*;
import org.apache.xerces.parsers.*;
public class VelocityServletExample extends VelocityServlet {
public Template handleRequest( HttpServletRequest request,
HttpServletResponse response,
Context context ) {
try {
SAXBuilder builder = new SAXBuilder(
"org.apache.xerces.parsers.SAXParser" );
Document root = builder.build("cds.xml");
VelocityContext context = new VelocityContext();
context.put("root", root );
} catch(Exception e){
e.printStackTrace();
}
Template template = null;
try {
template = getTemplate("displayxml.vm");
} catch( Exception e ) {
System.out.println("Error " + e);
}
return template;
}
}
Listing 11.2 The Velocity controller.
在这里,Listing 11.2里的Servlets和任何一个你在前面章节里看到的Servlets控制器类似。唯一的改变只涉及XML文件。我们假定这个在Listing 11.1里的XML被保存在一个名叫cds.xml的文件里,并且它和Servlets在同上目录。
第一个任务是把这个文档放到应用程序里,并把它解析到一个你很容易操作的格式。如果你熟悉XML处理的话,你应该知道这里有两条主要的途径来把这个基于文本的XML文档解析到数据结构,以便在应用程序里能够使用,这两条途径是:SAX和DOM。最后,采用哪种方法对你来说并不是难事,然而SAX比较适合用处理大型文档。
在这里,我们选择使用Xerces里的SAXParser类作为这个示例的控制器。Xerces是一个XML解析包,是Apache旗下的一个产品,你可以在http://xml.apache.org/xerces2-j/index.html下找到它。使用Xerces相当容易。首先,初始化一个新的SAXBuilder对象,并指定你想要使用的Xerces家族中的一个解析器。正如你在Listing 11.2看到的一样,我们使用了SAXParser类。这个SAXBuilder类允许你传递原始(raw)XML并返回一个文档对象。这个文档对象是一个用XML表示的数据结构。这点非常重要,因为你知道,任何对象类型可以被加入到Velocity上下文里,并能够将它传递给模板。
在这些代码里,你通过名叫“root”的引用把XML数据结构附加到上下文里。接着,控制器通过调用displayxml. vm加载模板,并且返回模板给VelocityServlet进行处理。这个displayxml.vm(见Listing 11.3)完成了所有的输出工作。
<html>
<body>
CD title is $root.getChild("cds").getChild("cd").getChild("title").get-
Text()
CD artist is $root.getChild("cds").getChild("cd").getChild("artist").get-
Text()
</body>
</html>
Listing 11.3 The displayxml.vm template file.
Listing 11.3的模板被设计用于放置来自控制器处理XML后产生的文章和标题数据。正如你看到的一样,你正在使用一个普通的XML方法去访问数据结构里面的数据。因为Velocity可以让你完全访问上下文里的对象,你可以使用在Document类或其父类节点里定义的任何方法。
例如,你可以使用(带有所有<cd>元素)的getChildNodes()方法来返回一个NodeList(节点列表)对象,也就是“NodeList list = $root.getChild (“cds”).getChildNodes();”。现在,你就可以使用#foreach指令来遍历节点,并且显示他们每一个节点的信息。
Velocity和Anakia
我们刚才描述的处理过程其实是Anakia项目(支持Velocity的部分)的一个功能库。Anakia是一个Ant任务,用于把XML转换到一个你选择好的输出介质,以用于代替Velocity模拟的扩展层叠语言(XSL)。Ant任务代码可以在org.apache.velocity.anakia.AnakiaTask类里找到,同时你也可以在/examples/anakia目录下找到一个完整的示例。
The Ant Build Task
你在自行尝试之前,让我们看一个示例。我们之前曾经提及,Anakia基本上是一个Ant任务,用于合并Velocity模板和XML文档。这个Ant任务代码见Listing 11.4。
<project name="build-site" default="docs" basedir=".">
<property name="docs.src" value="../xdocs"/>
<property name="docs.dest" value="../docs"/>
<target name="prepare">
<available classname="org.apache.velocity.anakia.AnakiaTask"
property="AnakiaTask.present"/>
</target>
<target depends="prepare" name="prepare-error"
unless="AnakiaTask.present">
<echo>
AnakiaTask is not present! Please check to make sure that
velocity.jar is in your classpath.
</echo>
</target>
<target name="docs" depends="prepare-error" if="AnakiaTask.present">
<taskdef name="anakia" classname="org.apache.velocity.anakia.AnakiaTask"/>
<anakia basedir="${docs.src}" destdir="${docs.dest}/"
extension=".html" style="./site.vsl"
projectFile="./stylesheets/project.xml"
excludes="**/stylesheets/**"
includes="**/*.xml"
lastModifiedCheck="false"
velocityPropertiesFile="velocity.properties">
</anakia>
<copy todir="${docs.dest}/images" filtering="no">
<fileset dir="${docs.src}/images">
<include name="**/*.gif"/>
<include name="**/*.jpeg"/>
<include name="**/*.jpg"/>
</fileset>
</copy>
</target>
</project>
Listing 11.4 The Anakia Ant task.
这个Ant任务首先尝试定位AnakiaTask类,同时设置源和目的文件的目录。在这个示例里,/xdocs目录用于容纳源文件,/docs目录用于容纳目的文件。表11.1解释了这个Ant任务的每一个元素。
Table 11.1 Anakia Ant Task Definitions
源文档
示例程序包含了一两个源文档。第一个叫index.xml,在/xdocs目录,见Listing 11.5;第二个也叫index.xml,在/xdocs/about目录下,它将被用于根层index文档的链接。
<?xml version="1.0" encoding="UTF-8"?>
<document>
<properties>
<author email="jon@latchkey.com">Jon S. Stevens</author>
<title>The Jakarta Project</title>
</properties>
<body>
<section name="Section 1">
<p>This is an example template that gets processed.</p>
<img src="/images/velocity.gif" width="329" height="105"/>
<table border="1">
<tr>
<td>It even has a table in it!</td>
</tr>
</table>
<h3>And an h3 tag</h3>
</section>
<section name="Section 2">
<p> here is another section </p>
</section>
<section name="section 3">
<p>
<a href="./about/index.html">A link to a sub page</a>
</p>
</section>
</body>
</document>
Listing 11.5 The index.xml source document.
在Listing 11.5里的XML文档提供了一个你将用于Velocity模板的信息。正如你看见的一样,它包括传统的HTML标记和用户自定义标记。当Anakia Ant任务合并这两个文档时,这些用户自定义标记将靠着(against)Velocity模板进行匹配。注意,在页面里有一个指向index.html的链接。
在某些情况下,你或许想要有一个描述导航的项目文件。该项目文件看来起来和下面的代码类似。当处理projectFile时,<menu>元素被用作页面左边部分的导航菜单里的实体。
<?xml version="1.0" encoding="ISO-8859-1"?>
<project name="Jakarta Site"
href="http://jakarta.apache.org/">
<title>Jakarta Site</title>
<body>
<menu name="Home">
<item name="Front Page" href="/index.html"/>
</menu>
<menu name="About">
<item name="About" href="/about/index.html"/>
</menu>
</body>
</project>
现在,让我们来看一下Velocity模板里的这些标记将要产生什么结果。
Anakia Velocity Stylesheets
Listing 11.6是一个示例模板,用于处理Listing 11.5的index.xml文件。这个模板使用了许多我们曾经学习的Velocity特性,包括和宏。让我们遍历发生在模板里的处理过程。首先让我们来关注一下Anakia自动为模板提供的不同上下文引用。我们将在下一节讨论变量。为了理解这个模板,你必须熟悉以下这些引用:
■ $xpath—所提供的XML的节点列表
■ $root—所解析的XML的根
■ $project—项目文件的根
模板从用于模板自身的本地定义开始,接着,一个名叫document()的方法用于产生宏。模板基本上由许多不同的宏构成,每一个都用于输出XML文件特定的部分,以用于它的HTML呈现。Anakia Ant任务不了解模板文件里的宏,因此,必须从他们中的一个调用开始。在这种情况下,document()宏定义了主要的HTML输出部分。
在document()宏的代码中可以看到,和在大多数Web页面一样,该代码由所有主要的HTML标记组成。第一个不同之处在<title>元素,标题的值由以下代码获得:
$root.getChild("properties").getChild("title").getText()
这个代码使用$root引用并通过该引用的子节点标题来查找属性元素,这个$root引用是在Velocity上下文里构建的,与你打算用于模板输入的XML文档相关。它通过标题元素获得文本,并在新创建的页面中将这个文本输出。回顾一下XML数据,你可以发现通过getText()调用的输出将是“The Jakarta Project”。接着,这个宏开始在这个页面上创建一个表。表的左面部分是一个导航菜单,右面部分显示了输入的XML信息(如果你回忆一下就可以知道,这个导航菜单是由之前讨论的projectFile构建的)。
为了构建导航菜单,document()调用了makeProject()宏。这个makeProject()宏从项目XML里找到所有的菜单成员,并且按次序把他们输出到一个列表中。当makeProject()宏运行结束,控制将返回到document()宏。
document()宏创建的表的左面部分由所有输入的XML元素组成。这些元素的不同部分被提取(extract),同时提取从子元素中提取出信息,用于产生表的信息。最后,所有HTML关闭标记被输出,从而产生一个完整的页面,见Listing 11.7。Figure 11.1显示了最后的Web页面在浏览器里的样子。
## Defined variables
#set ($bodybg = "#ffffff")
#set ($bodyfg = "#000000")
#set ($bodylink = "#525D76")
#set ($bannerbg = "#525D76")
#set ($bannerfg = "#ffffff")
#set ($tablethbg = "#039acc")
#set ($tabletdbg = "#a0ddf0")
<!-- start the processing -->
#document()
<!-- end the processing -->
## This is where the macros live
#macro ( makeProject )
#set ($menus = $xpath.applyTo("body/menu", $project))
#foreach ( $menu in $menus )
<strong>$menu.getAttributeValue("name")</strong>
<ul>
#foreach ( $item in $menu.getChildren() )
#set ($name = $item.getAttributeValue("name"))
<li>
#projectanchor($name $item.getAttributeValue("href"))
</li>
#end
</ul>
#end
#end
#macro ( image $value )
#if ($value.getAttributeValue("width"))
#set ($width=$value.getAttributeValue("width"))
#end
#if ($value.getAttributeValue("height"))
#set ($height=$value.getAttributeValue("height"))
#end
#if ($value.getAttributeValue("align"))
#set ($align=$value.getAttributeValue("align"))
#end
<img src="$relativePath$value.getAttributeValue("src")"
width="$!width" height="$!height" align="$!align">
#end
#macro ( projectanchor $name $value )
<a href="$relativePath$value">$name</a>
#end
#macro ( metaauthor $author $email )
<meta name="author" value="$author">
<meta name="email" value="$email">
#end
#macro (document)
<html>
<head>
<title>
$root.getChild("properties").getChild("title").getText()
</title>
</head>
<body bgcolor="$bodybg" text="$bodyfg" link="$bodylink">
<table border="1">
<tr>
<td>#makeProject()</td>
<td>
#set ($allSections = $xpath.applyTo("body/section",
$root))
#foreach ( $section in $allSections )
#foreach ( $item in $section.getChildren() )
#if ($item.getName().equals("img"))
#image ($item)
#else
$xmlout.outputString($item)
#end
#end
#end
</td>
</tr>
</table>
</body>
</html>
#end
Listing 11.6 The example stylesheet.
<!-- Content Stylesheet for Site -->
<!-- start the processing -->
<!--================== -->
<!-- Main Page Section -->
<!--================== -->
<html>
<head>
<meta http-equiv="Content-Type"
content="text/html; charset=iso-8859-1"/>
<meta name="author" value="Jon S. Stevens">
<meta name="email" value="jon@latchkey.com">
<title>The Jakarta Project</title>
</head>
<body bgcolor="#ffffff" text="#000000" link="#525D76">
<table border="1">
<tr>
<td>
<strong>Home</strong>
<ul>
<li> <a href="./index.html">Front Page</a></li>
</ul>
<strong>About</strong>
<ul>
<li> <a href="./about/index.html">About</a></li>
</ul>
</td>
<td>
<p>
This is an example template that gets processed.
</p>
<img src="./images/velocity.gif" width="329"
height="105" align="">
<table border="1">
<tr>
<td>
It even has a table in it!
</td>
</tr>
</table>
<h3>And an h3 tag</h3>
<p> here is another section </p>
<p><a href="./about/index.html">A link to a sub page</a></p>
</td>
</tr>
</table>
</body>
</html>
<!-- end the processing -->
Listing 11.7 The completed Web page.
Figure 11.1 The Web page output.
上下文引用
当Anakia Ant任务合并XML和模板时,它为上下文增加了一些引用,以便Velocity模板可以使用数据进行工作。你已经看到了一些将要进入上下文里的对象。表11.2描述了所有可能的对象。
Table 11.2 Anakia Context References
注意带有任何元素引用的XPath表达式。比如,你可以用$root.selectNodes(“cds/cd”)来获得一个和<cd>元素类型匹配的节点列表。
用Velocity输出XML
如果在你拥有一些存储在数据库或通过Servlets应用程序产生的数据,那么你可能会遇到一种情形,那就是你想要输出XML给用户,不管是通过浏览器还是通过下载文件。这里有一个在之前章节里开发的CD应用程序示例,这个应用程序提供了增加CD和记录查询数据库操作功能。你即可查询一条单独的文章,也可显示特定CD的歌曲(track)信息。你极有可能想生成一个XML格式的输出,现在就来看一看如何来实现。
在这个部分,让我们考虑两个不同的情形:XML用于文章查询和XML用于数据里所有CDs的报表。
文章XML查询
你回忆一下就可以发现,我们的CD应用程序利用一个Servlets控制来解释主屏上的不同提交按钮。在文章查询窗体里,提交按钮调用这个Servlets控制,并向其传递得到的值。Servlets执行的代码见Listing 11.8。
else if (req.getParameter("submit").equals("obtain")) {
try {
if (cdHome == null) {
context.put("message", "Sorry we had an error");
} else {
Collection cds = cdHome.findByArtist(req.getParameter("artist"));
context.put ("cds", cds);
try {
template = getTemplate("displaycds.vm");
} catch( Exception e ) {
e.printStackTrace();
}
}
} catch(Exception e) {
e.printStackTrace();
}
}
Listing 11.8 The artist query code.
非常简单,代码使用CD Bean调用了一个查询,返回的集合通过Velocity模板displaycds.vm进行显示。让我们改动一下代码来提交obtainxml的值,其作用是从数据库中取出相同的信息。然而,我们用的不是displaycds.vm模板,而用的是producecdxml. vm模板。新代码见Listing 11.9。
else if ((req.getParameter("submit").equals("obtain")) ||
(req.getParameter("submit").equals("obtainxml"))) {
try {
if (cdHome == null) {
context.put("message", "Sorry we had an error");
} else {
Collection cds = cdHome.findByArtist(req.getParameter("artist"));
context.put ("cds", cds);
try {
if (req.getParameter("submit").equals("obtainxml")) {
template = getTemplate("producecdxml.vm");
} else {
template = getTemplate("displaycds.vm");
} catch( Exception e ) {
e.printStackTrace();
}
}
} catch(Exception e) {
e.printStackTrace();
}
}
Listing 11.9 The artist query code with XML output.
正如你看到的一样,要相进入这个代码块,需要把“obtain”或“obtainxml”提交按钮的值传递到Servlets才行。所有相同的CD将从查询中返回,但依赖于提交的值,不管是用producecdxml. vm还是用displaycds.vm Velocity模板。为了使用这些代码,首先你得定义producec dxml.vm,代码见Listing 11.10。
<?xml version="1.0" ?>
<cds>
#foreach($value in $cds)
<cd id="$value.id">
<title>$value.title</title>
</cd>
#end
</cds>
Listing 11.10 The producecdxml.vm Velocity template.
在Listing 11.10里的Velocity模板为文章查询产生一个XML文件。要注意,CD的ID和标题是如何作为属性和元素被分别捕获的。
完整CD XML报表
在之前的示例里,所有的输出都产生在用户浏览器上。你是否想过去下载一个包含所有数据库里CD信息的XML文件?其实只需要改变一点代码就可以实现这个愿望。让我们在CD主页面加一个按钮,用于调用Servlets控制器来请求一个完整的CD数据库报表。代码如下:
<form action="http://localhost:8080/cd/cdVelocityHandler" method="post">
<input type="submit" name="submit" value="fullreport"> -
download 'report.txt' to your local system
</form>
当用户单击FullReport按钮时,控制被传递到Servlets,在这里fullreport值将进行验证。Listing 11.11的是之后将要执行的代码。
else if (req.getParameter("submit").equals("fullreport")) {
try {
if (cdHome == null) {
context.put("message", "Sorry we had an error");
} else {
Collection cds = cdHome.findAllCDs();
context.put ("cds", cds);
try {
res.setContentType("APPLICATION/OCTET-STREAM");
res.setHeader("Content-Disposition","attachment;
filename=report.txt");
template = getTemplate("fullreport.vm");
} catch( Exception e ) {
e.printStackTrace();
}
}
} catch(Exception e) {
e.printStackTrace();
}
Listing 11.11 The fullreport code.
注意Listing 11.11里的两种改变,第一处是一个被加入到CDRecordBean类、名叫findAll-CDs()的新查询,详见Listing 11.12。
<query>
<query-method>
<method-name>findAllCDs</method-name>
</query-method>
<ejb-ql>SELECT o FROM CDTable o</ejb-ql>
</query>
Listing 11.12 The CDRecordBean All CD query.
第二处改变由下面两行代码组成:
Res.setContentType("APPLICATION/OCTET-STREAM");
res.setHeader("Content-Disposition","attachment;filename=report.txt");
这个代码用于告诉用户的浏览器,有一个名叫report.txt的文件将出现在窗体里,并且是一个附件,因此,在用户浏览器应该会出现另存为对话框。这点很重要,因为我们的Velocity模板将用于产生一个可下载的文件。Listing 11.13展示了这个模板。
<cds>
#foreach($value in $cds)
<cd id="$value.id">
<artist>$value.artist</artist>
<title>$value.title</title>
</cd>
#end
</cds>
Listing 11.13 The Velocity template for the XML output.
现在,用户可以浏览这个CD应用程序的index页,并且可以单击Full-Report按钮。代码将把所有的CD从数据里取出,并用Velocity的fullreport.vm文件进行格式化,结果见Figure 11.2。
Figure 11.2 The XML output.
本章小节和下间介绍
在这一章里,我们重点介绍了使用Velocity来处理和使用XML数据。开发者可以为设计者的模板提供一个标准格式的数据,并且设计者可以使用方法的包容集来访问数据。在下一章里,我们将讨论如何混合Velocity和Servlets。