Basics

Page Contents

Note:

本部分使用 DOM 树和在previous chapter中创建的变量。

假设程序员已经将 XML 文档作为变量doc放入了数据模型中。此变量对应于DOM tree的根,即“文档”。 doc后面的实际变量结构足够巧妙,并且仅大致类似于 DOM 树。因此,让我们以示例的方式了解如何使用它,而不是迷失在细节上。

按名称访问元素

此 FTL 打印该书的标题:

<h1>${doc.book.title}</h1>

输出将是:

<h1>Test Book</h1>

如您所见,docbook都可以用作哈希值;您将其子节点作为子变量。基本上,您描述了到达 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 XalanJaxen(至少 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 directivens_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/ebookbook元素。不幸的是,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 &amp; Juliet</h1>
...