(七)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
 在CODE上查看代碼片派生到我的代碼片
  1. <book>  
  2.   <title>Test Book</title>  
  3.   <chapter>  
  4.     <title>Ch1</title>  
  5.     <para>p1.1</para>  
  6.     <para>p1.2</para>  
  7.     <para>p1.3</para>  
  8.   </chapter>  
  9.   <chapter>  
  10.     <title>Ch2</title>  
  11.     <para>p2.1</para>  
  12.     <para>p2.2</para>  
  13.   </chapter>  
  14. </book>  
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”部分:

[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  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:<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示例:

[html] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  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>  
這將打印出:

<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、訪問屬性

[html] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. <!-- THIS XML IS USED FOR THE "Accessing attributes" CHAPTER ONLY! -->  
  2. <!-- Outside this chapter examples use the XML from earlier. -->  
  3. <book title="Test">  
  4. <chapter title="Ch1">  
  5. <para>p1.1</para>  
  6. <para>p1.2</para>  
  7. <para>p1.3</para>  
  8. </chapter>  
  9. <chapter title="Ch2">  
  10. <para>p2.1</para>  
  11. <para>p2.2</para>  
  12. </chapter>  
  13. </book>  
這個XML和原來的那個是相同的,除了它使用title屬性,而不是元素

一個元素的屬性可以通過和元素的子元素一樣的方式來訪問,除了你在屬性名的前面放置一個@符號:

[html] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  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}
</#list>

3.3、探索DOM樹

這個FTL將會枚舉所有book元素的子節點:

[html] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  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的第一行修改爲這樣:   <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元素。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章