/*
 * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved.
 * Copyright (c) 2020 IBM Corporation. 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:
//     Oracle - initial API and implementation from Oracle TopLink
//     GYorke - non-bug update to set accessor in case of connection failure.  Thi
//              will allow the retry code to function.
package org.eclipse.persistence.internal.databaseaccess;

import java.util.*;
import org.eclipse.persistence.exceptions.*;
import org.eclipse.persistence.sessions.Login;
import org.eclipse.persistence.queries.Call;
import org.eclipse.persistence.internal.sequencing.SequencingCallback;
import org.eclipse.persistence.internal.sequencing.SequencingCallbackFactory;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.logging.SessionLog;
import org.eclipse.persistence.sessions.SessionProfiler;
import org.eclipse.persistence.sessions.server.ConnectionPool;

/**
 * INTERNAL:
 * <code>DatasourceAccessor</code> is an abstract implementation
 * of the <code>Accessor</code> interface providing common functionality to the concrete database and EIS accessors.
 * It is responsible for
 * connecting,
 * transactions,
 * call execution
 *
 * @see Call
 * @see Login
 *
 * @author James
 * @since OracleAS TopLink 10<i>g</i> (10.0.3)
 *
 * 05/28/2008-1.0M8 Andrei Ilitchev.
 *   - 224964: Provide support for Proxy Authentication through JPA.
 *     Added ConnectionCustomizer, also fixed  postConnect/preDisconnect ExternalConnection calls so that they called in case of reads, too.
 */
public abstract class DatasourceAccessor implements Accessor {

    /** Store the reference to the driver level connection. */
    protected Object datasourceConnection;

    /** Store the login information that connected this accessor. */
    protected Login login;

    /**
     * Keep track of the number of concurrent active calls.
     * This is used for connection pooling for loadbalancing and for external connection pooling.
     */
    protected int callCount;

    /**
     * Keep track of the number of the storedprocedure statement that being executed.
     */
    public int storedProcedureStatementsCount;
    /**
     * Keep track of the number of the read statement that being executed.
     */
    public int readStatementsCount;

    /**
     * Keep track of the number of the write statement that being executed.
     */
    public int writeStatementsCount;

    //Stores the number of executed read SQL statements
    public static final String READ_STATEMENTS_COUNT_PROPERTY = "Read_Statements_Count_Property";

    //Stores the number of executed write SQL statements
    public static final String WRITE_STATEMENTS_COUNT_PROPERTY = "Write_Statements_Count_Property";

    //Stores the number of executed store procedure statements
    public static final String STOREDPROCEDURE_STATEMENTS_COUNT_PROPERTY = "StoredProcedure_Statements_Count_Property";


    /** Keep track if the accessor is within a transaction context */
    protected boolean isInTransaction;

    /** Keep track of whether the accessor is "connected". */
    protected boolean isConnected;

    /** PERF: Cache platform to avoid gets (small but can add up). */
    /** This is also required to ensure all accessors for a session are using the same platform. */
    protected DatasourcePlatform platform;

    /**
     *  This attribute is used to determine if the connection should be returned to the pool or
     *  removed from the pool and closed.  It will be set to false if an exception occurs during
     *  Call execution.
     */
    protected boolean isValid;

    /**
     *  During (not external) transaction, SequencingManager may set SequencingCallback on the accessor,
     *  The callback's only method is called when transaction commits,
     *  after transaction is completed the callback is discarded.
     */
    protected transient SequencingCallback sequencingCallback;

    /**
     * This attribute is used to track failures on an accessor that may be communication based.
     * If a failure is detected executing a call with a query timeout this flag is set.  If an error happens
     * twice in a row on the same accessor then that accessor will be checked for a comm error.  If there is no
     * query timeout then the flag is not set and the accessor will be checked immediately.
     */
    protected boolean possibleFailure;

