/FileExtraction/Java_unseen/dlitz_resin/modules/resin/src/com/caucho/sql/DriverConfig.java
/*
 * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source 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 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.sql;

import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.PostConstruct;
import javax.resource.spi.ManagedConnectionFactory;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.DataSource;
import javax.sql.PooledConnection;
import javax.sql.XADataSource;

import com.caucho.config.Config;
import com.caucho.config.ConfigException;
import com.caucho.config.program.ConfigProgram;
import com.caucho.config.program.ContainerProgram;
import com.caucho.config.program.PropertyStringProgram;
import com.caucho.config.program.PropertyValueProgram;
import com.caucho.config.type.ConfigType;
import com.caucho.config.type.TypeFactory;
import com.caucho.config.types.InitParam;
import com.caucho.lifecycle.Lifecycle;
import com.caucho.management.server.JdbcDriverMXBean;
import com.caucho.naming.Jndi;
import com.caucho.sql.spy.SpyConnectionPoolDataSource;
import com.caucho.tools.profiler.ConnectionPoolDataSourceWrapper;
import com.caucho.tools.profiler.DriverWrapper;
import com.caucho.tools.profiler.ProfilerPoint;
import com.caucho.tools.profiler.ProfilerPointConfig;
import com.caucho.tools.profiler.XADataSourceWrapper;
import com.caucho.util.Alarm;
import com.caucho.util.IoUtil;
import com.caucho.util.L10N;
import com.caucho.vfs.ReadStream;
import com.caucho.vfs.Vfs;
import com.caucho.xml.QName;

/**
 * Configures the database driver.
 */
public class DriverConfig
{
  protected static final Logger log
    = Logger.getLogger(DriverConfig.class.getName());
  private static final L10N L = new L10N(DriverConfig.class);

  @SuppressWarnings("unused")
  private static final int TYPE_UNKNOWN = 0;
  private static final int TYPE_DRIVER = 1;
  private static final int TYPE_POOL = 2;
  private static final int TYPE_XA = 3;
  private static final int TYPE_JCA = 4;
  private static final int TYPE_DATA_SOURCE = 5;

  /**
   * The key used to look into the properties passed to the
   * connect method to find the username.
   */
  public static final String PROPERTY_USER = "user" ;
  /**
   * The key used to look into the properties passed to the
   * connect method to find the password.
   */
  public static final String PROPERTY_PASSWORD = "password" ;

  private static final QName URL = new QName("url");
  private static final QName USER = new QName("user");
  private static final QName PASSWORD = new QName("password");

  private DBPoolImpl _dbPool;

  private Class<?> _driverClass;

  private String _driverURL;
  private String _user;
  private String _password;
  private Properties _info;

  private ContainerProgram _init = new ContainerProgram();

  private int _driverType;
  private Object _driverObject;

  private ManagedConnectionFactory _jcaDataSource;
  private ConnectionPoolDataSource _poolDataSource;
  private XADataSource _xaDataSource;
  private Driver _driver;

  private int _index;

  private Lifecycle _lifecycle = new Lifecycle();
  private DriverAdmin _admin = new DriverAdmin(this);

  // statistics
  private long _connectionCountTotal;
  private AtomicLong _connectionFailCountTotal = new AtomicLong();
  private long _lastFailTime;

  private ProfilerPoint _profilerPoint;

  /**
   * Null constructor for the Driver interface; called by the JNDI
   * configuration.  Applications should not call this directly.
   */
  public DriverConfig(DBPoolImpl pool)
  {
    _dbPool = pool;

    _info = new Properties();
  }

  /**
   * Returns the DBPool.
   */
  public DBPoolImpl getDBPool()
  {
    return _dbPool;
  }

  /**
   * Sets the driver as data source.
   */
  public void setDriverType(String type)
    throws ConfigException
  {
    if ("ConnectionPoolDataSource".equals(type)) {
      _driverType = TYPE_POOL;
    }
    else if ("XADataSource".equals(type)) {
      _driverType = TYPE_XA;
    }
    else if ("ManagedConnectionFactory".equals(type)) {
      _driverType = TYPE_JCA;
    }
    else if ("Driver".equals(type)) {
      _driverType = TYPE_DRIVER;
    }
    else if ("DataSource".equals(type)) {
      _driverType = TYPE_DATA_SOURCE;
    }
    else if (hasDriverTypeMethod(_driverClass)) {
      _init.addProgram(new PropertyStringProgram("driverType", type));
    }
    else {
      throw new ConfigException(L.l("'{0}' is an unknown driver type. Valid types are 'ConnectionPoolDataSource', 'XADataSource' and 'Driver'"));
    }
  }

