/*
 * CB3JdbcTemplate.java
 * Copyright (C) 2008 Cyber Beans Corporation. All rights reserved.
 */
package jp.co.cybec.cb3.accessor.dbaccess;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import jp.co.cybec.cb3.util.logging.ErrorCodeConstant;
import jp.co.cybec.cb3.util.logging.LogMessageConstant;
import jp.co.cybec.cb3.util.logging.LogMessageFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.support.JdbcUtils;

/**
 * データアクセスクラス。
 * @author Lijuan Sun
 */
public class CB3JdbcTemplate {

	/** ログ */
	private static final Log LOG = LogFactory.getLog(CB3JdbcTemplate.class);

	/** メッセージ */
	private static final LogMessageFactory MESSAGE = new LogMessageFactory(
			LogMessageConstant.CB3_MESSAGE_FILENAME);

	/** SQLパラメータの区切り文字 */
	private static final char[] PARAMETER_SEPARATORS = new char[] { '"', '\'', ':', '&', ',', ';',
			'(', ')', '|', '=', '+', '-', '*', '%', '/', '\\', '<', '>', '^' };

	/** JdbcTemplate */
	private JdbcTemplate jdbcTemplate = null;

	/** PreparedStatementをキャッシュする */
	private Map<String, PreparedStatement> preparedStatementCache = new HashMap<String, PreparedStatement>();

	/** ParsedSqlをキャッシュする */
	private Map<String, ParsedSql> parsedSqlCache = new HashMap<String, ParsedSql>();

	/** ResultSetをキャッシュする */
	private List<ResultSet> resultSetCache = new ArrayList<ResultSet>();

	/** コネクション */
	private Connection con = null;

	/**
	 * コンストラクタ。
	 *
	 * @param jdbcTemplate JdbcTemplate
	 */
	public CB3JdbcTemplate(JdbcTemplate jdbcTemplate) {

		this.jdbcTemplate = jdbcTemplate;
	}

	/**
	 * SpringのJdbcTemplateを取得します。
	 * @return JdbcTemplate
	 */
	public JdbcTemplate getJdbcTemplate() {
		return this.jdbcTemplate;
	}

	/**
	 * 後処理をします。<br>
	 * 保持しているResultSet、PreparedStatementをすべてクローズします。
	 * また、コネクションをリリースします。
	 */
	public void close() {

		// resultSetCacheの中身をすべてクローズする
		Iterator<ResultSet> rsCache = resultSetCache.iterator();
		while (rsCache.hasNext()) {
			JdbcUtils.closeResultSet(rsCache.next());
		}

		// PreparedStatementの中身をクローズする
		for (String key : preparedStatementCache.keySet()) {
			if (preparedStatementCache.get(key) != null) {
				JdbcUtils.closeStatement(preparedStatementCache.get(key));
			}
		}

		// コネクションをリリースする
		if (con != null) {
			DataSourceUtils.releaseConnection(con, getJdbcTemplate().getDataSource());
			con = null;
		}

		// キャッシュをクリアする
		preparedStatementCache.clear();
		resultSetCache.clear();
	}

	/**
	 * データを取得するメソッド(パラメータを付けるSQL)。
	 *
	 * @param sql SQL文
	 * @param parameter パラメータ
	 * @return 検索結果のResultSet
	 * @throws DataAccessException 何らかのデータベースアクセスエラーが発生した場合。
	 */
	public ResultSet query(String sql, Map<String, Object> parameter) throws DataAccessException {

		// デバッグログ出力
		if (LOG.isDebugEnabled()) {
			LOG.debug(MESSAGE.getMessage(LogMessageConstant.CB3_ACCESSOR_DBACCESS_012, sql, parameter));
		}

		// コネクションを取得する
		con = getConnection();

		try {
			// PreparedStatementを取得する
			PreparedStatement ps = getPreparedStatement(sql, parameter);

			// SQLを実行する
			ResultSet rs = ps.executeQuery();

			// デバッグログ出力
			if (LOG.isDebugEnabled()) {
				LOG.debug(MESSAGE.getMessage(LogMessageConstant.CB3_ACCESSOR_DBACCESS_013));
			}

			// resultSetをキャッシュする
			resultSetCache.add(rs);

			// 結果を戻す
			return rs;

		} catch (SQLException e) {
			// ログメッセージを作成する
			Object[] msg = { sql, parameter };
			DataAccessException ex = jdbcTemplate.getExceptionTranslator().translate(
					MESSAGE.getErrorMessage(ErrorCodeConstant.ACCESSOR_DBACCESS_001,
							LogMessageConstant.CB3_ACCESSOR_DBACCESS_001, msg), sql, e);
			// ログ出力
			LOG.info(ex.getMessage(), ex);
			// DataAccessExceptionをスローする
			throw ex;
		}

	}

