- 具convention
- 可configuration (例如可選擇用field或是method來mapping)
- 支援generic types
- 使用Embedded Domain Specific Language (EDSL) 自訂mapping規則
- High performance (http://code.google.com/p/modelmapper/wiki/Performance)
2013年1月31日 星期四
ModelMapper
我會選用ModelMapper有以下幾個特點:
Unit Test to Struts2 Action with Spring
我將展示使用JUnit3和JUnit4兩種測試Struts2 Action的寫法,兩種都會enable Spring以及所有Struts2 plugins。
JUnit3:
JUnit4:
JUnit3:
TestActionJUnit3Test
import static org.apache.commons.lang.StringUtils.replace;
import lombok.SneakyThrows;
import org.apache.struts2.StrutsSpringTestCase;
import com.opensymphony.xwork2.ActionProxy;
public class TestActionJUnit3Test extends StrutsSpringTestCase {
@SneakyThrows
public void test() {
ActionProxy proxy = getActionProxy("/test/test.action");
String result = proxy.execute();
assertEquals("success", result);
assertEquals("Test by Linus", ((TestAction) proxy.getAction()).getMessage());
executeAction("/test/test");
assertFalse(((TestAction) findValueAfterExecute("action")).hasFieldErrors());
assertEquals("Test by Linus", findValueAfterExecute("message"));
}
@Override
protected String[] getContextLocations() {
return new String[] { "classpath:" + replace(getClass().getName(), ".", "/") + "-context.xml" };
}
}
上面我將預設讀取的Spring XML路徑改成和@ContextConfiguration一致。import lombok.SneakyThrows;
import org.apache.struts2.StrutsSpringTestCase;
import com.opensymphony.xwork2.ActionProxy;
public class TestActionJUnit3Test extends StrutsSpringTestCase {
@SneakyThrows
public void test() {
ActionProxy proxy = getActionProxy("/test/test.action");
String result = proxy.execute();
assertEquals("success", result);
assertEquals("Test by Linus", ((TestAction) proxy.getAction()).getMessage());
executeAction("/test/test");
assertFalse(((TestAction) findValueAfterExecute("action")).hasFieldErrors());
assertEquals("Test by Linus", findValueAfterExecute("message"));
}
@Override
protected String[] getContextLocations() {
return new String[] { "classpath:" + replace(getClass().getName(), ".", "/") + "-context.xml" };
}
}
JUnit4:
TestActionJUnit4Test
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import org.apache.struts2.StrutsSpringJUnit4TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.opensymphony.xwork2.ActionProxy;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class TestActionJUnit4Test extends StrutsSpringJUnit4TestCase<TestAction> {
@Override
protected String getConfigPath() {
return "struts-plugin.xml";
}
@Test
public void testExecute() throws Exception {
ActionProxy proxy = getActionProxy("/test/test.action");
String result = proxy.execute();
assertEquals("success", result);
assertEquals("Test by Linus", ((TestAction) proxy.getAction()).getMessage());
executeAction("/test/test");
assertFalse(getAction().hasFieldErrors());
assertEquals("Test by Linus", findValueAfterExecute("message"));
}
}
上面getConfigPath()可以回傳其它Struts2的XML檔,利用這個method可以啟動其它plugins,但JUnit3的寫法不需要這個動作。
import static org.junit.Assert.assertFalse;
import org.apache.struts2.StrutsSpringJUnit4TestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.opensymphony.xwork2.ActionProxy;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class TestActionJUnit4Test extends StrutsSpringJUnit4TestCase<TestAction> {
@Override
protected String getConfigPath() {
return "struts-plugin.xml";
}
@Test
public void testExecute() throws Exception {
ActionProxy proxy = getActionProxy("/test/test.action");
String result = proxy.execute();
assertEquals("success", result);
assertEquals("Test by Linus", ((TestAction) proxy.getAction()).getMessage());
executeAction("/test/test");
assertFalse(getAction().hasFieldErrors());
assertEquals("Test by Linus", findValueAfterExecute("message"));
}
}
ISO8601 and User Timezone in JavaScript
以下程式碼主要是參考:
除了Date.prototype.setISO8601之外我都改寫過。
Date.prototype.setISO8601
Date.prototype.setISO8601 = function(string) {
var regexp = "([0-9]{4})(-([0-9]{2})(-([0-9]{2})"
+ "(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?"
+ "(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?";
var d = string.match(new RegExp(regexp));
var offset = 0;
var date = new Date(d[1], 0, 1);
if (d[3]) {
date.setMonth(d[3] - 1);
}
if (d[5]) {
date.setDate(d[5]);
}
if (d[7]) {
date.setHours(d[7]);
}
if (d[8]) {
date.setMinutes(d[8]);
}
if (d[10]) {
date.setSeconds(d[10]);
}
if (d[12]) {
date.setMilliseconds(Number("0." + d[12]) * 1000);
}
if (d[14]) {
offset = (Number(d[16]) * 60) + Number(d[17]);
offset *= ((d[15] == '-') ? 1 : -1);
}
offset -= date.getTimezoneOffset();
time = (Number(date) + (offset * 60 * 1000));
this.setTime(Number(time));
};
var regexp = "([0-9]{4})(-([0-9]{2})(-([0-9]{2})"
+ "(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?"
+ "(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?";
var d = string.match(new RegExp(regexp));
var offset = 0;
var date = new Date(d[1], 0, 1);
if (d[3]) {
date.setMonth(d[3] - 1);
}
if (d[5]) {
date.setDate(d[5]);
}
if (d[7]) {
date.setHours(d[7]);
}
if (d[8]) {
date.setMinutes(d[8]);
}
if (d[10]) {
date.setSeconds(d[10]);
}
if (d[12]) {
date.setMilliseconds(Number("0." + d[12]) * 1000);
}
if (d[14]) {
offset = (Number(d[16]) * 60) + Number(d[17]);
offset *= ((d[15] == '-') ? 1 : -1);
}
offset -= date.getTimezoneOffset();
time = (Number(date) + (offset * 60 * 1000));
this.setTime(Number(time));
};
Date.prototype.getISO8601
Date.prototype.getISO8601 = function() {
return this.getFullYear() + '-' + (this.getMonth() + 1).format(2) + '-'
+ this.getDate().format(2) + 'T' + this.getHours().format(2) + ':'
+ this.getMinutes().format(2) + ':' + this.getSeconds().format(2)
+ '.' + this.getMilliseconds().format(3) + this.getTimezone();
};
return this.getFullYear() + '-' + (this.getMonth() + 1).format(2) + '-'
+ this.getDate().format(2) + 'T' + this.getHours().format(2) + ':'
+ this.getMinutes().format(2) + ':' + this.getSeconds().format(2)
+ '.' + this.getMilliseconds().format(3) + this.getTimezone();
};
Date.prototype.getTimezone
Date.prototype.getTimezone = function() {
var offset = -this.getTimezoneOffset();
if (offset == 0) {
return "Z";
}
var hour = parseInt(Math.abs(offset / 60)).format(2);
var minute = Math.abs(offset % 60).format(2)
var sign = offset > 0 ? "+" : "-";
return sign + hour + ":" + minute;
};
var offset = -this.getTimezoneOffset();
if (offset == 0) {
return "Z";
}
var hour = parseInt(Math.abs(offset / 60)).format(2);
var minute = Math.abs(offset % 60).format(2)
var sign = offset > 0 ? "+" : "-";
return sign + hour + ":" + minute;
};
Number.prototype.format
Number.prototype.format = function(length) {
var str = "" + this;
while (str.length < length) {
str = '0' + str;
}
return str;
};
var str = "" + this;
while (str.length < length) {
str = '0' + str;
}
return str;
};
Test Case
var date = new Date();
date.setISO8601('2012-01-30T16:07:03.007Z');
print(date.getISO8601());
date.setISO8601('2012-01-30T16:07:03.012+08:00');
print(date.getISO8601());
var today = new Date();
print(today.getISO8601());
date.setISO8601('2012-01-30T16:07:03.007Z');
print(date.getISO8601());
date.setISO8601('2012-01-30T16:07:03.012+08:00');
print(date.getISO8601());
var today = new Date();
print(today.getISO8601());
Test Result
2013年1月30日 星期三
DBeaver - Universal Database Manager
DBeaver是一款好用的Java資料庫管理工具,有standalone version也有Eclipse plugin,速度上有不錯的表現,支援絕大多數資料庫,最酷的是它不需要我們提供JDBC Driver,而是可以選擇自動下載,非常方便。
ER Diagram也只要勾選table就可以了,產出速度也是不錯。
官方screenshots:http://dbeaver.jkiss.org/screenshots/
ER Diagram也只要勾選table就可以了,產出速度也是不錯。
官方screenshots:http://dbeaver.jkiss.org/screenshots/
Tuning Artifactory
通常我們會架設內部的Maven repository當做proxy來節省頻寬並加快下載速度,另一方面也將包好的jar / war上傳到repository讓大家使用,這邊記錄的是架設好Artifactory之後應該要調整的參數。
- Custom URL Base:自動產生Maven Settings裡的URL才會正確。
- Remote Repository Cache:預設是永久保留,但是太久沒使用的舊版library應該被清除。
- Max Unique Snapshots:上傳的snapshot版本其實只需要最新的一份,放太多只是浪費空間,預設也是無限制。
Java Scripting API
JDK在1.6版之後新增了Scripting API,預設已經內含著名的JavaScript Engine Rhino,所以我們不需要再額外下載Rhino就可以在Java內部執行JavaScript。
Test Case:
import static org.junit.Assert.assertEquals;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import org.junit.Test;
public class JavaScriptEngineFactoryTest {
@Test
public void test() throws ScriptException {
ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
engine.put("x", 10);
engine.put("y", 20);
assertEquals(30.0, engine.eval("x + y"));
}
}
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import org.junit.Test;
public class JavaScriptEngineFactoryTest {
@Test
public void test() throws ScriptException {
ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
engine.put("x", 10);
engine.put("y", 20);
assertEquals(30.0, engine.eval("x + y"));
}
}
2013年1月29日 星期二
Data Driven Testing by JUnit Theories
JUnit Theories是另外一種實現Data Driven Testing的方式,它的強項在於它會自動排列組合所有@DataPoint和@DataPoints的data傳入test method裡面,然後我們可以搭配Assume過濾掉一些不合理的情境。
以下是一個使用Theories的test case:
以下是一個使用Theories的test case:
IdentificationNumberValidatorTheoriesTest
package com.gss.gmo.cao.validator.constraints.impl;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import java.util.ArrayList;
import java.util.Collection;
import org.junit.BeforeClass;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
@RunWith(Theories.class)
public class IdentificationNumberValidatorTheoriesTest {
@DataPoints
public static DataBean[] identificationNumber;
@BeforeClass
public static void prepareData() {
Collection<DataBean> data = new ArrayList<DataBean>();
data.add(new DataBean(true, null));
data.add(new DataBean(false, ""));
data.add(new DataBean(false, "1qazxsw2"));
data.add(new DataBean(true, "H120178472"));
data.add(new DataBean(true, "h120178472"));
data.add(new DataBean(false, "h120178470"));
identificationNumber = data.toArray(new DataBean[] {});
}
private IdentificationNumberValidator identificationNumberValidator = new IdentificationNumberValidator();
@Theory
public void testTrue(DataBean data) {
assumeTrue(data.isResult());
assertTrue(identificationNumberValidator.isValid(data.getIdentificationNumber(), null));
}
@Theory
public void testFalse(DataBean data) {
assumeFalse(data.isResult());
assertFalse(identificationNumberValidator.isValid(data.getIdentificationNumber(), null));
}
static class DataBean {
private final boolean result;
private final String identificationNumber;
DataBean(boolean result, String identificationNumber) {
this.result = result;
this.identificationNumber = identificationNumber;
}
public boolean isResult() {
return result;
}
public String getIdentificationNumber() {
return identificationNumber;
}
}
}
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
import java.util.ArrayList;
import java.util.Collection;
import org.junit.BeforeClass;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
@RunWith(Theories.class)
public class IdentificationNumberValidatorTheoriesTest {
@DataPoints
public static DataBean[] identificationNumber;
@BeforeClass
public static void prepareData() {
Collection<DataBean> data = new ArrayList<DataBean>();
data.add(new DataBean(true, null));
data.add(new DataBean(false, ""));
data.add(new DataBean(false, "1qazxsw2"));
data.add(new DataBean(true, "H120178472"));
data.add(new DataBean(true, "h120178472"));
data.add(new DataBean(false, "h120178470"));
identificationNumber = data.toArray(new DataBean[] {});
}
private IdentificationNumberValidator identificationNumberValidator = new IdentificationNumberValidator();
@Theory
public void testTrue(DataBean data) {
assumeTrue(data.isResult());
assertTrue(identificationNumberValidator.isValid(data.getIdentificationNumber(), null));
}
@Theory
public void testFalse(DataBean data) {
assumeFalse(data.isResult());
assertFalse(identificationNumberValidator.isValid(data.getIdentificationNumber(), null));
}
static class DataBean {
private final boolean result;
private final String identificationNumber;
DataBean(boolean result, String identificationNumber) {
this.result = result;
this.identificationNumber = identificationNumber;
}
public boolean isResult() {
return result;
}
public String getIdentificationNumber() {
return identificationNumber;
}
}
}
Data Driven Testing by JUnit Parameterized Tests
本文直接展示兩種unit test的寫法來比較JUnit Parameterized Tests的好處在哪。
首先是傳統的寫法:
接下來我們利用JUnit Parameterized Tests的寫法改寫一次這個unit test:
須注意的是@Parameters可以下name attribute是4.11版之後才支援。
UnifiedBusinessNumberValidatorTest
package com.gss.gmo.cao.validator.constraints.impl;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class UnifiedBusinessNumberValidatorTest {
@Test
public void testIsValid() {
UnifiedBusinessNumberValidator unifiedBusinessNumberValidator = new UnifiedBusinessNumberValidator();
assertTrue(unifiedBusinessNumberValidator.isValid(null, null));
assertFalse(unifiedBusinessNumberValidator.isValid("", null));
assertFalse(unifiedBusinessNumberValidator.isValid("12345678", null));
assertTrue(unifiedBusinessNumberValidator.isValid("00651474", null));
assertFalse(unifiedBusinessNumberValidator.isValid("00651479", null));
assertTrue(unifiedBusinessNumberValidator.isValid("22425662", null));
assertFalse(unifiedBusinessNumberValidator.isValid("22425669", null));
assertTrue(unifiedBusinessNumberValidator.isValid("28706210", null));
assertTrue(unifiedBusinessNumberValidator.isValid("53112454", null));
assertTrue(unifiedBusinessNumberValidator.isValid("04607774", null));
assertTrue(unifiedBusinessNumberValidator.isValid("22446771", null));
assertTrue(unifiedBusinessNumberValidator.isValid("22515072", null));
assertTrue(unifiedBusinessNumberValidator.isValid("22595770", null));
assertTrue(unifiedBusinessNumberValidator.isValid("22227375", null));
assertTrue(unifiedBusinessNumberValidator.isValid("07006628", null));
}
}
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class UnifiedBusinessNumberValidatorTest {
@Test
public void testIsValid() {
UnifiedBusinessNumberValidator unifiedBusinessNumberValidator = new UnifiedBusinessNumberValidator();
assertTrue(unifiedBusinessNumberValidator.isValid(null, null));
assertFalse(unifiedBusinessNumberValidator.isValid("", null));
assertFalse(unifiedBusinessNumberValidator.isValid("12345678", null));
assertTrue(unifiedBusinessNumberValidator.isValid("00651474", null));
assertFalse(unifiedBusinessNumberValidator.isValid("00651479", null));
assertTrue(unifiedBusinessNumberValidator.isValid("22425662", null));
assertFalse(unifiedBusinessNumberValidator.isValid("22425669", null));
assertTrue(unifiedBusinessNumberValidator.isValid("28706210", null));
assertTrue(unifiedBusinessNumberValidator.isValid("53112454", null));
assertTrue(unifiedBusinessNumberValidator.isValid("04607774", null));
assertTrue(unifiedBusinessNumberValidator.isValid("22446771", null));
assertTrue(unifiedBusinessNumberValidator.isValid("22515072", null));
assertTrue(unifiedBusinessNumberValidator.isValid("22595770", null));
assertTrue(unifiedBusinessNumberValidator.isValid("22227375", null));
assertTrue(unifiedBusinessNumberValidator.isValid("07006628", null));
}
}
執行結果:
接下來我們利用JUnit Parameterized Tests的寫法改寫一次這個unit test:
UnifiedBusinessNumberValidatorParameterizedTest
package com.gss.gmo.cao.validator.constraints.impl;
import static org.junit.Assert.assertEquals;
import java.util.ArrayList;
import java.util.Collection;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
/**
* @author linus_chien
*
*/
@RunWith(value = Parameterized.class)
public class UnifiedBusinessNumberValidatorParameterizedTest {
@Parameters(name = "[{index}]: [{1}] is valid? [{0}]")
public static Collection<Object[]> data() {
Collection<Object[]> data = new ArrayList<Object[]>();
data.add(new Object[] { Boolean.TRUE, null });
data.add(new Object[] { Boolean.FALSE, "" });
data.add(new Object[] { Boolean.FALSE, "12345678" });
data.add(new Object[] { Boolean.TRUE, "00651474" });
data.add(new Object[] { Boolean.FALSE, "00651479" });
data.add(new Object[] { Boolean.TRUE, "22425662" });
data.add(new Object[] { Boolean.FALSE, "22425669" });
data.add(new Object[] { Boolean.TRUE, "28706210" });
data.add(new Object[] { Boolean.TRUE, "53112454" });
data.add(new Object[] { Boolean.TRUE, "04607774" });
data.add(new Object[] { Boolean.TRUE, "22446771" });
data.add(new Object[] { Boolean.TRUE, "22515072" });
data.add(new Object[] { Boolean.TRUE, "22595770" });
data.add(new Object[] { Boolean.TRUE, "22227375" });
data.add(new Object[] { Boolean.TRUE, "07006628" });
return data;
}
private final boolean result;
private final String unifiedBusinessNumber;
public UnifiedBusinessNumberValidatorParameterizedTest(Boolean result, String unifiedBusinessNumber) {
this.result = result;
this.unifiedBusinessNumber = unifiedBusinessNumber;
}
@Test
public void test() {
UnifiedBusinessNumberValidator unifiedBusinessNumberValidator = new UnifiedBusinessNumberValidator();
assertEquals(result, unifiedBusinessNumberValidator.isValid(unifiedBusinessNumber, null));
}
}
import static org.junit.Assert.assertEquals;
import java.util.ArrayList;
import java.util.Collection;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
/**
* @author linus_chien
*
*/
@RunWith(value = Parameterized.class)
public class UnifiedBusinessNumberValidatorParameterizedTest {
@Parameters(name = "[{index}]: [{1}] is valid? [{0}]")
public static Collection<Object[]> data() {
Collection<Object[]> data = new ArrayList<Object[]>();
data.add(new Object[] { Boolean.TRUE, null });
data.add(new Object[] { Boolean.FALSE, "" });
data.add(new Object[] { Boolean.FALSE, "12345678" });
data.add(new Object[] { Boolean.TRUE, "00651474" });
data.add(new Object[] { Boolean.FALSE, "00651479" });
data.add(new Object[] { Boolean.TRUE, "22425662" });
data.add(new Object[] { Boolean.FALSE, "22425669" });
data.add(new Object[] { Boolean.TRUE, "28706210" });
data.add(new Object[] { Boolean.TRUE, "53112454" });
data.add(new Object[] { Boolean.TRUE, "04607774" });
data.add(new Object[] { Boolean.TRUE, "22446771" });
data.add(new Object[] { Boolean.TRUE, "22515072" });
data.add(new Object[] { Boolean.TRUE, "22595770" });
data.add(new Object[] { Boolean.TRUE, "22227375" });
data.add(new Object[] { Boolean.TRUE, "07006628" });
return data;
}
private final boolean result;
private final String unifiedBusinessNumber;
public UnifiedBusinessNumberValidatorParameterizedTest(Boolean result, String unifiedBusinessNumber) {
this.result = result;
this.unifiedBusinessNumber = unifiedBusinessNumber;
}
@Test
public void test() {
UnifiedBusinessNumberValidator unifiedBusinessNumberValidator = new UnifiedBusinessNumberValidator();
assertEquals(result, unifiedBusinessNumberValidator.isValid(unifiedBusinessNumber, null));
}
}
執行結果:
從執行結果我們可以發現每一筆資料都變成一個test case,而且可以從名稱上了解測試資料內容,這樣在test case failed的時候可以很清楚知道是哪筆資料出錯,從報表上就可以一目瞭然。須注意的是@Parameters可以下name attribute是4.11版之後才支援。
2013年1月28日 星期一
2013年1月24日 星期四
How to Check Apache CXF Services
Apache CXF同時支援傳統的SOAP Web Services和RESTful Web Services,所以它也替RESTful Web Services提供了WSDL文件,只是這並不是標準。
直接從browser連上CXF servlet URL會看見如下圖的畫面:
點連結進去就會看到RESTful Web Services的細節,當然這裡無法像SOAP有嚴謹的XML定義一樣看見JSON的格式內容,但至少URL和HTTP method是很清楚的:Enable Hibernate Filter Dynamically
Hibernate Filter功能的主要目的是預先設定好filter definition和filter condition,然後在操作Hibernate session時決定要不要enable filter並提供要傳入condition的變數,將一些公用的condition抽離到filter中,Hibernate會在符合filter設定的任何查詢上都補上定義好的condition。
Test Case包含四小部分:
最後我們看一下Hibernate實際下的SQL statement:
但這樣還不夠好,原因是這樣的使用方式會讓DAO裡面充滿enable filter的程式碼,developer必須了解這些filter的規範才能正確使用,如果我們會依照使用者的身份決定condition的值,那麼與security有關的API就會汙染DAO,為此,我們利用Spring AOP的功能來完成enable filter dynamically的機制,讓interceptor來決定目前被攔截的DAO是否需要enable filter並傳入condition變數。
以下的程式碼分為兩大部分,interceptor and test case。
Interceptor:
Interceptor:
EnableFilterInterceptor
package com.gss.gmo.cao.hibernate.filter;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Set;
import lombok.SneakyThrows;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.hibernate.Filter;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.util.ReflectionUtils;
import com.gss.gmo.cao.hibernate.HibernateGenericDao;
import com.gss.gmo.cao.hibernate.HibernateGenericPagingDao;
/**
* @author linus_chien
*
*/
public class EnableFilterInterceptor implements MethodInterceptor {
/**
* Provide filter name and parameters to enable.
*/
private EnableFilterInfoAware enableFilterInfoAware;
/**
* @param enableFilterInfoAware
*/
@Required
public void setEnableFilterInfoAware(EnableFilterInfoAware enableFilterInfoAware) {
this.enableFilterInfoAware = enableFilterInfoAware;
}
@Override
@SneakyThrows
public Object invoke(MethodInvocation invocation) {
Object target = invocation.getThis();
if (target instanceof HibernateGenericDao || target instanceof HibernateGenericPagingDao) {
SessionFactory sessionFactory = getSessionFactory(target);
Session session = sessionFactory.getCurrentSession();
@SuppressWarnings("unchecked")
Set<String> filterNames = sessionFactory.getDefinedFilterNames();
Map<String, Map<String, Object>> filterInfo = enableFilterInfoAware.getEnableFilterInfo();
for (String filterName : filterNames) {
if (filterInfo.containsKey(filterName)) {
Filter filter = session.getEnabledFilter(filterName);
if (filter == null) {
filter = session.enableFilter(filterName);
Map<String, Object> parameters = filterInfo.get(filterName);
for (String parameterName : parameters.keySet()) {
Object parameterValue = parameters.get(parameterName);
filter.setParameter(parameterName, parameterValue);
}
filter.validate();
}
}
}
}
Object result = invocation.proceed();
return result;
}
/**
* @param target
* @return SessionFactory
*/
private SessionFactory getSessionFactory(Object target) {
Field sessionFactoryField = ReflectionUtils.findField(target.getClass(), "sessionFactory", SessionFactory.class);
ReflectionUtils.makeAccessible(sessionFactoryField);
return (SessionFactory) ReflectionUtils.getField(sessionFactoryField, target);
}
}
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Set;
import lombok.SneakyThrows;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.hibernate.Filter;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.util.ReflectionUtils;
import com.gss.gmo.cao.hibernate.HibernateGenericDao;
import com.gss.gmo.cao.hibernate.HibernateGenericPagingDao;
/**
* @author linus_chien
*
*/
public class EnableFilterInterceptor implements MethodInterceptor {
/**
* Provide filter name and parameters to enable.
*/
private EnableFilterInfoAware enableFilterInfoAware;
/**
* @param enableFilterInfoAware
*/
@Required
public void setEnableFilterInfoAware(EnableFilterInfoAware enableFilterInfoAware) {
this.enableFilterInfoAware = enableFilterInfoAware;
}
@Override
@SneakyThrows
public Object invoke(MethodInvocation invocation) {
Object target = invocation.getThis();
if (target instanceof HibernateGenericDao || target instanceof HibernateGenericPagingDao) {
SessionFactory sessionFactory = getSessionFactory(target);
Session session = sessionFactory.getCurrentSession();
@SuppressWarnings("unchecked")
Set<String> filterNames = sessionFactory.getDefinedFilterNames();
Map<String, Map<String, Object>> filterInfo = enableFilterInfoAware.getEnableFilterInfo();
for (String filterName : filterNames) {
if (filterInfo.containsKey(filterName)) {
Filter filter = session.getEnabledFilter(filterName);
if (filter == null) {
filter = session.enableFilter(filterName);
Map<String, Object> parameters = filterInfo.get(filterName);
for (String parameterName : parameters.keySet()) {
Object parameterValue = parameters.get(parameterName);
filter.setParameter(parameterName, parameterValue);
}
filter.validate();
}
}
}
}
Object result = invocation.proceed();
return result;
}
/**
* @param target
* @return SessionFactory
*/
private SessionFactory getSessionFactory(Object target) {
Field sessionFactoryField = ReflectionUtils.findField(target.getClass(), "sessionFactory", SessionFactory.class);
ReflectionUtils.makeAccessible(sessionFactoryField);
return (SessionFactory) ReflectionUtils.getField(sessionFactoryField, target);
}
}
EnableFilterInfoAware
package com.gss.gmo.cao.hibernate.filter;
import java.util.Map;
/**
* @author linus_chien
*
*/
public interface EnableFilterInfoAware {
/**
* Key: filter name, Value: parameters (Key: parameter name, Value: value).
*
* @return
*/
Map<String, Map<String, Object>> getEnableFilterInfo();
}
EnableFilterInfoAware的用意事實上就是Strategy Pattern的一種應用,再一次將怎麼enable filter這件事交給strategy object負責。import java.util.Map;
/**
* @author linus_chien
*
*/
public interface EnableFilterInfoAware {
/**
* Key: filter name, Value: parameters (Key: parameter name, Value: value).
*
* @return
*/
Map<String, Map<String, Object>> getEnableFilterInfo();
}
Test Case包含四小部分:
- Filter Definition:package-info.java
- Filter:Person
- Implement Strategy:EnableFilterInfoProvider
- Setup AOP:Spring XML
package-info.java
/**
* @author linus_chien
*
*/
@FilterDef(name = "personFilter", parameters = { @ParamDef(name = "name", type = "string"), @ParamDef(name = "address", type = "string") })
@TypeDef(name = "listType", typeClass = ListJsonType.class)
package com.gss.gmo.cao.hibernate.impl;
import org.hibernate.annotations.FilterDef;
import org.hibernate.annotations.ParamDef;
import org.hibernate.annotations.TypeDef;
* @author linus_chien
*
*/
@FilterDef(name = "personFilter", parameters = { @ParamDef(name = "name", type = "string"), @ParamDef(name = "address", type = "string") })
@TypeDef(name = "listType", typeClass = ListJsonType.class)
package com.gss.gmo.cao.hibernate.impl;
import org.hibernate.annotations.FilterDef;
import org.hibernate.annotations.ParamDef;
import org.hibernate.annotations.TypeDef;
Person
package com.gss.gmo.cao.hibernate.impl;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.hibernate.annotations.Filter;
import org.hibernate.annotations.Type;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import com.gss.gmo.cao.base.BaseDto;
import com.gss.gmo.cao.hibernate.audit.AuditInfo;
/**
* @author linus_chien
*
*/
@Getter
@Setter
@EqualsAndHashCode(callSuper = false, of = { "id" })
@ToString
@Entity
@Filter(name = "personFilter", condition = "name like :name and address is null")
public class Person extends BaseDto {
private static final long serialVersionUID = 1L;
@Id
private String id;
@Column
private String name;
@Column
private String address;
@Embedded
private AuditInfo auditInfo;
@Type(type = "listType")
private List<String> emails;
}
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.hibernate.annotations.Filter;
import org.hibernate.annotations.Type;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import com.gss.gmo.cao.base.BaseDto;
import com.gss.gmo.cao.hibernate.audit.AuditInfo;
/**
* @author linus_chien
*
*/
@Getter
@Setter
@EqualsAndHashCode(callSuper = false, of = { "id" })
@ToString
@Entity
@Filter(name = "personFilter", condition = "name like :name and address is null")
public class Person extends BaseDto {
private static final long serialVersionUID = 1L;
@Id
private String id;
@Column
private String name;
@Column
private String address;
@Embedded
private AuditInfo auditInfo;
@Type(type = "listType")
private List<String> emails;
}
EnableFilterInfoProvider
package com.gss.gmo.cao.hibernate.impl;
import java.util.HashMap;
import java.util.Map;
import com.gss.gmo.cao.hibernate.filter.EnableFilterInfoAware;
public class EnableFilterInfoProvider implements EnableFilterInfoAware {
@Override
public Map<String, Map<String, Object>> getEnableFilterInfo() {
Map<String, Map<String, Object>> enableFilterInfo = new HashMap<String, Map<String, Object>>();
Map<String, Object> parameter = new HashMap<String, Object>();
parameter.put("name", "%Linus%");
parameter.put("address", "abc");
enableFilterInfo.put("personFilter", parameter);
return enableFilterInfo;
}
}
import java.util.HashMap;
import java.util.Map;
import com.gss.gmo.cao.hibernate.filter.EnableFilterInfoAware;
public class EnableFilterInfoProvider implements EnableFilterInfoAware {
@Override
public Map<String, Map<String, Object>> getEnableFilterInfo() {
Map<String, Map<String, Object>> enableFilterInfo = new HashMap<String, Map<String, Object>>();
Map<String, Object> parameter = new HashMap<String, Object>();
parameter.put("name", "%Linus%");
parameter.put("address", "abc");
enableFilterInfo.put("personFilter", parameter);
return enableFilterInfo;
}
}
Spring XML
<aop:config>
<aop:pointcut id="daoPointcut" expression="execution(* com.gss.gmo.cao.hibernate.*Dao.*(..))" />
<aop:advisor advice-ref="daoAdvice" pointcut-ref="daoPointcut" />
</aop:config>
<bean id="daoAdvice" class="com.gss.gmo.cao.hibernate.filter.EnableFilterInterceptor">
<property name="enableFilterInfoAware">
<bean class="com.gss.gmo.cao.hibernate.impl.EnableFilterInfoProvider" />
</property>
</bean>
這樣只要是被攔截到的DAO就會enable filter,然後只要查詢Person就會補上額外的condition,而相同的filter definition套在每個entity都能有不同的condition。想像一下,如果今天要做到個人只能看個人的資料,而主管可以看到部門內所有人的資料,利用這個方式就不需要在DAO裡面implement這樣的邏輯,而是交給Hibernate Filter和Strategy處理了。<aop:pointcut id="daoPointcut" expression="execution(* com.gss.gmo.cao.hibernate.*Dao.*(..))" />
<aop:advisor advice-ref="daoAdvice" pointcut-ref="daoPointcut" />
</aop:config>
<bean id="daoAdvice" class="com.gss.gmo.cao.hibernate.filter.EnableFilterInterceptor">
<property name="enableFilterInfoAware">
<bean class="com.gss.gmo.cao.hibernate.impl.EnableFilterInfoProvider" />
</property>
</bean>
最後我們看一下Hibernate實際下的SQL statement:
Show SQL
Hibernate:
select
count(*) as y0_
from
Person this_
where
this_.name like ?
and this_.address is null
and (
this_.name=?
)
select
count(*) as y0_
from
Person this_
where
this_.name like ?
and this_.address is null
and (
this_.name=?
)
JExcelApi Cell Mapper
本文將解說如何設計一組annotation和API,我命名為Cell Mapper,讓開發人員只要在POJO上面標記想要對應到Excel的欄位位置,就可以自動匯出Excel或是從Excel讀取資料,底層的library使用老牌的JExcelApi。
因為JExcelApi沒有定義ReadException,所以我們自訂一個:
接下來定義兩個annotation,@Sheet和@Column:
然後我們做兩個type converter,分別將JExcelApi的Cell轉換成Java type,以及將Java type轉換成WritableCell:
重頭戲,利用這組annotation實作的Cell Mapper implementation:
最後是一個簡單的test case:
首先定義interface:
CellMapper
package com.gss.gmo.cao.jexcel;
import java.util.List;
import jxl.Workbook;
import jxl.write.WritableWorkbook;
import jxl.write.WriteException;
import com.gss.gmo.cao.jexcel.read.ReadException;
/**
* @author linus_chien
*
*/
public interface CellMapper {
/**
* Read the sheet according to the model class.
*
* @param workbook
* @param modelClass
* @return
* @throws ReadException
*/
<Model> List<Model> read(Workbook workbook, Class<Model> modelClass) throws ReadException;
/**
* Write the sheet according to the model class.
*
* @param workbook
* @param data
* @param modelClass
* @throws WriteException
*/
<Model> void write(WritableWorkbook workbook, List<Model> data, Class<Model> modelClass) throws WriteException;
}
這裡利用method level generic type保留彈性。import java.util.List;
import jxl.Workbook;
import jxl.write.WritableWorkbook;
import jxl.write.WriteException;
import com.gss.gmo.cao.jexcel.read.ReadException;
/**
* @author linus_chien
*
*/
public interface CellMapper {
/**
* Read the sheet according to the model class.
*
* @param workbook
* @param modelClass
* @return
* @throws ReadException
*/
<Model> List<Model> read(Workbook workbook, Class<Model> modelClass) throws ReadException;
/**
* Write the sheet according to the model class.
*
* @param workbook
* @param data
* @param modelClass
* @throws WriteException
*/
<Model> void write(WritableWorkbook workbook, List<Model> data, Class<Model> modelClass) throws WriteException;
}
因為JExcelApi沒有定義ReadException,所以我們自訂一個:
ReadException
package com.gss.gmo.cao.jexcel.read;
import jxl.JXLException;
/**
* @author linus_chien
*
*/
public class ReadException extends JXLException {
private static final long serialVersionUID = 1L;
public ReadException(String message) {
super(message);
}
}
import jxl.JXLException;
/**
* @author linus_chien
*
*/
public class ReadException extends JXLException {
private static final long serialVersionUID = 1L;
public ReadException(String message) {
super(message);
}
}
接下來定義兩個annotation,@Sheet和@Column:
@Sheet
package com.gss.gmo.cao.jexcel.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Excel sheet.
*
* @author linus_chien
*
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Sheet {
/**
* @return sheet name
*/
String name();
}
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Excel sheet.
*
* @author linus_chien
*
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Sheet {
/**
* @return sheet name
*/
String name();
}
@Column
package com.gss.gmo.cao.jexcel.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Excel column.
*
* @author linus_chien
*
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Column {
/**
* @return column index, numbered from 0
*/
int index();
/**
* @return column title
*/
String title();
}
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Excel column.
*
* @author linus_chien
*
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Column {
/**
* @return column index, numbered from 0
*/
int index();
/**
* @return column title
*/
String title();
}
然後我們做兩個type converter,分別將JExcelApi的Cell轉換成Java type,以及將Java type轉換成WritableCell:
CellTypeReader
package com.gss.gmo.cao.jexcel.read;
import java.util.Date;
import jxl.Cell;
import jxl.CellType;
import jxl.DateCell;
import jxl.LabelCell;
import jxl.NumberCell;
/**
* Read cell content by cell type.
*
* @author linus_chien
*
*/
public class CellTypeReader {
/**
* @param cell
* @return
*/
public String read(LabelCell cell) {
return cell.getString();
}
/**
* @param cell
* @return
*/
public Double read(NumberCell cell) {
return cell.getValue();
}
/**
* @param cell
* @return
*/
public Date read(DateCell cell) {
return cell.getDate();
}
/**
* @param cell
* @return
*/
public Object read(Cell cell) {
CellType cellType = cell.getType();
if (cellType == CellType.LABEL) {
return read((LabelCell) cell);
} else if (cellType == CellType.NUMBER) {
return read((NumberCell) cell);
} else if (cellType == CellType.DATE) {
return read((DateCell) cell);
} else {
return cell.getContents();
}
}
}
import java.util.Date;
import jxl.Cell;
import jxl.CellType;
import jxl.DateCell;
import jxl.LabelCell;
import jxl.NumberCell;
/**
* Read cell content by cell type.
*
* @author linus_chien
*
*/
public class CellTypeReader {
/**
* @param cell
* @return
*/
public String read(LabelCell cell) {
return cell.getString();
}
/**
* @param cell
* @return
*/
public Double read(NumberCell cell) {
return cell.getValue();
}
/**
* @param cell
* @return
*/
public Date read(DateCell cell) {
return cell.getDate();
}
/**
* @param cell
* @return
*/
public Object read(Cell cell) {
CellType cellType = cell.getType();
if (cellType == CellType.LABEL) {
return read((LabelCell) cell);
} else if (cellType == CellType.NUMBER) {
return read((NumberCell) cell);
} else if (cellType == CellType.DATE) {
return read((DateCell) cell);
} else {
return cell.getContents();
}
}
}
WritableCellFactory
package com.gss.gmo.cao.jexcel.write;
import java.util.Date;
import jxl.write.Blank;
import jxl.write.DateTime;
import jxl.write.Label;
import jxl.write.WritableCell;
import org.apache.commons.lang3.StringUtils;
/**
* Create WritableCell instance by value type.
*
* @author linus_chien
*
*/
public class WritableCellFactory {
/**
* @param col
* @param row
* @param value
* @return Label or Blank
*/
public WritableCell create(int col, int row, String value) {
if (StringUtils.isBlank(value)) {
return create(col, row);
} else {
return new Label(col, row, value);
}
}
/**
* @param col
* @param row
* @param value
* @return jxl.write.Number or Blank
*/
public WritableCell create(int col, int row, Number value) {
if (value == null) {
return create(col, row);
} else {
return new jxl.write.Number(col, row, value.doubleValue());
}
}
/**
* @param col
* @param row
* @param value
* @return DateTime or Blank
*/
public WritableCell create(int col, int row, Date value) {
if (value == null) {
return create(col, row);
} else {
return new DateTime(col, row, value);
}
}
/**
* @param col
* @param row
* @param value
* @return Label or jxl.write.Number or DateTime or Blank
*/
public WritableCell create(int col, int row, Object value) {
if (value == null) {
return create(col, row);
} else if (value instanceof String) {
return create(col, row, (String) value);
} else if (value instanceof Number) {
return create(col, row, (Number) value);
} else if (value instanceof Date) {
return create(col, row, (Date) value);
} else {
return create(col, row, value.toString());
}
}
/**
* @param col
* @param row
* @return Blank
*/
public WritableCell create(int col, int row) {
return new Blank(col, row);
}
}
import java.util.Date;
import jxl.write.Blank;
import jxl.write.DateTime;
import jxl.write.Label;
import jxl.write.WritableCell;
import org.apache.commons.lang3.StringUtils;
/**
* Create WritableCell instance by value type.
*
* @author linus_chien
*
*/
public class WritableCellFactory {
/**
* @param col
* @param row
* @param value
* @return Label or Blank
*/
public WritableCell create(int col, int row, String value) {
if (StringUtils.isBlank(value)) {
return create(col, row);
} else {
return new Label(col, row, value);
}
}
/**
* @param col
* @param row
* @param value
* @return jxl.write.Number or Blank
*/
public WritableCell create(int col, int row, Number value) {
if (value == null) {
return create(col, row);
} else {
return new jxl.write.Number(col, row, value.doubleValue());
}
}
/**
* @param col
* @param row
* @param value
* @return DateTime or Blank
*/
public WritableCell create(int col, int row, Date value) {
if (value == null) {
return create(col, row);
} else {
return new DateTime(col, row, value);
}
}
/**
* @param col
* @param row
* @param value
* @return Label or jxl.write.Number or DateTime or Blank
*/
public WritableCell create(int col, int row, Object value) {
if (value == null) {
return create(col, row);
} else if (value instanceof String) {
return create(col, row, (String) value);
} else if (value instanceof Number) {
return create(col, row, (Number) value);
} else if (value instanceof Date) {
return create(col, row, (Date) value);
} else {
return create(col, row, value.toString());
}
}
/**
* @param col
* @param row
* @return Blank
*/
public WritableCell create(int col, int row) {
return new Blank(col, row);
}
}
重頭戲,利用這組annotation實作的Cell Mapper implementation:
AnnotationCellMapperImpl
package com.gss.gmo.cao.jexcel.impl;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jxl.Cell;
import jxl.Sheet;
import jxl.Workbook;
import jxl.write.WritableSheet;
import jxl.write.WritableWorkbook;
import jxl.write.WriteException;
import org.apache.commons.beanutils.BeanUtils;
import org.springframework.util.ReflectionUtils;
import com.gss.gmo.cao.jexcel.CellMapper;
import com.gss.gmo.cao.jexcel.annotation.Helper;
import com.gss.gmo.cao.jexcel.read.CellTypeReader;
import com.gss.gmo.cao.jexcel.read.ReadException;
import com.gss.gmo.cao.jexcel.write.WritableCellFactory;
/**
* Depend on annotation.
*
* @author linus_chien
*
*/
public class AnnotationCellMapperImpl implements CellMapper {
/**
* Helper instance.
*/
private Helper helper = new Helper();
/**
* WritableCellFactory instance.
*/
private WritableCellFactory factory = new WritableCellFactory();
/**
* CellTypeReader instance.
*/
private CellTypeReader reader = new CellTypeReader();
@Override
public <Model> List<Model> read(Workbook workbook, Class<Model> modelClass) throws ReadException {
List<Model> results = new ArrayList<Model>();
if (helper.isClassWithSheetAnnotation(modelClass)) {
List<Field> fields = helper.getAnnotatedFields(modelClass);
Sheet sheet = getSheet(workbook, modelClass);
for (int row = 1; row < sheet.getRows(); row++) {
Map<String, Object> data = new HashMap<String, Object>();
for (Field field : fields) {
int col = helper.getAnnotatedColumnIndex(field);
Cell cell = sheet.getCell(col, row);
data.put(field.getName(), reader.read(cell));
}
try {
Model model = modelClass.newInstance();
BeanUtils.populate(model, data);
results.add(model);
} catch (Exception e) {
e.printStackTrace();
throw new ReadException(e.getMessage());
}
}
}
return results;
}
@Override
public <Model> void write(WritableWorkbook workbook, List<Model> datas, Class<Model> modelClass) throws WriteException {
if (!helper.isClassWithSheetAnnotation(modelClass)) {
return;
}
List<Field> fields = helper.getAnnotatedFields(modelClass);
WritableSheet sheet = createSheet(workbook, modelClass);
for (Field field : fields) {
int col = helper.getAnnotatedColumnIndex(field);
sheet.addCell(factory.create(col, 0, helper.getAnnotatedColumnTitle(field)));
}
for (int row = 0; row < datas.size(); row++) {
Model data = datas.get(row);
for (Field field : fields) {
int col = helper.getAnnotatedColumnIndex(field);
ReflectionUtils.makeAccessible(field);
Object value = ReflectionUtils.getField(field, data);
sheet.addCell(factory.create(col, row + 1, value));
}
}
}
/**
* @param workbook
* @param modelClass
* @return WritableSheet
*/
private WritableSheet createSheet(WritableWorkbook workbook, Class<?> modelClass) {
String sheetName = helper.getAnnotatedSheetName(modelClass);
WritableSheet sheet = workbook.createSheet(sheetName, workbook.getNumberOfSheets());
return sheet;
}
/**
* @param workbook
* @param modelClass
* @return
*/
private Sheet getSheet(Workbook workbook, Class<?> modelClass) {
String sheetName = helper.getAnnotatedSheetName(modelClass);
Sheet sheet = workbook.getSheet(sheetName);
return sheet;
}
}
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jxl.Cell;
import jxl.Sheet;
import jxl.Workbook;
import jxl.write.WritableSheet;
import jxl.write.WritableWorkbook;
import jxl.write.WriteException;
import org.apache.commons.beanutils.BeanUtils;
import org.springframework.util.ReflectionUtils;
import com.gss.gmo.cao.jexcel.CellMapper;
import com.gss.gmo.cao.jexcel.annotation.Helper;
import com.gss.gmo.cao.jexcel.read.CellTypeReader;
import com.gss.gmo.cao.jexcel.read.ReadException;
import com.gss.gmo.cao.jexcel.write.WritableCellFactory;
/**
* Depend on annotation.
*
* @author linus_chien
*
*/
public class AnnotationCellMapperImpl implements CellMapper {
/**
* Helper instance.
*/
private Helper helper = new Helper();
/**
* WritableCellFactory instance.
*/
private WritableCellFactory factory = new WritableCellFactory();
/**
* CellTypeReader instance.
*/
private CellTypeReader reader = new CellTypeReader();
@Override
public <Model> List<Model> read(Workbook workbook, Class<Model> modelClass) throws ReadException {
List<Model> results = new ArrayList<Model>();
if (helper.isClassWithSheetAnnotation(modelClass)) {
List<Field> fields = helper.getAnnotatedFields(modelClass);
Sheet sheet = getSheet(workbook, modelClass);
for (int row = 1; row < sheet.getRows(); row++) {
Map<String, Object> data = new HashMap<String, Object>();
for (Field field : fields) {
int col = helper.getAnnotatedColumnIndex(field);
Cell cell = sheet.getCell(col, row);
data.put(field.getName(), reader.read(cell));
}
try {
Model model = modelClass.newInstance();
BeanUtils.populate(model, data);
results.add(model);
} catch (Exception e) {
e.printStackTrace();
throw new ReadException(e.getMessage());
}
}
}
return results;
}
@Override
public <Model> void write(WritableWorkbook workbook, List<Model> datas, Class<Model> modelClass) throws WriteException {
if (!helper.isClassWithSheetAnnotation(modelClass)) {
return;
}
List<Field> fields = helper.getAnnotatedFields(modelClass);
WritableSheet sheet = createSheet(workbook, modelClass);
for (Field field : fields) {
int col = helper.getAnnotatedColumnIndex(field);
sheet.addCell(factory.create(col, 0, helper.getAnnotatedColumnTitle(field)));
}
for (int row = 0; row < datas.size(); row++) {
Model data = datas.get(row);
for (Field field : fields) {
int col = helper.getAnnotatedColumnIndex(field);
ReflectionUtils.makeAccessible(field);
Object value = ReflectionUtils.getField(field, data);
sheet.addCell(factory.create(col, row + 1, value));
}
}
}
/**
* @param workbook
* @param modelClass
* @return WritableSheet
*/
private WritableSheet createSheet(WritableWorkbook workbook, Class<?> modelClass) {
String sheetName = helper.getAnnotatedSheetName(modelClass);
WritableSheet sheet = workbook.createSheet(sheetName, workbook.getNumberOfSheets());
return sheet;
}
/**
* @param workbook
* @param modelClass
* @return
*/
private Sheet getSheet(Workbook workbook, Class<?> modelClass) {
String sheetName = helper.getAnnotatedSheetName(modelClass);
Sheet sheet = workbook.getSheet(sheetName);
return sheet;
}
}
Helper
package com.gss.gmo.cao.jexcel.annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
/**
* Help CellMapper to retrieve annotation data.
*
* @author linus_chien
*
*/
public class Helper {
/**
* @param modelClass
* @return
*/
public boolean isClassWithSheetAnnotation(Class<?> modelClass) {
return modelClass.isAnnotationPresent(Sheet.class);
}
/**
* @param modelClass
* @return
*/
public String getAnnotatedSheetName(Class<?> modelClass) {
String sheetName;
Sheet sheetAnnotation = modelClass.getAnnotation(Sheet.class);
if (StringUtils.isBlank(sheetAnnotation.name())) {
sheetName = modelClass.getSimpleName();
} else {
sheetName = sheetAnnotation.name();
}
return sheetName;
}
/**
* @param modelClass
* @return
*/
public List<Field> getAnnotatedFields(Class<?> modelClass) {
List<Field> annotatedFields = new ArrayList<Field>();
Field[] fields = modelClass.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Column.class)) {
annotatedFields.add(field);
}
}
return annotatedFields;
}
/**
* @param field
* @return
*/
public String getAnnotatedColumnTitle(Field field) {
String title;
Column columnAnnotation = field.getAnnotation(Column.class);
if (StringUtils.isBlank(columnAnnotation.title())) {
title = field.getName();
} else {
title = columnAnnotation.title();
}
return title;
}
/**
* @param field
* @return
*/
public int getAnnotatedColumnIndex(Field field) {
Column columnAnnotation = field.getAnnotation(Column.class);
return columnAnnotation.index();
}
}
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
/**
* Help CellMapper to retrieve annotation data.
*
* @author linus_chien
*
*/
public class Helper {
/**
* @param modelClass
* @return
*/
public boolean isClassWithSheetAnnotation(Class<?> modelClass) {
return modelClass.isAnnotationPresent(Sheet.class);
}
/**
* @param modelClass
* @return
*/
public String getAnnotatedSheetName(Class<?> modelClass) {
String sheetName;
Sheet sheetAnnotation = modelClass.getAnnotation(Sheet.class);
if (StringUtils.isBlank(sheetAnnotation.name())) {
sheetName = modelClass.getSimpleName();
} else {
sheetName = sheetAnnotation.name();
}
return sheetName;
}
/**
* @param modelClass
* @return
*/
public List<Field> getAnnotatedFields(Class<?> modelClass) {
List<Field> annotatedFields = new ArrayList<Field>();
Field[] fields = modelClass.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Column.class)) {
annotatedFields.add(field);
}
}
return annotatedFields;
}
/**
* @param field
* @return
*/
public String getAnnotatedColumnTitle(Field field) {
String title;
Column columnAnnotation = field.getAnnotation(Column.class);
if (StringUtils.isBlank(columnAnnotation.title())) {
title = field.getName();
} else {
title = columnAnnotation.title();
}
return title;
}
/**
* @param field
* @return
*/
public int getAnnotatedColumnIndex(Field field) {
Column columnAnnotation = field.getAnnotation(Column.class);
return columnAnnotation.index();
}
}
最後是一個簡單的test case:
AnnotationCellMapperImplTest
package com.gss.gmo.cao.jexcel.impl;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import junit.framework.Assert;
import jxl.Workbook;
import jxl.write.WritableWorkbook;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import com.gss.gmo.cao.jexcel.CellMapper;
import com.gss.gmo.cao.jexcel.annotation.Column;
import com.gss.gmo.cao.jexcel.annotation.Sheet;
public class AnnotationCellMapperImplTest {
private static ByteArrayOutputStream os;
private static ByteArrayInputStream is;
@BeforeClass
public static void setUpBeforeClass() throws Exception {
os = new ByteArrayOutputStream();
}
@AfterClass
public static void tearDownAfterClass() throws Exception {
os.close();
is.close();
}
@Test
public void testWrite() throws Exception {
WritableWorkbook workbook = Workbook.createWorkbook(os);
CellMapper cellMapper = new AnnotationCellMapperImpl();
List<User> users = new ArrayList<User>();
User u1 = new User();
u1.setName("Linus");
u1.setAge(36);
u1.setBirthday(new Date());
users.add(u1);
User u2 = new User();
u2.setName("Mime");
u2.setAge(35);
u2.setBirthday(new Date());
users.add(u2);
cellMapper.write(workbook, users, User.class);
workbook.write();
workbook.close();
}
@Test
public void testRead() throws Exception {
is = new ByteArrayInputStream(os.toByteArray());
Workbook workbook = Workbook.getWorkbook(is);
CellMapper cellMapper = new AnnotationCellMapperImpl();
List<User> users = cellMapper.read(workbook, User.class);
Assert.assertEquals("Linus", users.get(0).getName());
Assert.assertEquals("Mime", users.get(1).getName());
workbook.close();
}
@Sheet(name = "User")
public static class User {
@Column(index = 0, title = "姓名")
private String name;
@Column(index = 1, title = "年齡")
private int age;
@Column(index = 2, title = "生日")
private Date birthday;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return "User [name=" + name + ", age=" + age + ", birthday=" + birthday + "]";
}
}
}
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import junit.framework.Assert;
import jxl.Workbook;
import jxl.write.WritableWorkbook;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import com.gss.gmo.cao.jexcel.CellMapper;
import com.gss.gmo.cao.jexcel.annotation.Column;
import com.gss.gmo.cao.jexcel.annotation.Sheet;
public class AnnotationCellMapperImplTest {
private static ByteArrayOutputStream os;
private static ByteArrayInputStream is;
@BeforeClass
public static void setUpBeforeClass() throws Exception {
os = new ByteArrayOutputStream();
}
@AfterClass
public static void tearDownAfterClass() throws Exception {
os.close();
is.close();
}
@Test
public void testWrite() throws Exception {
WritableWorkbook workbook = Workbook.createWorkbook(os);
CellMapper cellMapper = new AnnotationCellMapperImpl();
List<User> users = new ArrayList<User>();
User u1 = new User();
u1.setName("Linus");
u1.setAge(36);
u1.setBirthday(new Date());
users.add(u1);
User u2 = new User();
u2.setName("Mime");
u2.setAge(35);
u2.setBirthday(new Date());
users.add(u2);
cellMapper.write(workbook, users, User.class);
workbook.write();
workbook.close();
}
@Test
public void testRead() throws Exception {
is = new ByteArrayInputStream(os.toByteArray());
Workbook workbook = Workbook.getWorkbook(is);
CellMapper cellMapper = new AnnotationCellMapperImpl();
List<User> users = cellMapper.read(workbook, User.class);
Assert.assertEquals("Linus", users.get(0).getName());
Assert.assertEquals("Mime", users.get(1).getName());
workbook.close();
}
@Sheet(name = "User")
public static class User {
@Column(index = 0, title = "姓名")
private String name;
@Column(index = 1, title = "年齡")
private int age;
@Column(index = 2, title = "生日")
private Date birthday;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return "User [name=" + name + ", age=" + age + ", birthday=" + birthday + "]";
}
}
}
2013年1月23日 星期三
Applying Bean Validation (JSR303) for Design by Contract
原本method level validation應該在Bean Validation 1.1 (JSR349)才有,但是Hibernate Validator和Spring已經搶先提供這個功能,所以我們現在可以用method validation輕易做到design by contract的precondition和postcondition。
以下是一組設定範例:
它的運作原理很簡單,Spring利用AOP攔截需要進行validation的method,在method執行的前、後進行驗證,如果有違反condition就拋出exception。
以下是一組設定範例:
Spring XML
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor" />
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor" />
Method Validation Sample
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.NotBlank;
import org.springframework.validation.annotation.Validated;
import com.gss.acl.service.ws.domain.AclUserDetails;
@Validated
public interface AclUserDetailsWebService {
@NotNull
AclUserDetails loadUserByUsername(@NotBlank String username);
}
這樣就能在interface中規範condition並強化domain service的完整性,而且將validation邏輯從implementation class中抽離。
import org.hibernate.validator.constraints.NotBlank;
import org.springframework.validation.annotation.Validated;
import com.gss.acl.service.ws.domain.AclUserDetails;
@Validated
public interface AclUserDetailsWebService {
@NotNull
AclUserDetails loadUserByUsername(@NotBlank String username);
}
Precondition of Design by Contract
如果在callee裡面要做precondition的話,我們可以使用org.apache.commons.lang3.Validate,在method一開始執行就進行arguments的驗證,並且丟出合理易懂的exception,避免method執行到很後面時才拋出不太容易理解的exception,也減少stack trace的複雜性有助於debug。
Applying Spring Security for SSO of Cross Web Applications
因應雲端世代的來臨,在架構設計上必須讓系統具有分散式佈署的能力,所以將一個大的系統用多個web application來開發是無可避免的方式,除了在module上可以做功能性的分割之外,也可以將不同的web application佈署到不同的application server上,做到server loading分散的目的,只要架設一個Apache HTTP Server做為load balancer,對前端使用者來說就不會感覺到系統後面其實有很多application server在服務。
在這個前提之下,我們會面臨不同web application的session其實是被隔離的問題,也就是說在一個web application登入之後,另外一個web application並不會知道使用者已經登入過,或許會有人用cross servlet context的方式讓不同的web application交換資料,但這牽涉到所使用application server的特性,不見得是好的方法。
我們使用Spring Security的Pre-Authentication機制來處理SSO of Cross Web Applications問題,在某個master web application中實作登入機制,其它的slave web application只需要套用Pre-Authentication機制取得使用者曾經登入的資訊,那麼就不會發生登入不同步的問題了。
以下是我們簡單利用cookie記錄使用者曾經登入過的帳號密碼來做Pre-Authentication,當然帳號、密碼是經過加密的。
在這個前提之下,我們會面臨不同web application的session其實是被隔離的問題,也就是說在一個web application登入之後,另外一個web application並不會知道使用者已經登入過,或許會有人用cross servlet context的方式讓不同的web application交換資料,但這牽涉到所使用application server的特性,不見得是好的方法。
我們使用Spring Security的Pre-Authentication機制來處理SSO of Cross Web Applications問題,在某個master web application中實作登入機制,其它的slave web application只需要套用Pre-Authentication機制取得使用者曾經登入的資訊,那麼就不會發生登入不同步的問題了。
以下是我們簡單利用cookie記錄使用者曾經登入過的帳號密碼來做Pre-Authentication,當然帳號、密碼是經過加密的。
Spring XML for Master Web Application
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<security:http pattern="/login.action" security="none" />
<security:http pattern="/struts/**" security="none" />
<security:http pattern="/css/**" security="none" />
<security:http pattern="/images/**" security="none" />
<security:http pattern="/js/**" security="none" />
<security:http pattern="/styles/**" security="none" />
<security:http pattern="/template/**" security="none" />
<security:http auto-config="true" authentication-manager-ref="authenticationManager" use-expressions="true">
<security:form-login authentication-success-handler-ref="cookieTokenAwareAuthenticationSuccessHandler"
login-page="/login.action" authentication-failure-url="/login.action?login_error=1" />
<security:logout invalidate-session="true" success-handler-ref="cookieTokenClearingLogoutSuccessHandler"
logout-url="/spring_security_logout" />
<security:custom-filter position="PRE_AUTH_FILTER" ref="cookieTokenAuthenticationFilter" />
<security:custom-filter position="CAS_FILTER" ref="casFilter" />
<security:intercept-url pattern="/**" access="authenticated" />
</security:http>
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="preauthAuthProvider" />
<security:authentication-provider ref="casAuthenticationProvider" />
<security:authentication-provider ref="ldapAuthenticationProvider" />
<security:authentication-provider user-service-ref="userDetailService">
<!-- <security:password-encoder ref="passwordEncoder"/> -->
</security:authentication-provider>
</security:authentication-manager>
<!-- <bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder"/> -->
<bean id="cookieTokenAwareAuthenticationSuccessHandler" class="com.gss.gmo.cao.spring.security.web.authentication.CookieTokenAwareAuthenticationSuccessHandler">
<property name="defaultTargetUrl" value="/init/menu.jsp" />
<property name="alwaysUseDefaultTargetUrl" value="true" />
</bean>
<bean id="cookieTokenClearingLogoutSuccessHandler" class="com.gss.gmo.cao.spring.security.web.authentication.logout.CookieTokenClearingLogoutSuccessHandler">
<property name="defaultTargetUrl" value="/login.action" />
<property name="alwaysUseDefaultTargetUrl" value="true" />
</bean>
<bean id="cookieTokenAuthenticationFilter" class="com.gss.gmo.cao.spring.security.web.authentication.preauth.CookieTokenAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager" />
<property name="continueFilterChainOnUnsuccessfulAuthentication" value="true" />
<property name="checkForPrincipalChanges" value="true" />
</bean>
<bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
<property name="preAuthenticatedUserDetailsService" ref="authenticationUserDetailsService" />
</bean>
<bean id="authenticationUserDetailsService" class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
<property name="userDetailsService" ref="userDetailService" />
</bean>
<bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">
<property name="service" value="http://localhost:8080/portal-web/gss/j_spring_cas_security_check" />
</bean>
<bean id="casFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
<property name="filterProcessesUrl" value="/gss/j_spring_cas_security_check" />
<property name="authenticationManager" ref="authenticationManager" />
<property name="authenticationSuccessHandler" ref="cookieTokenAwareAuthenticationSuccessHandler" />
<property name="authenticationFailureHandler">
<bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
<property name="defaultFailureUrl" value="/login.action?login_error=1" />
</bean>
</property>
</bean>
<bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
<property name="serviceProperties" ref="serviceProperties" />
<property name="ticketValidator">
<bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
<constructor-arg value="http://teamkube.gss.com.tw/cas" />
</bean>
</property>
<property name="key" value="teamkube-cas" />
<property name="authenticationUserDetailsService" ref="authenticationUserDetailsService" />
</bean>
</beans>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<security:http pattern="/login.action" security="none" />
<security:http pattern="/struts/**" security="none" />
<security:http pattern="/css/**" security="none" />
<security:http pattern="/images/**" security="none" />
<security:http pattern="/js/**" security="none" />
<security:http pattern="/styles/**" security="none" />
<security:http pattern="/template/**" security="none" />
<security:http auto-config="true" authentication-manager-ref="authenticationManager" use-expressions="true">
<security:form-login authentication-success-handler-ref="cookieTokenAwareAuthenticationSuccessHandler"
login-page="/login.action" authentication-failure-url="/login.action?login_error=1" />
<security:logout invalidate-session="true" success-handler-ref="cookieTokenClearingLogoutSuccessHandler"
logout-url="/spring_security_logout" />
<security:custom-filter position="PRE_AUTH_FILTER" ref="cookieTokenAuthenticationFilter" />
<security:custom-filter position="CAS_FILTER" ref="casFilter" />
<security:intercept-url pattern="/**" access="authenticated" />
</security:http>
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="preauthAuthProvider" />
<security:authentication-provider ref="casAuthenticationProvider" />
<security:authentication-provider ref="ldapAuthenticationProvider" />
<security:authentication-provider user-service-ref="userDetailService">
<!-- <security:password-encoder ref="passwordEncoder"/> -->
</security:authentication-provider>
</security:authentication-manager>
<!-- <bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder"/> -->
<bean id="cookieTokenAwareAuthenticationSuccessHandler" class="com.gss.gmo.cao.spring.security.web.authentication.CookieTokenAwareAuthenticationSuccessHandler">
<property name="defaultTargetUrl" value="/init/menu.jsp" />
<property name="alwaysUseDefaultTargetUrl" value="true" />
</bean>
<bean id="cookieTokenClearingLogoutSuccessHandler" class="com.gss.gmo.cao.spring.security.web.authentication.logout.CookieTokenClearingLogoutSuccessHandler">
<property name="defaultTargetUrl" value="/login.action" />
<property name="alwaysUseDefaultTargetUrl" value="true" />
</bean>
<bean id="cookieTokenAuthenticationFilter" class="com.gss.gmo.cao.spring.security.web.authentication.preauth.CookieTokenAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager" />
<property name="continueFilterChainOnUnsuccessfulAuthentication" value="true" />
<property name="checkForPrincipalChanges" value="true" />
</bean>
<bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
<property name="preAuthenticatedUserDetailsService" ref="authenticationUserDetailsService" />
</bean>
<bean id="authenticationUserDetailsService" class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
<property name="userDetailsService" ref="userDetailService" />
</bean>
<bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">
<property name="service" value="http://localhost:8080/portal-web/gss/j_spring_cas_security_check" />
</bean>
<bean id="casFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
<property name="filterProcessesUrl" value="/gss/j_spring_cas_security_check" />
<property name="authenticationManager" ref="authenticationManager" />
<property name="authenticationSuccessHandler" ref="cookieTokenAwareAuthenticationSuccessHandler" />
<property name="authenticationFailureHandler">
<bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
<property name="defaultFailureUrl" value="/login.action?login_error=1" />
</bean>
</property>
</bean>
<bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
<property name="serviceProperties" ref="serviceProperties" />
<property name="ticketValidator">
<bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
<constructor-arg value="http://teamkube.gss.com.tw/cas" />
</bean>
</property>
<property name="key" value="teamkube-cas" />
<property name="authenticationUserDetailsService" ref="authenticationUserDetailsService" />
</bean>
</beans>
Spring XML for Slave Web Application
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<import resource="classpath:spring-acm-core.xml" />
<import resource="classpath:gmo-cao-struts2-spring-context.xml" />
<context:annotation-config />
<context:component-scan base-package="com.gss.acm.web" />
<security:http pattern="/struts/**" security="none" />
<security:http pattern="/css/**" security="none" />
<security:http pattern="/images/**" security="none" />
<security:http pattern="/js/**" security="none" />
<security:http pattern="/styles/**" security="none" />
<security:http pattern="/template/**" security="none" />
<security:http auto-config="true" use-expressions="true">
<!-- Additional http configuration omitted -->
<security:form-login login-page="/../portal-web/login.action" />
<security:logout invalidate-session="true" logout-success-url="/../portal-web/spring_security_logout"
logout-url="/spring_security_logout" />
<security:custom-filter position="PRE_AUTH_FILTER" ref="cookieTokenAuthenticationFilter" />
<security:intercept-url pattern="/**" access="authenticated" />
</security:http>
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="preauthAuthProvider" />
</security:authentication-manager>
</beans>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<import resource="classpath:spring-acm-core.xml" />
<import resource="classpath:gmo-cao-struts2-spring-context.xml" />
<context:annotation-config />
<context:component-scan base-package="com.gss.acm.web" />
<security:http pattern="/struts/**" security="none" />
<security:http pattern="/css/**" security="none" />
<security:http pattern="/images/**" security="none" />
<security:http pattern="/js/**" security="none" />
<security:http pattern="/styles/**" security="none" />
<security:http pattern="/template/**" security="none" />
<security:http auto-config="true" use-expressions="true">
<!-- Additional http configuration omitted -->
<security:form-login login-page="/../portal-web/login.action" />
<security:logout invalidate-session="true" logout-success-url="/../portal-web/spring_security_logout"
logout-url="/spring_security_logout" />
<security:custom-filter position="PRE_AUTH_FILTER" ref="cookieTokenAuthenticationFilter" />
<security:intercept-url pattern="/**" access="authenticated" />
</security:http>
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="preauthAuthProvider" />
</security:authentication-manager>
</beans>
CookieTokenAuthenticationFilter
package com.gss.gmo.cao.spring.security.web.authentication.preauth;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import org.jasypt.encryption.StringEncryptor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.web.util.WebUtils;
import com.gss.gmo.cao.spring.security.util.StringEncryptorFactory;
/**
* Get username and password from cookie for SSO.
*
* @author linus_chien
*
*/
public class CookieTokenAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter {
/**
* Encryptor.
*/
private StringEncryptor encryptor = StringEncryptorFactory.createInstance();
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (getPreAuthenticatedPrincipal((HttpServletRequest) request) == null) {
SecurityContextHolder.clearContext();
}
super.doFilter(request, response, chain);
}
@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
Cookie passwordCookie = WebUtils.getCookie(request, UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY);
if (passwordCookie != null) {
return encryptor.decrypt(passwordCookie.getValue());
}
return null;
}
@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
Cookie usernameCookie = WebUtils.getCookie(request, UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY);
if (usernameCookie != null) {
return encryptor.decrypt(usernameCookie.getValue());
}
return null;
}
}
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import org.jasypt.encryption.StringEncryptor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.web.util.WebUtils;
import com.gss.gmo.cao.spring.security.util.StringEncryptorFactory;
/**
* Get username and password from cookie for SSO.
*
* @author linus_chien
*
*/
public class CookieTokenAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter {
/**
* Encryptor.
*/
private StringEncryptor encryptor = StringEncryptorFactory.createInstance();
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (getPreAuthenticatedPrincipal((HttpServletRequest) request) == null) {
SecurityContextHolder.clearContext();
}
super.doFilter(request, response, chain);
}
@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
Cookie passwordCookie = WebUtils.getCookie(request, UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY);
if (passwordCookie != null) {
return encryptor.decrypt(passwordCookie.getValue());
}
return null;
}
@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
Cookie usernameCookie = WebUtils.getCookie(request, UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY);
if (usernameCookie != null) {
return encryptor.decrypt(usernameCookie.getValue());
}
return null;
}
}
CookieTokenAwareAuthenticationSuccessHandler
package com.gss.gmo.cao.spring.security.web.authentication;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jasypt.encryption.StringEncryptor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.gss.gmo.cao.spring.security.util.StringEncryptorFactory;
/**
* Set username/password to cookie for cross web context SSO.
*
* @author linus_chien
*
*/
public class CookieTokenAwareAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
/**
* Encryptor.
*/
private StringEncryptor encryptor = StringEncryptorFactory.createInstance();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException,
IOException {
UserDetails user = (UserDetails) authentication.getPrincipal();
Cookie username = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY, encryptor.encrypt(user.getUsername()));
username.setPath("/");
Cookie password = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY, encryptor.encrypt(user.getPassword()));
password.setPath("/");
response.addCookie(username);
response.addCookie(password);
super.onAuthenticationSuccess(request, response, authentication);
}
}
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jasypt.encryption.StringEncryptor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.gss.gmo.cao.spring.security.util.StringEncryptorFactory;
/**
* Set username/password to cookie for cross web context SSO.
*
* @author linus_chien
*
*/
public class CookieTokenAwareAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
/**
* Encryptor.
*/
private StringEncryptor encryptor = StringEncryptorFactory.createInstance();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException,
IOException {
UserDetails user = (UserDetails) authentication.getPrincipal();
Cookie username = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY, encryptor.encrypt(user.getUsername()));
username.setPath("/");
Cookie password = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY, encryptor.encrypt(user.getPassword()));
password.setPath("/");
response.addCookie(username);
response.addCookie(password);
super.onAuthenticationSuccess(request, response, authentication);
}
}
CookieTokenClearingLogoutSuccessHandler
package com.gss.gmo.cao.spring.security.web.authentication.logout;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
/**
* Clear username/password from cookie for cross web context SSO.
*
* @author linus_chien
*
*/
public class CookieTokenClearingLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Cookie username = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY, null);
username.setPath("/");
username.setMaxAge(0);
Cookie password = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY, null);
password.setPath("/");
password.setMaxAge(0);
response.addCookie(username);
response.addCookie(password);
super.onLogoutSuccess(request, response, authentication);
}
}
最後需要強調的是AbstractPreAuthenticatedProcessingFilter和AbstractAuthenticationProcessingFilter是不相關的filter類別,如果是內部系統彼此間的SSO可以使用AbstractPreAuthenticatedProcessingFilter,但是外部系統的SSO最好還是使用AbstractAuthenticationProcessingFilter的子類別來處理,但無論使用哪種方式,我們都會reuse UserDetailsService instance。
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
/**
* Clear username/password from cookie for cross web context SSO.
*
* @author linus_chien
*
*/
public class CookieTokenClearingLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Cookie username = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY, null);
username.setPath("/");
username.setMaxAge(0);
Cookie password = new Cookie(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY, null);
password.setPath("/");
password.setMaxAge(0);
response.addCookie(username);
response.addCookie(password);
super.onLogoutSuccess(request, response, authentication);
}
}
訂閱:
文章 (Atom)