  private boolean hasDriverTypeMethod(Class<?> cl)
  {
    if (cl == null)
      return false;

    for (Method method : cl.getMethods()) {
      if (method.getName().equals("setDriverType")
          && method.getParameterTypes().length == 1) {
        return true;
      }
    }

    return false;
  }

  /**
   * Sets the driver as data source.
   */
  public void setDataSource(Object dataSource)
    throws ConfigException
  {
    if (dataSource instanceof String)
      dataSource = Jndi.lookup((String) dataSource);

    if (_driverType == TYPE_XA)
      _xaDataSource = (XADataSource) dataSource;
    else if (_driverType == TYPE_POOL)
      _poolDataSource = (ConnectionPoolDataSource) dataSource;
    else if (dataSource instanceof XADataSource)
      _xaDataSource = (XADataSource) dataSource;
    else if (dataSource instanceof ConnectionPoolDataSource)
      _poolDataSource = (ConnectionPoolDataSource) dataSource;
    else if (dataSource instanceof ManagedConnectionFactory)
      _jcaDataSource = (ManagedConnectionFactory) dataSource;
    else
      throw new ConfigException(L.l("data-source '{0}' is of type '{1}' which does not implement XADataSource or ConnectionPoolDataSource.",
                                    dataSource,
                                    dataSource.getClass().getName()));
  }

  /**
   * Returns the JDBC driver class for the pooled object.
   */
  public Class<?> getDriverClass()
  {
    return _driverClass;
  }

  /**
   * Sets the JDBC driver class underlying the pooled object.
   */
  public void setType(Class<?> driverClass)
    throws ConfigException
  {
    _driverClass = driverClass;

    if (! Driver.class.isAssignableFrom(driverClass)
        && ! XADataSource.class.isAssignableFrom(driverClass)
        && ! ConnectionPoolDataSource.class.isAssignableFrom(driverClass)
        && ! ManagedConnectionFactory.class.isAssignableFrom(driverClass)
        && ! DataSource.class.isAssignableFrom(driverClass)) {
      throw new ConfigException(L.l("'{0}' is not a valid database type, because it does not implement Driver, XADataSource, ConnectionPoolDataSource, ManagedConnectionFactory, or DataSource..",
                                    driverClass.getName()));
    }

    Config.checkCanInstantiate(driverClass);
  }

  public String getType()
  {
    return _driverClass.getName();
  }

  /**
   * Returns the connection's JDBC url.
   */
  public String getURL()
  {
    return _driverURL;
  }

  /**
   * Sets the connection's JDBC url.
   */
  public void setURL(String url)
  {
    _driverURL = url;

    _lifecycle.setName("JdbcDriver[" + url + "]");
  }

  /**
   * Adds to the builder program.
   */
  public void addBuilderProgram(ConfigProgram program)
  {
    _init.addProgram(program);
  }
  
  public void setProperty(String name, Object value)
  {
    _init.addProgram(new PropertyValueProgram(name, value));
  }

  /**
   * Returns the connection's user.
   */
  public String getUser()
  {
    return _user;
  }

  /**
   * Sets the connection's user.
   */
  public void setUser(String user)
  {
    _user = user;
  }

  /**
   * Returns the connection's password
   */
  public String getPassword()
  {
    return _password;
  }

  /**
   * Sets the connection's password
   */
  public void setPassword(String password)
  {
    _password = password;
  }

  /**
   * Sets a property from the underlying driver.  Used to set driver
   * properties not handled by DBPool.
   *
   * @param name property name for the driver
   * @param value the driver's value of the property name
   */
  public void setInitParam(InitParam initParam)
  {
    validateInitParam();
    
    HashMap<String,String> paramMap = initParam.getParameters();

    Iterator<String> iter = paramMap.keySet().iterator();
    while (iter.hasNext()) {
      String key = iter.next();

      _info.setProperty(key, paramMap.get(key));
    }
  }

  /**
   * Sets a property from the underlying driver.  Used to set driver
   * properties not handled by DBPool.
   *
   * @param name property name for the driver
   * @param value the driver's value of the property name
   */
  public void setInitParam(String key, String value)
  {
    _info.setProperty(key, value);
  }

  /**
   * Returns the properties.
   */
  public Properties getInfo()
  {
    return _info;
  }

  /**
   * Returns the driver object.
   */
  public Driver getDriver()
    throws SQLException
  {
    Object obj = getDriverObject();

    if (obj instanceof Driver)
      return (Driver) obj;
    else
      return null;
  }