    /**
     *  Used only in externalConnectionPooling case. Indicates which session is currently using connection.
     *  Allows to rise appropriate session's event when connection is already/still alive.
     *  Events will be risen when ClientSession or DatabaseSession acquires connection in the beginning
     *  and releases connection in the end of transaction (in afterCompletion in jta case).
     *  In case the session requires exclusive connection (ExclusiveIsolatedClientSession) the events
     *  will be risen every time the session acquires or releases connection -
     *  which is a rare event - the connection is kept for the duration of jta transaction,
     *  after jta transaction is completed the new connection is acquired on a first query execution
     *  and kept until the next beginTransaction call.
     *  In non-jta case the connection is acquired for session's life and release only by session's release.
     *  Note that the attribute is nullified only in one place - by closeConnection method.
     */
    protected transient AbstractSession currentSession;

    /**
     * PERF: Cache connection pooling flag.
     */
    protected boolean usesExternalConnectionPooling;

    /**
     * Back-door to allow isConnect checks.
     * Since we now support fail-over and retry, removing old isConnected usage which can
     * cause major performance issues (on Sybase), and minor ones in general.
     */
    public static boolean shouldCheckConnection = false;

    /**
     * Allows session-specific connection customization.
     */
    protected ConnectionCustomizer customizer;

    protected ConnectionPool pool;

    /**
     *    Default Constructor.
     */
    public DatasourceAccessor() {
        this.isInTransaction = false;
        this.callCount = 0;
        this.isConnected = false;
        this.isValid = true;
    }

    /**
     * Clone the accessor.
     */
    public Object clone() {
        try {
            DatasourceAccessor accessor = (DatasourceAccessor)super.clone();
            if(accessor.customizer != null) {
                accessor.customizer.setAccessor(accessor);
            }
            return accessor;
        } catch (CloneNotSupportedException exception) {
            throw new InternalError("clone not supported");
        }
    }

    /**
     * Called from beforeCompletion external transaction synchronization listener callback
     * to close the external connection corresponding to the completing external transaction.
     * Final sql calls could be sent through the connection by this method
     * before it closes the connection.
     */
    public void closeJTSConnection() {
        if (usesExternalTransactionController()) {
            this.isInTransaction = false;
            if (this.usesExternalConnectionPooling) {
                closeConnection();
            }
        }
    }

    /**
     * Set the transaction transaction status of the receiver.
     */
    protected void setIsInTransaction(boolean value) {
        isInTransaction = value;
    }

    /**
     * This should be set to false if a communication failure occurred during a call execution.
     * In the case of an invalid accessor the Accessor will not be returned to the pool.
     */
    public void setIsValid(boolean isValid){
        this.isValid = isValid;
    }

    /**
     * Return the transaction status of the receiver.
     */
    public boolean isInTransaction() {
        return isInTransaction;
    }

    /**
     * Returns true if this Accessor can continue to be used.  This will be false if a communication
     * failure occurred during a call execution.  In the case of an invalid accessor the Accessor
     * will not be returned to the pool.
     */
    public boolean isValid(){
        return this.isValid;
    }

    public boolean isPossibleFailure() {
        return possibleFailure;
    }

    public void setPossibleFailure(boolean possibleFailure) {
        this.possibleFailure = possibleFailure;
    }

    /**
     * Return true if some external connection pool is in use.
     */
    public boolean usesExternalConnectionPooling() {
        return usesExternalConnectionPooling;
    }

    /**
     *    Begin a transaction on the database. If not using managed transaction begin a local transaction.
     */
    public void beginTransaction(AbstractSession session) throws DatabaseException {
        if (usesExternalTransactionController()) {
            if (session.isExclusiveConnectionRequired() && !this.isInTransaction && this.usesExternalConnectionPooling) {
                closeConnection();
            }
            this.isInTransaction = true;
            return;
        }

        session.log(SessionLog.FINER, SessionLog.TRANSACTION, "begin_transaction", (Object[])null, this);

        try {
            session.startOperationProfile(SessionProfiler.Transaction);
            incrementCallCount(session);
            basicBeginTransaction(session);
            this.isInTransaction = true;
        } finally {
            decrementCallCount();
            session.endOperationProfile(SessionProfiler.Transaction);
        }
    }

