/*
 * Copyright (c) 1998-2012 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 com.caucho.loader.EnvironmentLocal;
import com.caucho.util.L10N;
import com.caucho.config.ConfigException;
import com.caucho.vfs.*;

import java.net.*;
import java.sql.*;
import javax.sql.*;
import java.util.*;
import java.util.logging.*;

/**
 * Manages databases in a local environment, e.g. for PHP dynamic
 * database lookup.
 */
public class DatabaseManager {
  protected static final Logger log
    = Logger.getLogger(DatabaseManager.class.getName());
  private static final L10N L = new L10N(DatabaseManager.class);

  private static final EnvironmentLocal<DatabaseManager> _localManager
    = new EnvironmentLocal<DatabaseManager>();

  private final HashMap<String,DBPool> _databaseMap
    = new HashMap<String,DBPool>();

  private final ArrayList<Driver> _driverList
    = new ArrayList<Driver>();

  private int _gId;

  /**
   * The manager is never instantiated.
   */
  private DatabaseManager()
  {
    initDriverList();
  }

  /**
   * Returns the database manager for the local environment.
   */
  private static DatabaseManager getLocalManager()
  {
    synchronized (_localManager) {
      DatabaseManager manager = _localManager.getLevel();

      if (manager == null) {
        manager = new DatabaseManager();

        _localManager.set(manager);
      }

      return manager;
    }
  }

  /**
   * Returns a matching dbpool.
   */
  public static DataSource findDatabase(String url)
    throws SQLException
  {
    String driver = findDriverByUrl(url);

    return getLocalManager().findDatabaseImpl(url, url, driver);
  }

  /**
   * Returns a matching dbpool.
   */
  public static DataSource findDatabase(String url, String driver)
    throws SQLException
  {
    return getLocalManager().findDatabaseImpl(url, url, driver);
  }

  /**
   * Returns a matching dbpool.
   */
  public static DataSource findDatabase(String id, String url, String driver)
    throws SQLException
  {
    return getLocalManager().findDatabaseImpl(id, url, driver);
  }

  public static void closeDatabase(DataSource ds)
  {
    getLocalManager().closeImpl(ds);
  }

  /**
   * Looks up the local database, creating if necessary.
   */
  private DataSource findDatabaseImpl(String id,
                                      String url,
                                      String driverName)
    throws SQLException
  {
    try {
      synchronized (_databaseMap) {
        DBPool db = _databaseMap.get(url);

        if (db == null) {
          db = new DBPool();

          db.setVar(id + "-" + _gId++);

          DriverConfig driver = db.createDriver();

          ClassLoader loader = Thread.currentThread().getContextClassLoader();

          Class<?> driverClass = Class.forName(driverName, false, loader);

          driver.setType(driverClass);
          driver.setURL(url);

          db.init();

          _databaseMap.put(url, db);
        }

        return db;
      }
    } catch (RuntimeException e) {
      throw e;
    } catch (SQLException e) {
      throw e;
    } catch (Exception e) {
      throw ConfigException.create(e);
    }
  }

  /**
   * Looks up the local database, creating if necessary.
   */
  private void closeImpl(DataSource dataSource)
  {
    DBPool db = (DBPool) dataSource;

    if (db == null)
      return;

    try {
      String key = null;

      synchronized (_databaseMap) {
        for (Map.Entry<String,DBPool> entry : _databaseMap.entrySet()) {
          if (entry.getValue() == db) {
            key = entry.getKey();
          }
        }

        if (key == null)
          return;

        _databaseMap.remove(key);
      }

      db.close();
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw ConfigException.create(e);
    }
  }

  public static String findDriverByUrl(String url)
  {
    return getLocalManager().findDriverByUrlImpl(url);
  }

  private String findDriverByUrlImpl(String url)
  {
    for (int i = 0; i < _driverList.size(); i++) {
      try {
        Driver driver = (Driver) _driverList.get(i);

        if (driver.acceptsURL(url))
          return driver.getClass().getName();
      } catch (Exception e) {
        log.log(Level.FINE, e.toString(), e);
      }
    }

    return null;
  }

  private void initDriverList()
  {
    try {
      Thread thread = Thread.currentThread();
      ClassLoader loader = thread.getContextClassLoader();

      Enumeration<URL> iter
        = loader.getResources("META-INF/services/java.sql.Driver");
      while (iter.hasMoreElements()) {
        URL url = iter.nextElement();

        ReadStream is = null;
        try {
          is = Vfs.lookup(url.toString()).openRead();

          String filename;

          while ((filename = is.readLine()) != null) {
            int p = filename.indexOf('#');

            if (p >= 0)
              filename = filename.substring(0, p);

            filename = filename.trim();
            if (filename.length() == 0)
              continue;

            try {
              Class<?> cl = Class.forName(filename, false, loader);
              Driver driver = null;

              if (Driver.class.isAssignableFrom(cl))
                driver = (Driver) cl.newInstance();

              if (driver != null) {
                log.fine(L.l("DatabaseManager adding driver '{0}'",
                             driver.getClass().getName()));

                _driverList.add(driver);
              }
            } catch (Exception e) {
              log.log(Level.FINE, e.toString(), e);
            }
          }
        } catch (Exception e) {
          log.log(Level.FINE, e.toString(), e);
        } finally {
          if (is != null)
            is.close();
        }
      }
    } catch (Exception e) {
      log.log(Level.FINE, e.toString(), e);
    }
  }

  static class DatabaseKey {
    private String _url;
    private String _catalog;

    DatabaseKey(String url, String catalog)
    {
      _url = url;
      _catalog = catalog;
    }

    public int hashCode()
    {
      int hash = 37;

      hash = 65521 * hash + _url.hashCode();

      if (_catalog != null)
        hash = 65521 * hash + _catalog.hashCode();

      return hash;
    }

    public boolean equals(Object o)
    {
      if (! (o instanceof DatabaseKey))
        return false;

      DatabaseKey key = (DatabaseKey) o;

      if (! _url.equals(key._url))
        return false;

      return (_catalog == key._catalog
              || _catalog != null && _catalog.equals(key._catalog));
    }
  }
}

