28 12月 2011

jQuery Plugin : DataTables (5)

前接jQuery Plugin : DataTables (4)

這裡開始的思路跟我早期的想法不一樣,主要原因是在DataTables call ajax 的時刻。
對我先前的BaseCRUD模式而言,在init時就會自DB取得第一頁的資料以供顯示。
如果透過DataTables來處理,就代表不需要在init階段處理這部份。
但無論如何,Search Form的存在還是必要的,DataTables第一次找回來的資料也必需符合Form初始化的選項;也就代表DataTables要能加入目前現有Form的欄位做為參數。

第一步還是先要訂個Form吧,並將submit改掉,改用button呼叫javascript。因為DataTables的聯絡對象是用“sAjaxSource“指定的,所以Form的action就沒有作用了。

	<form id="myform">
	<table>
		<tr><td>NAME:</td><td><input type="text" name="name" /></td></tr>
		<tr><td>ID:</td><td><input type="text" name="id" /></td></tr>
		<tr><td>PHONE:</td><td><input type="text" name="phone" /></td></tr>
		<tr><td colspan="2"><input id="btnSearch" type="button" value="Search" onclick="disTable.fnDraw();"/></td></tr>
	</table>
	</form>
至於disTable.fnDraw()就稍後再解釋。

第二步就要修改DataTables的呼叫,加入fnServerParams。

"fnServerParams": function ( aoData ) {
					$.merge(aoData, $("#myform").serializeArray());
				}
藉用jQuery.merge(),將Form裡的欄位資料加入DataTables ajax呼叫參數中。

第三步為了讓Button能觸發DataTables的更新,所以必需保留被DataTables enhanced 過的變數,然後以var_enhanced.fnDraw();來更新資料修改過的javascript 大約如下

	<script type="text/javascript">
		var disTable; //保留被DataTables enhanced 過的變數
		$().ready(function() {
			disTable = $("#sample").dataTable({
				"sPaginationType":"full_numbers",
				"bProcessing": true,
				"bServerSide":true,
				"sAjaxSource": '/jquery/DataTableServlet02',
				"fnServerParams": function ( aoData ) {
					$.merge(aoData, $("#myform").serializeArray());},
				"aoColumns": [
							  { "mDataProp": "id" },
				              { "mDataProp": "name" },
				              { "mDataProp": "phone" }
				          ]
			});
		});
	</script>

再來就是修改Search method用到的參數,與DataTables就無關了。

jQuery Plugin : DataTables (4)

前接jQuery Plugin : DataTables (3)

如果要讓DataTables完全聽命於Server端,無論大小時都要問過Server時,就要加入“bServerSide“來告訴DataTables。
只要設定“bServerSide“為true之後,無論是換頁、排序、搜尋,DataTables一定會與sAjaxSource聯絡來取得正確的結果。
Server與Client之間的溝通當然必需先講好各自要用的參數,請先參考Server-side processing,下列僅說明必要的部份

Sent to Server Side:

  • sEcho : Client端在每次呼叫sAjaxSource時會產生一個特定的sEcho做為驗証碼,Server端必需在JSON中回傳相同的值做為認證
  • iDisplayStart : 目前顯示的是第幾筆資料
  • iDisplayLength : 畫面上單次顯示的資料筆數

Reply from Server Sise:

  • sEcho : Client送來的驗証碼,直接回傳不要做改變
  • iTotalRecords : 未經過濾的資料總筆數
  • iTotalDisplayRecords : 經過濾的資料總筆數,但在大部份的使用上情形上幾乎同於iTotalRecords
  • aaData : 回傳的資料的JSON Array

所以稍為改一下javascript

	<script type="text/javascript">
		$().ready(function() {
			$("#sample").dataTable({
				"sPaginationType":"full_numbers", //顯示頁數而非僅有上下頁的按鈕
				"bProcessing": true, //在進行ajax呼叫時,會提示Processing,並防止Double submit
				"bServerSide":true,
				"sAjaxSource": '/jquery/DataTableServlet02',
				"aoColumns": [
				              { "mDataProp": "id" },
				              { "mDataProp": "name" },
				              { "mDataProp": "phone" }
				          ]
		});
		});
	</script>

Server這端可以自訂一個TO,再利用jackson這樣的library將TO轉為JSON字串。

public class DataTableTO<T> {
	private List<T> aaData;
	private String sEcho;
	private Integer iTotalRecords;
	private Integer iTotalDisplayRecords;
	//--省略getter/setter.
}
public static final String toJson(DataTableTO<?> dt) {
	ObjectMapper mapper = new ObjectMapper(); //do some registry
	return mapper.writeValueAsString(dt);
}

這樣還未竟全功,畢竟我們在做查詢時多半都是透過一個<Form>來進行。

jQuery Plugin : DataTables (3)

前接jQuery Plugin : DataTables (2)

經過前面的測試,雖然可以改變資料來源為動態而非固定值,但馬上遇到回傳格式的問題。
看看先前回傳的資料

{"aaData":[
	["A123","Adam","0910"],
	["B123","Bob","0819"],
	["C123","Child","0739"]
]}


這不就代表我Server端輸出時還要考慮property的順序?

 

