2009年4月30日 星期四

[Struts2] 深入探討 OGNL

在之前我們已經知道 OGNL expression 可以幫助我們將 data 在網頁上與後端 Javabean 之間作搬移與轉換型態,而且我們也瞭解到我們的 Actions 會被放在 ValueStack 中讓 OGNL 可以存取。現在我們要告訴你,OGNL 可以存取任何集合的物件,而 ValueStack 只是其中一個集合物件而已!更完整的來說,OGNL 是用來存取 ActionContext 中的 data,而 ActionContext 中包含了 ValueStack。

ActionContext
ActionContext 儲存了 Struts2 framework 中所有的 data,這些 data 包含 ValueStack、Request、Session 與 Application 等。而所有的 OGNL 都會到 ActionContext 中查詢 data,不過如果我們沒有指定 OGNL 要到哪裡存取資料的話,OGNL 預設是到 ValueStack 中進行存取。所謂沒有指定的意思是,如果我們撰寫的 OGNL 為:user.age,這就是採用 OGNL 預設的存取位置。如下圖。
在這裡所謂的 Context 不是指環境,而是一種有 container 概念的物件,主要是蒐集整個 framework 在執行時所需要的 data 與 resources。所有的 OGNL 都會先選擇所要存取的初始地點,如果沒有指定,就是到 ValueStack 中存取。如果我們將之前的 User 物件儲存在 session 中,那我們要存取 session 中 user 的 age,那我們就要改用:#session['user'].age。我們採用 # 開頭並且指定我們要存取的位置為 session,因為 session 是一個 Map 物件(回想我們之前所使用 SessionAware interface 中,session 物件是以 Map 方式被宣告!),我們要採用之前在內建的 OGNL Type Converter 中提到的 Map property 存取方式,所以就會是 #session['user'],取得 user 物件後我們就可以操作內部的 properties!
那我們還有哪些存取位置可以使用呢?下面就列出可用的位置:
  • parameters - 此 request 中的 parameter map
  • request - 如同 JSP 中的 request
  • session - 如同 JSP 中的 session
  • application - 如同 JSP 中的 application
  • attr - 根據 page, request, session 與 application 的順序查詢第一個找到的 data
  • ValueStack - 預設的存取位置
ValueStack
之前我們已經討論過很多關於 ValueStack 的資訊,現在我們要深入的討論 ValueStack。當 Strus2 framework 接收到使用者的 request 後,會先建立一個 ActionContext、ValueStack 與使用者呼叫的 action 物件,並且將 action 中的 properties 儲存到 ValueStack 中。這樣使用者就可以透過 OGNL 存取 ActionContext 中的 data。不過這裡有一點需要注意的是,在 ValueStack 中的 properties 只會出現最上層的 properties,也就是說如果有兩個 properties 是相同的變數名稱,只有最上層的 property 可以被存取。
你可能會好奇為什麼會有這樣情形發生呢?一個 class 中不可能有兩個變數是相同的名稱阿!沒錯!這的確有點弔詭,不過確實是有可能發生的,當我們的 Action 中的 Javabean property 採用 ModelDriven 的方式就會有可能:
public class ModelDrivenAction extends ActionSupport implements ModelDriven<User>
{
private User user = new User();
private String name;
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
@Override
public User getModel()
{
return this.user;
}
@Override
public String execute() throws Exception
{
System.out.println("This.name = "+this.name+"\n User.name="+this.user.getName());
return SUCCESS;
}

}

上面的 ModelDrivenAction 中我們採用 ModelDriven 方式讓 user property 直接存取,然後我們在 execute method 中印出使用者在 textfield 中輸入的值會存在哪個 property 之下,回想一下我們如果使用 ModelDriven 方式,我們的 OGNL 就可以直接寫成:name 而不是 user.name,從 console 中可以發現,ModelDrivenAction 自己定義的 name property 被隱藏了!所以使用者輸入的值只會被除存在 user property 下的 name property!所以在 ValueStack 中會如下:
所以我在將物件作為 Action property 中建議採用 Object-backed Javabean property 方式,這樣可以減少日後維護的問題!