    /**
     * Begin the driver level transaction.
     */
    protected abstract void basicBeginTransaction(AbstractSession session);

    /**
     * Commit the driver level transaction.
     */
    protected abstract void basicCommitTransaction(AbstractSession session);

    /**
     * Rollback the driver level transaction.
     */
    protected abstract void basicRollbackTransaction(AbstractSession session);

    /**
     * Used for load balancing and external pooling.
     */
    public synchronized void decrementCallCount() {
        int count = this.callCount;
        // Avoid decrementing count if already zero, (failure before increment).
        if (count <= 0) {
            return;
        }
        this.callCount--;
        if (this.usesExternalConnectionPooling && (!this.isInTransaction) && (currentSession == null || !currentSession.isExclusiveConnectionRequired()) && (count == 1)) {
            try {
                closeConnection();
            } catch (DatabaseException ignore) {
                // Don't allow for errors to be masked by disconnect.
            }
        }
    }

    /**
     * Used for load balancing and external pooling.
     */
    public synchronized void incrementCallCount(AbstractSession session) {
        this.callCount++;

        if (this.callCount == 1) {
            // If the login is null, then this accessor has never been connected.
            if (this.login == null) {
                throw DatabaseException.databaseAccessorNotConnected();
            }

            // If the connection is no longer connected, it may have timed out.
            if (this.datasourceConnection != null) {
                if (shouldCheckConnection && !isConnected()) {
                    if (this.isInTransaction) {
                        throw DatabaseException.databaseAccessorNotConnected();
                    } else {
                        reconnect(session);
                    }
                }
            } else {
                // If ExternalConnectionPooling is used, the connection can be re-established.
                if (this.usesExternalConnectionPooling) {
                    reconnect(session);
                    session.postAcquireConnection(this);
                    currentSession = session;
                } else {
                    throw DatabaseException.databaseAccessorNotConnected();
                }
            }
        }
    }

    /**
     * Reset statement count.
     */
    public void reset() {
        this.readStatementsCount = 0;
        this.writeStatementsCount = 0;
        this.storedProcedureStatementsCount = 0;
    }

    /**
     * Connect to the database.
     * Exceptions are caught and re-thrown as EclipseLink exceptions.
     */
    protected void connectInternal(Login login, AbstractSession session) throws DatabaseException {
        try{
            this.datasourceConnection = login.connectToDatasource(this, session);
            this.isConnected = true;
            if(this.customizer != null) {
                customizer.customize();
            }
        }catch (DatabaseException ex){
            //Set the accessor to ensure the retry code has an opportunity to retry.
            ex.setAccessor(this);
            throw ex;
        }
    }

    /**
     * Set whether the accessor has a connection to the "data store".
     */
    protected void setIsConnected(boolean isConnected) {
        this.isConnected = isConnected;
    }

    /**
     * Used for load balancing and external pooling.
     */
    protected void setCallCount(int callCount) {
        this.callCount = callCount;
    }

    /**
     * Used for load balancing and external pooling.
     */
    public int getCallCount() {
        return callCount;
    }

    /**
     * Commit a transaction on the database. If using non-managed transaction commit the local transaction.
     */
    public void commitTransaction(AbstractSession session) throws DatabaseException {
        if (usesExternalTransactionController()) {
            // if there is no external TX controller, then that means we are currently not synchronized
            // with a global JTS transaction.  In this case, there won't be any 'afterCompletion'
            // callbacks so we have to release the connection here.  It is possible (WLS 5.1) to choose
            // 'usesExternalTransactionController' on the login, but still acquire a uow that WON'T be
            // synchronized with a global TX.
            if (!session.isSynchronized()) {
                this.isInTransaction = false;
                if (this.usesExternalConnectionPooling) {
                    // closeConnection method uses currentSession and then sets it to null.
                    currentSession = session;
                    closeConnection();
                }
            }
            return;
        }

        session.log(SessionLog.FINER, SessionLog.TRANSACTION, "commit_transaction", (Object[])null, this);

        try {
            session.startOperationProfile(SessionProfiler.Transaction);
            incrementCallCount(session);
            basicCommitTransaction(session);

            if(sequencingCallback != null) {
                sequencingCallback.afterCommit(this);
            }
            this.isInTransaction = false;
        } finally {
            sequencingCallback = null;
            decrementCallCount();
            session.endOperationProfile(SessionProfiler.Transaction);
        }
    }

