(七)freeMarker之XML处理

尽管FreeMarker最初被设计用作Web页面的模板引擎,对于2.3版本来说,它的另外一个应用领域目标是:转换XML到任意的文本输出(比如HTML)。因此,在很多情况下,FreeMarker也是一个可选的XSLT。
从技术上来说,在转换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文档

[html]  view plain  copy
 
  1. <book>  
  2.   <title>Test Booktitle>  
  3.   <chapter>  
  4.     <title>Ch1title>  
  5.     <para>p1.1para>  
  6.     <para>p1.2para>  
  7.     <para>p1.3para>  
  8.   chapter>  
  9.   <chapter>  
  10.     <title>Ch2title>  
  11.     <para>p2.1para>  
  12.     <para>p2.2para>  
  13.   chapter>  
  14. book>  
W3C的DOM定义XML文档模型为节点树。上面XML的节点树可以被视为:

(七)freeMarker之XML处理_第1张图片

要注意,烦扰的“\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”部分:

[java]  view plain  copy
 
  1. /* Create a data-model */  
  2. Map root = new HashMap();  
  3. root.put("doc",freemarker.ext.dom.NodeModel.parse(new File("the/path/of/the.xml")));  
然后你可以在基本的输出(通常是终端屏幕)中得到一个程序可以输出XML转换的结果。
注意:
● parse方法默认移除注释和处理指令节点。
● NodeModel也允许你直接包装org.w3c.dom.Node。首先你也许想用静态的实用方法清空DOM树,比如NodeModel.simplify或你自定义的清空规则。 

3、必要的XML处理

假设程序员在数据模型中放置了一个XML文档,就是名为doc的变量。这个变量和DOM树的根节点“document”对应。真实的变量doc之后结构是非常复杂的,大约类似DOM树。

3.1、通过名称来访问元素

这个FTL打印book的title:

${doc.book.title}


输出是:

Test Book


正如你所看到的,doc和book都可以当作哈希表来使用。你可以按照子变量的形式来获得它们的子节点。基本上,你用描述路径的方法来访问在DOM树中的目标(元素title)。你也许注意到了上面有一些是假象:使用${doc.book.title},就好像我们指示FreeMarker打印title元素本身,但是我们应该打印它的子元素文本(看看DOM树)。那也可以办到,因为元素不仅仅是哈希表变量,也是字符串变量。元素节点的标量是从它的文本子节点级联中获取的字符串结果。然而,如果元素有子元素,尝试使用一个元素作为标量会引起错误。比如${doc.book}将会以错误而终止。
FTL打印2个chapter的title:

${doc.book.chapter[0].title}


${doc.book.chapter[1].title}


这里,book有两个chapter子元素,doc.book.chapter是存储两个元素节点的序列。因此,我们可以概括上面的FTL,所以它以任意chapter的数量起作用:

<#list doc.book.chapter as ch>

${ch.title}



但是如果只有一个chapter会怎么样呢?实际上,当你访问一个作为哈希表子变量的元素时,通常也可以是序列(不仅仅是哈希表和字符串),但如果序列只包含一个项,那么变量也作为项目自身。所以,回到第一个示例中,它也会打印book的title:

${doc.book[0].title[0]}


但是你知道那里就只有一个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示例:

[html]  view plain  copy
 
  1. <h1>${doc.book.title}h1>  
  2. <#list doc.book.chapter as ch>  
  3. <h2>${ch.title}h2>  
  4. <#list ch.para as p>  
  5. <p>${p}  
  6. #list>  
  7. #list>  
这将打印出:

Test


Ch1


p1.1

p1.2

p1.3

Ch2


p2.1

p2.2

3.2、访问属性

[html]  view plain  copy
 
  1.   
  2.   
  3. <book title="Test">  
  4. <chapter title="Ch1">  
  5. <para>p1.1para>  
  6. <para>p1.2para>  
  7. <para>p1.3para>  
  8. chapter>  
  9. <chapter title="Ch2">  
  10. <para>p2.1para>  
  11. <para>p2.2para>  
  12. chapter>  
  13. book>  
这个XML和原来的那个是相同的,除了它使用title属性,而不是元素

一个元素的属性可以通过和元素的子元素一样的方式来访问,除了你在属性名的前面放置一个@符号:

[html]  view plain  copy
 
  1. <#assign book = doc.book>  
  2. <h1>${book.@title}h1>  
  3. <#list book.chapter as ch>  
  4. <h2>${ch.@title}h2>  
  5. <#list ch.para as p>  
  6. <p>${p}  
  7. #list>  
  8. #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}

3.3、探索DOM树

这个FTL将会枚举所有book元素的子节点:

[html]  view plain  copy
 
  1. <#list doc.book?children as c>  
  2. - ${c?node_type} <#if c?node_type = 'element'>${c?node_name}#if>  
  3. #list>  
会打印出:

- text
- element title
- text
- element chapter
- text
- element chapter
- text

关于?node_type的意思,有一些在DOM树中存在的节点类型,比如"element","text","comment","pi"等。
?node_name返回节点的节点名称。对于其他的节点类型,也会返回一些东西,但是它对声明的XML处理更有用,
如果book元素有属性,由于实际的原因它可能不会在上面的列表中出现。但是你可以获得包含元素所有属性的列表,使用变量元素的子变量@@。如果你将XML的第一行修改为这样:   

然后运行这个FTL:

<#list doc.book.@@ as attr>
- ${attr?node_name} = ${attr}

然后得到这个输出(或者其他相似的结果)

- baaz = Baaz
- bar = Bar
- foo = Foo

要返回子节点的列表,有一个方便的子变量来仅仅列出元素的子元素:
<#list doc.book.* as c>
- ${c?node_name}

将会打印:

- 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元素。

你可能感兴趣的:(freeMarker)