2009年4月21日 星期二

[Struts2] 建立自訂的 OGNL Type Converter

Struts2 framework 中的 OGNL Type Converter 已經滿足了我們大部分的情況,基本上我們可以不用自己建立自訂的 Type Converter,不過 Struts2 framework 還是提供了讓我們可以自訂 Type Converter 的機制!在這裡我將示範要如何建立一個自訂的 OGNL Type Converter!範例中主要是提供一個 UserTypeConverter:將頁面上的 User data 轉換成 Action 中的 User property。

Implement a type converter
Struts2 framework 中,所有的 OGNL Type Converter 都必須 implements TypeConverter interface,這個 interface 提供了 programmers 在任何兩種類型的 data 進行轉換,不過在 Web application 中,我們只需要在 String 與 Object 之間作轉換就可以了!所以,我們自訂的 Type Converter 就採用 Struts2 framework 定義的 StrutsTypeConverter。StrutsTypeConverter 是一個 Abstract class,我們的 Type Converter extends StrutsTypeConverter 之外,還需要實做出兩個 abstract methods:
public abstract Object convertFromString(Map context,
                      String[] values, Class toClass)
public abstract String convertToString(Map context, Object o)
這兩個 methods 從 method name 就可以很清楚的瞭解其目的,convertFromString 主要是將頁面上 String-based data 轉換成 Java type property;而 convertToString 就是將 Java type property 轉換成 String-based data。

Convert between Strings and User
接下來就整個重頭戲了!我們要實際的撰寫我們的 Type Converter:
public class UserTypeConverter extends StrutsTypeConverter
{
  @Override
  public Object convertFromString(Map context, String[] values, Class toClass)
  {
     String name = values[0];
     String password = values[1];
     User user = new User();
     user.setName(name);
     user.setPassword(password);
     return user;
  }
  @Override
  public String convertToString(Map context, Object o)
  {
     User user = (User) o;
     return "User:name="+user.getName()+", password="+user.getPassword();
  }
}

首先,就如我們之前提到的,我們的 Type Converter 要 extends StrutsTypeConverter class,然後實做出兩個 abstract methods。將字串轉換成物件的 method 中,我們主要是將使用者在頁面上輸入兩個字串,而這兩個字串會分別變成 User 物件中的 name 與 password!在這裡沒有什麼邏輯可言,因為我們只是要示範 Type Converter 罷了!反之,將物件轉換成字串時,我們就是將 User 物件顯示成一個字串,這就有點像是 toString() method 一樣,純粹將物件中的資訊轉換成字串!

Configure our type converter
撰寫完我們自訂的 Type Converter 之後,那我們要如何讓 Struts2 framework 知道呢?我們需要一個設定檔!在 Struts2 framework 中提供了兩種方式讓我們可以設定我們的 Type Converter :1) Property-specific 與 2) Global type converter。

1. Property-specific
這種方式的設定檔只能針對某個 Action 的某個 property 給予我們的 Type Converter!首先,我們要先建立一個設定檔,檔案的命名規則為:{ActionName}-conversion.properties。假設我們有一個 Action 為 UserTypeAction,此 Action 中當然要有 User property,然後我們就必須建立一個設定檔名為:UserTypeAction-conversion.properties,而這個 properties 檔案要跟 UserTypeAction 在同一個資料夾中!在設定檔中我們要撰寫如下:
user=silver8250.type_converter.UserTypeConverter

其中,先告知 Struts2 framework 我們要進行轉換的 property 為 user,然後在等號右邊給予我們的 Type Converter!這樣就完成了設定檔案!
這種設定檔到底有什麼好處呢?一開始你應該會很困惑,因為我們不可能為每一個 Action 中有使用 User property 都增加一個設定檔,我們一定是會使用等等會談到的 Global 設定檔!不過在某一種情況下會有用處!假設我們有兩個 Type Converters,第一個是 Global 的,所以我們採用 Global 的設定讓大多數的 property 都使用,不過如我們有某個 Action 的某個 property 要使用我們設計的第二個 Type Converter,這時候 property-specific 就派上用場了!因為 Struts2 framework 會先以 property-specific 設定優先!

