/*
 * Aipo is a groupware program developed by Aimluck,Inc.
 * Copyright (C) 2004-2011 Aimluck,Inc.
 * http://www.aipo.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.apache.cayenne.conf;

import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.apache.cayenne.ConfigurationException;
import org.apache.commons.dbcp.ConnectionFactory;
import org.apache.commons.dbcp.DriverManagerConnectionFactory;
import org.apache.commons.dbcp.PoolableConnectionFactory;
import org.apache.commons.dbcp.ThreadPoolingDataSource;
import org.apache.commons.pool.KeyedObjectPoolFactory;
import org.apache.commons.pool.ObjectPool;
import org.apache.commons.pool.impl.GenericKeyedObjectPoolFactory;
import org.apache.commons.pool.impl.GenericObjectPool;

/**
 * @see DBCPDataSourceBuilder
 */
class CustomDBCPDataSourceBuilder {

  // DataSource Properties
  static final String DRIVER_CLASS_NAME = "driverClassName";

  static final String URL = "url";

  static final String USER_NAME = "username";

  static final String PASSWORD = "password";

  // Connection Pool Properties
  static final String MAX_ACTIVE = "maxActive";

  static final String MIN_IDLE = "minIdle";

  static final String MAX_IDLE = "maxIdle";

  static final String MAX_WAIT = "maxWait";

  static final String VALIDATION_QUERY = "validationQuery";

  static final String TEST_ON_BORROW = "testOnBorrow";

  static final String TEST_ON_RETURN = "testOnReturn";

  static final String TEST_IDLE = "testWhileIdle";

  static final String TIME_BETWEEN_EVICTIONS = "timeBetweenEvictionRunsMillis";

  static final String NUM_TEST_PER_EVICTION = "numTestsPerEvictionRun";

  static final String MIN_EVICTABLE_TIME = "minEvictableIdleTimeMillis";

  static final String EXHAUSTED_ACTION = "whenExhaustedAction";

  static final String AUTO_COMMIT = "defaultAutoCommit";

  static final String READ_ONLY = "defaultReadOnly";

  static final String TRANSACTION_ISOLATION = "defaultTransactionIsolation";

  static final String CONNECTION_NOWRAP = "accessToUnderlyingConnectionAllowed";

  static final String CATALOG = "defaultCatalog";

  static final String INIT_SQL = "initSql";
  
  // PreparedStatementPool properties

  static final String POOL_PS = "poolPreparedStatements";

  static final String PS_MAX_ACTIVE = "ps.maxActive";

  static final String PS_MAX_IDLE = "ps.maxIdle";

  static final String PS_MAX_TOTAL = "ps.maxTotal";

  static final String PS_MAX_WAIT = "ps.maxWait";

  static final String PS_MIN_EVICTABLE_TIME = "ps.minEvictableIdleTimeMillis";

  static final String PS_NUM_TEST_PER_EVICTION = "ps.numTestsPerEvictionRun";

  static final String PS_TEST_ON_BORROW = "ps.testOnBorrow";

  static final String PS_TEST_ON_RETURN = "ps.testOnReturn";

  static final String PS_TEST_IDLE = "ps.testWhileIdle";

  static final String PS_TIME_BETWEEN_EVICTIONS =
    "ps.timeBetweenEvictionRunsMillis";

  static final String PS_EXHAUSTED_ACTION = "ps.whenExhaustedAction";

  private final DBCPDataSourceProperties config;

  CustomDBCPDataSourceBuilder(DBCPDataSourceProperties properties) {
    this.config = properties;
  }

  DataSource createDataSource() {
    boolean connectionNoWrap = config.getBoolean(CONNECTION_NOWRAP, false);
    ObjectPool connectionPool = createConnectionPool();
    ThreadPoolingDataSource dataSource =
      new ThreadPoolingDataSource(connectionPool);
    dataSource.setAccessToUnderlyingConnectionAllowed(connectionNoWrap);

    return dataSource;
  }

  private void loadDriverClass() {
    String driver = config.getString(DRIVER_CLASS_NAME, true);
    try {
      Class.forName(driver);
    } catch (ClassNotFoundException e) {
      throw new ConfigurationException("Error loading driver " + driver, e);
    }
  }

  private ObjectPool createConnectionPool() {

    ConnectionFactory factory = createConnectionFactory();
    GenericObjectPool.Config poolConfig = createConnectionPoolConfig();
    KeyedObjectPoolFactory statementPool = createPreparedStatementPool();

    String validationQuery = config.getString(VALIDATION_QUERY);
    boolean defaultReadOnly = config.getBoolean(READ_ONLY, false);
    boolean defaultAutoCommit = config.getBoolean(AUTO_COMMIT, false);
    int defaultTransactionIsolation =
      config.getTransactionIsolation(
        TRANSACTION_ISOLATION,
        Connection.TRANSACTION_SERIALIZABLE);
    String defaultCatalog = config.getString(CATALOG);

    String initSql = config.getString(INIT_SQL);
    List<String> initSqls = null;
    if (initSql != null && !initSql.isEmpty()) {
      initSqls = new ArrayList<String>();
      initSqls.add(initSql);
    }
    
    ObjectPool connectionPool = new GenericObjectPool(null, poolConfig);

    // a side effect of PoolableConnectionFactory constructor call is that newly
    // created factory object is assigned to "connectionPool", which is
    // definitely a
    // very confusing part of DBCP - new object is not visibly assigned to
    // anything,
    // still it is preserved...
    new PoolableConnectionFactory(
      factory,
      connectionPool,
      statementPool,
      validationQuery,
      initSqls,
      defaultReadOnly ? Boolean.TRUE : Boolean.FALSE,
      defaultAutoCommit,
      defaultTransactionIsolation,
      defaultCatalog,
      null);

    return connectionPool;
  }

