从技术上来说,在转换XML文档上没有什么特别之处。它和你使用FreeMarker做其他事情都是一样的:你将XML文档丢到数据模型中(和其他可能的变量),然后你将FTL模板和数据模型合并来生成输出文本。对于更好的XML处理的额外特性是节点FTL变量类型(在通用的树形结构中象征一个节点,不仅仅是对XML有用)和用内建函数,指令处理它们,你使用的XML包装器会暴露XML文档,并将作为模板的FTL变量。
使用FreeMarker和XSLT有什么不同?FTL语言有常规的命令式/过程式的逻辑。另一方面,XSLT是声明式的语言,由很聪明的人设计出来,所以它并不能轻易吸收它的逻辑,也不会在很多情况下使用。而且它的语法也非常繁琐。然而,当你处理XML文档时,XSLT的“应用模板”方法可以非常方便,因此FreeMarker支持称作“访问者模式”的相似事情。所以在很多应用程序中,写FTL的样式表要比写XSLT的样式表容易很多。另外一个根本的不同是FTL转换节点树到文本,而XSLT转换一课树到另一棵树。所以你不能经常在使用XSLT的地方使用FreeMarker。
1、XML文档
节点树
如下示例的XML文档
W3C的DOM定义XML文档模型为节点树。上面XML的节点树可以被视为:
要注意,烦扰的“\n”是行的中断(这里用\n指示,在FTL字符串中使用转义序列)和标记直接的缩进空格。
注意和DOM相关的术语:
● 一棵树最上面的节点称为root根,在XML文档中,它通常是“文档”节点,而不是最顶层元素(本例中的book)。
● 如果B是A的直接后继,我们说B节点是A节点的child子节点。比如,两个chapter元素是book元素的子节点,但是para元素就不是。
● 如果A是B的直接前驱,也就是说,如果B是A的子节点,我们说节点A是节点B的parent父节点。比如,book元素是两个chapter元素的父节点,但是它不是para元素的父节点。
● XML文档中可以出现几种成分,比如元素,文本,注释,处理指令等。所有这些成分都是DOM树的节点,所以就有元素节点,文本节点,注释节点等。原则上,元素的属性也是树的节点—它们是元素的子节点--,但是,通常我们(还有其他XML相关的技术)不包含元素的子节点。所以基本上它们不被记为子节点。
FTL中的DOM节点和node variable节点变量对应。这是变量类型,和字符串,数字,哈希表等类型相似。节点变量类型使得FreeMarker来获取一个节点的父节点和子节点成为可能。这是技术上需要允许模板开发人员在节点间操作,也就是,使用节点内建函数或者visit和recurse指令;
2、将XML放到数据模型中
创建一个简单的程序来运行下面的示例是非常容易的。仅仅用下面这个例子来替换程序开发指南中快速入门示例中的“Create a data-model”部分:
然后你可以在基本的输出(通常是终端屏幕)中得到一个程序可以输出XML转换的结果。
注意:
● parse方法默认移除注释和处理指令节点。
● NodeModel也允许你直接包装org.w3c.dom.Node。首先你也许想用静态的实用方法清空DOM树,比如NodeModel.simplify或你自定义的清空规则。
3、必要的XML处理
假设程序员在数据模型中放置了一个XML文档,就是名为doc的变量。这个变量和DOM树的根节点“document”对应。真实的变量doc之后结构是非常复杂的,大约类似DOM树。
3.1、通过名称来访问元素
这个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]。
现在我们完成了打印每个chapter所有的para示例:
这将打印出:
<h1>Test</h1>
<h2>Ch1</h2>
<p>p1.1
<p>p1.2
<p>p1.3
<h2>Ch2</h2>
<p>p2.1
<p>p2.2
3.2、访问属性
这个XML和原来的那个是相同的,除了它使用title属性,而不是元素
一个元素的属性可以通过和元素的子元素一样的方式来访问,除了你在属性名的前面放置一个@符号:
这会打印出和前面示例相同的结果。
按照和获取子节点一样的逻辑来获得属性,所以上面的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>
3.3、探索DOM树
这个FTL将会枚举所有book元素的子节点:
会打印出:
- 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
3.4、XML命名空间
默认来说,当你编写如doc.book这样的东西时,那么它会选择属于任何XML命名空间名字为book的元素。如果你想在XML命名空间中选择一个元素,你必须注册一个前缀,然后使用它。比如,如果元素book是命名空间http://example.com/ebook,那么你不得不关联一个前缀,要在模板的顶部使用ftl指令的the 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元素。