  /**
   * Sets the driver object.
   */
  public void setDriver(Driver driver)
    throws SQLException
  {
    _driver = driver;
    _driverObject = driver;
  }

  public void setDriverObject(Object driverObject)
  {
    _driverObject = driverObject;
  }

  /**
   * Returns the driver pool.
   */
  public ConnectionPoolDataSource getPoolDataSource()
    throws SQLException
  {
    return _poolDataSource;
  }

  /**
   * Sets the pooled data source driver.
   */
  public void setPoolDataSource(ConnectionPoolDataSource pDataSource)
    throws SQLException
  {
    _poolDataSource = pDataSource;
    _driverObject = _poolDataSource;
  }

  /**
   * Returns any XADataSource.
   */
  public XADataSource getXADataSource()
  {
    return _xaDataSource;
  }

  /**
   * Sets the xa data source driver.
   */
  public void setXADataSource(XADataSource xaDataSource)
    throws SQLException
  {
    _xaDataSource = xaDataSource;
    _driverObject = _xaDataSource;
  }

  /**
   * Returns the managed connection factory.
   */
  public ManagedConnectionFactory getManagedConnectionFactory()
  {
    return _jcaDataSource;
  }

  /**
   * Returns true if the driver is XA enabled.
   */
  public boolean isXATransaction()
  {
    return _xaDataSource != null && _dbPool.isXA();
  }

  /**
   * Returns true if the driver is XA enabled.
   */
  public boolean isLocalTransaction()
  {
    return _dbPool.isXA();
  }

  /**
   * Configure a ProfilerPointConfig, used to create a ProfilerPoint
   * that is then passed to setProfiler().
   * The returned ProfilerPointConfig has a default name set to the URL of
   * this driver,
   */
  public ProfilerPointConfig createProfilerPoint()
  {
    ProfilerPointConfig profilerPointConfig = new ProfilerPointConfig();

    profilerPointConfig.setName(getURL());
    profilerPointConfig.setCategorizing(true);

    return profilerPointConfig;
  }

  /**
   * Enables profiling for this driver.
   */
  public void setProfilerPoint(ProfilerPoint profilerPoint)
  {
    _profilerPoint = profilerPoint;
  }
  
  public void setIndex(int index)
  {
    _index = index;
  }
  
  public int getIndex()
  {
    return _index;
  }

  /**
   * Returns the driver admin.
   */
  public JdbcDriverMXBean getAdmin()
  {
    return _admin;
  }

  /**
   * Initialize the pool's data source
   *
   * <ul>
   * <li>If data-source is set, look it up in JNDI.
   * <li>Else if the driver is a pooled or xa data source, use it.
   * <li>Else create wrappers.
   * </ul>
   */
  synchronized void initDataSource(boolean isTransactional,
                                   String spyId,
                                   boolean isSpy)
    throws SQLException
  {
    if (! _lifecycle.toActive())
      return;
    
    if (_xaDataSource == null && _poolDataSource == null) {
      initDriver();

      Object driverObject = getDriverObject();

      if (driverObject == null) {
        throw new SQLExceptionWrapper(L.l("driver '{0}' has not been configured for pool {1}.  <database> needs a <driver type='...'>.",
                                          _driverClass, getDBPool().getName()));
      }

      if (_driverType == TYPE_XA)
        _xaDataSource = (XADataSource) _driverObject;
      else if (_driverType == TYPE_POOL)
        _poolDataSource = (ConnectionPoolDataSource) _driverObject;
      else if (_driverType == TYPE_DRIVER)
        _driver = (Driver) _driverObject;
      else if (driverObject instanceof XADataSource)
        _xaDataSource = (XADataSource) _driverObject;
      else if (_driverObject instanceof ConnectionPoolDataSource)
        _poolDataSource = (ConnectionPoolDataSource) _driverObject;
      else if (_driverObject instanceof ManagedConnectionFactory)
        _jcaDataSource = (ManagedConnectionFactory) _driverObject;
      else if (_driverObject instanceof Driver)
        _driver = (Driver) _driverObject;
      else if (_driverObject instanceof DataSource)
        _poolDataSource = new ConnectionPoolAdapter((DataSource) _driverObject);
      else
        throw new SQLExceptionWrapper(L.l("driver '{0}' has not been configured for pool {1}.  <database> needs a <driver type='...'>.",
                                          _driverClass, getDBPool().getName()));

      /*
      if (! isTransactional && _xaDataSource != null) {
        throw new SQLExceptionWrapper(L.l("XADataSource '{0}' must be configured as transactional.  Either configure it with <xa>true</xa> or use the database's ConnectionPoolDataSource driver or the old java.sql.Driver driver.",
                                          _xaDataSource));
      }
      */
    }
    
    spyId = spyId + ".d" + _index;

    if (! isSpy) {
    }
    else if (_poolDataSource != null) {
      _poolDataSource = new SpyConnectionPoolDataSource(_poolDataSource,
                                                        spyId);
    }

    _admin.register();

    if (_profilerPoint != null) {
      if (log.isLoggable(Level.FINE))
        log.fine(_profilerPoint.toString());

      if (_xaDataSource != null)
        _xaDataSource = new XADataSourceWrapper(_profilerPoint, _xaDataSource);
      else if (_poolDataSource != null)
        _poolDataSource = new ConnectionPoolDataSourceWrapper(_profilerPoint, _poolDataSource);
      else if (_driver != null)
        _driver = new DriverWrapper(_profilerPoint, _driver);
    }

    if (_info.size() != 0) {
      validateInitParam();
    }
  }