    /**
     * Connect to the datasource.  Through using a CCI ConnectionFactory.
     * Catch exceptions and re-throw as EclipseLink exceptions.
     */
    public void connect(Login login, AbstractSession session) throws DatabaseException {
        session.startOperationProfile(SessionProfiler.ConnectionManagement);
        session.incrementProfile(SessionProfiler.Connects);

        try {
            if (session.shouldLog(SessionLog.FINE, SessionLog.CONNECTION)) {// Avoid printing if no logging required.
                Object[] args = { login };
                session.log(SessionLog.FINE, SessionLog.CONNECTION, "connecting", args, this);
            }
            setLogin(login);
            this.setDatasourcePlatform((DatasourcePlatform)session.getDatasourceLogin().getDatasourcePlatform());
            createCustomizer(session);
            try {
                connectInternal(login, session);
                this.isInTransaction = false;
            } catch (RuntimeException exception) {
                session.handleSevere(exception);
            }
            if (session.hasEventManager()) {
                session.getEventManager().postConnect(this);
            }
            incrementCallCount(session);
            try {
                buildConnectLog(session);
            } finally {
                decrementCallCount();
            }
        } finally {
            session.endOperationProfile(SessionProfiler.ConnectionManagement);
        }
    }

    /**
     * Close the connection to the driver level datasource.
     */
    protected abstract void closeDatasourceConnection();

    /**
     * Execute the call to driver level datasource.
     */
    protected abstract Object basicExecuteCall(Call call, AbstractRecord row, AbstractSession session);

    /**
     * Build a log string of any driver metadata that can be obtained.
     */
    protected abstract void buildConnectLog(AbstractSession session);

    /**
     * Return the login
     */
    public Login getLogin() {
        return login;
    }

    /**
     * SECURE:
     * set the login
     */
    protected void setLogin(Login login) {
        this.login = login;
        this.usesExternalConnectionPooling = login.shouldUseExternalConnectionPooling();
    }

    /**
     * Disconnect from the datasource.
     */
    public void disconnect(AbstractSession session) throws DatabaseException {
        session.log(SessionLog.FINE, SessionLog.CONNECTION, "disconnect", (Object[])null, this);

        if (this.datasourceConnection == null) {
            return;
        }
        session.incrementProfile(SessionProfiler.Disconnects);
        session.startOperationProfile(SessionProfiler.ConnectionManagement);
        try {
            releaseCustomizer();
            closeDatasourceConnection();
            this.datasourceConnection = null;
            this.isInTransaction = true;
        } finally {
            session.endOperationProfile(SessionProfiler.ConnectionManagement);
        }
    }

    /**
     * Close the accessor's connection.
     * This is used only for external connection pooling
     * when it is intended for the connection to be reconnected in the future.
     */
    public void closeConnection() {
        try {
            if (this.datasourceConnection != null) {
                if (isDatasourceConnected()) {
                    if(currentSession != null) {
                        currentSession.preReleaseConnection(this);
                    }
                    if(customizer != null && customizer.isActive()) {
                        customizer.clear();
                    }
                    closeDatasourceConnection();
                }
                this.datasourceConnection = null;
            }
        } catch (DatabaseException exception) {
            // Ignore
            this.datasourceConnection = null;
        } finally {
            currentSession = null;
        }
    }

    /**
     * Execute the call.
     * @return depending of the type either the row count, row or vector of rows.
     */
    public Object executeCall(Call call, AbstractRecord translationRow, AbstractSession session) throws DatabaseException {
        // If the login is null, then this accessor has never been connected.
        if (this.login == null) {
            throw DatabaseException.databaseAccessorNotConnected();
        }

        if (session.shouldLog(SessionLog.FINE, SessionLog.SQL)) {// pre-check to improve performance
            session.log(SessionLog.FINE, SessionLog.SQL, call.getLogString(this), (Object[])null, this, false);
        }

        Object result = basicExecuteCall(call, translationRow, session);

        return result;
    }

