2009年4月15日 星期三

[Struts2] 內建的 OGNL Type Converter

我們已經了解到 OGNL 具備有自動的 data transfer 以及 type converter 的功能,現在我們就要介紹 OGNL 中內建的 type converter。之前我們 Introduce OGNL 中就有簡單的使用過將頁面上 String-based data 轉換成 Javabean 中的 int type data。讓我們來看看這樣的轉換機制到底有哪些其他的功能!

All converters
首先我們先看看 Struts2 framework 中到底有哪些內建的 converters:
  • String - 字串,幾乎是不用進行轉換,因為在 client 端與 server 端的型態就是一樣的!
  • boolean/Boolean - true 或 false 的字串會被轉換
  • char/Character - 可以想像是一個單位的字串
  • int/Integer, float/Float, double/Double, long/Long - 原始的格式被轉換成字串型態,你可以想像就像是使用 String.valueOf() 來進行轉換!
  • Date - 根據使用者目前的 Locale 而被轉換成 SHORT 格式的字串,例如:28/02/97
  • array - 每一個 array 中的 element 將會被轉換成 String 物件做處理
  • List - 預設設定的 Element 以 String 為主
  • Map - 預設設定的 Element 以 String 為主
上述的各種就是 OGNL 內建的 converters,基本上 Primitive type(例如:String, int 等) 是比較好處理也比較可以理解其運作原理;而 Colleciton type(例如:array, List 與 Map) 在轉換上比較複雜,但是對於 programmers 來說還是很直觀的!至於我們要怎樣讓 OGNL 可以使用適當得 type converter,這點我們就無須擔心,只要我們將要取的 data 放置到 ValueStack 中並且在 view-layer 中撰寫適當的 OGNL expression language 就可以了,OGNL 會自動知道使用哪個 type converter!
以下我們將所有的 type converter 分成兩類加以討論:1) Primitive type mapping 與 2) Collection type mapping。

Primitive type mapping
基本型別的轉換是最簡單的,也是很直觀的!我們就直接給個範例:
<s:form action="Register">
<s:textfield name="user.name" label="Name" />
<s:textfield name="user.password" label="Password" />
<s:textfield name="user.age" label="Age" />
<s:submit />
</s:form>

上面所顯示的 form 當使用者按下 submit 之後,就會根據 action attribute 中的 value 將表單傳送給 struts.xml 中設定的 Register action,透過 OGNL 的 expression language 會將各個欄位的值交由 param interceptor 找尋 mapping destination,在由 OGNL 個別進行 type conversion。由上述的 OGNL 中我們可以知道 Register action 中必定會有一個 User object 作為 property:
public class RegisterAction extends ActionSupport
{
private User user;
public void setUser(User user)
{
this.user = user;
}
public User getUser()
{
return this.user;
}
}

我們現在是透過 Object-back Javabean property 的方式(將物件作為 Action property 中提到)存取 User property。而 User object 中一定會有 name, password 與 age 的 property。所以 OGNL 會先透過 RegisterAction 取得 User 物件後,在依照 expression 中不同的 property 去進行 type conversion,例如:age 在 User 物件中屬於 int 型態,則會啟動 int type converter 將使用者輸入的 String type 的 age 轉換成 int type 的 age。
至於為甚麼 HTTP 中的 form 欄位一定都是 String 呢?這點就要牽涉到 HTTP 本身的設計,因為我們傳送的 form 欄位都會以 parameter 的方式傳送,只是有 GET 跟 POST 的差別,這兩種差別在於 GET 會將這些 key value pairs 顯示在 URL 上;而 POST 不會!但是最終都是以 parameter 的方式傳送!也正因為如此,Java EE 中設計的 HttpServletRequest 物件有一個 getParameter() method 就是用來取得使用者在頁面上所傳送的資料。而這個 method 回傳的資料都是 String,所以 OGNL 必須將這些 String-based data 進行 type conversion 了!
同樣的,如果我們要取得某個 property value,我們在頁面上就可以使用 property tag 來取值:
<s:form action="Register">
<s:textfield name="user.name" label="Name" />
<s:textfield name="user.password" label="Password" />
<s:textfield name="user.age" label="Age" />
<s:submit />
</s:form>

這個 OGNL 在後端工作時會轉換成:getUser().getAge();
另外,當 OGNL 在進行 type conversion 時也會進行 validation 的作業!舉個例子來說,如果使用者在 Age 欄位中輸入了非數字的字串,OGNL 在 type conversion 時就會出現錯誤,並且顯示錯誤訊息在使用者所輸入的欄位上,這樣的錯誤機制有點像我們在深入實做 Action 中提到的驗證失敗的訊息。

