/*
 * 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.tomcat.jdbc.pool;


import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.jdbc.pool.interceptor.ConnectionState;

/**
 * Represents a pooled connection
 * and holds a reference to the {@link java.sql.Connection} object
 * @version 1.0
 */
public class PooledConnection {
    /**
     * Logger
     */
    private static final Log log = LogFactory.getLog(PooledConnection.class);

    public static final String PROP_USER = PoolUtilities.PROP_USER;

    public static final String PROP_PASSWORD = PoolUtilities.PROP_PASSWORD;

    /**
     * Validate when connection is borrowed flag
     */
    public static final int VALIDATE_BORROW = 1;
    /**
     * Validate when connection is returned flag
     */
    public static final int VALIDATE_RETURN = 2;
    /**
     * Validate when connection is idle flag
     */
    public static final int VALIDATE_IDLE = 3;
    /**
     * Validate when connection is initialized flag
     */
    public static final int VALIDATE_INIT = 4;
    /**
     * The properties for the connection pool
     */
    protected PoolConfiguration poolProperties;
    /**
     * The underlying database connection
     */
    private volatile java.sql.Connection connection;

    /**
     * If using a XAConnection underneath.
     */
    protected volatile javax.sql.XAConnection xaConnection;
    /**
     * When we track abandon traces, this string holds the thread dump
     */
    private String abandonTrace = null;
    /**
     * Timestamp the connection was last 'touched' by the pool
     */
    private volatile long timestamp;
    /**
     * Lock for this connection only
     */
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false);
    /**
     * Set to true if this connection has been discarded by the pool
     */
    private volatile boolean discarded = false;
    /**
     * The Timestamp when the last time the connect() method was called successfully
     */
    private volatile long lastConnected = -1;
    /**
     * timestamp to keep track of validation intervals
     */
    private volatile long lastValidated = System.currentTimeMillis();
    /**
     * The parent
     */
    protected ConnectionPool parent;

    private HashMap<Object, Object> attributes = new HashMap<>();

    private volatile long connectionVersion=0;

    /**
     * Weak reference to cache the list of interceptors for this connection
     * so that we don't create a new list of interceptors each time we borrow
     * the connection
     */
    private volatile JdbcInterceptor handler = null;

    private AtomicBoolean released = new AtomicBoolean(false);

    private volatile boolean suspect = false;

    private java.sql.Driver driver = null;

    /**
     * Constructor
     * @param prop - pool properties
     * @param parent - the parent connection pool
     */
    public PooledConnection(PoolConfiguration prop, ConnectionPool parent) {
        poolProperties = prop;
        this.parent = parent;
        connectionVersion = parent.getPoolVersion();
    }

    public long getConnectionVersion() {
        return connectionVersion;
    }

    /**
     * @deprecated use {@link #shouldForceReconnect(String, String)}
     * method kept since it was public, to avoid changing interface. name was pooo
     */
    @Deprecated
    public boolean checkUser(String username, String password) {
        return !shouldForceReconnect(username, password);
    }

    /**
     * Returns true if we must force reconnect based on credentials passed in.
     * Returns false if {@link PoolConfiguration#isAlternateUsernameAllowed()} method returns false.
     * Returns false if the username/password has not changed since this connection was connected
     * @param username the username you wish to connect with, pass in null to accept the default username from {@link PoolConfiguration#getUsername()}
     * @param password the password you wish to connect with, pass in null to accept the default username from {@link org.apache.tomcat.jdbc.pool.PoolConfiguration#getPassword()}
     * @return true is the pool must reconnect
     */
    public boolean shouldForceReconnect(String username, String password) {

        if (!getPoolProperties().isAlternateUsernameAllowed()) return false;

        if (username==null) username = poolProperties.getUsername();
        if (password==null) password = poolProperties.getPassword();

        String storedUsr = (String)getAttributes().get(PROP_USER);
        String storedPwd = (String)getAttributes().get(PROP_PASSWORD);

        boolean noChangeInCredentials = (username==null && storedUsr==null);
        noChangeInCredentials = (noChangeInCredentials || (username!=null && username.equals(storedUsr)));

        noChangeInCredentials = noChangeInCredentials && ((password==null && storedPwd==null) || (password!=null && password.equals(storedPwd)));

        if (username==null)  getAttributes().remove(PROP_USER); else getAttributes().put(PROP_USER, username);
        if (password==null)  getAttributes().remove(PROP_PASSWORD); else getAttributes().put(PROP_PASSWORD, password);

        return !noChangeInCredentials;
    }

    /**
     * Connects the underlying connection to the database.
     * @throws SQLException if the method {@link #release()} has been called.
     * @throws SQLException if driver instantiation fails
     * @throws SQLException if a call to {@link java.sql.Driver#connect(String, java.util.Properties)} fails.
     * @throws SQLException if default properties are configured and a call to
     * {@link java.sql.Connection#setAutoCommit(boolean)}, {@link java.sql.Connection#setCatalog(String)},
     * {@link java.sql.Connection#setTransactionIsolation(int)} or {@link java.sql.Connection#setReadOnly(boolean)} fails.
     */
    public void connect() throws SQLException {
        if (released.get()) throw new SQLException("A connection once released, can't be reestablished.");
        if (connection != null) {
            try {
                this.disconnect(false);
            } catch (Exception x) {
                log.debug("Unable to disconnect previous connection.", x);
            } //catch
        } //end if
        if (poolProperties.getDataSource()==null && poolProperties.getDataSourceJNDI()!=null) {
            //TODO lookup JNDI name
        }

        if (poolProperties.getDataSource()!=null) {
            connectUsingDataSource();
        } else {
            connectUsingDriver();
        }

        //set up the default state, unless we expect the interceptor to do it
        if (poolProperties.getJdbcInterceptors()==null || poolProperties.getJdbcInterceptors().indexOf(ConnectionState.class.getName())<0 ||
                poolProperties.getJdbcInterceptors().indexOf(ConnectionState.class.getSimpleName())<0) {
            if (poolProperties.getDefaultTransactionIsolation()!=DataSourceFactory.UNKNOWN_TRANSACTIONISOLATION) connection.setTransactionIsolation(poolProperties.getDefaultTransactionIsolation());
            if (poolProperties.getDefaultReadOnly()!=null) connection.setReadOnly(poolProperties.getDefaultReadOnly().booleanValue());
            if (poolProperties.getDefaultAutoCommit()!=null) connection.setAutoCommit(poolProperties.getDefaultAutoCommit().booleanValue());
            if (poolProperties.getDefaultCatalog()!=null) connection.setCatalog(poolProperties.getDefaultCatalog());
        }
        this.discarded = false;
        this.lastConnected = System.currentTimeMillis();
    }

    protected void connectUsingDataSource() throws SQLException {
        String usr = null;
        String pwd = null;
        if (getAttributes().containsKey(PROP_USER)) {
            usr = (String) getAttributes().get(PROP_USER);
        } else {
            usr = poolProperties.getUsername();
            getAttributes().put(PROP_USER, usr);
        }
        if (getAttributes().containsKey(PROP_PASSWORD)) {
            pwd = (String) getAttributes().get(PROP_PASSWORD);
        } else {
            pwd = poolProperties.getPassword();
            getAttributes().put(PROP_PASSWORD, pwd);
        }
        if (poolProperties.getDataSource() instanceof javax.sql.XADataSource) {
            javax.sql.XADataSource xds = (javax.sql.XADataSource)poolProperties.getDataSource();
            if (usr!=null && pwd!=null) {
                xaConnection = xds.getXAConnection(usr, pwd);
                connection = xaConnection.getConnection();
            } else {
                xaConnection = xds.getXAConnection();
                connection = xaConnection.getConnection();
            }
        } else if (poolProperties.getDataSource() instanceof javax.sql.DataSource){
            javax.sql.DataSource ds = (javax.sql.DataSource)poolProperties.getDataSource();
            if (usr!=null && pwd!=null) {
                connection = ds.getConnection(usr, pwd);
            } else {
                connection = ds.getConnection();
            }
        } else if (poolProperties.getDataSource() instanceof javax.sql.ConnectionPoolDataSource){
            javax.sql.ConnectionPoolDataSource ds = (javax.sql.ConnectionPoolDataSource)poolProperties.getDataSource();
            if (usr!=null && pwd!=null) {
                connection = ds.getPooledConnection(usr, pwd).getConnection();
            } else {
                connection = ds.getPooledConnection().getConnection();
            }
        } else {
            throw new SQLException("DataSource is of unknown class:"+(poolProperties.getDataSource()!=null?poolProperties.getDataSource().getClass():"null"));
        }
    }
    protected void connectUsingDriver() throws SQLException {

        try {
            if (driver==null) {
                if (log.isDebugEnabled()) {
                    log.debug("Instantiating driver using class: "+poolProperties.getDriverClassName()+" [url="+poolProperties.getUrl()+"]");
                }
                if (poolProperties.getDriverClassName()==null) {
                    //rely on DriverManager
                    log.warn("Not loading a JDBC driver as driverClassName property is null.");
                } else {
                    driver = (java.sql.Driver)
                        ClassLoaderUtil.loadClass(
                            poolProperties.getDriverClassName(),
                            PooledConnection.class.getClassLoader(),
                            Thread.currentThread().getContextClassLoader()
                        ).newInstance();
                }
            }
        } catch (java.lang.Exception cn) {
            if (log.isDebugEnabled()) {
                log.debug("Unable to instantiate JDBC driver.", cn);
            }
            SQLException ex = new SQLException(cn.getMessage());
            ex.initCause(cn);
            throw ex;
        }
        String driverURL = poolProperties.getUrl();
        String usr = null;
        String pwd = null;
        if (getAttributes().containsKey(PROP_USER)) {
            usr = (String) getAttributes().get(PROP_USER);
        } else {
            usr = poolProperties.getUsername();
            getAttributes().put(PROP_USER, usr);
        }
        if (getAttributes().containsKey(PROP_PASSWORD)) {
            pwd = (String) getAttributes().get(PROP_PASSWORD);
        } else {
            pwd = poolProperties.getPassword();
            getAttributes().put(PROP_PASSWORD, pwd);
        }
        Properties properties = PoolUtilities.clone(poolProperties.getDbProperties());
        if (usr != null) properties.setProperty(PROP_USER, usr);
        if (pwd != null) properties.setProperty(PROP_PASSWORD, pwd);

        try {
            if (driver==null) {
                connection = DriverManager.getConnection(driverURL, properties);
            } else {
                connection = driver.connect(driverURL, properties);
            }
        } catch (Exception x) {
            if (log.isDebugEnabled()) {
                log.debug("Unable to connect to database.", x);
            }
            if (parent.jmxPool!=null) {
                parent.jmxPool.notify(org.apache.tomcat.jdbc.pool.jmx.ConnectionPool.NOTIFY_CONNECT,
                        ConnectionPool.getStackTrace(x));
            }
            if (x instanceof SQLException) {
                throw (SQLException)x;
            } else {
                SQLException ex = new SQLException(x.getMessage());
                ex.initCause(x);
                throw ex;
            }
        }
        if (connection==null) {
            throw new SQLException("Driver:"+driver+" returned null for URL:"+driverURL);
        }
    }

    /**
     *
     * @return true if connect() was called successfully and disconnect has not yet been called
     */
    public boolean isInitialized() {
        return connection!=null;
    }

    /**
     * Returns true if the connection has been connected more than
     * {@link PoolConfiguration#getMaxAge()} milliseconds. false otherwise.
     * @return Returns true if the connection has been connected more than
     * {@link PoolConfiguration#getMaxAge()} milliseconds. false otherwise.
     */
    public boolean isMaxAgeExpired() {
        if (getPoolProperties().getMaxAge()>0 ) {
            return (System.currentTimeMillis() - getLastConnected()) > getPoolProperties().getMaxAge();
        } else {
            return false;
        }
    }
    /**
     * Issues a call to {@link #disconnect(boolean)} with the argument false followed by a call to
     * {@link #connect()}
     * @throws SQLException if the call to {@link #connect()} fails.
     */
    public void reconnect() throws SQLException {
        this.disconnect(false);
        this.connect();
    } //reconnect

    /**
     * Disconnects the connection. All exceptions are logged using debug level.
     * @param finalize if set to true, a call to {@link ConnectionPool#finalize(PooledConnection)} is called.
     */
    private void disconnect(boolean finalize) {
        if (isDiscarded() && connection == null) {
            return;
        }
        setDiscarded(true);
        if (connection != null) {
            try {
                parent.disconnectEvent(this, finalize);
                if (xaConnection == null) {
                    connection.close();
                } else {
                    xaConnection.close();
                }
            }catch (Exception ignore) {
                if (log.isDebugEnabled()) {
                    log.debug("Unable to close underlying SQL connection",ignore);
                }
            }
        }
        connection = null;
        xaConnection = null;
        lastConnected = -1;
        if (finalize) parent.finalize(this);
    }