    /**
     * PUBLIC:
     * Reconnect to the database.  This can be used if the connection was disconnected or timedout.
     * This ensures that the security is checked as it is public.
     * Because the messages can take a long time to build,
     * pre-check whether messages should be logged.
     */
    public void reestablishConnection(AbstractSession session) throws DatabaseException {
        if (session.shouldLog(SessionLog.FINE, SessionLog.CONNECTION)) {// Avoid printing if no logging required.
            Object[] args = { getLogin() };
            session.log(SessionLog.FINE, SessionLog.CONNECTION, "reconnecting", args, this);
        }
        reestablishCustomizer();
        reconnect(session);
        this.isInTransaction = false;
        this.isValid = true;
        if (session.hasEventManager()) {
            session.getEventManager().postConnect(this);
        }
    }

    /**
     * Attempt to save some of the cost associated with getting a fresh connection.
     * Assume the DatabaseDriver has been cached, if appropriate.
     * Note: Connections that are participating in transactions will not be refreshed.^M
     */
    protected void reconnect(AbstractSession session) throws DatabaseException {
        session.log(SessionLog.FINEST, SessionLog.CONNECTION, "reconnecting_to_external_connection_pool", (Object[])null, this);
        session.startOperationProfile(SessionProfiler.ConnectionManagement);
        try {
            connectInternal(this.login, session);
        } finally {
            session.endOperationProfile(SessionProfiler.ConnectionManagement);
        }
    }

    /**
     * Return the platform.
     */
    public DatasourcePlatform getDatasourcePlatform() {
        return platform;
    }

    /**
     * Set the platform.
     * This should be set to the session's platform, not the connections
     * which may not be configured correctly.
     */
    public void setDatasourcePlatform(DatasourcePlatform platform) {
        this.platform = platform;
    }

    /**
     * Return the driver level connection.
     */
    public Object getDatasourceConnection() {
        return datasourceConnection;
    }

    /**
     * Helper method to return the JDBC connection for DatabaseAccessor.
     * Was going to deprecate this, but since most clients are JDBC this is useful.
     */
    public java.sql.Connection getConnection() {
        return (java.sql.Connection)this.datasourceConnection;
    }

    /**
     * Return column information for the specified
     * database objects.
     */
    public Vector getColumnInfo(String catalog, String schema, String tableName, String columnName, AbstractSession session) throws DatabaseException {
        return new Vector();
    }

    /**
     * Return the number of read statements.
     */
    public int getReadStatementsCount() {
        return readStatementsCount;
    }

    /**
     * Return the number of write statements.
     */
    public int getWriteStatementsCount() {
        return writeStatementsCount;
    }

    /**
     * Return the number of stored procedure call.
     */
    public int getStoredProcedureStatementsCount() {
        return storedProcedureStatementsCount;
    }

    /**
     * Return table information for the specified
     * database objects.
     */
    public Vector getTableInfo(String catalog, String schema, String tableName, String[] types, AbstractSession session) throws DatabaseException {
        return new Vector();
    }

    /**
     * If client requires to manually set connection they can use the connection manager.
     */
    protected void setDatasourceConnection(Object connection) {
        this.datasourceConnection = connection;
    }

