/* $Id$ */

/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.manifoldcf.jdbc;

import org.apache.manifoldcf.core.interfaces.*;
import org.apache.manifoldcf.core.jdbcpool.*;
import org.apache.manifoldcf.agents.interfaces.*;
import org.apache.manifoldcf.crawler.system.Logging;
import org.apache.manifoldcf.crawler.system.ManifoldCF;

import java.util.*;
import java.sql.*;
import javax.naming.*;
import javax.sql.*;
import java.util.*;

/** This class creates a connection
*/
public class JDBCConnectionFactory
{
  public static final String _rcsid = "@(#)$Id$";

  private static Map<String,String> driverMap;

  private static ConnectionPoolManager _pool = null;

  static
  {
    driverMap = new HashMap<String,String>();
    driverMap.put("oracle:thin:@", "oracle.jdbc.OracleDriver");
    driverMap.put("postgresql://", "org.postgresql.Driver");
    driverMap.put("jtds:sqlserver://", "net.sourceforge.jtds.jdbc.Driver");
    driverMap.put("jtds:sybase://", "net.sourceforge.jtds.jdbc.Driver");
    driverMap.put("mysql://", "com.mysql.jdbc.Driver");
    driverMap.put("mariadb://", "org.mariadb.jdbc.Driver");
    driverMap.put("xbib:csv:", "org.xbib.jdbc.csv.CsvDriver");
    try
    {
      _pool = new ConnectionPoolManager(120,false);
    }
    catch (Exception e)
    {
      System.err.println("Can't set up pool");
      e.printStackTrace(System.err);
    }
  }

  private JDBCConnectionFactory()
  {
  }

  /** Convert various connection parameters to a JDBC connection string, used in conjunction with the
  * provider name.
  */
  public static String getJDBCDriverString(String providerName, String host, String database, String rawDriverString)
  {
    if (rawDriverString != null && rawDriverString.length() > 0)
      return rawDriverString;

    if (database.length() == 0)
      database = "_root_";

    String instanceName = null;
    // Special for MSSQL: Allow database spec to contain an instance name too, in form:
    // <instance>/<database>
    if (providerName.startsWith("jtds:"))
    {
      int slashIndex = database.indexOf("/");
      if (slashIndex != -1)
      {
        instanceName = database.substring(0,slashIndex);
        database = database.substring(slashIndex+1);
      }
    }

    return host + "/" + database + ((instanceName==null)?"":";instance="+instanceName);
  }
  
  public static WrappedConnection getConnection(String providerName, String jdbcDriverString, String userName, String password)
    throws ManifoldCFException, ServiceInterruption
  {
    String driverClassName = driverMap.get(providerName);
    if (driverClassName == null)
      throw new ManifoldCFException("Unrecognized jdbc provider: '"+providerName+"'");

    String dburl = "jdbc:" + providerName + jdbcDriverString;
    if (Logging.connectors != null && Logging.connectors.isDebugEnabled())
      Logging.connectors.debug("JDBC: The connect string is '"+dburl+"'");
    try
    {
      // Hope for a connection now
      if (_pool != null)
      {
        // Build a unique string to identify the pool.  This has to include
        // the database and host at a minimum.

        // Provider is part of the pool key, so that the pools can distinguish between different databases
        String poolKey = providerName + "/" + jdbcDriverString;

        // Better include the credentials on the pool key, or we won't be able to change those and have it build new connections
        // The password value is SHA-1 hashed, because the pool driver reports the password in many exceptions and we don't want it
        // to be displayed.
        poolKey += "/" + userName + "/" + ManifoldCF.hash(password);

        ConnectionPool cp;
        synchronized (_pool)
        {
          cp = _pool.getPool(poolKey);
          if (cp == null)
          {
            // Register the driver here
            Class.forName(driverClassName);
            //System.out.println("Class name '"+driverClassName+"'; URL = '"+dburl+"'");
            cp =_pool.addAlias(poolKey, driverClassName, dburl,
              userName, password, 30, 300000L);
          }
        }
        return cp.getConnection();
      }
      else
        throw new ManifoldCFException("Can't get connection since pool driver did not initialize properly");
    }
    catch (InterruptedException e)
    {
      throw new ManifoldCFException(e.getMessage(),ManifoldCFException.INTERRUPTED);
    }
    catch (java.sql.SQLException e)
    {
      e.printStackTrace();
      // Unfortunately, the connection pool manager manages to eat all actual connection setup errors.  This makes it very hard to figure anything out
      // when something goes wrong.  So, we try again, going directly this time as a means of getting decent error feedback.
      try
      {
        if (userName != null && userName.length() > 0)
        {
          DriverManager.getConnection(dburl, userName, password).close();
        }
        else
        {
          DriverManager.getConnection(dburl).close();
        }
      }
      catch (java.sql.SQLException e2)
      {
        throw new ManifoldCFException("Error getting connection: "+e2.getMessage(),e2,ManifoldCFException.SETUP_ERROR);
      }
      // By definition, this must be a service interruption, because the direct route in setting up the connection succeeded.
      long currentTime = System.currentTimeMillis();
      throw new ServiceInterruption("Error getting connection: "+e.getMessage(),e,currentTime + 300000L,currentTime + 6 * 60 * 60000L,-1,true);
    }
    catch (java.lang.ClassNotFoundException e)
    {
      throw new ManifoldCFException("Driver class not found: "+e.getMessage(),e,ManifoldCFException.SETUP_ERROR);
    }
    catch (java.lang.InstantiationException e)
    {
      throw new ManifoldCFException("Driver class not instantiable: "+e.getMessage(),e,ManifoldCFException.SETUP_ERROR);
    }
    catch (java.lang.IllegalAccessException e)
    {
      throw new ManifoldCFException("Driver class not accessible: "+e.getMessage(),e,ManifoldCFException.SETUP_ERROR);
    }
  }

  public static void releaseConnection(WrappedConnection c)
  {
    c.release();
  }

}

