/*******************************************************************************
 * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved.
 * This program and the accompanying materials are made available under the 
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 
 * which accompanies this distribution. 
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at 
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Oracle - initial API and implementation from Oracle TopLink
 ******************************************************************************/  
package org.eclipse.persistence.sessions;

import java.util.*;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.sql.*;
import java.io.*;
import org.eclipse.persistence.internal.helper.*;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.internal.localization.*;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.eclipse.persistence.internal.security.PrivilegedClassForName;
import org.eclipse.persistence.internal.security.PrivilegedNewInstanceFromClass;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.logging.SessionLog;

/**
 * <p>
 * <b>Purpose</b>:Use this Connector to build a java.sql.Connection in the
 * "standard" fashion, via the DriverManager.
 *
 * @author Big Country
 * @since TOPLink/Java 2.1
 */
public class DefaultConnector implements Connector {
    protected String driverClassName;
    protected String driverURLHeader;
    protected String databaseURL;
    /** cache up the driver class to speed up reconnects */
    protected Class driverClass;
    /** cache up the instantiated Driver to speed up reconnects */
    protected Driver driver;

    /** Save whether we have switched to direct connect (sql.Driver.connect()) mode */
    private boolean connectDirectly = false;
    
    /**
     * PUBLIC:
     * Construct a Connector with default settings
     * The database URL will still need to be set.
     */
    public DefaultConnector() {
    }

    /**
     * PUBLIC:
     * Construct a Connector with the specified settings.
     */
    public DefaultConnector(String driverClassName, String driverURLHeader, String databaseURL) {
        this.initialize(driverClassName, driverURLHeader, databaseURL);
    }

    /**
     * INTERNAL:
     * Clone the connector.
     */
    public Object clone() {
        try {
            return super.clone();
        } catch (Exception exception) {
            throw new InternalError("Clone failed");
        }
    }

    /**
     * INTERNAL:
     * Connect with the specified properties and session. Return the Connection.
     * @return java.sql.Connection
     */
    public Connection connect(Properties properties, Session session) throws DatabaseException {
        // if driver class name is given, ensure the driver has been loaded and registered
        if (this.driverClassName != null && this.driverClass == null) {
             this.loadDriverClass(session);
        }
         
        String connectionString = this.getConnectionString();
        // verify if connection URL is not null and empty
        if(connectionString == null || connectionString.trim().equals("")) {
            throw DatabaseException.unableToAcquireConnectionFromDriverException(
                    this.driverClassName, null, connectionString);
        }
        
        SQLException driverManagerException = null;
        if (this.shouldUseDriverManager(properties, session)) {
            try {
                return DriverManager.getConnection(this.getConnectionString(), properties);
            } catch (SQLException sqlException) {
                driverManagerException = sqlException;
                if (session != null) {
                    ((AbstractSession)session).log(SessionLog.FINER, SessionLog.CONNECTION, "connect_drivermanager_fail");
                    ((AbstractSession)session).logThrowable(SessionLog.FINER, SessionLog.CONNECTION, sqlException);
                }
            }
        }
        
        if (this.driverClass == null) {
            throw DatabaseException.sqlException(driverManagerException, (org.eclipse.persistence.internal.sessions.AbstractSession) session, true);
        }
        
        // try driver class directly 
        // Save secondary exception state and don't clear original exception
		boolean wrongDriverExceptionOccurred = false;
		try {
			// A return of null indicates wrong type of driver, an SQLException means connection problems
			Connection directConnection = this.directConnect(properties);
			if (null == directConnection) {
			    // This connection failed specifically because of a wrong or unknown driver
			    if (null != driverManagerException) {
					wrongDriverExceptionOccurred = true;
					// Nest root exception
					throw DatabaseException.unableToAcquireConnectionFromDriverException(driverManagerException,
							getDriverClassName(), properties.getProperty("user"), getDatabaseURL());
				} else {
					throw DatabaseException.unableToAcquireConnectionFromDriverException(
							getDriverClassName(), driverClass.getCanonicalName(), getDatabaseURL());
				}
			} else {
				if (null != driverManagerException) {
					// If this connection succeeded where the previous DriverManager connection failed - save state and continue
					connectDirectly = true;
				}
			}
			return directConnection;
		} catch (DatabaseException directConnectException) {
			if (driverManagerException != null && !wrongDriverExceptionOccurred) {
                throw DatabaseException.sqlException(driverManagerException, (AbstractSession) session, true);
            } else {
                throw directConnectException;
            }
        }
    }
        

    /**
     * INTERNAL: 
     * Indicates whether DriverManager should be used.
     * @return boolean 
     */
     public boolean shouldUseDriverManager(Properties properties, Session session) {
         return  !connectDirectly && ((session == null) || session.getServerPlatform().shouldUseDriverManager());
     }
     
    /**
     * INTERNAL:
     * Connect directly - without using DriverManager. Return the Connection.
     * @return java.sql.Connection
     */
    protected Connection directConnect(Properties properties) throws DatabaseException {
        if(this.driver == null) {
            this.instantiateDriver();
        }
        try {
            return this.driver.connect(this.getConnectionString(), properties);
        } catch (SQLException exception) {
            this.clearDriverClassAndDriver();
            throw DatabaseException.sqlException(exception, true);
        }
    }

    /**
     * PUBLIC:
     * Return the JDBC connection string.
     * This is a combination of the driver-specific URL header and the database URL.
     */
     public String getConnectionString() {
        if(this.getDriverURLHeader() == null) {
            return this.getDatabaseURL();
        } else if(this.getDatabaseURL() == null) {
            return this.getDriverURLHeader();
        }
         return this.getDriverURLHeader() + this.getDatabaseURL();
     }