Collection type mapping
對於上面所提到的 primitive type conversion 的確比較直觀,不過對於 programmer 來說,要讓 String-based 的 HTTP data 能夠 mapping 到 Collection 類的 Java-type 的確比較棘手!好在 OGNL 已經提供了這樣的功能,讓 programmer 不必擔心這類型的轉換工作!
在 Struts2 framework 中提供了將 multivalued request parameters 轉換到有變化性的 Collection 型態 property,而且也包含了原始的 array 型態,畢竟 array 是所有 Collection-type 的基礎型態!以下將分成三種不同型態的 Java-type 分別介紹:1) array, 2) List 與 3) Map。

1. array
Array 其實也算是基本的 Java 型態,只是收集了一堆相同型態的資料罷了!Struts2 framework 提供了這樣的轉換功能,也就是如果我們的 Action 中宣告了 array property(又稱為 indexed Javabeans property),我們可以很輕鬆的完成由網頁上的資料轉換到 Javabean 中的 array。這樣的功能源自於 OGNL 的 navigate(導覽) 能力,因為具有導覽的功能,我們可以期望 OGNL 在 Collection-type 中幫我們找到某個 elements。話不多說,我們就看以下的範例吧:
<s:form action="Regist" method="get">
<s:textfield name="age" label="age"></s:textfield>
<s:textfield name="age" label="age"></s:textfield>
<s:textfield name="age" label="age"></s:textfield>

<s:textfield name="name[0]" label="Name"></s:textfield>
<s:textfield name="name[2]" label="Name"></s:textfield>
<s:textfield name="name[3]" label="Name"></s:textfield>

<s:textfield name="ageInt" label="age int"></s:textfield>
<s:textfield name="ageInt" label="age int"></s:textfield>
<s:textfield name="ageInt" label="age int"></s:textfield>

<s:submit />
</s:form>

首先是頁面的部份,在上面我們宣告了一個 form,不過我們將資料傳送的方式設定為 GET 模式,因為這樣我們就可以觀察 array 型態的資料是怎樣被安排被傳送的。接下來就是重頭戲了!我們有三種 properties,age, name 跟 ageInt。在 age property 中我們測試不要給 index ,看看資料會怎樣被安排;另外就是 name property,這裡的寫法很像是在 Java 中對 array 給值的寫法,我們在這裡故意跳過 index=1 的 array!至於 ageInt 我等等會解釋!
接著就是 Javabean,我們看看 age 跟 name properties 要怎樣在 Javabean 中撰寫:
public class RegistAction extends ActionSupport
{
private Integer[] age;
private String[] name = new String[4];
private int[] ageInt;
public Integer[] getAge()
{
return age;
}
public void setAge(Integer[] age)
{
this.age = age;
}
public String[] getName()
{
return name;
}
public void setName(String[] name)
{
this.name = name;
}
public int[] getAgeInt()
{
return ageInt;
}
public void setAgeInt(int[] ageInt)
{
this.ageInt = ageInt;
}
@Override
public String execute() throws Exception
{
for (int i=0,n=this.age.length;i<n;i++)
System.out.println(this.age[i]);

for (int i=0,n=this.name.length;i<n;i++)
System.out.println(this.name[i]);

for (int i=0,n=this.ageInt.length;i<n;i++)
System.out.println(this.ageInt[i]);

return SUCCESS;
}
}

首先是 age,我們在這裡將他宣告為 Integer 的物件陣列,而 ageInt 則是宣告為 int 的原始型態陣列,等等我們會看到再取值時的差別!再來就是 name property,我們在這裡採用 String 型態,由於 String 不屬於 primitive type 或是 primitive type wrapper 所以我們要自行先宣告陣列大小!這點需要注意一下~否則在 assign value 時 console 會出現錯誤訊息!
在這裡使用陣列跟一般的 property 沒兩樣,我們不必擔心說 assign 具有 index 的 value,因為 OGNL 會幫我們處理,我們只要如同往常提供 getter/setter method 就可以!接下來就是要觀察一下這些 parameters 會怎樣傳送?我們就將 form submit 出去,由 URL 上方可以看到這些 parameters 的結果:
http://localhost:8080/HelloStruts2/Regist.action?age=1&age=2&age=3&name[0]=4&name[2]=5&name[3]=6&ageInt=7&ageInt=8&ageInt=9
由於我們的 form 中有兩種類型的 textfield:一種是有給 index,另一種則是沒有!我們會看到沒有 index 的 value 會被照順序的安排,並且使用相同的 key(如上的 age 與 ageInt),而有給 index 的 value 則會按照我們的 index 作為 key(如上的 name[2])!在 Action 接收後,我們特別在 execute() method 中將這些 array values 印出來觀察,我們發現到 name[1] 確實是沒有值!
最後我們就要看看怎樣將這些 array values 顯示在頁面上:
Age:<s:property value="age"/><br />
Name:<s:property value="name"/><br />
Age Int:<s:property value="ageInt"/><br />