2. Global type converter
另一種設定檔就是 Global 型態的 Type Converter,我們只需要一個設定檔為:xwork-conversion.properties 檔案,並且放在 WEB-INF/classes 之下就可以!內容如下:
silver8250.bean.User=silver8250.type_converter.UserTypeConverter

等號左邊是告訴 Struts2 framework 是哪一個 Object 要執行 Type Conversion,右邊則是要使用哪個 Type Converter 進行轉換!這樣就完成了 Global 的設定檔!大多數的情況下我們都是以 Global 設定檔為主!

Use our type converter
最後就是我們在頁面上要如何撰寫呢?其實沒有任何的差別!例如:我們的 Type Converter 是要讓頁面上的兩個字串轉換到 User 中的 name 與 password,所以我們在頁面上就要設計兩個 textfield 讓使用者輸入:
<s:form action="UserType" method="GET">
   <s:textfield name="user.name" label="User Name"></s:textfield>
   <s:textfield name="user.password" label="User Password"></s:textfield>
   <s:submit></s:submit>
</s:form>

你沒看錯!真的沒有差別!真的~

在這裡我們示範了要怎樣設計自己的 Type Converter,不過大多倏地行況下,我們不需要這樣作!

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 時也是很輕鬆容易的!

2009年4月14日 星期二

[SQL] SQL Server 中如何知道某個 table 是否存在

今天剛好碰到這樣的問題,以往我都是在既有的 tables 下對資料庫進行存取,不過因為系統中出現了暫存 table,所以我在每一次執行完成後就必須刪除(drop)該 table,也因為如此,我在 SQL 終究必須先判斷該 table 是否存在,如果存在就代表上次的作業沒有將此 table 刪除!
由於上述的原因,透過 Google 找到了相關的資源:

How do I determine if a table exists in a SQL Server database?


這樣的 SQL 撰寫起來蠻容易的:

IF EXISTS(
SELECT 1
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE='BASE TABLE'
AND TABLE_NAME='temp_table'
)
--如果存在在
DROP TABLE temp_table
ELSE
--如果不存在在

上述的 SQL 應該還蠻容易瞭解的,透過 IF...ELSE 敘述並且配合 EXISTS 語法就可以用來判斷存在性,整個 SQL 語法中的核心在於 SELECT 語句中,我們在 SQL Server 中是要透過 INFORMATION_SCHEMA 這個 system view 可以得知系統的中繼資料,透過 TABLES 就可以取得所有的 tables,然後我們在 WHERE 條件終將 TABLE_TYPE 設定為 BASE TABLE,這代表我們要查詢的是基本的 table,而不是 view;然後在告知 TABLE_NAME 為我們想要取得的 table 名稱,這樣就可以!至於為何是 SELECT 1 呢?這個數字 1 跟 C 語言中的 true 是類似的,因為在 SQL 中不存在 true 這樣的 boolean type,所以如果我們的 WHERE 條件成立,就會回傳 1 告知 EXISTS 為 true!

今天學到的東西,與大家分享之~

2009年4月8日 星期三

[Struts2] Introduce OGNL

Object-Graph Navigation Language(OGNL) 是 Struts2 framework 中很重要的一個 component 之一,OGNL 負責作 data transfer 與 type conversion 的工作,並且讓 programmers 可以很簡單的將網頁上的 String-based data 轉換成 Java type-based data。其實,我們在之前就已經使用過 OGNL 將資料由 ValueStack 中搬進跟搬出,而且使用的方式很簡單!
OGNL 並不是 Struts2 framework 中所特別發明的,而是被 Struts2 framework 所整合的一種技術,從 programmer 角度來看,OGNL 包含了兩種功能:1) expression language 與 2) type converter。