    /**
     * Rollback the transaction on the datasource. If not using managed transaction rollback the local transaction.
     */
    public void rollbackTransaction(AbstractSession session) throws DatabaseException {
        if (usesExternalTransactionController()) {
            // if there is no external TX controller, then that means we are currently not synchronized
            // with a global JTS transaction.  In this case, there won't be any 'afterCompletion'
            // callbacks so we have to release the connection here.  It is possible (WLS 5.1) to choose
            // 'usesExternalTransactionController' on the login, but still acquire a uow that WON'T be
            // synchronized with a global TX.
            if (!session.isSynchronized()) {
                this.isInTransaction = false;
                if (this.usesExternalConnectionPooling) {
                    // closeConnection method uses currentSession and then sets it to null.
                    currentSession = session;
                    closeConnection();
                }
            }
            return;
        }

        session.log(SessionLog.FINER, SessionLog.TRANSACTION, "rollback_transaction", (Object[])null, this);

        try {
            session.startOperationProfile(SessionProfiler.Transaction);
            incrementCallCount(session);
            basicRollbackTransaction(session);
        } finally {
            this.isInTransaction = false;
            sequencingCallback = null;
            decrementCallCount();
            session.endOperationProfile(SessionProfiler.Transaction);
        }
    }

    /**
     * Return true if some external transaction service is controlling transactions.
     */
    public boolean usesExternalTransactionController() {
        if (this.login == null) {
            throw DatabaseException.databaseAccessorNotConnected();
        }
        return this.login.shouldUseExternalTransactionController();
    }

    /**
     * Return true if the accessor is currently connected to a data source.
     * Return false otherwise.
     */
    public boolean isConnected() {
        if ((this.datasourceConnection == null) && (this.login == null)) {
            return false;
        }
        if (this.usesExternalConnectionPooling) {
            return true;// As can always reconnect.
        }

        if (this.datasourceConnection == null) {
            return false;
        }

        return isDatasourceConnected();
    }

    /**
     * Return if the driver level connection is connected.
     */
    protected abstract boolean isDatasourceConnected();

    /**
     * Added as a result of Bug 2804663 - satisfy the Accessor interface
     * implementation.
     */
    public void flushSelectCalls(AbstractSession session) {
        // By default do nothing.
    }

    /**
     * This method will be called after a series of writes have been issued to
     * mark where a particular set of writes has completed.  It will be called
     * from commitTransaction and may be called from writeChanges.   Its main
     * purpose is to ensure that the batched statements have been executed
     */
    public void writesCompleted(AbstractSession session) {
        //this is a no-op in this method as we do not batch on this accessor
    }

    /**
     * Return sequencing callback.
     */
    public SequencingCallback getSequencingCallback(SequencingCallbackFactory sequencingCallbackFactory) {
        if(sequencingCallback == null) {
            sequencingCallback = sequencingCallbackFactory.createSequencingCallback();
        }
        return sequencingCallback;
    }

    /**
     * Attempts to create ConnectionCustomizer. If created the customizer is cached by the accessor.
     * Called by the owner of accessor (DatabaseSession, ServerSession through ConnectionPool) just once,
     * typically right after the accessor is created.
     * Also called by ClientSession when it acquires write accessor.
     * If accessor already has a customizer set by ConnectionPool then ClientSession's customizer
     * compared with the existing one and if they are not equal (don't produce identical customization)
     * then the new customizer set onto accessor, caching the old customizer so that it could be restored later.
     */
    public void createCustomizer(AbstractSession session) {
        ConnectionCustomizer newCustomizer;
        if(customizer == null) {
            // Create a new customizer. The platform may be null if the accessor hasn't yet been connected.
            if(platform != null) {
                newCustomizer = platform.createConnectionCustomizer(this, session);
            } else {
                newCustomizer = ((DatasourcePlatform)session.getDatasourcePlatform()).createConnectionCustomizer(this, session);
            }
            if(newCustomizer == null) {
                // Neither old nor new exists - nothing to do.
            } else {
                // Old customizer doesn't exist - just set the new one.
                setCustomizer(newCustomizer);
            }
        } else {
            // the passed session has built the old customizer - no need to build the new one.
            if(customizer.getSession() == session) {
                return;
            }
            // Create a new customizer. The platform may be null if the accessor hasn't yet been connected.
            if(platform != null) {
                newCustomizer = platform.createConnectionCustomizer(this, session);
            } else {
                newCustomizer = ((DatasourcePlatform)session.getDatasourcePlatform()).createConnectionCustomizer(this, session);
            }
            if(newCustomizer == null) {
                // New customizer doesn't exist - but the old one does.
                if(customizer.isActive()) {
                    customizer.clear();
                }
                // The only reason for setting empty customizer is to preserve the previous customizer
                // until releaseCustomizer(session) is called - where session is the one set in empty customizer.
                // Happens when ServerSession defines customization but ClientSession explicitly demands no customization.
                newCustomizer = ConnectionCustomizer.createEmptyCustomizer(session);
                newCustomizer.setPrevCustomizer(customizer);
                // No need to call customize on Empty customizer - it does nothing.
                customizer = newCustomizer;
            } else {
                // Both old and new customizers exist.
                if(newCustomizer.equals(customizer)) {
                    // The equality of customizers means they customize connection in exactly the same way.
                    // Therefore clearing the old customization followed by application of the new one could be skipped:
                    // just keep the old customizer.
                    // Happens when ServerSession and ClientSession define equivalent customizers.
                } else {
                    // The old customizer substituted for the new one.
                    if(customizer.isActive()) {
                        customizer.clear();
                    }
                    // Note that the old one is cached in the new one and will be restored
                    // when releaseCustomizer(session( is called - where session is the one set in the new customizer.
                    // Happens when ClientSession customizer overrides ServerSession's customizer.
                    newCustomizer.setPrevCustomizer(customizer);
                    setCustomizer(newCustomizer);
                }
            }
        }
    }