Age[1]:<s:property value="age[1]"/><br />
Name[1]:<s:property value="name[1]"/><br />
Name[2]:<s:property value="name[2]"/><br />
Age Int[1]:<s:property value="ageInt[1]"/><br />

我們分成兩個部份,第一個部份是不給予 index 看看值會怎樣被取出來!第二部份就是給予 index 取出某個 element 的值!第二個部份很好猜測,就和我們先前將 value 寫回 action 中的方式一樣!不過第一部份就比較難猜了!我就直接執行看看結果吧!
Age:1
Name:ognl.NoConversionPossible
Age Int:7, 8, 9
Age[1]:2
Name[1]:
Name[2]:5
Age Int[1]:8
第二部份的結果我就不多說了!只是提醒一下如果是 NULL 的值,回傳到頁面上就會是空白的內容。現在我們就探討一下第一部份的結果。首先,如果我們在 Action 中採用 Primitive type wrapper 物件(例如:Integer, Double 等)宣告陣列,當我們想要印出所有陣列中的元件,我們就要自行 iterate array,如同我們的 age property;如果我們是使用 primitive type 宣告陣列,Struts2 framework 就會自動幫我們 iterate array;另外,如果我們的 array 不屬於 primitive type 或 primitive type wrapper 的話,OGNL 就無法幫我們顯示內容了!

2. List
List 也是在 Struts2 framework 中有支援 navigate 功能,我們可以將上面的範例簡單的修改為 List:
public class RegistAction extends ActionSupport
{
private List<Integer> age;
private List<String> name;
public List<Integer> getAge()
{
return age;
}
public void setAge(List<Integer> age)
{
this.age = age;
}
public List<String> getName()
{
return name;
}
public void setName(List<String> name)
{
this.name = name;
}
}

頁面的部份我們不必修改,因為在 Struts2 framework 中對於 List 與 array 的處理方式其實是類似的,上面的例子中我們使用 Primitive type 作為 List 中的 Element,我們也可以使用非 Primitive type,只要我們使用 J2SE 5.0 的重要特性-Generic 就可以輕鬆完成!如下的 Action 讓我們可以在 List 放我們自己定義的物件:
public class RegistAction extends ActionSupport
{
private List<User> users;
public List<User> getUsers()
{
return users;
}
public void setUsers(List<User> users)
{
this.users = users;
}
}

在頁面上我們就可以採用這樣的 OGNL expression language:users[1].name

3. Map
最後一種 OGNL 可以進行轉換的就是 Map,Map 跟使用 List 上沒有太大的差別,主要差別在於 Map 是以物件作為 key,而 List 是以 index 作為 key。
private Map<String, User> users;
private Map<Integer, User> otherUsers;
public Map<String, User> getUsers()
{
return users;
}
public void setUsers(Map<String, User> users)
{
this.users = users;
}
public Map<Integer, User> getOtherUsers()
{
return otherUsers;
}
public void setOtherUsers(Map<Integer, User> otherUsers)
{
this.otherUsers = otherUsers;
}

上面的 Action 中我們宣告了兩個 Map properties,第一個是用 String 作為 Map 的 Key,第二個則是用 Integer 作為 Map 的 Key。而這兩種 Map 都是存放 User 物件。而我們在頁面存取這兩種 Map 時就要撰寫成如下:
<s:form action="Regist" method="get">
<s:textfield name="users['silver'].name" label="Silver Name"></s:textfield>
<s:textfield name="users.kent.name" label="Kent Name"></s:textfield>

<s:textfield name="otherUsers['0'].name" label="Other User Name"></s:textfield>
<s:submit />
</s:form>

我們存取 Map 的方式可以用 [] 將 Key 寫在裡面,不過要加上 ' ' 符號!或者我們可以直接指定 Key(如:user.kent) 方式,但是當我們的 Key 採用 Integer 時,我們就只能使用第一種表示法存取了!

在這裡我們討論了進階的 OGNL 的 type conversion 機制,從 primitive type 到 collection-based type,如:array, List 與 Map。OGNL 讓 programmers 可以不必煩惱如何將 String-based 的 HTTP value 轉換成 Java-type based 的 Javabean,甚至我們在撰寫 OGNL expression language 時也是很輕鬆容易的!

2 則留言:

WEI 提到...

Good article and let me deeply understand how ONGL works in Struts2. Thankyou!!!

Unknown 提到...

原來我的問題在這裡有解答
謝謝~