Expression Language
如果你有用過 JSP 中標準的 EL 的話,這裡的 expression language 你應該就會有點感覺,因為 OGNL 的撰寫方式跟 JSP 中的 EL 是很類似的!OGNL 的 expression language 一種連結 HTTP 端的 string-based data 與 Server 端 Javabean 的 Java type-based data,並且可以將這兩端的 data 作相互的轉換。讓我們回顧一個小小的範例:
<s:property value="user.age" />

這個 property tag 是之前我們就使用過的,我們的 OGNL 就是寫在 value attribute 中的 String,這個 OGNL 就是將目前 Action 中的 user property 取出,再進一步的取出 user 中的 age property,所以 user property 是另一個 Javabean。這個 OGNL 很簡單,但是 OGNL 也可以撰寫得很複雜,OGNL 本身有提供很多好用的 built-in 讓 programmers 可以很容易的存取 ValueStack 中的值!
這裡你可能會有一個問題:Struts2 framework 怎樣知道哪個 String 是屬於 OGNL 哪個 String 是真正的字串呢?其實這就是 Struts2 tag 本身的設定了,有些 tag 的某些 attributes 可以寫 OGNL,而有些只能是純字串,而 OGNL 為了讓 programmer 在純字串的 attribute 中可以撰寫,我們可以採用 %{ expression } 的方式,這樣就保證一定是 OGNL!所以我們也可以將上面的 OGNL 撰寫成這樣:
<s:property value="%{user.age}" />

不過要知道哪個 tag 的哪些 attributes 是可以 parse 或不能 parse OGNL 語法,我們就要參考 Struts2 framework 官方文件的 Tag reference,這份文件包含了所有的 Struts2 tags,我們在這裡不會詳談,不過你可以點進去各個 tag 看看該 attribute 的 type,例如:property tag 的 value attribute 屬於 Object,這就代表 value attribute 可以 parse OGNL 語法!

Type Converter
OGNL 不僅僅是一種 expression language,他還會幫我們將資料型態進行轉換,也就是扮演著 type converter 的角色。其實在上面的範例中我們就示範了 OGNL 的 type conversion。因為在 user class 中的 age property 是屬於 int 型態,我們透過 OGNL 存取 Action 中的這個值,OGNL 就會幫助我們將 int 型態的 property 轉換成 string-based data,因為在 HTTP 之下,所有的資料都會是字串而已,但是在 server 端的 Javabean 中卻可以有很多種型態的資料!所以,OGNL 將資料由 Javabean 搬移到網頁上或是由網頁上搬移到 Javabean 中都會進行 type conversion。這樣的功能無須 programmer 的插手,OGNL 會自動的偵測 data 要轉換的型態!
在 Struts2 framework 中有針對 OGNL 的 type conversion 機制提供了很多種內建的 converter,當然,Struts2 framework 也有讓 programmer 可以自行定義 convert er 的功能!不過那是之後我們才會在提到了!

由上面的敘述中我們瞭解了 OGNL 在 Struts2 framework 中所扮演的角色後,我們就要來描述一開始提到 OGNL 的另咦項中要的工作:Data transfer。也就是 OGNL 如何與 Struts2 framework 之間進行互動。讓我們先看看下圖:
寄件者 阿信 (Silver8250) 的冷泡咖啡館~

這張圖顯示了整個 data 如何在 OGNL 之下由 client 端到 server 端的進出!這裡所謂的 client 端就是圖中最下方的兩個 HTML 檔案,因為這最終還是使用者看到的格式。以下我們將這圖中的兩個部份分開介紹:1) Data in 與 2) Data out。

