/*
 * This file is part of Nuts Framework.
 * Copyright (C) 2009 Nuts Develop Team.
 *
 * Nuts Framework is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License any later version.
 *
 * Nuts Framework is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Nuts Framework. If not, see <http://www.gnu.org/licenses/>.
 */
package nuts.exts.struts2.actions;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import nuts.core.collections.CollectionUtils;
import nuts.core.lang.ObjectUtils;
import nuts.core.lang.StringUtils;
import nuts.core.orm.dao.DataAccessException;
import nuts.core.orm.dao.ModelDAO;
import nuts.core.orm.dao.ModelMetaData;
import nuts.core.orm.dao.QueryParameter;
import nuts.core.orm.restriction.Orders;
import nuts.core.orm.restriction.Restrictions;
import nuts.core.servlet.HttpServletUtils;
import nuts.core.servlet.URLHelper;
import nuts.core.util.Filter;
import nuts.core.util.Pager;
import nuts.core.util.Sorter;
import nuts.exts.struts2.CookieStateProvider;
import nuts.exts.struts2.components.Property;
import nuts.exts.struts2.util.StrutsContextUtils;
import nuts.exts.xwork2.SessionStateProvider;
import nuts.exts.xwork2.StateProvider;
import nuts.exts.xwork2.util.ContextUtils;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ArrayUtils;

import com.opensymphony.xwork2.util.ValueStack;


/**
 * ModelDrivenAction
 * @param <T> data type
 * @param <E> example type
 */