    /**
     * Set customizer, customize the connection if it's available.
     */
    protected void setCustomizer(ConnectionCustomizer newCustomizer) {
        this.customizer = newCustomizer;
        if(getDatasourceConnection() != null) {
            customizer.customize();
        }
    }

    /**
     * Clear customizer if it's active and set it to null.
     * Called by the same object that has created customizer (DatabaseSession, ConnectionPool) when
     * the latter is no longer required, typically before releasing the accessor.
     * Ignored if there's no customizer.
     */
    public void releaseCustomizer() {
        if(customizer != null) {
            if(customizer.isActive()) {
                customizer.clear();
            }
            customizer = null;
        }
    }

   /**
    * Clear and remove customizer if its session is the same as the passed one;
    * in case prevCustomizer exists set it as a new customizer.
    * Called when ClientSession releases write accessor:
    * if the customizer was created by the ClientSession it's removed, and
    * the previous customizer (that ConnectionPool had set) is brought back;
    * otherwise the customizer (created by ConnectionPool) is kept.
    * Ignored if there's no customizer.
    */
   public void releaseCustomizer(AbstractSession session) {
       if(customizer != null) {
           if(customizer.getSession() == session) {
               if(customizer.isActive()) {
                   customizer.clear();
               }
               if(customizer.getPrevCustomizer() == null) {
                   customizer = null;
               } else {
                   setCustomizer(customizer.getPrevCustomizer());
               }
           }
       }
   }

   /**
    * This method is called by reestablishConnection.
    * Nothing needs to be done in case customize is not active (customization hasn't been applied yet).
    * to repair existing customizer after connection became invalid.
    * However if connection has been customized then
    * if connection is still there and deemed to be valid - clear customization.
    * Otherwise (or if clear fails) remove customizer and set its prevCustomizer as a new customizer,
    * then re-create customizer using the same session as the original one.
    */
   protected void reestablishCustomizer() {
       if(customizer != null && customizer.isActive()) {
           if(isValid()) {
               // the method eats SQLException in case of a failure.
               customizer.clear();
           } else {
               // It's an invalid connection - don't bother trying to clear customization.
               AbstractSession customizerSession = (AbstractSession)customizer.getSession();
               // need this so that the new customizer has the same prevCustomizer as the old one.
               customizer = customizer.getPrevCustomizer();
               // customizer recreated - it's the same as the original one, but not active.
               createCustomizer(customizerSession);
           }
       }
   }

   /**
    * Return the associated connection pool this connection was obtained from.
    */
   public ConnectionPool getPool() {
       return pool;
   }

   /**
    * Set the associated connection pool this connection was obtained from.
    */
   public void setPool(ConnectionPool pool) {
       this.pool = pool;
   }
}