Data In
一開始使用者會先看到 index.html,當然,這個檔案是我們的 JSP 檔案,不過透過使用者的瀏覽器看到則是一些純粹的 HTML。然後使用者透過瀏覽器輸入資料後,就會將資料轉交給 Servlet,透過 HTTP 方式以 key value pair 方式傳送,就如圖中的 Servlet Request 的內容,Servlet Request 會將這些 key value pairs 轉交給 OGNL Expression Language and Type Converter 作處理,接下來就是 OGNL 的工作了!
首先,OGNL 會知道資料要去 ValueStack 中存取,並且 OGNL 會自動的知道要存取在 ValueStack 中的哪個 Action 物件,如同圖中的 HelloUser 這個 Action 物件。接下來就是 param interceptor 的工作了,我們在內建的 Interceptors 中提到 param interceptor,他會根據 OGNL expression 將資料與 ValueStack 中的 Action 物件作 mapping,並且找出該 data 最終要存放的位置。例如 param interceptor 會將圖中的 user.age 對應到 HelloUser 中 User 物件的 age property。
一旦 param interceptor 決定好該 data 的最後歸宿,OGNL 就會負責呼叫該 property 的 setter method,不過我們原始的資料是 string-based data,在 User 的 age property 確定 int 型態,這時候就會用到 OGNL 的 Type Converter,OGNL 會先使用內建的 converter 來試圖轉換型態,如果沒有才會用使用者自訂的 converter 來轉換,如果都沒有辦法轉換,則會拋出 Exception。最後 string-based 的 "24" 會被轉換成 Java type 的 int 型態的 24,並且儲存到 ValueStack 中。
在這裡我們不用知道太多關於 ValueStack 的內容,我們現在只要知道 ValueStack 就是一個 Stack 的容器,他會負責儲存所有的 Action 物件等等。如果有兩個物件都有 age property,則只會有一個 property 會被存取!

Data Out
至於另一半的作業其實也是很類似,只是方向相反罷了!當 Struts2 framework 接收到使用者的 request 後會執行我們之前所談到的 interceptor stack,並且執行 Action 中定義的 business logic,當產生了使用的 Result 頁面後,OGNL 就會開始啟動作業了!就如同圖中的 Welcome.jsp 頁面,裡面宣告了 OGNL expression:user.age,OGNL 就會根據 expression 到 ValueStack 中取得確實的值,並且將這些值轉型成 String 型態。最後就會變成使用者瀏覽器看到的 Welcome.html 的內容。

在這裡我們瞭解到 OGNL 在 Struts2 framework 中的主要工作以及其扮演的角色,並且瞭解到 OGNL 如何與 Struts2 framework 中的 component 互動的資訊。

2009年4月6日 星期一

[Struts2] 建立自訂的 Interceptor

我們已經介紹了 Struts2 framework 中很重要的 component 之一的 Interceptor 的重要特性,雖然 Struts2 framework 中已經有很多內建的 interceptors 幫助我們減少很多工作,但是有時候我們還是有自己的特殊需求,所以我們就必須自己撰寫我們的 interceptor。
根據我們之前在深入探討 Interceptor 中提過的,一個 interceptor 中的生命週期有三步驟:1) Preprocessing、2) ActionInvocation.invoke() 與 3) Postprocessing。現在我們就遵循這三個步驟來撰寫我們自己的 interceptor。
以下的範例目的在於判斷使用者是否有登入本系統,如果沒有登入或是 session 中沒有使用者登入的紀錄就將頁面回到首頁。我們將整個 web application 規劃如下:

首先是 interceptor package 中的 MyInterceptor,這個 class 就是我們的主角,這個 interceptor 就是要負責檢查使用者是否有登入系統。
另外我規劃了一個 interface,就是 aware package 中的 PrivateAware interface,這個 interface 只是一個標記用的 interface,用來標記某個 action 是否需要經過 interceptor 的檢查。
在 bean package 中的 User class 就如同之前的範例一樣,用來儲存使用者的資料,因為目前不是 focus 在資料儲存,所以不會將這些資料儲存在資料庫中。
在 login package 中的兩個 class:LoginAction 與 LogoutAction 主要是用來執行登入與登出的 action。這兩個 actions 負責對 session 進行存取,並更新使用者是否有登入的狀態。
最後就是 info package 中的 UserInformationAction class,主要是負責顯示使用者登入後的資訊,這也是我們用來測試如果使用者沒有登入系統而想要執行 UserInformation action,系統會自動將網頁轉跳至登入的頁面。

