On this page
Basics
Page Contents
Note:
本部分使用 DOM 树和在previous chapter中创建的变量。
假设程序员已经将 XML 文档作为变量doc
放入了数据模型中。此变量对应于DOM tree的根,即“文档”。 doc
后面的实际变量结构足够巧妙,并且仅大致类似于 DOM 树。因此,让我们以示例的方式了解如何使用它,而不是迷失在细节上。
按名称访问元素
此 FTL 打印该书的标题:
<h1>${doc.book.title}</h1>
输出将是:
<h1>Test Book</h1>
如您所见,doc
和book
都可以用作哈希值;您将其子节点作为子变量。基本上,您描述了到达 DOM 树中目标(元素title
)的路径。您可能会注意到上面有一些骗局:使用${doc.book.title}
,似乎我们指示 FreeMarker 打印title
元素本身,但是我们应该打印其子文本节点(检查DOM tree)。它仍然有效,因为元素不仅是哈希变量,而且还是字符串变量。元素节点的标量值是由其所有文本子节点的串联产生的字符串。但是,如果元素具有子元素,则尝试将其用作标量会导致错误。例如${doc.book}
会因错误而停止。
此 FTL 打印两章的标题:
<h2>${doc.book.chapter[0].title}</h2>
<h2>${doc.book.chapter[1].title}</h2>
在这里,由于book
具有 2 个chapter
个元素子元素,因此doc.book.chapter
是存储两个元素节点的序列。因此,我们可以概括上述 FTL,因此它适用于任意数量的章节:
<#list doc.book.chapter as ch>
<h2>${ch.title}</h2>
</#list>
但是如果只有一章怎么办?实际上,当您将元素访问为 hash 子变量时,它也总是是一个序列(不仅是 hash 和 string),但是如果序列中恰好包含 1 个项目,则变量本身也将充当该项目。因此,回到第一个示例,这也将打印书名:
<h1>${doc.book[0].title[0]}</h1>
但是您知道精确地有 1 个book
元素,而一本书中恰好有 1 个标题,因此您可以省略[0]
-s。如果这本书恰好只有 1 个chapter
-s,则${doc.book.chapter.title}
也可以工作(否则它是模棱两可的:如何知道您想要的title
中的title
是否是?所以它以错误停止.)。但是由于一本书可以包含多个章节,所以您不必使用这种形式。如果元素book
没有子级chapter
,那么doc.book.chapter
将是一个长度为 0 的序列,因此带有<#list ...>
的 FTL 仍将起作用。
重要的是要意识到这样的结果,例如,如果book
没有chapter
-s,则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]
。
Note:
大小为 1 的序列的规则是 XML 包装器的便利功能(通过多类型 FTL 变量实现)。通常,它不能与其他序列一起使用。
现在,我们通过打印每章的所有para
-s 来完成示例:
<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>
最后,子 selectors 机制的一般用法:此模板列出了示例 XML 文档的所有para
-s:
<#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
-s),然后子变量para
选择该序列中所有节点的para
子节点。
这种机制的负面影响是,诸如doc.somethingNonsense.otherNonsesne.totalNonsense
之类的东西只会计算为空序列,并且不会收到任何错误消息。
Accessing attributes
该 XML 与原始 XML 相同,不同之处在于它使用标题的属性代替元素:
<!-- 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 的序列。 ins 在这里也很棘手:如果您好奇foo
是否具有bar
属性,则必须编写foo.@bar[0]??
。 (foo.@bar??
是错误的,因为它总是返回true
.)类似地,如果您想要bar
属性的默认值,则必须编写foo.@bar[0]!"theDefaultValue"
。
与子元素一样,您可以选择多个节点的属性。例如,此模板打印所有章节的标题:
<#list doc.book.chapter.@title as t>
${t}
</#list>
探索树
该 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 处理,这将在later chapter中进行讨论。
如果 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 表达式
Note:
XPath 表达式仅在Apache Xalan或Jaxen(至少 1.1)个类可用时才起作用。但是,在 Java 1.8 之前的版本中,您不需要任何其他依赖项,因为 Java X 中包含 Apache Xalan,其软件包名称已更改,FreeMarker 将自动使用该软件包名称(除非还存在普通的 Apache Xalan)。内部 Xalan 在 OpenJDK 9 上不再可用,但在 Oracle JDK/JRE 9 上仍然可用(至少在官方稳定发行版“ build 9 181”上可用)。
Note:
不要使用上一节中的示例 XML,其中title
是属性;仅适用于该部分。
如果不能以其他方式解释与节点变量一起使用的哈希键(请参阅next section以获得精确的定义),则它将被解释为 XPath 表达式。有关 XPath 的更多信息,请访问http://www.w3.org/TR/xpath。
例如,在这里我们列出该章的para
元素,其中title
元素(不是属性!)的内容为“ Ch1”:
<#list doc["book/chapter[title='Ch1']/para"] as p>
<p>${p}
</#list>
它将打印:
<p>p1.1
<p>p1.2
<p>p1.3
长度为 1 的序列的规则(在前面的部分中有说明)也代表 XPath 结果。也就是说,如果所得序列恰好包含 1 个节点,则它也将充当节点本身。例如,打印“ 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]"]}
请注意,当前只有在程序员将 FreeMarker 设置为使用 Jaxen 而不是 Xalan 的情况下,才可以将 0 或多个(超过 1 个)节点的序列用作上下文。
还要注意,XPath 索引从 1 开始的序列项,而 FTL 索引从 0 开始的序列项。因此,要选择第一章,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 namespaces
默认情况下,当您编写类似doc.book
的东西时,它将选择名称book
的元素,该元素不属于任何 XML 命名空间(类似于 XPath)。如果要选择 XML 名称空间中的元素,则必须注册并使用前缀。例如,如果元素book
在 XML 名称空间http://example.com/ebook
中,则必须在模板顶部将其前缀与ftl directive的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 namespace。实际上,这意味着您在主页模板中注册的前缀将在所有<#include ...>
-d 模板中可见,而在<#imported ...>
-d 模板(通常称为 FTL 库)中不可见。或者从另一个角度来看,FTL 库可以注册 XML 名称空间前缀供自己使用,而不会干扰主模板和其他库的前缀注册。
请注意,如果 Importing 文档由给定的 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 格式将字符保留为<
,&
等,我们必须确保将这些字符转义。为此,FreeMarker 必须为properly configured,或将模板中的output_format="HTML"
添加到ftl
指令调用中。
因此,如果书名是“ Romeo&Juliet”,则得到的 HTML 输出将正确:
...
<h1>Romeo & Juliet</h1>
...