    /**
     * PUBLIC:
     * Provide the details of my connection information. This is primarily for JMX runtime services.
     * @return java.lang.String
     */
    public String getConnectionDetails() {
        return this.getConnectionString();
    }

    /**
     * PUBLIC:
     * The database URL is the JDBC URL for the database server.
     * The driver header is <i>not</i> be included in this URL
     * (e.g. "dbase files"; not "jdbc:odbc:dbase files").
     */
    public String getDatabaseURL() {
        return databaseURL;
    }

    /**
     * PUBLIC:
     * The driver class is the name of the Java class for the JDBC driver being used
     * (e.g. "sun.jdbc.odbc.JdbcOdbcDriver").
     */
    public String getDriverClassName() {
        return driverClassName;
    }

    /**
     * PUBLIC:
     * The driver URL header is the string predetermined by the JDBC driver to be
     * part of the URL connection string, (e.g. "jdbc:odbc:").
     * This is required to connect to the database.
     */
    public String getDriverURLHeader() {
        return driverURLHeader;
    }

    /**
     * INTERNAL:
     * Initialize the connector with the specified settings.
     */
    protected void initialize(String driverClassName, String driverURLHeader, String databaseURL) {
        this.setDriverClassName(driverClassName);
        this.setDriverURLHeader(driverURLHeader);
        this.setDatabaseURL(databaseURL);
    }

    /**
     * INTERNAL:
     * Ensure that the driver has been loaded and registered with the
     * DriverManager. Just loading the class should cause the static
     * initialization code to do the necessary registration.
     * Return the loaded driver Class.
     */
    protected void loadDriverClass(Session session) throws DatabaseException {
        // CR#... The correct class loader must be used to load the class,
        // not that Class.forName must be used to initialize the class a simple loadClass may not.
        try {
            if(session != null) {
                if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()){
                    try {
                        driverClass = AccessController.doPrivileged(new PrivilegedClassForName(this.getDriverClassName(), true, session.getPlatform().getConversionManager().getLoader()));
                    } catch (PrivilegedActionException exception) {
                        throw DatabaseException.configurationErrorClassNotFound(this.getDriverClassName());
                    }
                } else {
                    driverClass = PrivilegedAccessHelper.getClassForName(this.getDriverClassName(), true, session.getPlatform().getConversionManager().getLoader());
                }
            } else {
                if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()){
                    try {
                        driverClass = AccessController.doPrivileged(new PrivilegedClassForName(this.getDriverClassName(), true, ConversionManager.getDefaultManager().getLoader()));
                    } catch (PrivilegedActionException exception) {
                        throw DatabaseException.configurationErrorClassNotFound(this.getDriverClassName());                }
                } else {
                    driverClass = PrivilegedAccessHelper.getClassForName(this.getDriverClassName(), true, ConversionManager.getDefaultManager().getLoader());
                }
            }
        } catch (ClassNotFoundException exception) {
            clearDriverClassAndDriver();
            throw DatabaseException.configurationErrorClassNotFound(this.getDriverClassName());
        }
    }

    /**
     * PUBLIC:
     * The database URL is the JDBC URL for the database server.
     * The driver header is <i>not</i> be included in this URL
     * (e.g. "dbase files"; not "jdbc:odbc:dbase files").
     */
    public void setDatabaseURL(String databaseURL) {
        this.databaseURL = databaseURL;
    }

    /**
     * PUBLIC:
     * The driver class is the name of the Java class for the JDBC driver being used
     * (e.g. "sun.jdbc.odbc.JdbcOdbcDriver").
     */
    public void setDriverClassName(String driverClassName) {
        this.driverClassName = driverClassName;
        // if driver class name changed then discard the cached driver class and driver.
        clearDriverClassAndDriver();
    }

    /**
     * PUBLIC:
     * The driver URL header is the string predetermined by the JDBC driver to be
     * part of the URL connection string, (e.g. "jdbc:odbc:").
     * This is required to connect to the database.
     */
    public void setDriverURLHeader(String driverURLHeader) {
        this.driverURLHeader = driverURLHeader;
    }

    /**
     * PUBLIC:
     * Print connection string.
     */
    public String toString() {
        return Helper.getShortClassName(getClass()) + "(" + getConnectionString() + ")";
    }

    /**
     * INTERNAL:
     * Print something useful on the log.
     */
    public void toString(PrintWriter writer) {
        writer.println(ToStringLocalization.buildMessage("datasource_URL", (Object[])null) + "=> \"" + this.getConnectionString() + "\"");
    }

    /**
     * INTERNAL:
     * Instantiate the Driver.
     */
    protected void instantiateDriver() throws DatabaseException {
        try {
            if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()){
                try{
                    this.driver = (Driver)AccessController.doPrivileged(new PrivilegedNewInstanceFromClass(this.driverClass));
                }catch (PrivilegedActionException ex){
                    if (ex.getCause() instanceof IllegalAccessException){
                        throw (IllegalAccessException)ex.getCause();
                    }else if (ex.getCause() instanceof InstantiationException){
                        throw (InstantiationException)ex.getCause();
                    }
                    throw (RuntimeException)ex.getCause();
                }
            }else{
                this.driver = (Driver)PrivilegedAccessHelper.newInstanceFromClass(this.driverClass);
            }
       } catch (InstantiationException ie) {
            this.clearDriverClassAndDriver();
            throw DatabaseException.configurationErrorNewInstanceInstantiationException(ie, driverClass);
        } catch (IllegalAccessException iae) {
            this.clearDriverClassAndDriver();
            throw DatabaseException.configurationErrorNewInstanceIllegalAccessException(iae, driverClass);
        }
    }

    /**
     * INTERNAL:
     * Discard the cached driver class and driver.
     */
    public void clearDriverClassAndDriver() {
        this.driverClass = null;
        this.driver = null;
    }
}