接下來我們就將這幾個部份深入探討。
首先我們先將我們整個範例所需要的程式先探討,接著才會探討我們今天的主角 MyInterceptor。一開始就是用來標記 action 是否需要進行檢查使用者登入狀態的 interface:
public interface PrivateAware
{
public static final String USER_SESSION = "user";
}

這個 interface 很簡單,沒有任何 method 需要實做,這就是為什麼他被稱為標記用的 interface。另外我們在 PrivateAware interface 中另外宣告了一個 constant USER_SESSION,這是用來儲存使用者資訊除存在 session 的字串。

接著就是登入跟登出的 action,我們的登入登出 actions 都會將使用者資訊儲存到 session 中,所以這兩個 actions 都會 implements SessionAware,這兩個 actions 的內容都很簡單我們就不贅述了:
public class LoginAction extends ActionSupport implements SessionAware
{
private User user;
private Map<String,Object> session;

public User getUser()
{
return user;
}

public void setUser(User user)
{
this.user = user;
}

@Override
public String execute() throws Exception
{
String result = SUCCESS;
if (this.user.getName().equals("Silver") && this.user.getPassword().equals("hi"))
{
this.session.put(PrivateAware.USER_SESSION, this.user);
}
else
{
this.session.remove(PrivateAware.USER_SESSION);
result = LOGIN;
}
return result;
}

@Override
public void setSession(Map session)
{
this.session = session;
}

}
public class LogoutAction extends ActionSupport implements SessionAware
{
private Map<String, Object> session;

@Override
public String execute() throws Exception
{
this.session.remove(PrivateAware.USER_SESSION);
return LOGIN;
}

@Override
public void setSession(Map session)
{
this.session = session;
}

}

接下來就是我們的重頭戲了!我們自己設計的 interceptor 都需要 implements Interceptor interface,並且要 implements intercept() method!以下就是我們的 MyInterceptor:
public class MyInterceptor implements Interceptor
{
@Override
public void destroy()
{
}
@Override
public void init()
{
}
@SuppressWarnings("unchecked")
@Override
public String intercept(ActionInvocation actionInvocation) throws Exception
{
String result = null;
//Proprocessing
if (actionInvocation.getAction() instanceof PrivateAware)
{
Map<String, Object> session = actionInvocation.getInvocationContext().getSession();
User user = (User) session.get(PrivateAware.USER_SESSION);
if (user == null)
{
return Action.LOGIN;
}
}
//ActionInvocation.invoke()
result = actionInvocation.invoke();
//Postprocessing
return result;
}

}

我們在我們的 preprocessing 階段就檢查現在使用者所呼叫的 action 是否有 implements PrivateAware,如果有我們才會進行檢查的作業。接下來我們就會檢查 session 有沒有使用者登入過得狀態,也就是查詢 PrivateAware.USER_SESSION 字串。如果沒有就回傳 LOGIN 的控制字串給 Struts2 framework,而這個控制字串是定義在 struts.xml 中的 global-result,也就是整個在使設定檔之下的 components 都可以共用的!接這就是呼叫 actionInvocation.invoke() method,利用 recursive 的方式來走訪整個 interceptor stack,當我們在 postprocessing 時,我們已經無法更改使用者接下來會看到的頁面了!因為在呼叫 invoke() method 之後,使用者的下一個頁面就已經被決定了!所以我們的程式要在 preprocessing 中執行判斷使用者登入狀況,而不能在 postprocessing 中判斷!

