这部分我们使用的DOM树和变量都是前一章做的那个。
假设程序员在数据模型中放置了一个XML文档,就是名为
doc 的变量。这个变量和DOM 树的根结点"document"对应。
真实的变量 doc 之后结构是非常复杂的,
大约类似DOM树。所以为了避免钻牛角尖,我们通过例子来看看如何使用。
通过名称来访问元素
这个FTL打印book的title:
<h1>${doc.book.title}</h1>将会输出:
<h1>Test Book</h1>
正如你所看到的,doc 和 book
都可以当作哈希表来使用。你可以按照子变量的形式来获得它们的子结点。
基本上,你用描述路径的方法来访问在DOM树中的目标(元素title)。
你也许注意到了上面有一些是假象:使用 ${doc.book.title},
就好像我们指示 FreeMarker 打印 title 元素本身,
但是我们应该打印它的子元素文本(看看 DOM 树)。
那也可以办到,因为元素不仅仅是哈希表变量,也是字符串变量。
元素结点的标量是从它的文本子结点级联中获取的字符串结果。然而,如果元素有子元素,
尝试使用一个元素作为标量会引起错误。比如${doc.book}将会以错误而终止。
该FTL打印2个chapter的title:
<h2>${doc.book.chapter[0].title}</h2>
<h2>${doc.book.chapter[1].title}</h2>这里,book 有两个 chapter 子元素,
doc.book.chapter 是存储两个元素结点的序列。
因此,我们可以概括上面的FTL,所以它以任意chapter的数量起作用:
<#list doc.book.chapter as ch>
<h2>${ch.title}</h2>
</#list>但是如果只有一个chapter会怎么样呢?实际上, 当你访问一个作为哈希表子变量的元素时,通常 也可以是序列(不仅仅是哈希表和字符串),但如果序列只包含一个项, 那么变量也作为项目自身。所以,回到第一个示例中,它也会打印book的title:
<h1>${doc.book[0].title[0]}</h1>但是你知道那里就只有一个 book 元素,
而且book也就只有一个title,所以你可以忽略那些 [0]。
如果book恰好有一个 chapter(否则它就是模糊的:
它怎么知道你想要的是哪个 chapter 的 title?
所以它就会以错误而停止),${doc.book.chapter.title} 也可以正常进行。
但是因为一个book可以有很多chapter,你不能使用这种形式。如果元素
book 没有子元素 chapter,
那么 doc.book.chapter 将是一个长度为零的序列,
所以用FTL <#list ...> 也可以进行。
知道这样一个结果是很重要的,比如,如果 book
没有 chapter,那么 book.chapter
就是一个空序列,所以 doc.book.chapter??
就 不会 是 false,它就一直是
true!类似地,doc.book.somethingTotallyNonsense??
也不会是 false。来检查是否有子结点,可以使用
doc.book.chapter[0]??(或doc.book.chapter?size == 0)。
当然你可以使用类似所有的空值处理操作符
(比如 doc.book.author[0]!"Anonymous" ),只是不要忘了那个
[0]。
序列的大小是1的规则是方便XML包装的方便特性(通过多类型的FTL变量来实现)。 通常其他序列将不会起作用。
现在我们完成了打印每个chapter所有的 para 示例:
<h1>${doc.book.title}</h1>
<#list doc.book.chapter as ch>
<h2>${ch.title}</h2>
<#list ch.para as p>
<p>${p}
</#list>
</#list>将会输出:
<h1>Test</h1>
<h2>Ch1</h2>
<p>p1.1
<p>p1.2
<p>p1.3
<h2>Ch2</h2>
<p>p2.1
<p>p2.2上面的FTL可以书写的更加漂亮:
<#assign book = doc.book>
<h1>${book.title}</h1>
<#list book.chapter as ch>
<h2>${ch.title}</h2>
<#list ch.para as p>
<p>${p}
</#list>
</#list>最终,一个"广义的"子结点选择机制是:
模板列出所有示例XML文档的para:
<#list doc.book.chapter.para as p>
<p>${p}
</#list>将会输出:
<p>p1.1 <p>p1.2 <p>p1.3 <p>p2.1 <p>p2.2
这个示例说明了哈希表变量选择序列子结点的做法
(在前面示例中的序列大小为1)。在这个具体的例子中,子变量
chapter 返回一个大小为2的序列
(因为有两个 chapter),之后子变量
para 在那个序列中选择 para
所有结点的子结点。
这种机制的一个负面结果是类似于
doc.somethingNonsense.otherNonsesne.totalNonsense
这样的东西会被算作是空序列,而且你也不会得到任何错误信息。
访问属性
这个XML和原来的那个是相同的,除了它使用title属性,而不是元素:
<!-- THIS XML IS USED FOR THE "Accessing attributes" CHAPTER ONLY! -->
<!-- Outside this chapter examples use the XML from earlier. -->
<book title="Test">
<chapter title="Ch1">
<para>p1.1</para>
<para>p1.2</para>
<para>p1.3</para>
</chapter>
<chapter title="Ch2">
<para>p2.1</para>
<para>p2.2</para>
</chapter>
</book>一个元素的属性可以通过和元素的子元素一样的方式来访问, 除了你在属性名的前面放置一个@符号:
<#assign book = doc.book>
<h1>${book.@title}</h1>
<#list book.chapter as ch>
<h2>${ch.@title}</h2>
<#list ch.para as p>
<p>${p}
</#list>
</#list>这会打印出和前面示例相同的结果。
按照和获取子结点一样的逻辑来获得属性,所以上面的
ch.@title 结果就是大小为1的序列。如果没有
title 属性,那么结果就是一个大小为0的序列。
所以要注意,这里使用内建函数也是有问题的:如果你很好奇
foo 是否含有属性 bar,
那么你不得不写 foo.@bar[0]??。
(foo.@bar?? 是不对的,因为它总是返回
true)。类似地,如果你想要一个
bar 属性的默认值,那么你就不得不写
foo.@bar[0]!"theDefaultValue"。
正如子元素那样,你可以选择多结点的属性。 例如,这个模板将打印所有chapter的title属性:
<#list doc.book.chapter.@title as t>
${t}
</#list>探索DOM树
这个FTL将会枚举所有book元素的子结点:
<#list doc.book?children as c>
- ${c?node_type} <#if c?node_type == 'element'>${c?node_name}</#if>
</#list>将会输出:
- text - element title - text - element chapter - text - element chapter - text
?node_type 的意思可能没有解释清楚。
有一些在DOM树中存在的结点类型,比如 "element",
"text","comment",
"pi"等。
?node_name 返回结点的结点名称。
对于其他的结点类型,也会返回一些东西,但是它对声明的XML处理更有用,这会在后面章节中讨论。
如果book元素有属性,由于实际的原因它可能
不会 在上面的列表中出现。
但是你可以获得包含元素所有属性的列表,使用变量元素的子变量
@@。如果你将XML的第一行修改为这样:
<book foo="Foo" bar="Bar" baaz="Baaz">
然后运行这个FTL:
<#list doc.book.@@ as attr>
- ${attr?node_name} = ${attr}
</#list>然后得到这个输出(或者其他相似的结果):
- baaz = Baaz - bar = Bar - foo = Foo
要返回子结点的列表,有一个方便的子变量来仅仅列出元素的子元素:
<#list doc.book.* as c>
- ${c?node_name}
</#list>将会输出:
- title - chapter - chapter
你可以使用内建函数 parent 来获得元素的父结点:
<#assign e = doc.book.chapter[0].para[0]>
<#-- Now e is the first para of the first chapter -->
${e?node_name}
${e?parent?node_name}
${e?parent?parent?node_name}
${e?parent?parent?parent?node_name}将会输出:
para chapter book @document
在最后一行你访问到了DOM树的根结点,文档结点。 它不是一个元素,这就是为什么得到了一个奇怪的名字; 现在我们不来处理它。很明显,文档结点没有父结点。
你可以使用内建函数 root 来快速返回到文档结点:
<#assign e = doc.book.chapter[0].para[0]>
${e?root?node_name}
${e?root.book.title}将会输出:
@document Test Book
在内建函数完整的列表中你可以用来在DOM树中导航, 可以阅读结点内建函数参考。
使用XPath表达式
XPath表达式仅在Jaxen(推荐使用, 但是使用至少Jaxen 1.1-beta-8版本,不能再老了)或Apache Xalan库可用时有效。 (Apache Xalan库在Sun J2SE 1.4,1.5和1.6(也许在后续版本)中已经包含了; 不需要独立的Xalan的jar包。)
不要使用前一部分的示例XML,那里的title
是一个属性;仅对那部分使用。
如果哈希表的键使用了结点变量而不能被解释(下一部分对此精确定义), 那么它就会被当作Xpath表达式被解释。要获得XPath的更多信息, 可以访问http://www.w3.org/TR/xpath。
例如,这里我们列出 title
元素(不是属性!)内容为"Ch1"的chapter的 para 元素:
<#list doc["book/chapter[title='Ch1']/para"] as p>
<p>${p}
</#list>将会输出:
<p>p1.1 <p>p1.2 <p>p1.3
长度为1(在前面部分解释过了)的序列的规则也代表XPath的结果。 也就是说,如果结果序列仅仅包含1个结点,它也会当作结点自身。 例如,打印chapter元素"Ch1"第一段:
${doc["book/chapter[title='Ch1']/para[1]"]}这也会输出相同内容:
${doc["book/chapter[title='Ch1']/para[1]"][0]}XPath表达式的内容结点(或者是结点序列) 是哈希表子变量被用作发布XPath表达式的结点。 因此,这将打印和上面例子相同的内容:
${doc.book["chapter[title='Ch1']/para[1]"]}请注意,现在你可以使用0序列或多(比1多)结点作为内容, 这只在程序员已经建立 FreeMarker 使用Jaxen而不是Xalan时才可以。
也要注意XPath序列的项索引从1开始,而FTL的序列项索引是用0开始的。
因此,要选择第一个chapter,XPath表达式是 "/book/chapter[1]",
而FTL表达式是 book.chapter[0]。
如果程序员设置 FreeMarker 使用Jaxen而不是Xalan, 那么 FreeMarker 的变量在使用XPath变量引用时是可见的:
<#assign currentTitle = "Ch1"> <#list doc["book/chapter[title=$currentTitle]/para"] as p> ...
请注意,$currentTitle 不是 FreeMarker 的插值,
因为那里没有 { 和 }。
那是XPath表达式。
一些XPath表达式的结果不是结点集,而是字符串,数字或者布尔值。
对于那些XPath表达式,结果分别是FTL字符串,数字或布尔值变量。
例如,下面的例子将会计算XML文档中 para 元素的总数,
所以结果是一个数字:
${x["count(//para)"]}将会输出:
5
XML命名空间
默认来说,当你编写如 doc.book 这样的东西时,
那么它会选择属于任何XML命名空间(和XPath相似)名字为
book 的元素。如果你想在XML命名空间中选择一个元素,
你必须注册一个前缀,然后使用它。比如,如果元素 book
是命名空间 http://example.com/ebook,
那么你不得不关联一个前缀,要在模板的顶部使用 ftl
指令 的 ns_prefixes 参数:
<#ftl ns_prefixes={"e":"http://example.com/ebook"}>现在你可以编写如 doc["e:book"] 的表达式。
(因为冒号会混淆 FreeMarker,方括号语法的使用是需要的。)
ns_prefixes 的值作为哈希表,
你可以注册多个前缀:
<#ftl ns_prefixes={
"e":"http://example.com/ebook",
"f":"http://example.com/form",
"vg":"http://example.com/vectorGraphics"}
>ns_prefixes 参数影响整个 FTL 命名空间。
这就意味着实际中,你在主页面模板中注册的前缀必须在所有的 <#include
...> 的模板中可见,而不是 <#imported
...> 的模板(经常用来引用FTL库)。从另外一种观点来说,
一个FTL库可以注册XML命名空间前缀来为自己使用,而前缀注册不会干扰主模板和其他库。
要注意,如果一个输入模板是给定XML命名空间域中的,
为了方便你可以设置它为默认命名空间。这就意味着如果你不使用前缀,
如在 doc.book 中,那么它会选择属于默认命名空间的元素。
这个默认命名空间的设置使用保留前缀 D,例如:
<#ftl ns_prefixes={"D":"http://example.com/ebook"}>现在表达式 doc.book 选择属于XML命名空间
http://example.com/ebook 的 book 元素。
不幸的是,XPath不支持默认命名空间。因此,在XPath表达式中,
没有前缀的元素名称通常选择不输入任何XML命名空间的元素。然而,
在默认命名空间中访问元素你可以直接使用前缀D,比如:
doc["D:book/D:chapter[title='Ch1']"]。
请注意,当你使用默认命名空间时,那么你可以使用保留前缀
N 来选择不属于任意结点空间的元素。比如
doc.book["N:foo"]。这对XPath表达式不起作用,
上述的都可以写作 doc["D:book/foo"]。
不要忘了转义!
我们在所有的示例中都犯了大错。我们生成HTML格式的输出,
HTML格式保留如 <,&
等的字符。所以当我们打印普通文本(比如标题和段落)时,
我们不得不转义它,因此,示例的正确版本是:
<#escape x as x?html>
<#assign book = doc.book>
<h1>${book.title}</h1>
<#list book.chapter as ch>
<h2>${ch.title}</h2>
<#list ch.para as p>
<p>${p}
</#list>
</#list>
</#escape>所以如果book的标题是"Romeo & Julia", 那么HTML输出的结果就是正确的:
... <h1>Romeo & Julia</h1> ...