/*
 * Copyright (c) 1998, 2022 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 v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     05/28/2008-1.0M8 Andrei Ilitchev
//        - New file introduced for bug 224964: Provide support for Proxy Authentication through JPA.
package org.eclipse.persistence.testing.framework.oracle;

import oracle.ucp.jdbc.PoolDataSource;
import oracle.ucp.jdbc.PoolDataSourceFactory;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.sessions.DatabaseSessionImpl;
import org.eclipse.persistence.sessions.DatabaseLogin;
import org.eclipse.persistence.sessions.DatabaseSession;
import org.eclipse.persistence.sessions.JNDIConnector;
import org.eclipse.persistence.sessions.server.ServerSession;
import org.eclipse.persistence.testing.framework.TestProblemException;

import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;

// Intended usage:
// 1.1 Obtains a session from the TestExecutor, logs it out, and keeps;
// 1.2 Using the original session's login and the passed parameters
// creates session that uses externalConnectionPools;
// 1.3 Logs the new session into TestExecutor so that it could be used by tests.
// 2.1 After done with the new session - kill it, shutdown all the data sources created for it;
// 2.2 Login the original session and set it back into TestExecutor.
//// Example: to create a new DatabaseSession that use externalConnectionPooling:
// SessionExchange exchange = new SessionExchange();
// testExecutor.setSession(exchange.createDatabaseSession((DatabaseSession)testExecutor.getSession()), true);
//// after the testing with the new session is completed:
// testExecutor.setSession(exchange.returnOriginalSession());

public class SessionExchanger {
    // indicates whether the original session should be disconnected
    public boolean shouldLogoutOriginalSession = true;
    // indicates whether the new session should be connected
    public boolean shouldLoginNewSession = true;

    DatabaseSession originalSession;
    DatabaseSession newSession;
    boolean hasLoggedOutOriginalSession;
    PoolDataSource dataSource;

    // pass the original session and params for the new one:
    //   useDatabaseSession - "true" means new session is DatabaseSession; "false" - ServerSession;
    //   useExternalConnectionPooling - indicates whether the new session use external connection pooling or not.
    // returns the new session
    public DatabaseSession createNewSession(DatabaseSession originalSession, boolean useDatabaseSession, boolean useExternalConnectionPooling) {
        return createNewSession(originalSession, useDatabaseSession, useExternalConnectionPooling, null, null);
    }

    // additional property parameters:
    //   loginProperties - if not null set into the new session's login;
    //   propertyProperties - if not null set into the new session;
    // returns the new session
    public DatabaseSession createNewSession(DatabaseSession originalSession, boolean useDatabaseSession, boolean useExternalConnectionPooling, Properties loginProperties, Map<String, Object> sessionProperties) {
        if(useExternalConnectionPooling) {
            return createNewSession(originalSession, useDatabaseSession, useExternalConnectionPooling, loginProperties, sessionProperties, -1, -1, -1, -1);
        } else {
            return createNewSession(originalSession, useDatabaseSession, useExternalConnectionPooling, loginProperties, sessionProperties, 2, 2, 2, 2);
        }
    }

    // additional min / max parameters used only in case of ServerSession:
    //   in external pooling case only minWriteConnection and maxWriteConnection used - they determine the min / max number of connections in the created data source,
    //        pass -1 value for minWriteConnection and/or maxWriteConnection if no min and/or max number of connections should be set in the data source.
    //   in internal pooling case the min / max values used for write and read connection pools.
    // returns the new session
    public DatabaseSession createNewSession(DatabaseSession originalSession, boolean useDatabaseSession, boolean useExternalConnectionPooling, Properties loginProperties, Map<String, Object> sessionProperties,
                                            int minWriteConnections, int maxWriteConnections, int minReadConnections, int maxReadConnections)
    {
        setup(originalSession);
        DatabaseLogin login = cloneAndSetDataSource(originalSession.getLogin(), useExternalConnectionPooling, minWriteConnections, maxWriteConnections);
        if(useDatabaseSession) {
            newSession = new DatabaseSessionImpl(login);
        } else {
            if(useExternalConnectionPooling) {
                newSession = new ServerSession(login);
            } else {
                newSession = new ServerSession(login, minWriteConnections, minWriteConnections);
            }
            if(!useExternalConnectionPooling) {
                ServerSession ss = (ServerSession)newSession;
                if(ss.getReadConnectionPool().getMaxNumberOfConnections() != maxReadConnections) {
                    ss.getReadConnectionPool().setMaxNumberOfConnections(maxReadConnections);
                }
                if(ss.getReadConnectionPool().getMinNumberOfConnections() != minReadConnections) {
                    ss.getReadConnectionPool().setMinNumberOfConnections(minReadConnections);
                }
            }
        }
        setProperties(loginProperties, sessionProperties);
        newSession.setSessionLog(originalSession.getSessionLog());
        newSession.setLogLevel(originalSession.getLogLevel());
        if(shouldLoginNewSession) {
            newSession.login();
        }
        return newSession;
    }

    // disconnects the temporary session, closes its datasource (if any);
    // in case the original session was disconnected in setup - reconnects it;
    // returns the original session
    public DatabaseSession returnOriginalSession() {
        try {
            clearNewSession();
            return originalSession;
        } finally {
            try {
                if(originalSession != null && hasLoggedOutOriginalSession) {
                    hasLoggedOutOriginalSession = false;
                    originalSession.login();
                }
            } finally {
                originalSession = null;
            }
        }
    }

    public boolean isBusy() {
        return newSession != null;
    }

    void setup(DatabaseSession originalSession) {
        if(isBusy()) {
            throw new TestProblemException("SessionExchanger: new session has been already created");
        }
        this.originalSession = originalSession;
        if(shouldLogoutOriginalSession) {
            if(originalSession.isConnected()) {
                originalSession.logout();
                hasLoggedOutOriginalSession = true;
            }
        }
    }

    void clearNewSession() {
        try {
            if(newSession != null) {
                if(newSession.isConnected()) {
                    newSession.logout();
                }
            }
        } finally {
            newSession = null;
            if(dataSource != null) {
                dataSource = null;
            }
        }
    }

    DatabaseLogin cloneAndSetDataSource(DatabaseLogin login, boolean useExternalConnectionPooling, int minConnections, int maxConnections) {
        DatabaseLogin cloneLogin = (DatabaseLogin)login.clone();
        if(useExternalConnectionPooling) {
            createDataSource(login.getConnectionString(), minConnections, maxConnections);
            cloneLogin.setConnector(new JNDIConnector(dataSource));
            cloneLogin.setUsesExternalConnectionPooling(true);
        }
        return cloneLogin;
    }

    void setProperties(Properties loginProperties, Map<String, Object> sessionProperties) {
        if(loginProperties != null) {
            newSession.getLogin().setProperties(loginProperties);
        }
        if(sessionProperties != null) {
            ((AbstractSession)newSession).setProperties(sessionProperties);
        }
    }

    // create a data source using the supplied connection string
    void createDataSource(String connectionString, int minConnections, int maxConnections) {
        try {
            dataSource = PoolDataSourceFactory.getPoolDataSource();
            dataSource.setConnectionFactoryClassName("oracle.jdbc.pool.OracleDataSource");
            Properties props = new Properties();
            if(minConnections >= 0) {
                dataSource.setMinPoolSize(minConnections);
                dataSource.setInitialPoolSize(minConnections);
            }
            if(maxConnections >= 0) {
                dataSource.setMaxPoolSize(maxConnections);
            }
            dataSource.setURL(connectionString);
        } catch (SQLException ex) {
            throw new TestProblemException("Failed to create OracleDataSource with " + connectionString + ".\n", ex);
        }
    }
}