撰寫好 interceptor 之後,我們就要在設定檔中設定我們的 interceptor:
<package name="default" extends="struts-default">
<interceptors>
<interceptor name="authInterceptor" class="silver8250.interceptor.MyInterceptor" />
<interceptor-stack name="authorizationStack">
<interceptor-ref name="authInterceptor" />
<interceptor-ref name="defaultStack" />
</interceptor-stack>
</interceptors>
<default-interceptor-ref name="authorizationStack" />
<global-results>
<result name="login">/index.jsp</result>
</global-results>
<!-- 設定 actions -->
</package>

首先我們要先透過 interceptors tag 來宣告我們自己的 interceptor stack,接著我們就將我們的 MyInterceptor 宣告,並且設定我們的 interceptor-stack tag,因為我們的 package 是繼承 struts-default,所以我們在 interceptor-stack 中可以 reuse defaultStack。最後我們就設定我們的 default-interceptor-ref tag,將預設的 interceptor stack 更改為我們剛剛設定的 interceptor stack!

最後就是設定 global-result tag,當 interceptor 檢查到使用者沒有登入的話,就將頁面轉回到 index.jsp。

而我們的 UserInformationAction class 就是一個 action 並且 implements PrivateAware interface,來告知 MyInterceptor 要檢查此 action 在執行前使用者是否有登入!

在這裡我們簡單的介紹了如何撰寫我們自己的 interceptor,其實要撰寫這樣的 component 不難,也因為 interceptor 採用了 AOP(aspect-oriented programming) 的精神,讓我們撰寫的 interceptor 可以在 action 的生命週期中被 reuse!

2009年4月4日 星期六

[JUnit] 如何測試一個 private method

上次幫老師帶了一堂課後,那天同學有反應說關於測試的問題:要如何測試 private method 呢?這個問題好像有點難,不過如果我們利用 Java 提供的 reflection 機制就可以輕鬆的辦到!不過那也要會用才行囉!所以我就示範一下如何撰寫這樣的程式。
假設我們有一個程式如下:
public class HavePrivateMethod
{
private int add(int x, int y)
{
return x + y;
}
}

這個程式很簡單,可能你連用都不想用XD,不過因為是示範就別太計較了!反正就是有個 private method 為 add,parameter 與 return type 都是 int。以下就是我們的測試程式:

public class HavePrivateMethodTest
{
@Test
public void add() throws Exception
{
int x = 1;
int y = 2;
int expectedResult = 3;
/*
* 1.取得 instance
*/
HavePrivateMethod havePrivateMethodClass =
HavePrivateMethod.class.newInstance();
/*
* 2.取得我們要測試的 method
*/
Method addMethod =
havePrivateMethodClass.getClass().getDeclaredMethod(
"add",int.class,int.class);
/*
* 3.設定存取性
*/
addMethod.setAccessible(true);
/*
* 4.實際呼叫
*/
Object actualResult =
addMethod.invoke(havePrivateMethodClass, x, y);
addMethod.setAccessible(false);
Assert.assertEquals(expectedResult, actualResult);
}
}

總之,我們要先取得一個該 class 的 instance,再來就是設定我們要測試的 private method,要取得此 method 的 Method instance,由於我們測試的 private method 有兩個 primitive type 的 int,所以我們就用 int.class,而不是用 Integer.class 喔!因為這不一樣的~
取得 Method instance 後,我們就要先設定讓此 method 是可以被存取的,接下來才是呼叫(invoke) 此 method,呼叫後的回傳值一律都是 Object 型態,我們就透過 JUnit 提供的 assertEquals() method 來幫助我們檢查!
這樣就完成我們的目標囉~

NOTE:(2009-05-06)
在上面的範例程式中,我們採用 HavePrivateMethod.class.newInstance(); 來初始化被測試的物件,不過這樣的初始化動作只限定用在被測的 class 具備有 default constructor!如果我們的被測試 class 沒有 default constructor 的話,我們就只能用一般的初始化方式,也就是 new Object 囉~這樣在 method reflection 也是會有效的!
感謝學弟大頭跟 Fred 提出這樣的問題~