ConnectionPools.java [src/csip/utils] Revision:   Date:
/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

package csip.utils;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import csip.Config;
import static csip.Config.CSIP_JDBC_CHECKVALID;
import csip.api.server.ServiceException;
import csip.SessionLogger;
import csip.Utils;
import csip.annotations.Resource;
import csip.annotations.ResourceType;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.tomcat.jdbc.pool.DataSourceProxy;
import org.apache.tomcat.jdbc.pool.PoolProperties;

/**
 * Connection Pool Managements
 *
 * @author od
 */
public class ConnectionPools {

  public static final String DRIVER = "driverClassName";

  static abstract class ConnPool {

    DataSource datasource;
    String url;
    Map<String, String> env;


    static ConnPool create(String type, String url, Map<String, String> env) {
      switch (type.toLowerCase()) {
        case "hakiri":
          return new Hikari(url, env);
        case "tomcat":
        default:
          return new Tomcat(url, env);
      }
    }


    abstract void close();


    abstract DataSource createDatasource(String url, Map<String, String> env);


    ConnPool(String url, Map<String, String> env) {
      this.url = Utils.resolve(url);
      this.env = env;
      setDriver(url, env);
    }


    Connection getConnection(SessionLogger log) throws ServiceException {
      try {
        if (datasource == null)
          datasource = createDatasource(url, env);
        return datasource.getConnection();
      } catch (SQLException ex) {
        throw new ServiceException(ex);
      }
    }


    private void setDriver(String resolved_url, Map<String, String> env) {
      if (!env.containsKey(DRIVER)) {
        String rurl = resolved_url.toLowerCase();
        if (rurl.contains(":postgresql:")) {
          env.put(DRIVER, "org.postgresql.Driver");
        } else if (rurl.contains(":sqlserver:")) {
          env.put(DRIVER, "com.microsoft.sqlserver.jdbc.SQLServerDriver");
        } else if (rurl.contains(":mysql:")) {
          env.put(DRIVER, "com.mysql.jdbc.Driver");
        } else if (rurl.contains(":sqlite:")) {
          env.put(DRIVER, "org.sqlite.JDBC");
        } else if (rurl.contains(":oracle:")) {
          env.put(DRIVER, "oracle.jdbc.driver.OracleDriver");
        } else if (rurl.contains(":db2:")) {
          env.put(DRIVER, "com.ibm.db2.jcc.DB2Driver");
        } else if (rurl.contains(":sdm:")) {
          env.put(DRIVER, "csip.sdm.SDMDriver");
        }
      }
    }

    /**
     * Tomcat Connection Pool.
     */
    static class Tomcat extends ConnPool {

      Tomcat(String url, Map<String, String> env) {
        super(url, env);
      }


      @Override
      DataSource createDatasource(String res_url, Map<String, String> env) {

        // overwrite the defaults.
        Properties dbp = new Properties();
        Properties pr = new Properties(defaultProperties());
        pr.putAll(env);

        PoolProperties p = new PoolProperties();
        for (String key : pr.stringPropertyNames()) {
          String val = pr.getProperty(key);
          try {
            BeanUtils.setProperty(p, key, val);
//              info("Set JDBC pool property: " + key + ": " + val);
          } catch (IllegalAccessException | InvocationTargetException E) {
            dbp.setProperty(key, val);
//              logger.info("Set DB property: " + key + ": " + val);
          }
        }
        if (!dbp.isEmpty()) {
          p.setDbProperties(dbp);
        }

        p.setUrl(res_url);
        return new org.apache.tomcat.jdbc.pool.DataSource(p);
      }


//      void setConfig(Object cfg, Map<String, String> env) {
//        for (String key : env.keySet()) {
//          String val = env.get(key);
//          try {
//            BeanUtils.setProperty(cfg, key, val);
//            System.out.println("Set DB property: " + key + ": " + val);
//          } catch (IllegalAccessException | InvocationTargetException E) {
//            System.out.println("Cannot set config property: " + key + ": " + val);
//          }
//        }
//      }
      /**
       * set the default properties.
       *
       * @param p PoolProperties
       */
      private Properties defaultProperties() {
        Properties p = new Properties();
        p.put("defaultAutoCommit", false);
        p.put("jmxEnabled", false);
        p.put("testOnBorrow", true);
        p.put("validationQuery", "SELECT 1");
        p.put("testOnReturn", false);
        p.put("validationInterval", 30000);
        p.put("maxWait", 10000);
        p.put("removeAbandonedTimeout", 60);
        p.put("removeAbandoned", true);
        p.put("initialSize", 10);
        p.put("maxActive", 250);
        p.put("maxIdle", 100);
        p.put("minIdle", 10);
        p.put("suspectTimeout", 60);
        p.put("timeBetweenEvictionRunsMillis", 30000);
        p.put("minEvictableIdleTimeMillis", 60000);
        return p;
      }