	/**
	 *
	 * データを取得するメソッド(パラメータを付けないSQL)。
	 *
	 * @param sql SQL文
	 * @return 検索結果のResultSet
	 * @throws DataAccessException 何らかのデータベースアクセスエラーが発生した場合。
	 */
	public ResultSet query(String sql) throws DataAccessException {

		// デバッグログ出力
		if (LOG.isDebugEnabled()) {
			LOG.debug(MESSAGE.getMessage(LogMessageConstant.CB3_ACCESSOR_DBACCESS_012, sql, ""));
		}

		// コネクションを取得する
		con = getConnection();

		try {
			// PreparedStatementを取得する
			PreparedStatement ps = getPreparedStatement(sql, null);

			// SQLを実行する
			ResultSet rs = ps.executeQuery();

			// デバッグログ出力
			if (LOG.isDebugEnabled()) {
				LOG.debug(MESSAGE.getMessage(LogMessageConstant.CB3_ACCESSOR_DBACCESS_013));
			}

			// キャッシュする
			resultSetCache.add(rs);

			// 結果を戻す
			return rs;

		} catch (SQLException e) {
			// ログメッセージを作成する
			Object[] msg = { sql, "" };
			DataAccessException ex = jdbcTemplate.getExceptionTranslator().translate(
					MESSAGE.getErrorMessage(ErrorCodeConstant.ACCESSOR_DBACCESS_002,
							LogMessageConstant.CB3_ACCESSOR_DBACCESS_001, msg), sql, e);
			// ログ出力
			LOG.info(ex.getMessage(), ex);
			// DataAccessExceptionをスローする
			throw ex;
		}
	}

	/**
	 *
	 * データを更新するメソッド(パラメータを付けるSQL)。
	 *
	 * @param sql SQL文
	 * @param parameter パラメータ
	 * @return 処理件数
	 * @throws DataAccessException 何らかのデータベースアクセスエラーが発生した場合。
	 */
	public int update(String sql, Map<String, Object> parameter) throws DataAccessException {

		// デバッグログ出力
		if (LOG.isDebugEnabled()) {
			LOG.debug(MESSAGE.getMessage(LogMessageConstant.CB3_ACCESSOR_DBACCESS_012, sql, parameter));
		}

		// コネクションを取得する
		con = getConnection();

		try {
			// PreparedStatementを取得する
			PreparedStatement ps = getPreparedStatement(sql, parameter);

			// SQLを実行する
			int recordNumber = ps.executeUpdate();

			// デバッグログ出力
			if (LOG.isDebugEnabled()) {
				LOG.debug(MESSAGE.getMessage(LogMessageConstant.CB3_ACCESSOR_DBACCESS_014, recordNumber));
			}

			return recordNumber;

		} catch (SQLException e) {
			// ログメッセージを作成する
			Object[] msg = { sql, parameter };
			DataAccessException ex = jdbcTemplate.getExceptionTranslator().translate(
					MESSAGE.getErrorMessage(ErrorCodeConstant.ACCESSOR_DBACCESS_003,
							LogMessageConstant.CB3_ACCESSOR_DBACCESS_001, msg), sql, e);
			// ログ出力
			LOG.info(ex.getMessage(), ex);
			// DataAccessExceptionをスローする
			throw ex;
		}
	}

	/**
	 *
	 * データを更新するメソッド(パラメータを付けないSQL)。
	 *
	 * @param sql SQL文
	 * @return 処理件数
	 * @throws DataAccessException 何らかのデータベースアクセスエラーが発生した場合。
	 */
	public int update(String sql) throws DataAccessException {

		// デバッグログ出力
		if (LOG.isDebugEnabled()) {
			LOG.debug(MESSAGE.getMessage(LogMessageConstant.CB3_ACCESSOR_DBACCESS_012, sql, ""));
		}

		// コネクションを取得する
		con = getConnection();

		try {
			// PreparedStatementを取得する
			PreparedStatement ps = getPreparedStatement(sql, null);

			// SQLを実行する
			int recordNumber = ps.executeUpdate();

			// デバッグログ出力
			if (LOG.isDebugEnabled()) {
				LOG.debug(MESSAGE.getMessage(LogMessageConstant.CB3_ACCESSOR_DBACCESS_014, recordNumber));
			}

			return recordNumber;

		} catch (SQLException e) {
			// ログメッセージを作成する
			Object[] msg = { sql, "" };
			DataAccessException ex = jdbcTemplate.getExceptionTranslator().translate(
					MESSAGE.getErrorMessage(ErrorCodeConstant.ACCESSOR_DBACCESS_004,
							LogMessageConstant.CB3_ACCESSOR_DBACCESS_001, msg), sql, e);
			// ログ出力
			LOG.info(ex.getMessage(), ex);
			// DataAccessExceptionをスローする
			throw ex;
		}
	}