@SuppressWarnings("serial")
public abstract class ModelDrivenAction<T, E extends QueryParameter> extends
		CommonDataAccessAction implements NutsTextConstants {

	/**
	 * DEFAULT_DATA_NAME = "d";
	 */
	public final static String DEFAULT_DATA_FIELD_NAME = "d";

	/**
	 * DEFAULT_DATA_LIST_NAME = "d";
	 */
	public final static String DEFAULT_DATA_LIST_FIELD_NAME = "ds";

	//------------------------------------------------------------
	// result name
	//------------------------------------------------------------
	/**
	 * RESULT_DEFAULT = "";
	 */
	public final static String RESULT_DEFAULT = "";
	
	/**
	 * RESULT_CONFIRM = "Confirm";
	 */
	public final static String RESULT_CONFIRM = "Confirm";
	
	/**
	 * RESULT_SUCCESS = "Success";
	 */
	public final static String RESULT_SUCCESS = "Success";
	
	/**
	 * STATE_LIST = "list";
	 */
	public final static String STATE_LIST = "list";
	
	//------------------------------------------------------------
	// scenario & result
	//------------------------------------------------------------
	private String actionScenario;
	private String methodResult = RESULT_DEFAULT;
	private String[] viewScenarios = { "view", "print", "delete" };

	//------------------------------------------------------------
	// parameters
	//------------------------------------------------------------
	private Pager pager = new Pager();
	private Sorter sorter = new Sorter();
	private Map<String, Filter> filters;

	private Boolean _load;
	private Boolean _save;

	//------------------------------------------------------------
	// config properties
	//------------------------------------------------------------
	private String dataFieldName = DEFAULT_DATA_FIELD_NAME;
	private String dataListFieldName = DEFAULT_DATA_LIST_FIELD_NAME;
	private boolean checkAbortOnError = false;
	private boolean updateSelective = false;
	private boolean clearPrimarys = true;
	private boolean clearIdentity = true;
	private Boolean listCountable;

	private String modelName;
	private ModelMetaData<T> modelMetaData;
	private ModelDAO<T, E> modelDAO;

	//------------------------------------------------------------
	// data properties
	//------------------------------------------------------------
	private T sourceData;
	private T data;
	private List<T> dataList;

	/**
	 * Constructor 
	 */
	public ModelDrivenAction() {
	}

	//--------------------------------------------------------
	// public parameters setter & getter shortcuts
	//--------------------------------------------------------
	/**
	 * @return pager
	 */
	public Pager getPg() {
		return getPager();
	}

	/**
	 * @param pager the pager to set
	 */
	public void setPg(Pager pager) {
		setPager(pager);
	}

	/**
	 * @return sorter
	 */
	public Sorter getSo() {
		return getSorter();
	}

	/**
	 * @param sorter the sorter to set
	 */
	public void setSo(Sorter sorter) {
		setSorter(sorter);
	}

	/**
	 * @return filters
	 */
	public Map<String, Filter> getFs() {
		return getFilters();
	}

	/**
	 * @param filters the filters to set
	 */
	public void setFs(Map<String, Filter> filters) {
		setFilters(filters);
	}

	//------------------------------------------------------------
	// public parameter setter & getter
	//------------------------------------------------------------
	/**
	 * @return the pager
	 */
	public Pager getPager() {
		return pager;
	}

	/**
	 * @param pager the pager to set
	 */
	public void setPager(Pager pager) {
		this.pager = pager;
	}

	/**
	 * @return the sorter
	 */
	public Sorter getSorter() {
		return sorter;
	}

	/**
	 * @param sorter the sorter to set
	 */
	public void setSorter(Sorter sorter) {
		this.sorter = sorter;
	}

	/**
	 * @return the filters
	 */
	public Map<String, Filter> getFilters() {
		return filters;
	}

	/**
	 * @param filters the filters to set
	 */
	public void setFilters(Map<String, Filter> filters) {
		this.filters = filters;
	}

	/**
	 * @return the load
	 */
	public Boolean get_load() {
		return _load;
	}

	/**
	 * @param load the load to set
	 */
	public void set_load(Boolean load) {
		this._load = load;
	}

	/**
	 * @return the save
	 */
	public Boolean get_save() {
		return _save;
	}

	/**
	 * @param save the save to set
	 */
	public void set_save(Boolean save) {
		this._save = save;
	}

	//------------------------------------------------------------
	// public properties getter
	//------------------------------------------------------------
	/**
	 * @return true if the result is input view
	 */
	public boolean isInputResult() {
		return isInputMethodResult() && !ArrayUtils.contains(viewScenarios, getActionScenario());
	}

	/**
	 * @return true if methodResult is Input
	 */
	public boolean isInputMethodResult() {
		return StringUtils.isEmpty(methodResult);
	}
	
	/**
	 * @return true if methodResult is Confirm
	 */
	public boolean isConfirmMethodResult() {
		return RESULT_CONFIRM.equals(methodResult);
	}
	
	/**
	 * @return true if methodResult is Success
	 */
	public boolean isSuccessMethodResult() {
		return RESULT_SUCCESS.equals(methodResult);
	}
	
	/**
	 * @return the methodResult
	 */
	public String getMethodResult() {
		return methodResult;
	}

	/**
	 * @return the action result
	 */
	public String getActionResult() {
		return getActionScenario() + getMethodResult();
	}

	/**
	 * @return the actionScenario
	 */
	public String getActionScenario() {
		if (actionScenario == null) {
			actionScenario = ContextUtils.getActionMethod();
			for (int i = 0; i < actionScenario.length(); i++) {
				if (Character.isUpperCase(actionScenario.charAt(i))) {
					actionScenario = actionScenario.substring(0, i);
					break;
				}
			}
		}
		return actionScenario;
	}

	//------------------------------------------------------------
	// protected getter & setter
	//------------------------------------------------------------
	/**
	 * @return the sourceData
	 */
	protected T getSourceData() {
		return sourceData;
	}

	/**
	 * @param sourceData the sourceData to set
	 */
	protected void setSourceData(T sourceData) {
		this.sourceData = sourceData;
	}

	/**
	 * @return the data
	 */
	protected T getData() {
		return data;
	}

	/**
	 * @param data the data to set
	 */
	protected void setData(T data) {
		this.data = data;
	}

	/**
	 * @return the dataList
	 */
	protected List<T> getDataList() {
		return dataList;
	}

	/**
	 * @param dataList the dataList to set
	 */
	protected void setDataList(List<T> dataList) {
		this.dataList = dataList;
	}

	/**
	 * @return dataName
	 */
	protected String getDataFieldName() {
		return dataFieldName;
	}

	/**
	 * @param dataName the dataName to set
	 */
	protected void setDataFieldName(String dataName) {
		this.dataFieldName = dataName;
	}

	/**
	 * @return the dataListFieldName
	 */
	protected String getDataListFieldName() {
		return dataListFieldName;
	}

	/**
	 * @param dataListFieldName the dataListFieldName to set
	 */
	protected void setDataListFieldName(String dataListFieldName) {
		this.dataListFieldName = dataListFieldName;
	}

	/**
	 * @return the updateSelective
	 */
	protected boolean isUpdateSelective() {
		return updateSelective;
	}

	/**
	 * @param updateSelective the updateSelective to set
	 */
	protected void setUpdateSelective(boolean updateSelective) {
		this.updateSelective = updateSelective;
	}

	/**
	 * @return the clearPrimarys
	 */
	public boolean isClearPrimarys() {
		return clearPrimarys;
	}

	/**
	 * @param clearPrimarys the clearPrimarys to set
	 */
	public void setClearPrimarys(boolean clearPrimarys) {
		this.clearPrimarys = clearPrimarys;
	}

	/**
	 * @return the clearIdentity
	 */
	protected boolean isClearIdentity() {
		return clearIdentity;
	}

	/**
	 * @param clearIdentity the clearIdentity to set
	 */
	protected void setClearIdentity(boolean clearIdentity) {
		this.clearIdentity = clearIdentity;
	}

	/**
	 * @return the checkAbortOnError
	 */
	protected boolean isCheckAbortOnError() {
		return checkAbortOnError;
	}

	/**
	 * @param checkAbortOnError the checkAbortOnError to set
	 */
	protected void setCheckAbortOnError(boolean checkAbortOnError) {
		this.checkAbortOnError = checkAbortOnError;
	}

	/**
	 * @return the listCountable
	 */
	protected Boolean getListCountable() {
		return listCountable;
	}

	protected void setListCountable(Boolean listCountable) {
		this.listCountable = listCountable;
	}

	/**
	 * @return the modelName
	 */
	protected String getModelName() {
		return modelName;
	}

	/**
	 * @param modelName modelName
	 */
	protected void setModelName(String modelName) {
		this.modelName = modelName;
	}
	
	/**
	 * @param methodResult the methodResult to set
	 */
	protected void setMethodResult(String methodResult) {
		this.methodResult = methodResult;
	}

	/**
	 * @return the modelMetaData
	 */
	protected ModelMetaData<T> getModelMetaData() {
		if (modelMetaData == null) {
			modelMetaData = getDataAccessSession().getMetaData(modelName);
		}
		return modelMetaData;
	}

	/**
	 * @param modelMetaData the modelMetaData to set
	 */
	protected void setModelMetaData(ModelMetaData<T> modelMetaData) {
		this.modelMetaData = modelMetaData;
	}

	/**
	 * @return the viewScenarios
	 */
	protected String[] getViewScenarios() {
		return viewScenarios;
	}

	/**
	 * @param viewScenarios the viewScenarios to set
	 */
	protected void setViewScenarios(String[] viewScenarios) {
		this.viewScenarios = viewScenarios;
	}

	/**
	 * @param actionScenario the actionScenario to set
	 */
	protected void setActionScenario(String actionScenario) {
		this.actionScenario = actionScenario;
	}

	//------------------------------------------------------------
	// dao methods
	//------------------------------------------------------------
	/**
	 * @return the modelDAO
	 */
	protected ModelDAO<T, E> getModelDAO() {
		if (modelDAO == null) {
			modelDAO = getDataAccessSession().getModelDAO(modelName);
		}
		return modelDAO;
	}

	/**
	 * @throws DataAccessException if a data access error occurs
	 */
	protected void daoCommit() throws DataAccessException {
		getDataAccessSession().commit();
	}

	
	/**
	 * @param modelDAO the modelDAO to set
	 */
	public void setModelDAO(ModelDAO<T, E> modelDAO) {
		this.modelDAO = modelDAO;
	}

	/**
	 * @throws DataAccessException if a data access error occurs
	 */
	protected void daoRollback() throws DataAccessException {
		getDataAccessSession().rollback();
	}

	/**
	 * daoCreateExample
	 * @return example instance
	 */
	protected E daoCreateExample() {
		return getModelDAO().createExample();
	}
	
	/**
	 * daoCountByExample
	 * 
	 * @param exp example
	 * @return count
	 * @throws DataAccessException if a data access error occurs
	 */ 
	protected int daoCountByExample(E exp) throws DataAccessException {
		return getModelDAO().countByExample(exp);
	}
	
	/**
	 * daoExists
	 * 
	 * @param key T
	 * @return T
	 * @throws DataAccessException if a data access error occurs
	 */ 
	protected boolean daoExists(T key) throws DataAccessException {
		return getModelDAO().exists(key);
	}

	/**
	 * daoSelectByPrimaryKey
	 * 
	 * @param key primary key
	 * @return data
	 * @throws DataAccessException if a data access error occurs
	 */ 
	protected T daoSelectByPrimaryKey(T key) throws DataAccessException {
		return getModelDAO().selectByPrimaryKey(key);
	}

	/**
	 * daoSelectByExample
	 * 
	 * @param exp example
	 * @return data list
	 * @throws Exception if an error occurs
	 */ 
	protected List<T> daoSelectByExample(E exp) throws Exception {
		return getModelDAO().selectByExample(exp);
	}

	/**
	 * daoInsert
	 * 
	 * @param data data
	 * @throws DataAccessException if a data access error occurs
	 */ 
	protected void daoInsert(T data) throws DataAccessException {
		getModelDAO().insert(data);
	}

	/**
	 * daoDeleteByPrimaryKey
	 * 
	 * @param key T
	 * @return count of deleted records
	 * @throws DataAccessException if a data access error occurs
	 */ 
	protected int daoDeleteByPrimaryKey(T key) throws DataAccessException {
		return getModelDAO().deleteByPrimaryKey(key);
	}

	/**
	 * daoDeleteByExample
	 * 
	 * @param exp example
	 * @return count of deleted records
	 * @throws DataAccessException if a data access error occurs
	 */ 
	protected int daoDeleteByExample(E exp) throws DataAccessException {
		return getModelDAO().deleteByExample(exp);
	}

	/**
	 * daoUpdateByPrimaryKey
	 * 
	 * @param data T
	 * @return count of updated records
	 * @throws DataAccessException if a data access error occurs
	 */ 
	protected int daoUpdateByPrimaryKey(T data) throws DataAccessException {
		return getModelDAO().updateByPrimaryKey(data);
	}

	/**
	 * daoUpdateByPrimaryKeySelective (ignore null properties)
	 * 
	 * @param data T
	 * @return count of updated records
	 * @throws DataAccessException if a data access error occurs
	 */ 
	protected int daoUpdateByPrimaryKeySelective(T data) throws DataAccessException {
		return getModelDAO().updateByPrimaryKeySelective(data);
	}

	/**
	 * daoUpdateByExample
	 * 
	 * @param data T
	 * @param exp example
	 * @return count of updated records
	 * @throws DataAccessException if a data access error occurs
	 */ 
	protected int daoUpdateByExample(T data, E exp) throws DataAccessException {
		return getModelDAO().updateByExample(data, exp);
	}

	/**
	 * daoUpdateByExampleSelective (ignore null properties)
	 * 
	 * @param data T
	 * @param exp example
	 * @return count of updated records
	 * @throws DataAccessException if a data access error occurs
	 */ 
	protected int daoUpdateByExampleSelective(T data, E exp) throws DataAccessException {
		return getModelDAO().updateByExampleSelective(data, exp);
	}

	//------------------------------------------------------------
	// internal methods
	//------------------------------------------------------------
	/**
	 * resolveColumnName
	 * @param name property name
	 * @return column name
	 */
	protected String resolveColumnName(String name) {
		return getModelMetaData().getColumnName(name);
	}

	/**
	 * resolveColumnAlias
	 * @param name field name
	 * @return column alias
	 */
	protected String resolveColumnAlias(String name) {
		return getModelMetaData().getColumnAlias(name);
	}

	private void removeRedundantParams(Map params) {
		List keys = new ArrayList();
		Set<Entry> s = params.entrySet();
		for (Entry e : s) {
			if (e.getKey() != null) {
				if (e.getKey().toString().startsWith("_") 
						|| ObjectUtils.isEmpty(e.getValue())) {
					keys.add(e.getKey());
				}
			}
		}
		for (Object key : keys) {
			params.remove(key);
		}
	}

	/**
	 * load list parameters
	 * @return false - send redirect url
	 */
	@SuppressWarnings("unchecked")
	protected boolean loadListParameters() throws Exception {
		StateProvider sp = getStateProvider();
		if (sp instanceof SessionStateProvider) {
			Map<String, Object> pm = (Map<String, Object>)sp.loadState(STATE_LIST);
			if (pm != null) {
				pager = (Pager)pm.get("pager");
				sorter = (Sorter)pm.get("sorter");
				filters = (Map<String, Filter>)pm.get("filters");
			}
		}
		else if (sp instanceof CookieStateProvider) {
			String qs = (String)sp.loadState(STATE_LIST);
			if (StringUtils.isNotBlank(qs)) {
				Map params = URLHelper.parseQueryString(qs);
				removeRedundantParams(params);
				if (CollectionUtils.isNotEmpty(params)) {
					HttpServletRequest request = StrutsContextUtils.getServletRequest();
					HttpServletResponse response = StrutsContextUtils.getServletResponse();
					String url = URLHelper.buildURL(request, params, false);
					HttpServletUtils.sendRedirect(response, url);
					return false;
				}
			}
		}
		return true;
	}
	
	/**
	 * save list parameters
	 * @throws Exception exception
	 */
	protected void saveListParameters() throws Exception {
		StateProvider sp = getStateProvider();
		if (sp instanceof SessionStateProvider) {
			Map<String, Object> pm = new HashMap<String, Object>();
			pm.put("pager", pager);
			pm.put("sorter", sorter);
			pm.put("filters", filters);
			sp.saveState(STATE_LIST, pm);
		}
		else if (sp instanceof CookieStateProvider) {
			sp.saveState(STATE_LIST, getListParametersString());
		}
	}

	/**
	 * @return list parameters string
	 */
	public String getListParametersString() {
		Map<String, Object> params = getListParameters();
		StringBuilder qs = new StringBuilder();
		URLHelper.buildParametersString(params, qs);
		if (qs.length() > 0 && qs.charAt(0) == '?') {
			qs.delete(0, 1);
		}
		return qs.toString();
	}
	
	/**
	 * @return list parameters string
	 */
	public Map<String, Object> getListParameters() {
		Map<String, Object> params = new HashMap<String, Object>();
		if (pager.getStart() != null) {
			params.put("pg.s", pager.getStart());
		}
		if (pager.getLimit() != null) {
			params.put("pg.l", pager.getLimit());
		}
		
		if (StringUtils.isNotBlank(sorter.getColumn())) {
			params.put("so.c", sorter.getColumn());
		}
		if (StringUtils.isNotBlank(sorter.getDirection())) {
			params.put("so.d", sorter.getDirection());
		}
		
		if (CollectionUtils.isNotEmpty(filters)) {
			ValueStack stack = ContextUtils.getValueStack();
			for (Entry<String, Filter> e : filters.entrySet()) {
				Filter f = e.getValue();
				if (f != null && CollectionUtils.isNotEmpty(f.getValues())) {
					String prex = "fs." + e.getKey() + "."; 
					params.put(prex + "c", f.getComparator());
					
					List<?> vs = f.getValues();
					List<String> cvs = new ArrayList<String>(vs.size());
					for (Object v : vs) {
						if (v != null) {
							stack.push(v);
							try {
								cvs.add((String)stack.findValue("top", String.class));
							}
							finally {
								stack.pop();
							}
						}
						else {
							cvs.add("");
						}
					}
					params.put(prex + f.getValueType() + "vs", cvs);
				}
			}
		}

		return params;
	}

	/**
     * @return "success"
     */
	@Override
    public String getInputResultName() {
    	return SUCCESS;
    }

	/**
	 * getMessage
	 * @param msg msg id
	 * @return message string
	 */
	protected String getMessage(String msg) {
		return getText(msg);
	}
	
	/**
	 * getMessage
	 * @param msg msg id
	 * @param params parameters
	 * @return message string
	 */
	protected String getMessage(String msg, String[] params) {
		return getText(msg, params);
	}

	//------------------------------------------------------------
	// do method
	//------------------------------------------------------------
	/**
	 * doViewInput 
	 * @return SUCCESS
	 * @throws Exception if an error occurs
	 */
	protected String doViewInput() throws Exception {
		return SUCCESS;
	}

	/**
	 * doViewSelect
	 * @return SUCCESS
	 * @throws Exception if an error occurs
	 */
	protected String doViewSelect() throws Exception {
		T d = selectData(data);
		if (d != null) {
			data = d;
		}
		return SUCCESS;
	}

	/**
	 * doInsertClear
	 * @return SUCCESS
	 * @throws Exception if an error occurs
	 */
	protected String doInsertClear() throws Exception {
		data = prepareDefaultData(null);
		return SUCCESS;
	}

	/**
	 * doInsertSelect
	 * @return SUCCESS
	 * @throws Exception if an error occurs
	 */
	protected String doInsertSelect() throws Exception {
		T d = selectData(data);
		if (d != null) {
			data = d;
			clearOnCopy(d);
		}
		return SUCCESS;
	}

	/**
	 * doInsertInput
	 * @return SUCCESS
	 * @throws Exception if an error occurs
	 */
	protected String doInsertInput() throws Exception {
		clearOnCopy(data);
		data = prepareDefaultData(data);
		return SUCCESS;
	}

	/**
	 * doInsertConfirm
	 * @return SUCCESS
	 * @throws Exception if an error occurs
	 */
	protected String doInsertConfirm() throws Exception {
		if (checkOnInsert(data)) {
			addActionConfirm(getMessage(ACTION_CONFIRM_PREFIX + getActionScenario()));
			setMethodResult(RESULT_CONFIRM);
		}
		else {
			if (!hasErrors()) {
				setMethodResult(RESULT_CONFIRM);
			}
		}
		return SUCCESS;
	}

	/**
	 * doInsertExecute
	 * @return SUCCESS
	 * @throws Exception if an error occurs
	 */
	protected String doInsertExecute() throws Exception {
		if (checkOnInsert(data)) {
			try {
				startInsert(data);
				insertData(data);
				commitInsert(data);
				
				addActionMessage(getMessage(ACTION_SUCCESS_PREFIX + getActionScenario()));
				setMethodResult(RESULT_SUCCESS);
			}
			catch (Throwable e) {
				log.error(e.getMessage(), e);

				addActionError(getMessage(ACTION_FAILED_PREFIX + getActionScenario(), 
					new String[] { e.getMessage() }));

				rollbackInsert(data);
			}
		}
		else {
			if (!hasErrors() && getTextAsBoolean(UI_INPUT_CONFIRM, false)) {
				setMethodResult(RESULT_CONFIRM);
			}
		}
		return SUCCESS;
	}

	/**
	 * doUpdateInput
	 * @return SUCCESS
	 * @throws Exception if an error occurs
	 */
	protected String doUpdateInput() throws Exception {
		return SUCCESS;
	}

	/**
	 * doUpdateSelect
	 * @return SUCCESS
	 * @throws Exception if an error occurs
	 */
	protected String doUpdateSelect() throws Exception {
		T d = selectData(data);
		if (d != null) {
			data = d;
		}
		return SUCCESS;
	}

	/**
	 * doUpdateConfirm
	 * @return SUCCESS
	 * @throws Exception if an error occurs
	 */
	protected String doUpdateConfirm() throws Exception {
		sourceData = selectData(data);
		if (sourceData != null) {
			if (checkOnUpdate(data, sourceData)) {
				addActionConfirm(getMessage(ACTION_CONFIRM_PREFIX + getActionScenario()));
				setMethodResult(RESULT_CONFIRM);
			}
			else {
				if (!hasErrors()) {
					setMethodResult(RESULT_CONFIRM);
				}
			}
		}
		return SUCCESS;
	}

	/**
	 * doUpdateExecute
	 * @return SUCCESS
	 * @throws Exception if an error occurs
	 */
	protected String doUpdateExecute() throws Exception {
		sourceData = selectData(data);
		if (sourceData != null) {
			if (checkOnUpdate(data, sourceData)) {
				try {
					startUpdate(data, sourceData);
					updateData(data, sourceData);
					commitUpdate(data, sourceData);
					
					addActionMessage(getMessage(ACTION_SUCCESS_PREFIX + getActionScenario()));
					setMethodResult(RESULT_SUCCESS);
				}
				catch (Exception e) {
					log.error(e.getMessage(), e);

					addActionError(getMessage(ACTION_FAILED_PREFIX + getActionScenario(), 
						new String[] { e.getMessage() }));

					rollbackUpdate(data, sourceData);
				}
			}
			else {
				if (!hasErrors() && getTextAsBoolean(UI_INPUT_CONFIRM, false)) {
					setMethodResult(RESULT_CONFIRM);
				}
			}
		}
		return SUCCESS;
	}

	/**
	 * doDeleteSelect
	 * @return SUCCESS
	 * @throws Exception if an error occurs
	 */
	protected String doDeleteSelect() throws Exception {
		sourceData = selectData(data);
		if (sourceData != null) {
			if (checkOnDelete(data, sourceData)) {
				addActionConfirm(getMessage(ACTION_CONFIRM_PREFIX + getActionScenario()));
			}
			data = sourceData;
		}
		return SUCCESS;
	}

	/**
	 * doDeleteExecute
	 * @return SUCCESS
	 * @throws Exception if an error occurs
	 */
	protected String doDeleteExecute() throws Exception {
		sourceData = selectData(data);
		if (sourceData != null) {
			if (checkOnDelete(data, sourceData)) {
				data = sourceData;
				try {
					startDelete(data);
					deleteData(data);
					commitDelete(data);
					
					addActionMessage(getMessage(ACTION_SUCCESS_PREFIX + getActionScenario()));
					setMethodResult(RESULT_SUCCESS);
				}
				catch (Exception e) {
					log.error(e.getMessage(), e);

					addActionError(getMessage(ACTION_FAILED_PREFIX + getActionScenario(), 
						new String[] { e.getMessage() }));

					rollbackDelete(data);
				}
			}
			else {
				data = sourceData;
			}
		}

		return SUCCESS;
	}

	/**
	 * doBulkUpdateSelect
	 * @return SUCCESS
	 * @throws Exception if an error occurs
	 */
	protected String doBulkUpdateSelect() throws Exception {
		dataList = selectDataList(dataList);
		if (CollectionUtils.isNotEmpty(dataList)) {
			if (checkOnBulkUpdate(dataList)) {
				addActionConfirm(getMessage(ACTION_CONFIRM_PREFIX + getActionScenario(), 
						new String[] { String.valueOf(dataList.size()) }));
			}
		}
		return SUCCESS;
	}

	/**
	 * doBulkUpdateExecute
	 * @return SUCCESS
	 * @throws Exception if an error occurs
	 */
	protected String doBulkUpdateExecute() throws Exception {
		dataList = selectDataList(dataList);

		if (CollectionUtils.isNotEmpty(dataList) && checkOnBulkUpdate(dataList)) {
			try {
				startBulkUpdate(dataList);

				T sample = getBulkUpdateSample(dataList);
				
				int count = updateDataList(dataList, sample);
				
				commitBulkUpdate(dataList);
				
				if (count == dataList.size()) {
					addActionMessage(getMessage(ACTION_SUCCESS_PREFIX + getActionScenario(), 
							new String[] { String.valueOf(count) }));
				}
				else {
					addActionWarning(getMessage(ACTION_SUCCESS_PREFIX + getActionScenario(), 
							new String[] { String.valueOf(count) }));
				}
				setMethodResult(RESULT_SUCCESS);
			}
			catch (Exception e) {
				log.error(e.getMessage(), e);

				addActionError(getMessage(ACTION_FAILED_PREFIX + getActionScenario(), 
					new String[] { e.getMessage() }));

				rollbackBulkUpdate(dataList);
			}
		}
		return SUCCESS;
	}

	/**
	 * doBulkDeleteSelect
	 * @return SUCCESS
	 * @throws Exception if an error occurs
	 */
	protected String doBulkDeleteSelect() throws Exception {
		dataList = selectDataList(dataList);
		if (CollectionUtils.isNotEmpty(dataList)) {
			if (checkOnBulkDelete(dataList)) {
				addActionConfirm(getMessage(ACTION_CONFIRM_PREFIX + getActionScenario(), 
					new String[] { String.valueOf(dataList.size()) }));
			}
		}
		return SUCCESS;
	}

	/**
	 * doBulkDeleteExecute
	 * @return SUCCESS
	 * @throws Exception if an error occurs
	 */
	protected String doBulkDeleteExecute() throws Exception {
		dataList = selectDataList(dataList);

		if (CollectionUtils.isNotEmpty(dataList) && checkOnBulkDelete(dataList)) {
			try {
				startBulkDelete(dataList);
				int count = deleteDataList(dataList);
				commitBulkDelete(dataList);
				
				if (count == dataList.size()) {
					addActionMessage(getMessage(ACTION_SUCCESS_PREFIX + getActionScenario(), 
							new String[] { String.valueOf(count) }));
				}
				else {
					addActionWarning(getMessage(ACTION_SUCCESS_PREFIX + getActionScenario(), 
							new String[] { String.valueOf(count) }));
				}
				setMethodResult(RESULT_SUCCESS);
			}
			catch (Exception e) {
				log.error(e.getMessage(), e);

				addActionError(getMessage(ACTION_FAILED_PREFIX + getActionScenario(), 
					new String[] { e.getMessage() }));

				rollbackBulkDelete(dataList);
			}
		}
		return SUCCESS;
	}

	/**
	 * 
	 * @return true - if need load list parameters
	 */
	protected boolean isNeedLoadListParameters() {
		if (_load == null) {
			return CollectionUtils.isEmpty(getParameters());
		}
		return _load;
	}
	
	/**
	 * doList
	 * @return SUCCESS
	 * @throws Exception if an error occurs
	 */
	protected String doList() throws Exception {
		if (isNeedLoadListParameters()) {
			if (!loadListParameters()) {
				return NONE;
			}
		}

		queryList();
		
		if (Boolean.TRUE.equals(_save)) {
			saveListParameters();
		}
		return SUCCESS;
	}

	//------------------------------------------------------------
	// list methods
	//------------------------------------------------------------
	/**
	 * queryList
	 */
	protected void queryList() throws Exception {
		E exp = daoCreateExample();

		addQueryToExample(exp);
		addOrderToExample(exp);

		queryListByExample(exp);
	}

	/**
	 * queryListByExample
	 * @param exp example
	 */
	protected void queryListByExample(E exp) throws Exception {
		addLimitToPager(DEFAULT_LIST_PAGER_LIMIT);
		
		if (listCountable == null) {
			listCountable = getTextAsBoolean(UI_LIST_COUNTABLE, true);
		}
		if (listCountable) {
			pager.setTotal(daoCountByExample(exp));
			pager.normalize();
			if (pager.getTotal() > 0) {
				exp.setStart(pager.getStart());
				exp.setLimit(pager.getLimit());
				dataList = daoSelectByExample(exp);
			}
			else {
				dataList = new ArrayList<T>();
			}
		}
		else {
			exp.setStart(pager.getStart());
			exp.setLimit(pager.getLimit());
			dataList = daoSelectByExample(exp);
		}
	}

	/**
	 * @param def default limit
	 */
	protected void addLimitToPager(int def) {
		if (pager.getLimit() == null || pager.getLimit() < 1) {
			String tx = ContextUtils.getActionMethod() + PAGER_LIMIT_SUFFIX;
			Integer l = getTextAsInt(tx);
			if (l == null && !LIST_PAGER_LIMIT.equals(tx)) {
				l = getTextAsInt(LIST_PAGER_LIMIT);
			}
			if (l == null) {
				l = def;
			}
			pager.setLimit(l);
		}

		int maxLimit = getTextAsInt(PAGER_MAX_LIMIT, 100);
		if (pager.getLimit() == null 
				|| pager.getLimit() < 1 
				|| pager.getLimit() > maxLimit) {
			pager.setLimit(maxLimit);
		}
	}

	/**
	 * addQueryToExample
	 * @param exp E
	 */
	protected void addQueryToExample(E exp) throws Exception {
		Restrictions wc = exp.getRestrictions();

		Filter.normalize(filters);
		if (CollectionUtils.isNotEmpty(filters)) {
			for (Entry<String, Filter> e : filters.entrySet()) {
				Filter f = e.getValue();
				if (f == null) {
					continue;
				}

				List<?> values = f.getValues();
				if (values == null || values.isEmpty()) {
					continue;
				}

				Object value = values.get(0);
				if (value == null
						&& !Filter.IN.equals(f.getComparator())
						&& !Filter.BETWEEN.equals(f.getComparator())) {
					continue;
				}

				String name = StringUtils.isEmpty(f.getName()) ? e.getKey() : f.getName();
				String column = resolveColumnName(name);
				if (column == null) {
					continue;
				}

				if (Filter.EQUAL.equals(f.getComparator())) {
					wc.equalTo(column, value);
				}
				else if (Filter.NOT_EQUAL.equals(f.getComparator())) {
					wc.notEqualTo(column, value);
				}
				else if (Filter.GREATER_THAN.equals(f.getComparator())) {
					wc.greaterThan(column, value);
				}
				else if (Filter.GREATER_EQUAL.equals(f.getComparator())) {
					wc.greaterThanOrEqualTo(column, value);
				}
				else if (Filter.LESS_THAN.equals(f.getComparator())) {
					wc.lessThan(column, value);
				}
				else if (Filter.LESS_EQUAL.equals(f.getComparator())) {
					wc.lessThanOrEqualTo(column, value);
				}
				else if (Filter.LIKE.equals(f.getComparator())) {
					wc.like(column, value);
				}
				else if (Filter.NOT_LIKE.equals(f.getComparator())) {
					wc.notLike(column, value);
				}
				else if (Filter.MATCH.equals(f.getComparator())) {
					wc.match(column, value);
				}
				else if (Filter.LEFT_MATCH.equals(f.getComparator())) {
					wc.leftMatch(column, value);
				}
				else if (Filter.RIGHT_MATCH.equals(f.getComparator())) {
					wc.rightMatch(column, value);
				}
				else if (Filter.IN.equals(f.getComparator())) {
					wc.in(column, values);
				}
				else if (Filter.NOT_IN.equals(f.getComparator())) {
					wc.notIn(column, values);
				}
				else if (Filter.BETWEEN.equals(f.getComparator())) {
					Object v1 = values.get(0);
					Object v2 = values.size() > 1 ? values.get(1) : null;

					if (v1 == null && v2 == null) {
					}
					else if (v1 == null) {
						wc.lessThanOrEqualTo(column, v2);
					}
					else if (v2 == null) {
						wc.greaterThanOrEqualTo(column, v1);
					}
					else {
						wc.between(column, v1, v2);
					}
				}
			}
		}
	}

	/**
	 * addOrderToExample
	 * @param exp E
	 */
	protected void addOrderToExample(E exp) throws Exception {
		if (StringUtils.isEmpty(sorter.getColumn())) {
			String tx = ContextUtils.getActionMethod() + SORTER_COLUMN_SUFFIX;
			String sc = getText(tx, (String)null);
			if (sc == null && !LIST_SORTER_COLUMN.equals(tx)) {
				sc = getText(LIST_SORTER_COLUMN, (String)null);
			}
			if (StringUtils.isNotEmpty(sc)) {
				sorter.setColumn(sc);
			}
		}
		if (StringUtils.isEmpty(sorter.getDirection())) {
			String tx = ContextUtils.getActionMethod() + SORTER_DIRECTION_SUFFIX;
			String sd = getText(tx, (String)null);
			if (sd == null && !LIST_SORTER_DIRECTION.equals(tx)) {
				sd = getText(LIST_SORTER_DIRECTION, (String)null);
			}
			if (StringUtils.isNotEmpty(sd)) {
				sorter.setDirection(sd);
			}
		}
		if (!StringUtils.isEmpty(sorter.getColumn())) {
			if (StringUtils.isEmpty(sorter.getDirection())) {
				sorter.setDirection(Orders.ASC);
			}
			exp.getOrders().addOrder(resolveColumnAlias(sorter.getColumn()), sorter.getDirection());
		}
	}

	//------------------------------------------------------------
	// select methods
	//------------------------------------------------------------
	/**
	 * selectData
	 * @param data data
	 * @return data data found
	 * @throws Exception if an error occurs
	 */
	protected T selectData(T data) throws Exception {
		T d = daoSelectByPrimaryKey(data);
		if (d == null) {
			addActionError(getMessage(ERROR_DATA_NOTFOUND));
		}
		return d;
	}

	/**
	 * checkCommon
	 * @param data data
	 * @param srcData source data (null on insert)
	 * @return true if do something success
	 * @throws Exception if an error occurs
	 */
	protected boolean checkCommon(T data, T srcData) throws Exception {
		return true;
	}

	//------------------------------------------------------------
	// insert methods
	//------------------------------------------------------------
	/**
	 * prepareDefaultData
	 * @param data data
	 * @return data
	 */
	protected T prepareDefaultData(T data) throws Exception {
		return data;
	}
	
	/**
	 * insert data
	 * @param data data
	 * @throws Exception if an error occurs
	 */
	protected void insertData(T data) throws Exception {
		daoInsert(data);
	}

	/**
	 * checkOnInsert
	 * @param data data
	 * @return true if check success
	 * @throws Exception if an error occurs
	 */
	protected boolean checkOnInsert(T data) throws Exception {
		boolean c = true;

		if (!checkCommon(data, null)) {
			c = false;
			if (checkAbortOnError) {
				return false;
			}
		}
		
		if (!checkPrimaryKeyOnInsert(data)) {
			c = false;
			if (checkAbortOnError) {
				return false;
			}
		}
		if (!checkUniqueKeyOnInsert(data)) {
			c = false;
			if (checkAbortOnError) {
				return false;
			}
		}
		if (!checkForeignKey(data)) {
			c = false;
			if (checkAbortOnError) {
				return false;
			}
		}
		return c;
	}

	/**
	 * startInsert
	 * @param data data
	 * @throws Exception if an error occurs
	 */
	protected void startInsert(T data) throws Exception {
	}

	/**
	 * commitInsert
	 * @param data data
	 * @throws Exception if an error occurs
	 */
	protected void commitInsert(T data) throws Exception {
		daoCommit();
	}

	/**
	 * rollbackInsert
	 * @param data data
	 * @throws Exception if an error occurs
	 */
	protected void rollbackInsert(T data) throws Exception {
		daoRollback();
	}

	//------------------------------------------------------------
	// update methods
	//------------------------------------------------------------
	/**
	 * update data
	 * @param data data
	 * @param srcData source data
	 * @return update count
	 * @throws Exception if an error occurs
	 */
	protected int updateData(T data, T srcData) throws Exception {
		int cnt;
		if (updateSelective) {
			cnt = daoUpdateByPrimaryKeySelective(data);
		}
		else {
			cnt = daoUpdateByPrimaryKey(data);
		}
		if (cnt != 1) {
			throw new RuntimeException("The update data count (" + cnt + ") does not equals 1.");
		}
		return cnt;
	}

	/**
	 * checkOnUpdate
	 * @param data data
	 * @param srcData srcData
	 * @return true if check success
	 * @throws Exception if an error occurs
	 */
	protected boolean checkOnUpdate(T data, T srcData) throws Exception {
		boolean c = true;

		if (!checkCommon(data, srcData)) {
			c = false;
			if (checkAbortOnError) {
				return false;
			}
		}
		
		if (!checkUpdatedOnUpdate(data, srcData)) {
			c = false;
			if (checkAbortOnError) {
				return false;
			}
		}
		// primary key can not be modified or null
		if (!checkPrimaryKeyOnUpdate(data, srcData)) {
			c = false;
			if (checkAbortOnError) {
				return false;
			}
		}
		if (!checkUniqueKeyOnUpdate(data, srcData)) {
			c = false;
			if (checkAbortOnError) {
				return false;
			}
		}
		if (!checkForeignKey(data)) {
			c = false;
			if (checkAbortOnError) {
				return false;
			}
		}
		return c;
	}

	/**
	 * startUpdate
	 * @param data data
	 * @param srcData srcData
	 * @throws Exception if an error occurs
	 */
	protected void startUpdate(T data, T srcData) throws Exception {
	}

	/**
	 * commitUpdate
	 * @param data data
	 * @param srcData srcData
	 * @throws Exception if an error occurs
	 */
	protected void commitUpdate(T data, T srcData) throws Exception {
		daoCommit();
	}

	/**
	 * rollbackUpdate
	 * @param data data
	 * @param srcData srcData
	 * @throws Exception if an error occurs
	 */
	protected void rollbackUpdate(T data, T srcData) throws Exception {
		daoRollback();
	}

	//------------------------------------------------------------
	// delete methods
	//------------------------------------------------------------
	/**
	 * checkOnDelete
	 * @param data data
	 * @param srcData srcData
	 * @return true if check success
	 * @throws Exception if an error occurs
	 */
	protected boolean checkOnDelete(T data, T srcData) throws Exception {
		boolean c = true;
		
		if (!checkCommon(data, srcData)) {
			c = false;
			if (checkAbortOnError) {
				return false;
			}
		}
		
		if (!checkUpdatedOnDelete(data, srcData)) {
			c = false;
			if (checkAbortOnError) {
				return false;
			}
		}

		return c;
	}

	/**
	 * startDelete(T)
	 * @param data data
	 * @throws Exception if an error occurs
	 */
	protected void startDelete(T data) throws Exception {
	}

	/**
	 * commitDelete
	 * @param data data
	 * @throws Exception if an error occurs
	 */
	protected void commitDelete(T data) throws Exception {
		daoCommit();
	}

	/**
	 * rollbackDelete
	 * @param data data
	 * @throws Exception if an error occurs
	 */
	protected void rollbackDelete(T data) throws Exception {
		daoRollback();
	}

	/**
	 * delete data
	 * @param data data
	 * @throws Exception if an error occurs
	 */
	protected void deleteData(T data) throws Exception {
		int cnt = daoDeleteByPrimaryKey(data);
		if (cnt != 1) {
			throw new RuntimeException("The deleted data count (" + cnt + ") does not equals 1.");
		}
	}

	//------------------------------------------------------------
	// bulk methods
	//------------------------------------------------------------
	/**
	 * addKeyListToExample
	 * @param exp example
	 * @param dataList data list
	 * @return count
	 */
	protected int addKeyListToExample(E exp, List<T> dataList, boolean raiseError) throws Exception {
		ModelMetaData mmd = getModelMetaData(); 
		String[] keys = mmd.getPrimaryKeys();
		String[] cns = mmd.getPrimaryKeyColumnNames();

		if (keys.length == 1) {
			List<Object> vs = new ArrayList<Object>();
			for (int n = 0; n < dataList.size(); n++) {
				T d = dataList.get(n);
				Object v = mmd.getPropertyValue(d, keys[0]);
				if (v != null) {
					vs.add(v);
				}
				else {
					if (raiseError) {
						throw new RuntimeException("The item[" + n + "] has empty primary key value. (" + d + ")");
					}
				}
			}

			Restrictions wc = exp.getRestrictions();
			wc.in(cns[0], vs);

			return vs.size();
		}
		else if (keys.length > 1) {
			Restrictions wc = exp.getRestrictions();
			wc.setConjunctionToOr();
			
			int count = 0;
			for (int n = 0; n < dataList.size(); n++) {
				T d = dataList.get(n);
				Object[] vs = new Object[keys.length]; 
				for (int i = 0; i < keys.length; i++) {
					Object v = mmd.getPropertyValue(d, keys[i]);
					if (v != null) {
						vs[i] = v;
					}
					else {
						if (raiseError) {
							throw new RuntimeException("The item[" + n + "] has empty primary key value. (" + d + ")");
						}
						vs = null;
						break;
					}
				}
				if (vs != null) {
					wc.open();
					wc.setConjunctionToAnd();
					for (int i = 0; i < cns.length; i++) {
						wc.equalTo(cns[i], vs[i]);
					}
					wc.close();
					wc.setConjunctionToOr();
					count++;
				}
			}
			return count;
		}
		else {
			return 0;
		}
	}

	/**
	 * selectDataList
	 * @param dataList dataList
	 * @return dataList
	 */
	protected List<T> selectDataList(List<T> dataList) throws Exception {
		CollectionUtils.removeNull(dataList);
		if (dataList != null && dataList.size() > 0) {
			E exp = daoCreateExample();

			int count = addKeyListToExample(exp, dataList, false);
			if (count > 0) {
				dataList = daoSelectByExample(exp);
			}
			else {
				dataList = null;
			}
		}
		if (dataList == null || dataList.size() < 1) {
			addActionError(getMessage(ERROR_DATA_LIST_EMPTY));
		}
		return dataList;
	}
	
	//------------------------------------------------------------
	// bulk update methods
	//------------------------------------------------------------
	/**
	 * startBulkUpdate
	 * @param dataList data list
	 * @throws Exception if an error occurs
	 */
	protected void startBulkUpdate(List<T> dataList) throws Exception {
	}
	
	/**
	 * commitBulkUpdate
	 * @param dataList data list
	 * @param bid bulk id
	 * @throws Exception if an error occurs
	 */
	protected void commitBulkUpdate(List<T> dataList) throws Exception {
		daoCommit();
	}
	
	/**
	 * rollbackBulkUpdate
	 * @param dataList data list
	 * @throws Exception if an error occurs
	 */
	protected void rollbackBulkUpdate(List<T> dataList) throws Exception {
		daoRollback();
	}

	/**
	 * checkOnBulkUpdate
	 * @param dataList data list
	 * @return true to continue update
	 * @throws Exception if an error occurs
	 */
	protected boolean checkOnBulkUpdate(List<T> dataList) throws Exception {
		return true;
	}
	
	/**
	 * getBulkUpdateSample
	 * @param dataList data list
	 * @return sample data
	 * @throws Exception if an error occurs
	 */
	protected T getBulkUpdateSample(List<T> dataList) throws Exception {
		return null;
	}
	
	/**
	 * update data list
	 * @param dataList data list
	 * @param sample sample data
	 * @return updated count
	 * @throws Exception if an error occurs
	 */
	protected int updateDataList(List<T> dataList, T sample) throws Exception {
		int cnt = 0;
		if (dataList != null && dataList.size() > 0) {
			E exp = daoCreateExample();

			addKeyListToExample(exp, dataList, true);
			cnt = daoUpdateByExampleSelective(sample, exp);

			// update input data list
			ModelMetaData mmd = getModelMetaData();
			for (T d : dataList) {
				Map vs = mmd.getDataProperties(sample);
				CollectionUtils.removeNull(vs);
				mmd.setDataProperties(d, vs);
			}
//			if (cnt != dataList.size()) {
//				throw new RuntimeException("The updated data count (" + cnt + ") does not equals dataList.size(" + dataList.size() + ").");
//			}
		}
		return cnt;
	}

	//------------------------------------------------------------
	// bulk delete methods
	//------------------------------------------------------------
	/**
	 * startBulkDelete
	 * @param dataList data list
	 * @throws Exception if an error occurs
	 */
	protected void startBulkDelete(List<T> dataList) throws Exception {
	}
	
	/**
	 * commitBulkDelete
	 * @param dataList data list
	 * @throws Exception if an error occurs
	 */
	protected void commitBulkDelete(List<T> dataList) throws Exception {
		daoCommit();
	}
	
	/**
	 * rollbackBulkDelete
	 * @param dataList data list
	 * @throws Exception if an error occurs
	 */
	protected void rollbackBulkDelete(List<T> dataList) throws Exception {
		daoRollback();
	}
	
	/**
	 * checkOnBulkDelete
	 * @param dataList data list
	 * @return true to continue delete
	 * @throws Exception if an error occurs
	 */
	protected boolean checkOnBulkDelete(List<T> dataList) throws Exception {
		return true;
	}
	
	/**
	 * delete data list
	 * @param dataList data list
	 * @return deleted count
	 * @throws Exception if an error occurs
	 */
	protected int deleteDataList(List<T> dataList) throws Exception {
		int cnt = 0;
		if (dataList != null && dataList.size() > 0) {
			E exp = daoCreateExample();

			addKeyListToExample(exp, dataList, true);
			cnt = daoDeleteByExample(exp);
//			if (cnt != dataList.size()) {
//				throw new RuntimeException("The deleted data count (" + cnt + ") does not equals dataList.size(" + dataList.size() + ").");
//			}
		}
		return cnt;
	}

	//------------------------------------------------------------
	// check methods
	//------------------------------------------------------------
	protected void addDataDuplateError(T data, String[] pns) {
		ModelMetaData mmd = getModelMetaData();

		StringBuilder sb = new StringBuilder();
		for (String pn : pns) {
//			addFieldError(getModelFieldName(pn), getMessage(ERROR_FIELDVALUE_DUPLICATE));
			addFieldError(getDataFieldName(pn), "");

			sb.append(getText(getDataFieldName(pn)));
			sb.append(": ");
			
			
			Property ptag = new Property(ContextUtils.getValueStack());
			try {
				ptag.setValue(mmd.getPropertyValue(data, pn));
				ptag.setEscape(null);
				sb.append(ptag.formatValue());
			}
			finally {
				ptag.getComponentStack().pop();
			}
			
			sb.append(IOUtils.LINE_SEPARATOR);
		}

		addActionError(getMessage(ERROR_DATA_DUPLICATE, new String[] { sb.toString() }));
	}

	/**
	 * checkPrimaryKeyOnInsert
	 * @param data data
	 * @return true if check successfully
	 * @throws Exception if an error occurs
	 */
	protected boolean checkPrimaryKeyOnInsert(T data) throws Exception {
		ModelMetaData mmd = getModelMetaData();
		String in = mmd.getIdentityName();
		if (StringUtils.isEmpty(in)) {
			String[] pks = mmd.getPrimaryKeys();

			boolean hasNull = false;

			E exp = daoCreateExample();
			Restrictions wc = exp.getRestrictions();
			wc.setConjunctionToAnd();

			for (String pk : pks) {
				Object dv = mmd.getPropertyValue(data, pk);
				if (dv == null) {
					hasNull = true;
//					addFieldError(getModelFieldName(pk), getMessage(ERROR_FIELDVALUE_REQUIRED));
				}
				else {
					wc.equalTo(resolveColumnName(pk), dv);
				}
			}

			if (!hasNull) {
				exp.setLimit(1);
				if (daoCountByExample(exp) > 0) {
					addDataDuplateError(data, pks);
					return false;
				}
			}
		}
		else {
			Object id = mmd.getPropertyValue(data, in);
			if (id != null && daoExists(data)) {
				addDataDuplateError(data, new String[] { in });
				return false;
			}
		}
		return true;
	}

	/**
	 * @param data data
	 * @param srcData srcData
	 * @return true if check successfully
	 * @throws Exception if an error occurs
	 */
	protected boolean checkPrimaryKeyOnUpdate(T data, T srcData) throws Exception {
		ModelMetaData mmd = getModelMetaData();
		String[] pks = mmd.getPrimaryKeys();

		boolean hasNull = false;
		for (String pk : pks) {
			Object dv = mmd.getPropertyValue(data, pk);
			if (dv == null) {
				hasNull = true;
				addFieldError(getDataFieldName(pk), getMessage(ERROR_FIELDVALUE_REQUIRED));
			}
		}
		if (hasNull) {
			return false;
		}

		return true;
	}

	/**
	 * checkUniqueKeyOnInsert
	 * @param data data
	 * @return true if check successfully
	 * @throws Exception if an error occurs
	 */
	protected boolean checkUniqueKeyOnInsert(T data) throws Exception {
		ModelMetaData mmd = getModelMetaData();
		String[][] ucs = mmd.getUniqueKeyConstraints();
		for (String[] uc : ucs) {
			boolean allNull = true;

			E exp = daoCreateExample();
			Restrictions wc = exp.getRestrictions();
			wc.setConjunctionToAnd();
			for (String up : uc) {
				Object dv = mmd.getPropertyValue(data, up);
				if (dv == null) {
					wc.isNull(resolveColumnName(up));
				}
				else {
					allNull = false;
					wc.equalTo(resolveColumnName(up), dv);
				}
			}

			if (!allNull) {
				exp.setLimit(1);
				if (daoCountByExample(exp) > 0) {
					addDataDuplateError(data, uc);
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * checkUniqueKeyOnUpdate
	 * @param data data
	 * @param srcData srcData
	 * @return true if check successfully
	 * @throws Exception if an error occurs
	 */
	protected boolean checkUniqueKeyOnUpdate(T data, T srcData) throws Exception {
		ModelMetaData mmd = getModelMetaData();
		String[][] ucs = mmd.getUniqueKeyConstraints();

		for (String[] uc : ucs) {
			boolean dirty = false;
			for (String up : uc) {
				Object ov = mmd.getPropertyValue(srcData, up);
				Object dv = mmd.getPropertyValue(data, up);
				if (!ObjectUtils.equals(ov, dv)) {
					dirty = true;
					break;
				}
			}

			if (dirty) {
				boolean allNull = true;

				E exp = daoCreateExample();
				Restrictions wc = exp.getRestrictions();
				wc.setConjunctionToAnd();

				for (String p : mmd.getPrimaryKeys()) {
					Object ov = mmd.getPropertyValue(srcData, p);
					wc.notEqualTo(resolveColumnName(p), ov);
				}

				for (String up : uc) {
					Object dv = mmd.getPropertyValue(data, up);
					if (dv == null) {
						wc.isNull(resolveColumnName(up));
					}
					else {
						allNull = false;
						wc.equalTo(resolveColumnName(up), dv);
					}
				}
				if (!allNull) {
					exp.setLimit(1);
					if (daoCountByExample(exp) > 0) {
						for (String up : uc) {
							addFieldError(getDataFieldName(up), getMessage(ERROR_FIELDVALUE_DUPLICATE));
						}
						return false;
					}
				}
			}
		}
		return true;
	}

	/**
	 * checkForeignKey
	 * @param data
	 * @return true if check successfully
	 * @throws Exception if an error occurs
	 */
	protected boolean checkForeignKey(T data) throws Exception {
		ModelMetaData mmd = getModelMetaData();
		String[][] fa = mmd.getForeignKeyConstraints();
		for (String[] fs : fa) {
			boolean allNull = true;

			String foreign = fs[0];

			ModelDAO fdao = getDataAccessSession().getModelDAO(foreign);
			ModelMetaData fmmd = getDataAccessSession().getMetaData(foreign);

			QueryParameter qp = (QueryParameter)fdao.createExample();
			Restrictions wc = qp.getRestrictions();
			wc.setConjunctionToAnd();
			for (int i = 1; i < fs.length; i += 2) {
				Object dv = mmd.getPropertyValue(data, fs[i]);
				String cn = fmmd.getColumnName(fmmd.getPropertyName(fs[i + 1]));
				if (dv == null) {
					wc.isNull(cn);
				}
				else {
					allNull = false;
					wc.equalTo(cn, dv);
				}
			}

			if (!allNull) {
				qp.setLimit(1);
				if (fdao.countByExample(qp) < 1) {
					for (int i = 1; i < fs.length; i += 2) {
						addFieldError(getDataFieldName(fs[i]), getMessage(ERROR_FIELDVALUE_INCORRECT));
					}
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * checkUpdatedOnUpdate
	 * @param data data
	 * @param srcData srcData
	 * @return true if check successfully
	 * @throws Exception if an error occurs
	 */
	protected boolean checkUpdatedOnUpdate(T data, T srcData) throws Exception {
		if (!checkUpdated(data, srcData)) {
			addActionConfirm(getMessage(CONFIRM_DATA_OVERWRITE));
			return false;
		}
		return true;
	}

	/**
	 * checkUpdatedOnDelete
	 * @param data data
	 * @param srcData srcData
	 * @return true if check successfully
	 * @throws Exception if an error occurs
	 */
	protected boolean checkUpdatedOnDelete(T data, T srcData) throws Exception {
		if (!checkUpdated(data, srcData)) {
			addActionConfirm(getMessage(ACTION_CONFIRM_PREFIX + getActionScenario()));
			return false;
		}
		return true;
	}

	/**
	 * checkUpdated
	 * @param data data
	 * @param srcData srcData
	 * @return true if check successfully
	 * @throws Exception if an error occurs
	 */
	protected boolean checkUpdated(T data, T srcData) throws Exception {
		return true;
	}

	//------------------------------------------------------------
	// other methods
	//------------------------------------------------------------
	/**
	 * clear on copy
	 * @param data data
	 * @throws Exception
	 */
	protected void clearOnCopy(T data) throws Exception {
		if (data != null) {
			if (clearPrimarys) {
				clearPrimaryKeyValues(data);
			}
			else if (clearIdentity) {
				clearIdentityValue(data);
			}
		}
	}
	
	/**
	 * clear primary key value of data
	 * @param data data
	 * @throws Exception
	 */
	protected void clearPrimaryKeyValues(T data) throws Exception {
		ModelMetaData mmd = getModelMetaData();
		if (data != null) {
			String[] pks = mmd.getPrimaryKeys();
			for (String pk : pks) {
				mmd.setPropertyValue(data, pk, null);
			}
		}
	}

	/**
	 * clear identity value of data
	 * @param data data
	 * @throws Exception
	 */
	protected void clearIdentityValue(T data) throws Exception {
		ModelMetaData mmd = getModelMetaData();
		if (data != null && mmd.getIdentityName() != null) {
			mmd.setPropertyValue(data, mmd.getIdentityName(), null);
		}
	}

	/**
	 * @param propertyName property name
	 * @return dataName + "." + propertyName
	 */
	protected String getDataFieldName(String propertyName) {
		return dataFieldName + "." + propertyName;
	}

	/**
	 * @param propertyName property name
	 * @param index index
	 * @return dataListName + "[index]." + propertyName
	 */
	protected String getDataListFieldName(String propertyName, int index) {
		return dataListFieldName + '[' + index + "]." + propertyName;
	}
	
}