      @Override
      void close() {
        if (datasource != null)
          ((DataSourceProxy) datasource).close(true);
      }
    }

    /**
     * HikariCP implementation.
     */
    static class Hikari extends ConnPool {

      Hikari(String url, Map<String, String> env) {
        super(url, env);
      }


      @Override
      void close() {
        if (datasource != null)
          ((HikariDataSource) datasource).close();
      }


      /**
       * set the default properties.
       *
       * @param p PoolProperties
       */
      private Properties defaultProperties() {
        Properties p = new Properties();
        p.put("autoCommit", false);
        p.put("connectionTestQuery", "SELECT 1");
        p.put("connectionTimeout", 3000);
        p.put("maximumPoolSize", 100);
        p.put("minimumIdle", 10);
        return p;

//        p.setTestOnBorrow(true);
//        p.setTestOnReturn(false);
//        p.setValidationInterval(30000);
//        p.setMaxWait(10000);
//        p.setRemoveAbandonedTimeout(60);
//        p.setRemoveAbandoned(true);
//        p.setInitialSize(10);
//        p.setMaxActive(250);
//        p.setMaxIdle(100);
//        p.setMinIdle(10);
//        p.setSuspectTimeout(60);
//        p.setTimeBetweenEvictionRunsMillis(30000);
//        p.setMinEvictableIdleTimeMillis(60000);
      }


      @Override
      DataSource createDatasource(String url, Map<String, String> env) {
        Properties p = new Properties(defaultProperties());
        p.putAll(env);
        HikariConfig cfg = new HikariConfig(p);
        cfg.setJdbcUrl(url);
        return new HikariDataSource(cfg);
      }
    }
  }

  //////////////////////////////////
  private final Map<String, ConnPool> jdbcPools = new HashMap<>();

  private static volatile ConnectionPools instance;


  public static ConnectionPools singleton() {
    ConnectionPools ref = instance;
    if (ref == null) {
      synchronized (ConnectionPools.class) {
        ref = instance;
        if (ref == null)
          instance = ref = new ConnectionPools();
      }
    }
    return ref;
  }


  public void shutdownJDBC() {
    for (ConnPool cp : jdbcPools.values()) {
      cp.close();
    }
    jdbcPools.clear();
  }


  /**
   * Add to JDBC pool
   *
   * @param id
   * @param url
   */
  public void addToJDBCPool(String id, String url) {
    jdbcPools.put(id, ConnPool.create("tomcat", url, new HashMap<>()));
  }


  /**
   * Add to JDBC pool
   *
   * @param id
   * @param url
   * @param env
   */
  public void addToJDBCPool(String id, String url, Map<String, String> env) {
    jdbcPools.put(id, ConnPool.create("tomcat", url, env));
  }


  /**
   * Get Connection.
   *
   * @param id
   * @param logger
   * @return
   * @throws ServiceException
   */
  public Connection getConnection(String id, SessionLogger logger) throws ServiceException {
    ConnPool p = jdbcPools.get(id);
    if (p == null)
      throw new ServiceException("No such resource: " + id);
    return p.getConnection(logger);
  }


  /**
   * Get the JDBC connection from a resource definition.
   *
   * @param c
   * @param id
   * @param LOG
   * @return
   * @throws ServiceException
   */
  synchronized public Connection getResourceJDBC(Class<?> c, String id, SessionLogger LOG)
      throws ServiceException {
    ConnPool p = jdbcPools.get(id);
    if (p == null) {
      Resource r = Binaries.getResourceById(c, id);
      if (r != null) {
        if (r.type() == ResourceType.JDBC) {
          String url = r.file();
          if (url != null && !url.isEmpty()) {
            Map<String, String> env = Binaries.parseEnv(r.env());
            // the 'args' value might specify the cp type 
            // 'tomcat' or 'hakiri', if left blank, default is tomcat.
            String cpType = r.args().isEmpty() ? "tomcat" : r.args();
            jdbcPools.put(id, p = ConnPool.create(cpType, url, env));
          } else {
            throw new ServiceException("No url connection string in 'file': " + id);
          }
        } else {
          throw new ServiceException("Not a JDBC resource: " + id);
        }
      } else {
        throw new ServiceException("No such resource: " + id);
      }
    }
    Connection con = p.getConnection(LOG);
    checkValid(con);
    return con;
  }


  static void checkValid(Connection con) throws ServiceException {
    int valid = Config.getInt(CSIP_JDBC_CHECKVALID, -1); // default: no checking
    if (valid <= -1)
      return;
    try {
      if (!con.isValid(valid))
        throw new ServiceException("Invalid connection: " + con.getMetaData().getURL());
    } catch (SQLException ex) {
      throw new ServiceException(ex);
    }
  }

}