要注意,烦扰的“\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”部分:
- /* Create a data-model */
- Map root = new HashMap();
- 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:<h1>${doc.book.title}</h1>
输出是:<h1>Test Book</h1>
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所有的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
3.2、访问属性
- <!-- 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>
这个XML和原来的那个是相同的,除了它使用title属性,而不是元素
一个元素的属性可以通过和元素的子元素一样的方式来访问,除了你在属性名的前面放置一个@符号:
- <#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>
3.3、探索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
FreeMarker将输出内容写到你传递给Template.process方法的Writer对象中,它并不关心Writer对象将输出内容打印到控制台或是一个文件中,或是HttpServletResponse对象的输出流中。FreeMarker并不知道servlets和Web;它仅仅是使用模板文件来合并Java对象,之后从它们中间生成输出文本。
许多框架都是基于“Model 2”架构的,JSP页面来控制显示。
一、 在“Model 2”中使用FreeMarker
许多框架依照HTTP请求转发给用户自定义的“action”类,将数据作为属性放在ServletContext,HttpSession和HttpServletRequest对象中,之后请求被框架派发到一个JSP页面中(视图层),使用属性传递过来的数据来生成HTML页面,这样的策略通常就是所指的Model 2模型。
使用这样的框架,你就可以非常容易的用FTL文件来代替JSP文件。但是,因为你的Servlet容器(Web应用程序服务器),不像JSP文件,它可能并不知道如何处理FTL文件,那么就需要对Web应用程序进行一些额外的配置。
1. 复制freemarker.jar到(从FreeMarker发布包的lib目录中)你的Web应用程序的WEB-INF/lib目录下。
2. 将下面的部分添加到Web应用程序的WEB-INF/web.xml文件中(调整它是否需要)。
- <servlet>
- <servlet-name>freemarker</servlet-name>
- <servlet-class>freemarker.ext.servlet.FreemarkerServlet</servlet-class>
- <!-- FreemarkerServlet 设置: -->
- <init-param>
- <param-name>TemplatePath</param-name>
- <param-value>/</param-value>
- </init-param>
- <init-param>
- <param-name>NoCache</param-name>
- <param-value>true</param-value>
- </init-param>
- <init-param>
- <param-name>ContentType</param-name>
- <param-value>text/html; charset=UTF-8</param-value>
- <!-- 强制使用 UTF-8作为输出编码格式! -->
- </init-param>
- <!-- FreeMarker 设置: -->
- <init-param>
- <param-name>template_update_delay</param-name>
- <param-value>0</param-value>
- <!-- 0 只对开发使用! 否则使用大一点的值. -->
- </init-param>
- <init-param>
- <param-name>default_encoding</param-name>
- <param-value>ISO-8859-1</param-value>
- <!-- 模板文件的编码方式. -->
- </init-param>
- <init-param>
- <param-name>number_format</param-name>
- <param-value>0.##########</param-value>
- </init-param>
- <load-on-startup>1</load-on-startup>
- </servlet>
- <servlet-mapping>
- <servlet-name>freemarker</servlet-name>
- <url-pattern>*.ftl</url-pattern>
- </servlet-mapping>
- ...
- <!-- 为了阻止从Servlet容器外部访问MVC的视图层组件。
- RequestDispatcher.forward/include应该起到作用。
- 移除下面的代码可能开放安全漏洞!
- -->
- <security-constraint>
- <web-resource-collection>
- <web-resource-name>
- FreeMarker MVC Views
- </web-resource-name>
- <url-pattern>*.ftl</url-pattern>
- </web-resource-collection>
- <auth-constraint>
- <!-- 不允许任何人访问这里 -->
- </auth-constraint>
- </security-constraint>
在这之后,你可以像使用JSP(*.jsp)文件那样使用FTL文件(*.ftl)了。(当然你可以选择除ftl之外的扩展名;这只是惯例)
它是怎么工作的?让我们来看看JSP是怎么工作的。许多servlet容器处理JSP时使用一个映射为*.jsp的servlet请求URL格式。这样servlet就会接收所有URL是以.jsp结尾的请求,查找请求URL地址中的JSP文件,内部编译完后交给Servlet,然后调用生成信息的serlvet来生成页面。这里为URL类型是*.ftl映射的FreemarkerServlet也是相同功能,只是FTL文件不会编译给Servlet,而是给Template对象,之后Template对象的process方法就会被调用来生成页面。
比如,代替这个JSP页面(注意它使用了Struts标签库来保存设计,而不是嵌入可怕的Java代码):
- <%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
- <%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>
- <html>
- <head><title>Acmee Products International</title>
- <body>
- <h1>Hello <bean:write name="user"/>!</h1>
- <p>These are our latest offers:
- <ul>
- <logic:iterate name="latestProducts" id="prod">
- <li><bean:write name="prod" property="name"/>
- for <bean:write name="prod" property="price"/> Credits.
- </logic:iterate>
- </ul>
- </body>
- </html>
你可以使用这个FTL文件(使用ftl扩展名而不是jsp):
- <html>
- <head><title>Acmee Products International</title>
- <body>
- <h1>Hello ${user}!</h1>
- <p>These are our latest offers:
- <ul>
- <#list latestProducts as prod>
- <li>${prod.name} for ${prod.price} Credits.
- </#list>
- </ul>
- </body>
- </html>
注意:在FreeMarker中,<html:form action="/query">...</html:form>仅仅被视为是静态文本,所以它会按照原本输出出来了,就像其他XML或HTML标记一样。JSP标签也仅仅是FreeMarker的指令,没有什么特殊之处,所以你可以使用FreeMarker语法形式来调用它们,而不是JSP语法:<@html.form action="/query">...</@html.form>。注意在FreeMarker语法中你不能像JSP那样在参数中使用${...},而且不能给参数值加引号。所以这样是不对的:
- <#-- WRONG: -->
- <@my.jspTag color="${aVariable}" name="aStringLiteral"
- width="100" height=${a+b} />
但下面这样是正确的:
- <#-- Good: -->
- <@my.jspTag color=aVariable name="aStringLiteral"
- width=100 height=a+b />
在这两个模板中,当你要引用user和latestProduct时,它会首先试图去查找一个名字已经在模板中创建的变量(比如prod;如果你使用JSP:这是一个page范围内的属性)。如果那样做不行,它会尝试在对HttpServletRequest象中查找那个名字的属性,如果没有找到就在HttpSession中找,如果还没有找到那就在ServletContext中找。FTL按这种情况工作是因为FreemarkerServlet创建数据模型由上面提到的3个对象中的属性而来。那也就是说,这种情况下根哈希表不是java.util.Map(正如本手册中的一些例子那样),而是ServletContext+HttpSession+HttpServletRequest;FreeMarker在处理数据模型类型的时候非常灵活。所以如果你想将变量” name”放到数据模型中,那么你可以调用servletRequest.setAttribute("name", "Fred");这是模型2的逻辑,而FreeMarker将会适应它。
FreemarkerServlet也会在数据模型中放置3个哈希表,这样你就可以直接访问3个对象中的属性了。这些哈希表变量是:Request,Session,Application(和ServletContext对应)。它还会暴露另外一个名为RequestParameters的哈希表,这个哈希表提供访问HTTP请求中的参数。
FreemarkerServlet也有很多初始参数。它可以被设置从任意路径来加载模板,从类路径下,或相对于Web应用程序的目录。你可以设置模板使用的字符集。你还可以设置想使用的对象包装器等。
通过子类别,FreemarkerServlet易于定制特殊需要。那就是说,你需要对所有模板添加一个额外的可用变量,使用servlet的子类,覆盖preTemplateProcess()方法,在模板被执行前,将你需要的额外数据放到模型中。或者在servlet的子类中,在Configuration中设置这些全局的变量作为共享变量。
二、包含其它Web应用程序资源中的内容
你可以使用由FreemarkerServlet(2.3.15版本之后)提供的客户化标签<@include_page path="..."/>来包含另一个Web应用资源的内容到输出内容中;这对于整合JSP页面(在同一Web服务器中生活在FreeMarker模板旁边)的输出到FreeMarker模板的输出中非常有用。使用:
<@include_page path="path/to/some.jsp"/>
和使用JSP指令是相同的:
<jsp:include page="path/to/some.jsp">
注意:
<@include_page ...>不能和<#include ...>搞混,后者是为了包含FreeMarker模板而不会牵涉到Servlet容器。使用<#include ...>包含的模板和包含它的模板共享模板处理状态,比如数据模型和模板语言变量,而<@include_page ...>开始一个独立的HTTP请求处理。
路径可以是相对的,也可以是绝对的。相对路径被解释成相对于当前HTTP请求(一个可以触发模板执行的请求)的URL,而绝对路径在当前的servlet上下文(当前的Web应用)中是绝对的。你不能从当前Web应用的外部包含页面。注意你可以包含任意页面,而不仅仅是JSP页面;我们仅仅使用以.jsp结尾的页面作为说明。
除了参数path之外,你也可以用布尔值(当不指定时默认是true)指定一个名为inherit_params可选的参数来指定被包含的页面对当前的请求是否可见HTTP请求中的参数。
最后,你可以指定一个名为params的可选参数,来指定被包含页面可见的新请求参数。如果也传递继承的参数,那么指定参数的值将会得到前缀名称相同的继承参数的值。params的值必须是一个哈希表类型,它其中的每个值可以是字符串,或者是字符串序列(如果你需要多值参数)。这里给出一个完整的示例:
<@include_page path="path/to/some.jsp" inherit_params=true params={"foo": "99", "bar": ["a", "b"]}/>
这会包含path/to/some.jsp页面,传递它的所有的当前请求的参数,除了“foo”和“bar”,这两个会被分别设置为“99”和多值序列“a”,“b”。如果原来请求中已经有这些参数的值了,那么新值会添加到原来存在的值中。那就是说,如果“foo”有值“111”和“123”,那么现在它会有“99”,“111”,“123”。
事实上使用params给参数传递非字符串值是可能的。这样的一个值首先会被转换为适合的Java对象(数字,布尔值,日期等),之后调用它们Java对象的toString()方法来得到字符串值。最好不要依赖这种机制,作为替代,明确参数值在模板级别不能转换成字符串类型之后,在使用到它的地方可以使用内建函数?string和?c。
三、在FTL中使用JSP客户化标签
FreemarkerServlet将一个哈希表类型的JspTaglibs放到数据模型中,就可以使用它来访问JSP标签库了。JSP客户化标签库将被视为普通用户自定义指令来访问。例如,这是使用了Struts标签库的JSP文件:
- <%@page contentType="text/html;charset=ISO-8859-2" language="java"%>
- <%@taglib uri="/WEB-INF/struts-html.tld" prefix="html"%>
- <%@taglib uri="/WEB-INF/struts-bean.tld" prefix="bean"%>
- <html>
- <body>
- <h1><bean:message key="welcome.title"/></h1>
- <html:errors/>
- <html:form action="/query">
- Keyword: <html:text property="keyword"/><br>
- Exclude: <html:text property="exclude"/><br>
- <html:submit value="Send"/>
- </html:form>
- </body>
- </html>
这是一个(近似)等价的FTL:
- <#assign html=JspTaglibs["/WEB-INF/struts-html.tld"]>
- <#assign bean=JspTaglibs["/WEB-INF/struts-bean.tld"]>
- <html>
- <body>
- <h1><@bean.message key="welcome.title"/></h1>
- <@html.errors/>
- <@html.form action="/query">
- Keyword: <@html.text property="keyword"/><br>
- Exclude: <@html.text property="exclude"/><br>
- <@html.submit value="Send"/>
- </@html.form>
- </body>
- </html>
因为JSP客户化标签是在JSP环境中来书写操作的,它们假设变量(在JSP中常被指代“beans”)被存储在4个范围中:page范围,request范围,session范围和application范围。FTL没有这样的表示法(4种范围),但是FreemarkerServlet给客户化标签提供仿真的环境,这样就可以维持JSP范围中的“beans”和FTL变量之间的对应关系。对于自定义的JSP标签,请求,会话和应用范围是和真实JSP相同的:javax.servlet.ServletContext,HttpSession和ServerRequest对象中的属性。从FTL的角度来看,这三种范围都在数据模型中。page范围和FTL全局变量(参见global指令)是对应的。那也就是,如果你使用global指令创建一个变量,通过仿真的JSP环境,它会作为page范围变量对自定义标签可见。而且,如果一个JSP标签创建了一个新的page范围变量,那么结果和用global指令创建的是相同的。要注意在数据模型中的变量作为page范围的属性对JSP标签是不可见的,尽管它们在全局是可见的,因为数据模型和请求,会话,应用范围是对应的,而不是page范围。
在JSP页面中,你可以对所有属性值加引号,这和参数类型是字符串,布尔值或数字没有关系,但是因为在FTL模板中自定义标签可以被用户自定义FTL指令访问到,你将不得不在自定义标签中使用FTL语法规则,而不是JSP语法。所以当你指定一个属性的值时,那么在等号的右边是一个FTL表达式。因此,你不能对布尔值和数字值的参数加引号(比如:<@tiles.insert page="/layout.ftl" flush=true/>),否则它们将被解释为字符串值,当FreeMarker试图传递值到期望非字符串值的自定义标记中时,这就会引起类型不匹配错误。而且还要注意,这很自然,你可以使用任意FTL表达式作为属性的值,比如变量,计算的结果值等。(比如:<@tiles.insert page=layoutName flush=foo && bar/>)
servlet容器运行过程中,因为它实现了自身的轻量级JSP运行时环境,它用到JSP标签库,而FreeMarker并不依赖于JSP支持。这是一个很小但值得注意的地方:在它们的TLD文件中,开启FreeMarker的JSP运行时环境来分发事件到JSP标签库中注册时间监听器,你应该将下面的内容添加到Web应用下的WEB-INF/web.xml文件中:
- <listener>
- <listener-class>freemarker.ext.jsp.EventForwarding</listener-class>
- </listener>
用就行。如果你的servlet容器只对JSP 1.1支持,那么你不得不将下面六个类(比如你可以从Tomcat 5.x或Tomcat 4.x的jar包中提取)复制到Web应用的WEB-INF/classes/...目录下:
javax.servlet.jsp.tagext.IterationTag,
javax.servlet.jsp.tagext.TryCatchFinally,
javax.servlet.ServletContextListener,
javax.servlet.ServletContextAttributeListener,
javax.servlet.http.HttpSessionAttributeListener,
javax.servlet.http.HttpSessionListener.
但是要注意,因为容器只支持JSP 1.1,通常是使用较早的Servlet 2.3之前的版本,事件监听器可能就不支持,因此JSP 1.2标签库来注册事件监听器会正常工作。
四、为FreeMarker配置安全策略
当FreeMarker运行在装有安全管理器的Java虚拟机中时,你不得不再授与一些权限,确保运行良好。最值得注意的是,你需要为对freemarker.jar的安全策略文件添加这些条目:
- grant codeBase "file:/path/to/freemarker.jar"
- {
- permission java.util.PropertyPermission "file.encoding", "read";
- permission java.util.PropertyPermission "freemarker.*", "read";
- }
另外,如果从一个目录中加载模板,你还需要给FreeMarker授权来从那个目录下读取文件,使用如下的授权:
- grant codeBase "file:/path/to/freemarker.jar"
- { ...
- permission java.io.FilePermission "/path/to/templates/-", "read";
- }
最终,如果你使用默认的模板加载机制,也就是从当前文件夹下加载模板,那么需要指定这些授权内容:(主要表达式${user.dir}将会在运行时被策略解释器处理,几乎它就是一个FreeMarker模板)
- grant codeBase "file:/path/to/freemarker.jar"
- {
- ...
- permission java.util.PropertyPermission "user.dir", "read";
- permission java.io.FilePermission "${user.dir}/-", "read";
- }
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元素。