所以當然要試著讓DataTables可以動態的指定每個column要顯示的property.
這就必需在兩邊都有改動。
Server端回傳的格式要改,像是下列這樣

{"aaData":[
	{"id":"A123","name":"Adam","phone":"0910"},
	{"id":"B123","name":"Bob","phone":"0819"},
	{"id":"C123","name":"Child","phone":"0739"}
]}

 

而在Client這端,也必需在呼叫datatable()時,以aoColumns + mDataProp 來設定每個column要顯示的property.

"aoColumns": [
			{ "mDataProp": "id" },
			{ "mDataProp": "name" },
			{ "mDataProp": "phone" }
			]

所以只要用下列的script來處理,就可以達到基本的使用需求

	<script type="text/javascript">
		$().ready(function() {
			$("#sample").dataTable({
				"sAjaxSource": './serverdata.txt',
				"aoColumns": [
				              { "mDataProp": "id" },
				              { "mDataProp": "name" },
				              { "mDataProp": "phone" }
				          ]
		});
		});
	</script>

 

再仔細確認看看,目前所有的操作仍是停留在Client端,接下來就要處理換頁時的Server端呼叫

jQuery Plugin : DataTables (2)

在我開發過的系統,沒有一個可以接受一次把所有資料撈出來後在Client上做分頁的。
所以Server端的回應模式一定要能被處理。

先理解DataTables做法的重點。

  1. 要自Server端取得資料,必需要有一個參數“sAjaxSource“指定資料的來源,資料來源可以是文字檔或是PHP,ASP,Servlet等程式的URL
  2. Server端回傳資料的格式,可以是JSON或XML,但以JSON為佳;回傳資料的property名稱要為aaData。
  3. DataTables會將回傳的資料更新table中<tbody></tbody>的部份。
  4. 自Server端取得資料的時機是在呼叫$("#tableId").dataTable();後,所以一開始的<tbody></tbody>內不需要有資料。

以純文字檔做為Server回傳資料的來源做個示範。
先將下列文字存為檔案serverdata.txt
{"aaData":[["A123","Adam","0910"],["B123","Bob","0819"],["C123","Child","0739"]]}

再建一個<tbody>為空的table

	<table id="sample" class="display">
		<thead>
			<tr>
				<th>ID</th>
				<th>Name</th>
				<th>Phone</th>
			</tr>
		</thead>
		<tbody>
		</tbody>
	</table>

最後呼叫DataTables的javascript method.

	<script type="text/javascript">
		$().ready(function() {
			$("#sample").dataTable({
				"sAjaxSource": './serverdata.txt'
		});
		});
	</script>

就可以看到下列的結果
Server Side Response(跟之前一樣才是....)

jQuery Plugin : DataTables (1)

 

去年曾經有幸,短時間加入了X稅局的案子,大部份使用的技術都還算熟悉,像是SpringMVC, JPA, Maven這些東西,比較少接觸的大概是像jquery的眾多plugin,DataTables是其中之一。由於我自已是有習慣的分頁查詢處理模式,在當下我並沒有很喜愛這個Plugin,因為在各方面比較之下,我覺得我常用的模式要寫的程式碼並不會比較多,而且對於我BaseCRUD的操作模式有所影響。所以在prototyping 時我是將我常用的模式硬加在DataTables之上。不過沒幾個星期我就脫離案子,後續也沒再深入瞭解。

不過前段時間又有些新想法,記起了這個jQuery Plugin,稍為試了下順便做些筆記。以下的東西可能會有錯誤的部份...

DataTables的網站請參考:DataTables

最基本的例子,針對目前Html中現有的table加工,讓table裡的資料變得可以被分頁、排序及查詢。

table要注意的部份
(1)應該要有id,或特定的標記,以供DataTables識別出要處理的對象
(2)要有thead,tbody,tfoot的分別,tfoot可有可無,但還是建議加上。
(3)css部份,請import Datatables 提供demo_table.css,並將class設定為"display"

最基本的格式就像下列這樣

	<table id="sample" class="display">
		<thead>
			<tr>
				<th>ID</th>
				<th>Name</th>
				<th>Phone</th>
			</tr>
		</thead>
		<tbody>
			<tr>
				<td>A12345</td>
				<td>Adam</td>
				<td>09301234</td>
			</tr>
			<tr>
				<td>B12345</td>
				<td>Bob</td>
				<td>09301234</td>
			</tr>
			<tr>
				<td>C12345</td>
				<td>Child</td>
				<td>09301234</td>
			</tr>
		</tbody>
		<tfoot>
			<tr>
				<th>ID</th>
				<th>Name</th>
				<th>Phone</th>
			</tr>
		</tfoot>
	</table>

 

 

再加上javascript

	<script type="text/javascript">
		$().ready(function() {
			$("#sample").dataTable();
		});
	</script>

然後就可以看到下列這樣的結果。
DataTables Demo

這種分頁的效果算是客戶端分頁,因為必需一次提供所有的資料,除非重新更新頁面,否則無論是換頁或搜尋,Datatables都不會與Server端有任何溝通。

這種模式當然也有適用的時候,但對於資料量較大的系統而言,應該是會去避免使用。

 

28 9月 2011

好用的Java Date Library -- Joda Time