//============================================================================
//
//============================================================================

    /**
     * Returns abandon timeout in milliseconds
     * @return abandon timeout in milliseconds
     */
    public long getAbandonTimeout() {
        if (poolProperties.getRemoveAbandonedTimeout() <= 0) {
            return Long.MAX_VALUE;
        } else {
            return poolProperties.getRemoveAbandonedTimeout() * 1000L;
        } //end if
    }

    /**
     * Returns true if the connection pool is configured
     * to do validation for a certain action.
     * @param action
     */
    private boolean doValidate(int action) {
        if (action == PooledConnection.VALIDATE_BORROW &&
            poolProperties.isTestOnBorrow())
            return true;
        else if (action == PooledConnection.VALIDATE_RETURN &&
                 poolProperties.isTestOnReturn())
            return true;
        else if (action == PooledConnection.VALIDATE_IDLE &&
                 poolProperties.isTestWhileIdle())
            return true;
        else if (action == PooledConnection.VALIDATE_INIT &&
                 poolProperties.isTestOnConnect())
            return true;
        else if (action == PooledConnection.VALIDATE_INIT &&
                 poolProperties.getInitSQL()!=null)
           return true;
        else
            return false;
    }

    /**Returns true if the object is still valid. if not
     * the pool will call the getExpiredAction() and follow up with one
     * of the four expired methods
     */
    public boolean validate(int validateAction) {
        return validate(validateAction,null);
    }

    /**
     * Validates a connection.
     * @param validateAction the action used. One of {@link #VALIDATE_BORROW}, {@link #VALIDATE_IDLE},
     * {@link #VALIDATE_INIT} or {@link #VALIDATE_RETURN}
     * @param sql the SQL to be used during validation. If the {@link PoolConfiguration#setInitSQL(String)} has been called with a non null
     * value and the action is {@link #VALIDATE_INIT} the init SQL will be used for validation.
     *
     * @return true if the connection was validated successfully. It returns true even if validation was not performed, such as when
     * {@link PoolConfiguration#setValidationInterval(long)} has been called with a positive value.
     * <p>
     * false if the validation failed. The caller should close the connection if false is returned since a session could have been left in
     * an unknown state during initialization.
     */
    public boolean validate(int validateAction,String sql) {
        if (this.isDiscarded()) {
            return false;
        }

        if (!doValidate(validateAction)) {
            //no validation required, no init sql and props not set
            return true;
        }

        //Don't bother validating if already have recently enough
        long now = System.currentTimeMillis();
        if (validateAction!=VALIDATE_INIT &&
            poolProperties.getValidationInterval() > 0 &&
            (now - this.lastValidated) <
            poolProperties.getValidationInterval()) {
            return true;
        }

        if (poolProperties.getValidator() != null) {
            if (poolProperties.getValidator().validate(connection, validateAction)) {
                this.lastValidated = now;
                return true;
            } else {
                if (getPoolProperties().getLogValidationErrors()) {
                    log.error("Custom validation through "+poolProperties.getValidator()+" failed.");
                }
                return false;
            }
        }

        String query = sql;

        if (validateAction == VALIDATE_INIT && poolProperties.getInitSQL() != null) {
            query = poolProperties.getInitSQL();
        }

        if (query == null) {
            query = poolProperties.getValidationQuery();
        }

        if (query == null) {
            int validationQueryTimeout = poolProperties.getValidationQueryTimeout();
            if (validationQueryTimeout < 0) validationQueryTimeout = 0;
            try {
                if (connection.isValid(validationQueryTimeout)) {
                    this.lastValidated = now;
                    return true;
                } else {
                    if (getPoolProperties().getLogValidationErrors()) {
                        log.error("isValid() returned false.");
                    }
                    return false;
                }
            } catch (SQLException e) {
                if (getPoolProperties().getLogValidationErrors()) {
                    log.error("isValid() failed.", e);
                } else if (log.isDebugEnabled()) {
                    log.debug("isValid() failed.", e);
                }
                return false;
            }
        }

        Statement stmt = null;
        try {
            stmt = connection.createStatement();

            int validationQueryTimeout = poolProperties.getValidationQueryTimeout();
            if (validationQueryTimeout > 0) {
                stmt.setQueryTimeout(validationQueryTimeout);
            }

            stmt.execute(query);
            stmt.close();
            this.lastValidated = now;
            return true;
        } catch (Exception ex) {
            if (getPoolProperties().getLogValidationErrors()) {
                log.warn("SQL Validation error", ex);
            } else if (log.isDebugEnabled()) {
                log.debug("Unable to validate object:",ex);
            }
            if (stmt!=null)
                try { stmt.close();} catch (Exception ignore2){/*NOOP*/}
        }
        return false;
    } //validate

    /**
     * The time limit for how long the object
     * can remain unused before it is released
     * @return {@link PoolConfiguration#getMinEvictableIdleTimeMillis()}
     */
    public long getReleaseTime() {
        return this.poolProperties.getMinEvictableIdleTimeMillis();
    }

    /**
     * This method is called if (Now - timeCheckedIn &gt; getReleaseTime())
     * This method disconnects the connection, logs an error in debug mode if it happens
     * then sets the {@link #released} flag to false. Any attempts to connect this cached object again
     * will fail per {@link #connect()}
     * The connection pool uses the atomic return value to decrement the pool size counter.
     * @return true if this is the first time this method has been called. false if this method has been called before.
     */
    public boolean release() {
        try {
            disconnect(true);
        } catch (Exception x) {
            if (log.isDebugEnabled()) {
                log.debug("Unable to close SQL connection",x);
            }
        }
        return released.compareAndSet(false, true);

    }

    /**
     * The pool will set the stack trace when it is check out and
     * checked in
     * @param trace the stack trace for this connection
     */

    public void setStackTrace(String trace) {
        abandonTrace = trace;
    }

    /**
     * Returns the stack trace from when this connection was borrowed. Can return null if no stack trace was set.
     * @return the stack trace or null of no trace was set
     */
    public String getStackTrace() {
        return abandonTrace;
    }

    /**
     * Sets a timestamp on this connection. A timestamp usually means that some operation
     * performed successfully.
     * @param timestamp the timestamp as defined by {@link System#currentTimeMillis()}
     */
    public void setTimestamp(long timestamp) {
        this.timestamp = timestamp;
        setSuspect(false);
    }


    public boolean isSuspect() {
        return suspect;
    }

    public void setSuspect(boolean suspect) {
        this.suspect = suspect;
    }

    /**
     * An interceptor can call this method with the value true, and the connection will be closed when it is returned to the pool.
     * @param discarded - only valid value is true
     * @throws IllegalStateException if this method is called with the value false and the value true has already been set.
     */
    public void setDiscarded(boolean discarded) {
        if (this.discarded && !discarded) throw new IllegalStateException("Unable to change the state once the connection has been discarded");
        this.discarded = discarded;
    }

    /**
     * Set the timestamp the connection was last validated.
     * This flag is used to keep track when we are using a {@link PoolConfiguration#setValidationInterval(long) validation-interval}.
     * @param lastValidated a timestamp as defined by {@link System#currentTimeMillis()}
     */
    public void setLastValidated(long lastValidated) {
        this.lastValidated = lastValidated;
    }

    /**
     * Sets the pool configuration for this connection and connection pool.
     * Object is shared with the {@link ConnectionPool}
     * @param poolProperties
     */
    public void setPoolProperties(PoolConfiguration poolProperties) {
        this.poolProperties = poolProperties;
    }

    /**
     * Return the timestamps of last pool action. Timestamps are typically set when connections
     * are borrowed from the pool. It is used to keep track of {@link PoolConfiguration#setRemoveAbandonedTimeout(int) abandon-timeouts}.
     * This timestamp can also be reset by the {@link org.apache.tomcat.jdbc.pool.interceptor.ResetAbandonedTimer#invoke(Object, java.lang.reflect.Method, Object[])}
     * @return the timestamp of the last pool action as defined by {@link System#currentTimeMillis()}
     */
    public long getTimestamp() {
        return timestamp;
    }

    /**
     * Returns the discarded flag.
     * @return the discarded flag. If the value is true,
     * either {@link #disconnect(boolean)} has been called or it will be called when the connection is returned to the pool.
     */
    public boolean isDiscarded() {
        return discarded;
    }

    /**
     * Returns the timestamp of the last successful validation query execution.
     * @return the timestamp of the last successful validation query execution as defined by {@link System#currentTimeMillis()}
     */
    public long getLastValidated() {
        return lastValidated;
    }

    /**
     * Returns the configuration for this connection and pool
     * @return the configuration for this connection and pool
     */
    public PoolConfiguration getPoolProperties() {
        return poolProperties;
    }

    /**
     * Locks the connection only if either {@link PoolConfiguration#isPoolSweeperEnabled()} or
     * {@link PoolConfiguration#getUseLock()} return true. The per connection lock ensures thread safety is
     * multiple threads are performing operations on the connection.
     * Otherwise this is a noop for performance
     */
    public void lock() {
        if (poolProperties.getUseLock() || this.poolProperties.isPoolSweeperEnabled()) {
            //optimized, only use a lock when there is concurrency
            lock.writeLock().lock();
        }
    }

    /**
     * Unlocks the connection only if the sweeper is enabled
     * Otherwise this is a noop for performance
     */
    public void unlock() {
        if (poolProperties.getUseLock() || this.poolProperties.isPoolSweeperEnabled()) {
          //optimized, only use a lock when there is concurrency
            lock.writeLock().unlock();
        }
    }

    /**
     * Returns the underlying connection
     * @return the underlying JDBC connection as it was returned from the JDBC driver
     * @see javax.sql.PooledConnection#getConnection()
     */
    public java.sql.Connection getConnection() {
        return this.connection;
    }

    /**
     * Returns the underlying XA connection
     * @return the underlying XA connection as it was returned from the Datasource
     */
    public javax.sql.XAConnection getXAConnection() {
        return this.xaConnection;
    }


    /**
     * Returns the timestamp of when the connection was last connected to the database.
     * ie, a successful call to {@link java.sql.Driver#connect(String, java.util.Properties)}.
     * @return the timestamp when this connection was created as defined by {@link System#currentTimeMillis()}
     */
    public long getLastConnected() {
        return lastConnected;
    }

    /**
     * Returns the first handler in the interceptor chain
     * @return the first interceptor for this connection
     */
    public JdbcInterceptor getHandler() {
        return handler;
    }

    public void setHandler(JdbcInterceptor handler) {
        if (this.handler!=null && this.handler!=handler) {
            JdbcInterceptor interceptor = this.handler;
            while (interceptor!=null) {
                interceptor.reset(null, null);
                interceptor = interceptor.getNext();
            }//while
        }//end if
        this.handler = handler;
    }

    @Override
    public String toString() {
        return "PooledConnection["+(connection!=null?connection.toString():"null")+"]";
    }

    /**
     * Returns true if this connection has been released and wont be reused.
     * @return true if the method {@link #release()} has been called
     */
    public boolean isReleased() {
        return released.get();
    }

    public HashMap<Object,Object> getAttributes() {
        return attributes;
    }

}
