2013年1月31日 星期四

ModelMapper

我會選用ModelMapper有以下幾個特點:
  1. 具convention
  2. 可configuration (例如可選擇用field或是method來mapping)
  3. 支援generic types
  4. 使用Embedded Domain Specific Language (EDSL) 自訂mapping規則
  5. High performance (http://code.google.com/p/modelmapper/wiki/Performance)

Unit Test to Struts2 Action with Spring

我將展示使用JUnit3和JUnit4兩種測試Struts2 Action的寫法,兩種都會enable Spring以及所有Struts2 plugins。

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一致。

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的寫法不需要這個動作。

ISO8601 and User Timezone in JavaScript

以下程式碼主要是參考:
  1. JavaScript and ISO 8601
  2. Getting the client's 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));
};
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();
};
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;
};
Number.prototype.format
Number.prototype.format = function(length) {
    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());
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/

Tuning Artifactory

通常我們會架設內部的Maven repository當做proxy來節省頻寬並加快下載速度,另一方面也將包好的jar / war上傳到repository讓大家使用,這邊記錄的是架設好Artifactory之後應該要調整的參數。
  1. Custom URL Base:自動產生Maven Settings裡的URL才會正確。


  2. Remote Repository Cache:預設是永久保留,但是太久沒使用的舊版library應該被清除。

  3. 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"));
    }

}

2013年1月29日 星期二

Data Driven Testing by JUnit Theories

JUnit Theories是另外一種實現Data Driven Testing的方式,它的強項在於它會自動排列組合所有@DataPoint和@DataPoints的data傳入test method裡面,然後我們可以搭配Assume過濾掉一些不合理的情境。

以下是一個使用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;
        }

    }
}

Data Driven Testing by JUnit Parameterized Tests

本文直接展示兩種unit test的寫法來比較JUnit Parameterized Tests的好處在哪。

首先是傳統的寫法:
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));
    }

}
執行結果:

接下來我們利用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));
    }

}
執行結果:

從執行結果我們可以發現每一筆資料都變成一個test case,而且可以從名稱上了解測試資料內容,這樣在test case failed的時候可以很清楚知道是哪筆資料出錯,從報表上就可以一目瞭然。

須注意的是@Parameters可以下name attribute是4.11版之後才支援。

2013年1月28日 星期一

統一滿漢大餐蔥燒牛肉麵

吃來吃去還是覺得統一滿漢大餐蔥燒牛肉麵最合我的口味,不吃牛的也有蔥燒豬肉麵,一樣好吃。


首日封上郵戳的分別

集郵集了超過20年,去年才知道原來首日封的郵戳有分總局和地方郵局蓋的,美觀度和紀念性是天與地的差別啊。


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。

但這樣還不夠好,原因是這樣的使用方式會讓DAO裡面充滿enable filter的程式碼,developer必須了解這些filter的規範才能正確使用,如果我們會依照使用者的身份決定condition的值,那麼與security有關的API就會汙染DAO,為此,我們利用Spring AOP的功能來完成enable filter dynamically的機制,讓interceptor來決定目前被攔截的DAO是否需要enable filter並傳入condition變數。

以下的程式碼分為兩大部分,interceptor and test case。

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);
    }

}
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負責。

Test Case包含四小部分:
  1. Filter Definition:package-info.java
  2. Filter:Person
  3. Implement Strategy:EnableFilterInfoProvider
  4. 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;
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;

}
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;
    }

}
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處理了。

最後我們看一下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=?
        )

JExcelApi Cell Mapper

本文將解說如何設計一組annotation和API,我命名為Cell Mapper,讓開發人員只要在POJO上面標記想要對應到Excel的欄位位置,就可以自動匯出Excel或是從Excel讀取資料,底層的library使用老牌的JExcelApi。

首先定義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保留彈性。

因為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);
    }

}

接下來定義兩個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();

}
@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();

}

然後我們做兩個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();
        }
    }

}
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);
    }

}

重頭戲,利用這組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;
    }

}
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();
    }

}

最後是一個簡單的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 + "]";
        }
    }

}

2013年1月23日 星期三

一騎札幌拉麵









台北市中山區中山北路二段180號1樓

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" />
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中抽離。

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,當然帳號、密碼是經過加密的。
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>
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>
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;
    }

}
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);
    }

}
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。