  Lifecycle getLifecycle()
  {
    return _lifecycle;
  }

  boolean start()
  {
    return _lifecycle.toActive();
  }

  boolean stop()
  {
    return _lifecycle.toStop();
  }

  private void validateInitParam()
  {
    if (_jcaDataSource != null) {
      throw new ConfigException(L.l("<init-param> cannot be used with a JCA data source.  Use the init-param key as a tag, like <key>value</key>"));
    }
    else if (_poolDataSource != null) {
      throw new ConfigException(L.l("<init-param> cannot be used with a ConnectionPoolDataSource.  Use the init-param key as a tag, like <key>value</key>"));
    }
    else if (_xaDataSource != null) {
      throw new ConfigException(L.l("<init-param> cannot be used with an XADataSource.  Use the init-param key as a tag, like <key>value</key>"));
    }
  }

  /**
   * Returns the driver object configured for the database.
   */
  synchronized Object getDriverObject()
    throws SQLException
  {
    if (_driverObject != null)
      return _driverObject;
    else if (_driverClass == null)
      return null;

    if (log.isLoggable(Level.CONFIG))
      log.config("loading driver: " + _driverClass.getName());

    try {
      _driverObject = _driverClass.newInstance();
    } catch (Exception e) {
      throw new SQLExceptionWrapper(e);
    }

    return _driverObject;
  }

  /**
   * Creates a connection.
   */
  PooledConnection createPooledConnection(String user, String password)
    throws SQLException
  {
    PooledConnection conn = null;
    if (_xaDataSource != null) {
      if (user == null && password == null)
        conn = _xaDataSource.getXAConnection();
      else
        conn = _xaDataSource.getXAConnection(user, password);

      /*
      if (! _isTransactional) {
        throw new SQLExceptionWrapper(L.l("XADataSource '{0}' must be configured as transactional.  Either configure it with <xa>true</xa> or use the database's ConnectionPoolDataSource driver or the old java.sql.Driver driver.",
                                          _xaDataSource));
      }
      */
    }
    else if (_poolDataSource != null) {
      /*
      if (_isTransactional) {
        throw new SQLExceptionWrapper(L.l("ConnectionPoolDataSource '{0}' can not be configured as transactional.  Either use the database's XADataSource driver or the old java.sql.Driver driver.",
                                          _poolDataSource));
      }
      */

      if (user == null && password == null)
        conn = _poolDataSource.getPooledConnection();
      else
        conn = _poolDataSource.getPooledConnection(user, password);

    }

    return conn;
  }

  /**
   * Creates a connection.
   */
  Connection createDriverConnection(String user, String password)
    throws SQLException
  {
    if (! _lifecycle.isActive())
      return null;
    
    if (_xaDataSource != null || _poolDataSource != null)
      throw new IllegalStateException();

    if (_driver == null)
      throw new IllegalStateException();

    Driver driver = _driver;
    String url = getURL();

    if (url == null)
      throw new SQLException(L.l("can't create connection with null url"));

    try {
      Properties properties = new Properties();
      properties.putAll(getInfo());

      if (user != null)
        properties.put("user", user);
      else
        properties.put("user", "");

      if (password != null)
        properties.put("password", password);
      else
        properties.put("password", "");

      Connection conn;
      if (driver != null)
        conn = driver.connect(url, properties);
      else
        conn = java.sql.DriverManager.getConnection(url, properties);

      synchronized (this) {
        _connectionCountTotal++;
      }

      return conn;
    } catch (SQLException e) {
      _connectionFailCountTotal.incrementAndGet();
      _lastFailTime = Alarm.getCurrentTime();
      
      throw e;
    }
  }