  private ConnectionFactory createConnectionFactory() {
    loadDriverClass();
    String url = config.getString(URL, true);
    String userName = config.getString(USER_NAME);
    String password = config.getString(PASSWORD);
    return new DriverManagerConnectionFactory(url, userName, password);
  }

  private KeyedObjectPoolFactory createPreparedStatementPool() {

    if (!config.getBoolean("poolPreparedStatements", false)) {
      return null;
    }

    // the GenericKeyedObjectPool.Config object isn't used because
    // although it has provision for the maxTotal parameter when
    // passed to the GenericKeyedObjectPoolFactory constructor
    // this parameter is not being properly set as a default for
    // creating prepared statement pools

    int maxActive =
      config.getInt(PS_MAX_ACTIVE, GenericObjectPool.DEFAULT_MAX_ACTIVE);
    byte whenExhaustedAction =
      config.getWhenExhaustedAction(
        PS_EXHAUSTED_ACTION,
        GenericObjectPool.DEFAULT_WHEN_EXHAUSTED_ACTION);
    long maxWait =
      config.getLong(PS_MAX_WAIT, GenericObjectPool.DEFAULT_MAX_WAIT);
    int maxIdle =
      config.getInt(PS_MAX_IDLE, GenericObjectPool.DEFAULT_MAX_IDLE);
    int maxTotal = config.getInt(PS_MAX_TOTAL, 1);

    boolean testOnBorrow =
      config.getBoolean(
        PS_TEST_ON_BORROW,
        GenericObjectPool.DEFAULT_TEST_ON_BORROW);
    boolean testOnReturn =
      config.getBoolean(
        PS_TEST_ON_RETURN,
        GenericObjectPool.DEFAULT_TEST_ON_RETURN);

    long timeBetweenEvictionRunsMillis =
      config.getLong(
        PS_TIME_BETWEEN_EVICTIONS,
        GenericObjectPool.DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS);
    int numTestsPerEvictionRun =
      config.getInt(
        PS_NUM_TEST_PER_EVICTION,
        GenericObjectPool.DEFAULT_NUM_TESTS_PER_EVICTION_RUN);

    long minEvictableIdleTimeMillis =
      config.getLong(
        PS_MIN_EVICTABLE_TIME,
        GenericObjectPool.DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS);

    boolean testWhileIdle =
      config
        .getBoolean(PS_TEST_IDLE, GenericObjectPool.DEFAULT_TEST_WHILE_IDLE);

    return new GenericKeyedObjectPoolFactory(
      null,
      maxActive,
      whenExhaustedAction,
      maxWait,
      maxIdle,
      maxTotal,
      testOnBorrow,
      testOnReturn,
      timeBetweenEvictionRunsMillis,
      numTestsPerEvictionRun,
      minEvictableIdleTimeMillis,
      testWhileIdle);
  }

  private GenericObjectPool.Config createConnectionPoolConfig() {
    GenericObjectPool.Config poolConfig = new GenericObjectPool.Config();

    poolConfig.maxIdle =
      config.getInt(MAX_IDLE, GenericObjectPool.DEFAULT_MAX_IDLE);
    poolConfig.minIdle =
      config.getInt(MIN_IDLE, GenericObjectPool.DEFAULT_MIN_IDLE);
    poolConfig.maxActive =
      config.getInt(MAX_ACTIVE, GenericObjectPool.DEFAULT_MAX_ACTIVE);
    poolConfig.maxWait =
      config.getLong(MAX_WAIT, GenericObjectPool.DEFAULT_MAX_WAIT);

    poolConfig.testOnBorrow =
      config.getBoolean(
        TEST_ON_BORROW,
        GenericObjectPool.DEFAULT_TEST_ON_BORROW);
    poolConfig.testOnReturn =
      config.getBoolean(
        TEST_ON_RETURN,
        GenericObjectPool.DEFAULT_TEST_ON_RETURN);
    poolConfig.testWhileIdle =
      config.getBoolean(TEST_IDLE, GenericObjectPool.DEFAULT_TEST_WHILE_IDLE);

    poolConfig.timeBetweenEvictionRunsMillis =
      config.getLong(
        TIME_BETWEEN_EVICTIONS,
        GenericObjectPool.DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS);
    poolConfig.numTestsPerEvictionRun =
      config.getInt(
        NUM_TEST_PER_EVICTION,
        GenericObjectPool.DEFAULT_NUM_TESTS_PER_EVICTION_RUN);
    poolConfig.minEvictableIdleTimeMillis =
      config.getLong(
        MIN_EVICTABLE_TIME,
        GenericObjectPool.DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS);

    poolConfig.whenExhaustedAction =
      config.getWhenExhaustedAction(
        EXHAUSTED_ACTION,
        GenericObjectPool.DEFAULT_WHEN_EXHAUSTED_ACTION);

    return poolConfig;
  }
}