Java裡提供的Calendar 與 Date在實用上實在有許多不方便的地方,這點我想有異議的人應該不多,所以大部份的開發團隊都會有一些DateUtils來補足常用的日期操作。

例如像是
int getCurrentMonth(Calendar date); #Java Calendar的月份是從0開始....
boolean isBetween(Date begin, Date end);
boolean isBefore(Date source, Date target);
Calendar addDay(Calendar source, int days); #日期計算
String format(SimpleDateFormat sdf); #轉換日期格式

總歸一下大約是有下列幾種的需求

  1. 基本資訊的取得,例如年,月,日,星期幾等資訊
  2. 格式轉換,包換顯示日期字串的格式與反向將字串轉為日期物件
  3. 日期間的比較
  4. 日期計算,例如減個2天,加個1個月

自已寫也不是什麼困難的事,比起寫出ThreadSafe 的Collection算是小菜了。
但這兩天發現了一個Library -- Joda Time (http://joda-time.sourceforge.net/index.html),試用後覺得真的不錯!(好像有點後知後覺)
在jsr310沒有進入JDK前,我想Joda Time是個不錯的選擇。

用maven的朋友可以利用下列設定將joda time加入project裡
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.0</version>
</dependency>

再介紹Joda Time的用法

基本上Joda Time的DateTime就同於Java的Calendar,所以建立instance的方法也近於Calendar
DateTime dt = new DateTime();
DateTime dt = new DateTime(2011, 9, 11, 12, 0, 0, 0);

基本資訊就以get的方式來取得
int month = dt.getMonthOfYear(); #這個月份比較符合一般的習慣
int year = dt.getYear();

日期的推算,就用plus(), minus()來操作,
dt.plusMonths(2);
dt.minusDays(1);

也可用Period或Duration的方式來做。
dt.plus(Duration.standardDays(1));
dt.minus(Period.months(2));

日期的比較就以isXXX()來操作
dt.isBefore(target);
dt.isAfter(target);

格式的轉換就以dt.toString(DateTimeFormatter);

順帶一提,joda time還提供了Hibernate與Jsp tag的支援,整體上算是相當方便的工具程式了。

 

 

 

26 7月 2011

Struts2 處理404錯誤的方法

一個Member問起這個問題,我覺得很不錯。

Error Code 404 這個問題說大不大,不過是使用者敲錯了URL,又不是Error Code 500代表我們的系統出現錯誤,何況使用者要敲什麼,我們哪管得著?我想這也沒錯啦,如果我在Google Map敲個'我家',然後還希望它給我什麼好看的是不是有點超過?(也許會出現全家便利商店?)

在Struts2中,一般要解決的404URL大約是兩類,一類是struts2 的.action,另一類就是一般的resource,像jpg、js等

第一類Action的方式很容易,在struts.xml中設定一個default的package,然後用個default-action-ref就可以解決

	<package name="default" extends="struts-default">
		<default-action-ref name="index"/>
		<action name="index">
			<result>/index.jsp</result>
		</action>
	</package>

第二類非Action的話就要在xml中加上error-page的設定

	<error-page>
		<error-code>404</error-code>
		<location>/rootAction.action</location>
	</error-page>
這類的處理要注意的是,如果是直接導到jsp或html就不用在意,但若是想導到一個action的話,請注意你的struts action filter,是不是有加上error 的dispatcher。
	<filter>
		<filter-name>struts2</filter-name>
		<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
		<!-- <init-param> <param-name>actionPackages</param-name> <param-value>com.mycompany.myapp.actions</param-value> 
			</init-param> -->
	</filter>
	<filter-mapping>
		<filter-name>struts2</filter-name>
		<url-pattern>*.action</url-pattern>
		<dispatcher>REQUEST</dispatcher>
		<dispatcher>FORWARD</dispatcher>
		<dispatcher>INCLUDE</dispatcher>
		<dispatcher>ERROR</dispatcher>
	</filter-mapping>

31 5月 2011

A500 首發

用blogger-droid寫blog大概就只能寫出plurk的內容吧
Published with Blogger-droid v1.6.9

27 4月 2011

Maven Android Plugin

 

主要是參考 sonatype的reference - Android Application Development with Maven

1. Maven android sdk deployer
Android SDK的安裝就不在這篇記錄了,直接跳到maven android sdk deployer安裝,先連到https://github.com/mosabua/maven-android-sdk-deployer/,按下Download後自行解壓縮。

Github - Maven android sdk deployer

 

進入解壓縮後的目錄執行

mvn install

正常的話可以看到類似下列的結果
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO] 
[INFO] Maven Android SDK Deployer ........................ SUCCESS [0.598s]
[INFO] Android Platforms ................................. SUCCESS [0.007s]
[INFO] Android Platform 1.5 API 3 ........................ SUCCESS [0.336s]
[INFO] Android Platform 1.6 API 4 ........................ SUCCESS [0.034s]
[INFO] Android Platform 2.1 API 7 ........................ SUCCESS [0.022s]
[INFO] Android Platform 2.2 API 8 ........................ SUCCESS [0.016s]
[INFO] Android Platform 2.3 API 9 ........................ SUCCESS [0.033s]
[INFO] Android Platform 2.3.3 API 10 ..................... SUCCESS [0.019s]
[INFO] Android Platform 3.0 API 11 ....................... SUCCESS [0.037s]
[INFO] Android Add-Ons ................................... SUCCESS [0.011s]
[INFO] Android Add-On Google Platform 1.5 API 3 .......... SUCCESS [0.075s]
[INFO] Android Add-On Google Platform 1.6 API 4 .......... SUCCESS [0.034s]
[INFO] Android Add-On Google Platform 2.1 API 7 .......... SUCCESS [0.021s]
[INFO] Android Add-On Google Platform 2.2 API 8 .......... SUCCESS [0.027s]
[INFO] Android Add-On Google Platform 2.3 API 9 .......... SUCCESS [0.032s]
[INFO] Android Add-On Google Platform 2.3.3 API 10 ....... SUCCESS [0.028s]
[INFO] Android Add-On Google Platform 3.0 API 11 ......... SUCCESS [0.022s]
[INFO] Android Extras .................................... SUCCESS [0.012s]
[INFO] Android Compatibility Extra V4 .................... SUCCESS [0.016s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.922s
[INFO] Finished at: Wed Apr 27 17:28:29 CST 2011
[INFO] Final Memory: 4M/528M
[INFO] ------------------------------------------------------------------------
如果有特定的Platform或Add-On失敗的話,請確認在安裝AVD時是否有把所有的platform與add-on裝上。
其實全裝才有點不正常吧....
所以也可以改用指定profile的方式安裝
mvn install -P 2.3.3
 
可用的profile有1.5, 1.6, 2.1, 2.2, 2.3, 2.3.3, 3.0,就看本身AVD有裝哪些囉

2. pom.xml修改

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <groupId>Best1</groupId>
 <artifactId>Best1</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <packaging>apk</packaging>
 <properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 </properties>

 

<build> <finalName>best</finalName> <sourceDirectory>src</sourceDirectory> <plugins> <plugin> <groupId>com.jayway.maven.plugins.android.generation2</groupId> <artifactId>maven-android-plugin</artifactId> <version>2.9.0-beta-4</version> <configuration> <sdk> <platform>10</platform> </sdk> <emulator> <avd>Android233</avd> <wait>10000</wait> <!--<options>-no-skin</options>--> </emulator> <deleteConflictingFiles>true</deleteConflictingFiles> </configuration> <extensions>true</extensions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.3.2</version> <configuration> <source>1.6</source> <target>1.6</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>com.google.android</groupId> <artifactId>android</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> </dependencies> </project>

再來就可以執行mvn android:emulator-start, mvn android:deploy來測試程式囉

 

OGNL note

Basic Syntax:
用“.“取得property,例如:user.name => user.getName();
最後加上“()“則代表呼叫method,例如:user.name.toString()
陣列的使用則是以"[]"來操作, users[0]

利用'@'來指定特定的Class、Method及Property,
例:呼叫static method @my.package.Utils@staticMethod(args)取得static property @my.package.Utils@myproperty

在OGNL的概念裡,所有變數都是global variable,但有一個特定的變數做為root
要取得變數以'#'來標示,如:#currentUser,
若是僅見到currentUser,代表是呼叫root.getCurrentUser()以取得root的currentUser

Constant:
String : 用''或““標示字串,{'name',"password"}
Character : 用''標示字元Number : 一般int, long, float, double不用特別標記,但可以用'b'或'B'標示BigDecimal
Boolean : 以true, false標示Null : 以null標示
Variable:利用#取值,可以用#this代表現有的物件也可以為variable設值,如#var1='adam';

Collection:
建立Object Array : {"name",'password'}建立指定型別的Array : new int[] {100,200,300}, new int[3] => new int[] {0,0,0}
建立Map : #{ "name":"adam", "password":"psadam" }建立指定型別的Map : #@java.util.HashMap@{ "name":"adam", "password":"psadam" }

Selecting:
可利用條件篩選Collection裡的物件,如 : users.{? #this.age > 18},代表只有age大於18的人才放入結果
還可以利用類似expression的'^'或'$'來取得第一個或最後一個符合條件的物件:users.{^ #this.age > 18}, users.{$ #this.age > 18}

Projection:
OGNL提供了Projection,也就是可以針對collection裡的物件呼叫相同的method或取得特定的property值,並將其置入另一個Collection中
如:users.{name},這代表將取出所有user的name。
由於OGNL也提供了#this,所以也可以見到這樣的用法 : users.{#this.age > 18 ? #this : null},代表只有age大於18的人才放入projection,不然就放入null

特殊property:

  • Collection:(1)size(2)isEmpty
  • List:(1)iterator
  • Map:(1)keys(2)values
  • Set:(1)iterator
  • Iterator:(1)next(2)hasNext
  • Enumeration:(1)next(2)hasNext(3)nextElement(4)hasMoreElements

容易混淆的Operator

  • Index : array[index]
  • Projection : collection.{exp}
  • Selection : collection.{? exp}

 

 

 

21 3月 2011

Spring Security (3) Basic Configuration fo <authentication-provider>

<authentication-manager>共本上提供了兩種Authentication Provider,一個是authentication-provider,另一個是ldap-authentication-provider

預設的authentication-provide就是採用DaoAuthenticationProvider,有三種基本的帳號密碼檢核與提供人員資料方式

  • user-service:建立一個in-memory的UserDetailService,帳密可以自properties file載入或在xml以<user>建立
  • jdbc-user-service:Spring Security提供了SQL Schema,所以相關的帳密可以自DB載入,只要給jdbc-user-service一個DataSource就可以使用,或是現有欄位定稱與Spring Security不同,那也可以透過提供users-by-username-query、authorities-by-username-query等SQL來取得資料。
  • ldap-user-service:再明顯不過了吧,透過ldap取得帳密資料,在預設的情形下是沒意義的....

通常DB裡存的密碼不會是明碼(如果真的是明碼....),所以Spring Security也提供了基本的編碼方式,像sha、md5都有支援,我們可以透過<password-encoder hash="type">來設定這個authentication provider要用哪種編碼;如果我們的編碼方式不在Spring Security的支援範圍之內,也可以自行提供一個PasswordEncoder的實作。

如果可以完全接受SpringSecurity提供的Schema作為系統的帳密設定,那當然是很不錯的一件事,但我想這種事不太容易發生...所以修改的方式通常有二,一是自已提供UserDetailsService的實作將user service換掉,二是用自已的Authentication Provider實作,直接把authentication provider換掉囉。不過換Authentication Provider的原因通常不是因為帳密的DB Schema不同,通常是因為檢核的方式不同而更換,看看Spring Security提供的其他Authentication Provider名稱就知道,像JaasAuthenticationProvider、OpenIDAuthenticationProvider、CasAuthenticaionProvider等就知道需要更換Authentication Provider大約是在什麼情形才需要更動。

 

 

20 3月 2011

Spring Security (2) Basic Configuration of <http>

看過先前的說明應該會有些困惑,我們應該先稍為拆解一下<http>
<http auto-config='true'> 代表了三個預設定設定

  • <form-login>
  • <http-basic>
  • <logout>

<form-login>代表要使用基本的Form-based authentication,但由於沒有指定登入的頁面,所以SpringSecurity會直接採用內建的Servlet產生登入的頁面,如果只是展示一下系統,當然沒問題,但真實在使用的系統應該沒辦法接受,所以直接加上<form-login login-page="/your_login.jsp" />這樣的設定就可以改用目前系統使用的登入頁面,但由於其他設定都沒動,所以form的欄位及action url仍有一定的要求,action仍必需是“/j_spring_security_check“,account的欄位名稱必需是“j_username“,password欄位名稱必需是“j_password“。
form-login還可以設定其他的參數,像login-processing-url代表action要送出的url,default-target-url代表登入成功後要轉的url,authentication-failure-url代表登入失敗後要轉入的url,always-use-default-target設定為true則代表登入成功或失敗都要轉入default-target-url。
比較有趣的是authentication-success-handler-ref與 authentication-failure-handler-ref,這兩個參數不應該與default-target-url、authentication-failure-url共用,這兩個handler-rul的設定代表啟用AuthenticationSuccessHandler與AuthenticationFailureHandler,如果想實現轉入登入前的那一頁用這個設定會很容易達成。

<http-basic>則代表使用HTTP basic authentication header,主要是將帳密用冒號(:)組合,再以base64編碼後送出,這個設定讓系統可以很容易地支援REST的程式。

<logout>代表起用Logout Filter,就SpringSecurity認為,每個系統都應該要有一個Logout Filter才對,比較form-login,當然也可以設定logout-url、logout-success-url及success-handler-rel,另一項是invalidate-session,若設定為true,在登出的同時就會令session失效。

 

18 3月 2011

Spring Security (1) Basic Configuration

一個對外的系統通常有權限設定,主要的需求通常就是兩個

  • 判斷目前的操作人員是誰
  • 人員是否可以進行這個操作

Spring Security提供了一個快速而有彈性的方法可以處理上述兩個需求,當然,有彈性通常也代表較多的設定與較複雜的設計....

我們先假設一個基本的網頁系統權限需求,除index.jsp外,其他的頁面存取都必需要是登入後才能看到,但/admin.jsp則必需是具有admin角色的人員才能看到。

web.xml

第一步當然是載入Spring的設定檔....
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath*:applicationContext.xml</param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
再來就是利用Spring的filter來檢查url及使用者,設定welcome file主要是讓url為"/"的request直接送到index.jsp
<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.jsp</welcome-file>
</welcome-file-list>

接下來就是spring security的設定了,先用最基本的方式,由Spring提供登入的頁面,我們只要提供哪些url需要被檢查,登入的帳號密碼資料即可。

<http auto-config='true'>
    <intercept-url pattern="/" filters="none" />
    <intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
    <intercept-url pattern="/**" access="ROLE_USER" />
</http>
<authentication-manager>
    <authentication-provider>
        <user-service>
            <user name="admin" password="admin" authorities="ROLE_USER, ROLE_ADMIN" />
            <user name="user" password="user" authorities="ROLE_USER" />
        </user-service>
    </authentication-provider>
</authentication-manager>
<http>的<intercept-url>指出哪些url需要被檢查,像"/admin.jsp"就要是具有"ROLE_ADMIN"角色的人才能進人,而除了“/“之外的資源全部需要登入後才能使用。
<user-service>則是提供了帳密及角色的資料。

接下來在browser上試著要進入系統,你會發現除了"/"之外,都會跳出一個登入頁面 Spring Login
可以試著用admin或user的帳號進入。  

這就是最基本的Spring Security 體驗....

17 3月 2011

Maven 同時使用Emma 與Surefire會執行兩次Unit test的解決方式

emma-maven-plugin的開發人員認為,被instrumented過的test class不等同於原來的test class,可能會造成原來test不會通過,但在emma處理後而可以通過,或是反過來的情形發生。所以在pom中如果同時加上emma-maven-plugin與maven-surefire-plugin,就會跑兩次的unit test。

如果可以接受被emma instrumented過的test class可以代表原來的test class,那解決的方式就還蠻容易的,簡單說, 就是讓surefire執行被instrumented過的test class,但是要求emma在test或site之類的phase不做事。
設定如下



	org.sonatype.maven.plugin
	emma-maven-plugin
	1.2
	
		
			instrument
			process-classes
			
				instrument
			
		
		
			test
			test
			
				true
			
		
		
			site
			site
			
				true
			
		
	



	org.apache.maven.plugins
	maven-surefire-plugin
	2.8
	true
	
	
		once
		xml
		${project.build.directory}/generated-classes/emma/classes
	


Report的部份不用特別修改


	
		
		
			org.sonatype.maven.plugin
			emma-maven-plugin
			1.2
		

		
			org.apache.maven.plugins
			maven-surefire-report-plugin
			2.8
		

14 3月 2011

偶有所感

方才看了 綿羊的譯心譯意 的新博文“大師出手,高下立見“,見到下面這段文字

“I am not bound to win, but I am bound to be true. I am not bound to succeed but I am bound to live up to what light I have. I must stand with anybody that stands right, stand with him while he is right and part with him when he goes wrong.“

我覺得失敗也要敗得有格調,但我現在配合的人似乎不在意格調的問題....
突然有點感慨我現在做的工作,又要找下一步了嗎?

 

07 3月 2011

全型 <->半型

嗯,非常無聊,寫了還是記錄一下

字串裡的全型字元與半型字元轉換

public abstract class StringUtils {
	/**
	 * 全型轉半型
	 * @param source
	 * @return
	 */
	public static String convertToHalfWidth(final String source) {
		if (null == source) {
			return null;
		}
		
		char[] charArray = source.toCharArray();
		for (int i = 0; i < charArray.length; i++) {
			int ic = (int) charArray[i];
			
			if (ic >= 65281 && ic <= 65374) {
				charArray[i] = (char) (ic - 65248);
			} else if (ic == 12288) {
				charArray[i] = (char) 32;
			}
			
		}
		return new String(charArray);
	}
	
	/**
	 * 半型轉全型
	 * @param source
	 * @return
	 */
	public static String convertToFullWidth(final String source) {
		if (null == source) {
			return null;
		}
		
		char[] charArray = source.toCharArray();
		for (int i = 0; i < charArray.length; i++) {
			int ic = (int) charArray[i];
			
			if (ic >= 33 && ic <= 126) {
				charArray[i] = (char) (ic + 65248);
			} else if (ic == 32) {
				charArray[i] = (char) 12288;
			}
			
		}
		return new String(charArray);
	}
}

25 2月 2011

Manage Logger Level via JMX

常用的Log library不外乎logback跟log4j,logback自備了JMX控制的功能,只要在logback.xml中加入

<configuration> 
	<jmxConfigurator />
</configuration>

lo4j沒有,但只要簡單地寫個小小Util就可以達成

@ManagedResource(objectName = "log4j:name=Lo4jLoggerManager", description = "log4j logger level manager")
public class Lo4jLoggerManager {
	@ManagedOperation
	@ManagedOperationParameters({@ManagedOperationParameter(name="loggerName", description=""), 
		@ManagedOperationParameter(name="loggerLevel", description="")})
	public void setLoggerLevel(String loggerName, String loggerLevel) {
		Logger logger = Logger.getLogger(loggerName);
		logger.setLevel(Level.toLevel(loggerLevel));
	}
	
	@ManagedOperation
	@ManagedOperationParameters({@ManagedOperationParameter(name="loggerName", description="")})
	public String getLoggerLevel(String loggerName) {
		Logger logger = Logger.getLogger(loggerName);
		Level level = logger.getEffectiveLevel();
		
		return null != level ? level.toString():Logger.getRootLogger().getLevel().toString();
	}
}

Startup MX4J HttpAdaptor with Spring

我用JMX用的並不多,多半而言就是拿來控制Logger 的Level,最多加個清Cache,其餘就沒什麼特別在意的。
控制的介面用jconsole多半還過得去,部份案子還是會使用mx4j的HttpAdaptor來建立Web的操作介面,只是在使用上,要自動帶起HttpAdaptor還是有些困擾。

先看看MX4J 的HttpAdaptor,

public void start() throws IOException {
    final Logger logger = getLogger();
    if (server != null) {
         serverSocket = createServerSocket();
         .....
    } else {
        if (logger.isEnabledFor(Logger.INFO)) logger.info("Start failed, no server target server has been set");
    }
}
呼叫start()後,HttpAdaptor就可以提供Web的操作介面。
不過HttpAdaptor一開始就會先確認MBeanServer "server" 的有無,如果沒有就會寫個Log然後結束。
而這個MBeanServer什麼時候會被加進來呢?
是依JMX規格MBeanRegistration,執行preRegister()後才會帶進來,所以如果想在Spring的設定檔裡,加個init-method="start",是無法讓這個Adaptor正常提供Web操作。

那就看看Spring MBeanExporter的原始碼是如何操作MBean的Lifecycle,看到MBeanExporterLinstener這Interface,所以我們可以透過這interface讓系統自動帶起HttpAdaptor;但實際上執行有點困難,因為Spring 2.5以後提供了一個context:mbean-server,可以直接取得系統中現有的MBeanServer或是自行建立一個,所以比較無從加入MBeanExporterLinstener到MBeanServer中。

最後想想,還是從MBeanRegistration這個介面下手,所以直接extend HttpAdaptor後override postRegister()....

 

public class AutostartHttpAdaptor extends HttpAdaptor {
 
 private static final Logger logger = LoggerFactory.getLogger(AutostartHttpAdaptor.class);
 @Override
 public void postRegister(Boolean registrationDone) {
  super.postRegister(registrationDone);
  
  if (!registrationDone) {
   logger.warn("HttpAdaptor should not be invoked start() without registration success.");
   return;
  }
  
  logger.info("Post Register HttpAdaptor('"+registrationDone+"'):");

  try {
   this.start();
  } catch (IOException e) {
   logger.error("HttpAdaptor startup has been failed.", e);
  }
 }

 @Override
 public void postDeregister() {
  super.postDeregister();
  logger.info("Post Deregister HttpAdaptor():");
  this.stop();
 }
 
}

05 2月 2011

Struts2 上 i18n的小技巧

最近被問到的小問題
網頁程式上我們經常需要產生一些表單上的選項,而選項內容也需要參考到i18n的properties裡的設定,
例如要顯示“是:否“兩個選項,最糟的寫法可以是
<select>
<option value="true"><s:text name="msg.true" /></option>
<option value="false"><s:text name="msg.false" /></option>
</select>
要簡單一點,就用ognl產生一個map或list
map 的做法
<s:select list="#{'true':getText('msg.true'),'false':getText('msg.false')}"></s:select>
list 的做法
<s:select list="{'true','false'}" listValue="%{getText('msg.'+toString())}"></s:select> 
不過利用getText()這個method來取得i18n的文字,就必需extends ActionSupport。

這樣也許還不是最理想的,我比較喜歡利用enum來做,像是“男:女“這種選項
<s:radio list="@idv.elliot.domain.Gender@values()" listValue="%{getText('enum.Gender.'+toString())}"></s:radio>
利用@可以指出enum的Class及values(),再利用getText()取得i18n的設定

%{getText('enum.Gender.'+toString())}是很方便的用法,相信大家一定會想到從Context中傳出其他的map或list,可以用這方式產生選項的應用方式。

21 1月 2011

第五個季節

"你知道我最喜歡哪個季節嗎?",妳一如往常無厘頭地問起問題;
我手邊正翻著雜誌,有一搭沒一搭地回應反問:“哪一個?“
“第五個季節啊!“,妳瞪大眼睛,一副不可思議地表情看著我。

“第五個季節?哪是什麼樣的季節?“,我趕緊放下雜誌回應,我瞭解如果我再一副漫不經心的回應,妳雖然不會像對其他人一樣往我胸口捶,但蹭一下腳擰一下腿恐怕是免不了的。
“我還沒有想清楚耶,你也幫我想一想吧。“,妳低頭又想了一下,“對了,那你幫我的第五個季節寫首詩吧?“,“呿,那是什麼我都不知道,還寫什麼詩?有機會再聯絡。“,我又開始翻起我的雜誌,“等妳想清楚了,我再考慮。“,沒多久我腳上就多了兩個瘀青

“那妳知道我最喜歡哪個季節嗎?“,我只好反問妳轉移你的注意力,開始揉著我剛剛得到的兩個瘀青。
妳轉頭想了想,“誰在乎啊?“,踢了下我椅子又轉身去鬧別人了。

當時我們都不知道,這是我們一起渡過的最後一個季節。
我永遠都不知道妳的第五個季節是什麼樣的季節,但我想,妳知道我最喜歡的季節。
是啊,每個有妳陪的季節,就是我最喜歡的季節。

19 1月 2011

Web Application Security

Web Application Security要做的通常不過就是確認目前使用者是誰,能不能做目前要做的動作,就算提到到SSO、OpenID這些東西,也不過是在確認使用者上多些手續而已。

近來面談的人,跟兩三年前不同,多半對Spring都有些瞭解,但再深入問些應用卻又讓我有點失望,Sercurity就是很常讓我不滿意的地方。
有些人還停留在Submit button的enable or disable,這種情形就比較糟糕,有些完全沒有意識到這樣做會有問題的地方,所以開個Firebug,將disabled拿掉,這些人才覺得這是個問題...再有些人認為用method="GET" or "POST"就能阻止這些問題,但要送一個http post request又有何難?

有些人會很快回覆說利用Filter控制,我個人也認為基本上沒有問題,但我不認為全部都在Filter裡處理完是個好方法,所以我通常會再問如何在Spring的AOP裡得知現在的使用者為何,部份的人會說要修改API,將使用者當做參數之一,或是說傳入HttpSession,但沒有人跟我說過:因為是Web Application,所以可以利用ThreadLocal,如果是其他類型的Applicaton,利用InheritableThreadLocal也可以達成。

將使用者資料透過Filter,自Session中取得放入ThreadLocal中,這做法已經用了好幾年,我想這不是一個很具獨特性的做法,因為像是Spring Security已經到了3.X版,而它最重要的存放使用者的方式就是在放ThreadLocal中;能夠取到使用者的資料,那要在Filter、Service Layer或AOP中要進行控管或記錄都不會是問題,端看設計者的需要。

寫到這又有些擔心,擔心自己是不是自我意識過於良好,不過管他的! 我就是覺得目前看來,不把使用者資料放在ThreadLocal中就是一種設計上的缺陷!

jquerywtp : jQuery Editor Plugin for Eclipse

在Eclipse寫JavaScript比較麻煩的的是缺少API提示的工具,當然用其他IDE像是Spket也不算太差,但總是麻煩了些。

透過Goolge找到了jquerywtp這個Eclipse Plugin,雖然沒辦法配合最新的jQuery版本,但也總算提供了一個較佳的解決方式,必要時也可以修改libraries裡的js檔以符合個人需求。

jquerywtp是透過修改wtp裡的jar檔來達成,與其他一般的Eclipse Plugin不同,流程如下:

先至sourceforge download 最新的jar檔
我本身的Eclipse 是JEE 版3.6.1,可用最新的檔案連結在下:
jqueryWTP0.40foEn.jar

下載完成後 透過command line或直接連擊jqueryWTP0.40foEn.jar
會帶出下列conosle
FileChooserDemo.png

選取$Eclipse_Home/plugins的org.eclipse.wst.jsdt.core_XXX.jar (XXX會因使用WTP的版本而不同),然後選取輸出目錄按下"Generate",在輸出目錄下就會產生同名的jar檔,手動覆寫$Eclipse_Home/plugins下的檔案,重起Eclipse 即可,若發現沒有作用,可以以command line方式執行eclipse -clean清除plugin相關的cache即可。

在一般JSP或HTML裡就可以有jQuery相關API的提示囉

Eclipse.png

 

18 1月 2011

git ignore

  • (1)以#起始的該行資料會被當做註解
  • (2)使用Glog Pattern
    Glog patterns 相近於常見的Regular Expressions, 但是較為簡單,由於git在ignore裡使用的是glob patterns,所以還是需要稍為瞭解一下。
    • ?:代表任意的一個字元
    • *:代表任意數目的字元
    • {!ab}:必需不合於此pattern
    • {ab,bb,cx}:代表合於ab,bb,cx之一種pattern即可
    • [abc]:代表合於a,b,c中任一字元即可
    • [^abc]:代表必需不合於a,b,c中任一字元
  • (3)若要指出目錄請以"/"表示

 

下列是我常用的.gitignore內容

#Mac OSX Finder
.DS_Store
#Maven Build Folder : Target
target/
#Temp Folder
tmp/
#Log Folder or Files
log/
*.log

有些小地方比較有趣
例如想忽略repository目錄下的特定檔案,可以這樣表示
/eclipse.config
如果用了log/*.log,代表你是要忽略log目錄下,第一層檔名結尾為.log的檔案,但是/log/2010/11/12/ap.log就會被加入到repository中。
所以要忽略log目錄下包含子目錄的所有.log檔案,要這樣表示
log/**/*.log

不過如果要忽略特定目錄下的所有檔案,像是log目錄中的所有檔案,
雖然可以用這樣來表示
log/**/* (log本身這個目錄仍會被加入repository)
不過用
log/ (log本身這個目錄不會被加入repository)
會比較簡潔.... 
雖然意義上仍有不同

不過Git有些特性讓我比較困擾的,特別是空目錄不會被加到repository中,這點在最初設定時就會有些麻煩....

03 1月 2011

Subverion and Redmine Integration

Redmine與SCM結合有2個明顯的優點

1.列出目前SCM Repository資料,以subversion來說,subversion並未如github提供一個友善的介面,透過browser雖然可以看到檔案名稱及內容,但其他資訊如由誰commit或是附加的commet都必需使用其他軟體才能看到,Redmine與SCM Repository結合後就比較方便取得這些資訊。

2.將issue與commit結合,只要在commit message裡加上keyword+#+issueId,就可以在Redmine裡取透過issueId看到相關commit的程式,也可以反向透過commit message找到對應的issue;這個對於承接舊案的人員實在相當方便,因為常常在trace source code時比較容易瞭解前因後果。

Redmine add SCM Repository

(1)Adding Repository into project setting:

選取正確的SCM類型,輸入URL與可以檢視repository的帳號與密碼

Redmine Setting

設定正常的話就可以見到Repository顯示方才加入的SCM資料

svn repository

 

(2)commit message with issueId

在commet裡加上 refs, references, issueId, fixes, closes等keyword,並在issueId前加上#字符號,這樣redmine才知道如何去mapping現有issue

 

commit message

 

如此在issue裡就能看到相關的commit資訊

issue commit info

 

點下去後就就道commit了什麼東西

commit info