  @PostConstruct
  public void init()
  {
    if (_driverClass == null
        && _poolDataSource == null
        && _xaDataSource == null) {
      if (_driverURL == null)
        throw new ConfigException(L.l("<driver> requires a 'type' or 'url'"));

      String driver = findDriverByUrl(_driverURL);

      if (driver == null)
        throw new ConfigException(L.l("url='{0}' does not have a known driver.  The driver class must be specified by a 'type' parameter.",
                                      _driverURL));

      Class<?> driverClass = null;
      try {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();

        driverClass = Class.forName(driver, false, loader);
      } catch (RuntimeException e) {
        throw e;
      } catch (Exception e) {
        throw ConfigException.create(e);
      }

      setType(driverClass);
    }
  }
  
  /**
   * Initializes the JDBC driver.
   */
  public void initDriver()
    throws SQLException
  {
    if (! _lifecycle.toInit())
      return;

    Object driverObject = getDriverObject();

    if (driverObject != null) {
    }
    else if (_xaDataSource != null || _poolDataSource != null)
      return;
    else {
      throw new SQLExceptionWrapper(L.l("driver '{0}' has not been configured for pool {1}.  <database> needs either a <data-source> or a <type>.",
                                        _driverClass, getDBPool().getName()));
    }

    ConfigType<?> configType = TypeFactory.getType(driverObject);

    // server/14g1
    if (_driverURL != null) {
      if (! configType.setProperty(driverObject, URL, _driverURL)) {
        if (! (driverObject instanceof Driver)) {
          throw new ConfigException(L.l("database: 'url' is an unknown property of '{0}'",
                                        driverObject.getClass().getName()));
        }
      }
    }
    
    if (_user != null) {
      if (! configType.setProperty(driverObject, USER, _user)) {
        if (! (driverObject instanceof Driver)) {
          throw new ConfigException(L.l("database: 'user' is an unknown property of '{0}'",
                                        driverObject.getClass().getName()));
        }
      }
    }
    
    if (_password != null) {
      if (! configType.setProperty(driverObject, PASSWORD, _password)) {
        if (! (driverObject instanceof Driver)) {
          throw new ConfigException(L.l("database: 'password' is an unknown property of '{0}'",
                                        driverObject.getClass().getName()));
        }
      }
    }

    try {
      if (_init != null) {
        _init.configure(driverObject);
        _init = null;
      }

      Config.init(driverObject);
    } catch (Throwable e) {
      log.log(Level.FINE, e.toString(), e);
      throw new SQLExceptionWrapper(e);
    }
  }

  protected String findDriverByUrl(String url)
  {
    String driver = DatabaseManager.findDriverByUrl(_driverURL);

    if (driver != null)
      return driver;

    ClassLoader loader = Thread.currentThread().getContextClassLoader();
    try {
      Enumeration<URL> e = loader.getResources("META-INF/services/java.sql.Driver");

      while (e.hasMoreElements()) {
        URL serviceUrl = e.nextElement();

        driver = testDriver(url, serviceUrl);

        if (driver != null)
          return driver;
      }
    } catch (Exception e) {
      log.log(Level.WARNING, e.toString(), e);
    }

    return null;
  }

  private String testDriver(String url, URL serviceURL)
  {
    ClassLoader loader = Thread.currentThread().getContextClassLoader();
    InputStream is = null;
    
    try {
      is = serviceURL.openStream();

      ReadStream in = Vfs.openRead(is);
      String line;
      while ((line = in.readLine()) != null) {
        int p = line.indexOf('#');
        if (p >= 0)
          line = line.substring(p);

        line = line.trim();

        if (line.length() == 0)
          continue;

        try {
          Class<?> cl = Class.forName(line, false, loader);

          Driver driver = (Driver) cl.newInstance();

          if (driver.acceptsURL(url))
            return cl.getName();
        } catch (Exception e) {
          log.log(Level.WARNING, e.toString(), e);
        }
      }
    } catch (Exception e) {
      log.log(Level.WARNING, e.toString(), e);
    } finally {
      IoUtil.close(is);
    }

    return null;
  }

  //
  // statistics
  //
  
  /**
   * Returns the total number of connections made.
   */
  public long getConnectionCountTotal()
  {
    return _connectionCountTotal;
  }
  
  /**
   * Returns the total number of failing connections
   */
  public long getConnectionFailCountTotal()
  {
    return _connectionFailCountTotal.get();
  }
  
  /**
   * Returns the time of the last connection
   */
  public long getLastFailTime()
  {
    return _lastFailTime;
  }

  /**
   * Returns a string description of the pool.
   */
  public String toString()
  {
    return "JdbcDriver[" + _driverURL + "]";
  }
}