	/**
	 * コミットする
	 * @throws DataAccessException 何らかのデータベースアクセスエラーが発生した場合、
	 * 		もしくはauto-commitモードであった場合。
	 */
	public void commit() throws DataAccessException {

		// コネクションを取得する
		con = getConnection();

		try {
			// コミットする
			con.commit();
		} catch (SQLException e) {
			// ログメッセージを作成する
			DataAccessException ex = jdbcTemplate.getExceptionTranslator().translate(
					MESSAGE.getErrorMessage(ErrorCodeConstant.ACCESSOR_DBACCESS_005,
							LogMessageConstant.CB3_ACCESSOR_DBACCESS_002), "", e);
			// ログ出力
			LOG.info(ex.getMessage(), ex);
			// DataAccessExceptionをスローする
			throw ex;
		}
	}

	/**
	 * コネクションを取得する。
	 * @return コネクション
	 */
	private Connection getConnection() {

		// コネクションを取得する
		if (con == null) {
			con = DataSourceUtils.getConnection(getJdbcTemplate().getDataSource());
		}
		return con;
	}

	/**
	 * PreparedStatementを取得します。
	 *
	 * @param sql SQL文
	 * @param parameter SQLのパラメータ
	 * @return PreparedStatement
	 * @throws SQLException 何らかのデータベースアクセスエラーが発生した場合。
	 */
	private PreparedStatement getPreparedStatement(String sql, Map<String, Object> parameter)
			throws SQLException {

		// 解析済みのSQLを取得します。
		ParsedSql parsedSql = parsedSqlCache.get(sql);
		if (parsedSql == null) {
			// 解析済みのSQLがない場合は解析を行います。
			parsedSql = parseSql(sql);
			parsedSqlCache.put(sql, parsedSql);
		}

		// PreparedStatementを取得します。
		PreparedStatement ps = preparedStatementCache.get(sql);
		if (ps == null) {
			// PreparedStatementを作成する
			ps = con.prepareStatement(parsedSql.getSql());

			// Mapに追加する
			preparedStatementCache.put(sql, ps);
		}

		// 前回実行したときのパラメータをクリアします。
		ps.clearParameters();

		// パラメータがない場合はPreparedStatementを返します。
		if (parameter == null || parameter.size() == 0) {
			return ps;
		}

		// パラメータの値をPreparedStatementに設定します。
		parsedSql.getParameterNames();
		List<Object> paramValues = new ArrayList<Object>();
		int paramIndex = 1;
		for (String paramName : parsedSql.getParameterNames()) {
			Object value = parameter.get(paramName);
			ps.setObject(paramIndex, value);
			// ログ出力のためリストに格納。
			paramValues.add(value);
			paramIndex++;
		}

		// デバッグログ出力
		if (LOG.isDebugEnabled()) {
			LOG.debug(MESSAGE.getMessage(LogMessageConstant.CB3_ACCESSOR_DBACCESS_007, parsedSql.getSql(), paramValues));
		}

		return ps;
	}

	/**
	 * SQLを解析します。
	 *
	 * @param sql 解析前のSQL文
	 * @return 解析後のSQLを保持するParsedSql
	 */
	private ParsedSql parseSql(String sql) {

		ParsedSql parsedSql = new ParsedSql();

		char[] originalSql = sql.toCharArray();
		boolean withinQuotes = false;
		char currentQuote = '-';

		StringBuilder compiledSql = new StringBuilder();

		int i = 0;
		while (i < originalSql.length) {
			char c = originalSql[i];
			if (withinQuotes) {
				compiledSql.append(c);
				if (c == currentQuote) {
					withinQuotes = false;
					currentQuote = '-';
				}
			} else {
				if (c == '"' || c == '\'') {
					compiledSql.append(c);
					withinQuotes = true;
					currentQuote = c;
				} else {
					if (c == ':' || c == '&') {
						int j = i + 1;
						if (j < originalSql.length && originalSql[j] == ':' && c == ':') {
							i = i + 2;
							continue;
						}
						while (j < originalSql.length && !isParameterSeparator(originalSql[j])) {
							j++;
						}
						if (j - i > 1) {
							String parameter = sql.substring(i + 1, j);
							parsedSql.addParameterNames(parameter);
							compiledSql.append('?');
						}
						i = j - 1;
					} else {
						compiledSql.append(c);
					}
				}
			}
			i++;
		}

		parsedSql.setSql(compiledSql.toString());

		return parsedSql;

	}

	/**
	 * パラメータの区切りを判定します。
	 *
	 * @param c SQL内の文字
	 * @return パラメータの区切りの場合true、それ以外はfalse
	 */
	private boolean isParameterSeparator(char c) {
		if (Character.isWhitespace(c)) {
			return true;
		}
		for (int i = 0; i < PARAMETER_SEPARATORS.length; i++) {
			if (c == PARAMETER_SEPARATORS[i]) {
				return true;
			}
		}
		return false;
	}

}
