/*******************************************************************************
 * 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:
 *     05/28/2008-1.0M8 Andrei Ilitchev
 *        - New file introduced for bug 224964: Provide support for Proxy Authentication through JPA.
 ******************************************************************************/
package org.eclipse.persistence.platform.database.oracle;

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

import oracle.jdbc.OracleConnection;

import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.eclipse.persistence.exceptions.ConversionException;
import org.eclipse.persistence.exceptions.DatabaseException;
import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.internal.databaseaccess.Accessor;
import org.eclipse.persistence.internal.databaseaccess.ConnectionCustomizer;
import org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.logging.SessionLog;
import org.eclipse.persistence.sessions.Session;

/**
 * PUBLIC:
 * This class allows connection to open proxy session.
 */
public class OracleJDBC_10_1_0_2ProxyConnectionCustomizer extends ConnectionCustomizer {
    protected OracleConnection oracleConnection;
    protected int proxyType;
    protected Properties proxyProperties;

    /**
     * INTERNAL:
     * Should be instantiated only if session.getProperty(PersistenceUnitProperties.ORACLE_PROXY_TYPE) != null.
     */
    public OracleJDBC_10_1_0_2ProxyConnectionCustomizer(Accessor accessor, Session session) {
        super(accessor, session);
    }

    /**
     * INTERNAL:
     * Applies customization to connection.
     * Called only if connection is not already customized (isActive()==false).
     * The method may throw SQLException wrapped into DatabaseException.
     * isActive method called after this method should return true only in case
     * the connection was actually customized.
     */
    public void customize() {
        // Lazily initialize proxy properties - customize method may be never called
        // in case of ClientSession using external connection pooling.
        if(proxyProperties == null) {
            buildProxyProperties();
        }

        Connection connection = accessor.getConnection();
        if(connection instanceof OracleConnection) {
            oracleConnection = (OracleConnection)connection;
        } else {
            connection = session.getServerPlatform().unwrapConnection(connection);
            if(connection instanceof OracleConnection) {
                oracleConnection = (OracleConnection)connection;
            } else {
                throw ValidationException.oracleJDBC10_1_0_2ProxyConnectorRequiresOracleConnection();
            }
        }
        try {
            clearConnectionCache();
            Object[] args = null;
            if (this.session.shouldLog(SessionLog.FINEST, SessionLog.CONNECTION)) {
                Properties logProperties = proxyProperties;
                if(proxyProperties.containsKey(OracleConnection.PROXY_USER_PASSWORD)) {
                    logProperties = (Properties)proxyProperties.clone();
                    logProperties.setProperty(OracleConnection.PROXY_USER_PASSWORD, "******");
                }
                args = new Object[]{ oracleConnection, logProperties };
            }
            if(oracleConnection.isProxySession()) {
                // Unexpectedly oracleConnection already has a proxy session - probably it was not closed when connection was returned back to connection pool.
                // That may happen on jta transaction rollback (especially triggered outside of user's thread - such as timeout)
                // when beforeCompletion is never issued
                // and application server neither closes proxySession nor allows access to connection in afterCompletion.
                try {
                    if (args != null) {
                        ((AbstractSession)this.session).log(SessionLog.FINEST, SessionLog.CONNECTION, "proxy_connection_customizer_already_proxy_session", args);
                    }
                    oracleConnection.close(OracleConnection.PROXY_SESSION);
                } catch (SQLException exception) {
                    // Ignore
                    this.session.getSessionLog().logThrowable(SessionLog.WARNING, SessionLog.CONNECTION, exception);
                }
            }
            oracleConnection.openProxySession(proxyType, proxyProperties);
            // 12c driver will default to an autoCommit setting of true on calling openProxySession
            oracleConnection.setAutoCommit(false);
            if (args != null) {
                ((AbstractSession)this.session).log(SessionLog.FINEST, SessionLog.CONNECTION, "proxy_connection_customizer_opened_proxy_session", args);
            }
        } catch (SQLException exception) {
            oracleConnection = null;
            throw DatabaseException.sqlException(exception);
        } catch (NoSuchMethodError noSuchMethodError) {
            oracleConnection = null;
            throw ValidationException.oracleJDBC10_1_0_2ProxyConnectorRequiresOracleConnectionVersion();
        }
    }

    /**
     * INTERNAL:
     * Indicated whether the connection is currently customized.
     */
    public boolean isActive() {
        return oracleConnection != null;
    }

    /**
     * INTERNAL:
     * Clears customization from connection.
     * Called only if connection is customized (isActive()==true).
     * If the method fails due to SQLException it should "eat" it
     * (just like DatasourceAccessor.closeConnection method).
     * isActive method called after this method should always return false.
     */
    public void clear() {
        try {
            clearConnectionCache();
            if (this.session.shouldLog(SessionLog.FINEST, SessionLog.CONNECTION)) {
                Properties logProperties = proxyProperties;
                if(proxyProperties.containsKey(OracleConnection.PROXY_USER_PASSWORD)) {
                    logProperties = (Properties)proxyProperties.clone();
                    logProperties.setProperty(OracleConnection.PROXY_USER_PASSWORD, "******");
                }
                Object[] args = new Object[]{ oracleConnection, logProperties };
                ((AbstractSession)this.session).log(SessionLog.FINEST, SessionLog.CONNECTION, "proxy_connection_customizer_closing_proxy_session", args);
            }
            oracleConnection.close(OracleConnection.PROXY_SESSION);
        } catch (SQLException exception) {
            // Ignore
            this.session.getSessionLog().logThrowable(SessionLog.WARNING, SessionLog.CONNECTION, exception);
        } finally {
            oracleConnection = null;
        }
    }

    /**
     * INTERNAL:
     * Normally called only when customizer is in inactive state (isActive()==false)
     * and followed by setAccessor call on the clone.
     */
    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException exception) {
            throw new InternalError("clone not supported");
        }
    }

    /**
     * INTERNAL:
     * Two customizers considered equal if they produce the sane customization.
     */
    public boolean equals(Object obj) {
        if(obj instanceof OracleJDBC_10_1_0_2ProxyConnectionCustomizer) {
            return equals((OracleJDBC_10_1_0_2ProxyConnectionCustomizer)obj);
        } else {
            return false;
        }
    }

    /**
     * INTERNAL:
     * Two customizers considered equal if they produce the sane customization.
     */
    public boolean equals(OracleJDBC_10_1_0_2ProxyConnectionCustomizer customizer) {
        if(this == customizer) {
            return true;
        }
        if(this.proxyProperties == null) {
            buildProxyProperties();
        }
        if(customizer.proxyProperties == null) {
            customizer.buildProxyProperties();
        }
        return this.proxyType == customizer.proxyType && this.proxyProperties.equals(customizer.proxyProperties);
    }

    /**
     * INTERNAL:
     * Precondition: session.getProperty(PersistenceUnitProperties.ORACLE_PROXY_TYPE) != null
     */
    protected void buildProxyProperties() {
        Object proxyTypeValue = session.getProperty(PersistenceUnitProperties.ORACLE_PROXY_TYPE);
        try {
            proxyType = ((Integer)session.getPlatform().getConversionManager().convertObject(proxyTypeValue, Integer.class)).intValue();
        } catch (ConversionException conversionException) {
            throw ValidationException.oracleJDBC10_1_0_2ProxyConnectorRequiresIntProxytype();
        }
        proxyProperties = new Properties();
        if(proxyType == OracleConnection.PROXYTYPE_USER_NAME) {
            String proxyUserName = (String)session.getProperty(OracleConnection.PROXY_USER_NAME);
            if(proxyUserName != null) {
                proxyProperties.setProperty(OracleConnection.PROXY_USER_NAME, proxyUserName);
            } else {
                ValidationException.expectedProxyPropertyNotFound("OracleConnection.PROXYTYPE_USER_NAME", OracleConnection.PROXY_USER_NAME);
            }
        } else if(proxyType == OracleConnection.PROXYTYPE_DISTINGUISHED_NAME) {
            String distinguishedName = (String)session.getProperty(OracleConnection.PROXY_DISTINGUISHED_NAME);
            if(distinguishedName != null) {
                proxyProperties.setProperty(OracleConnection.PROXY_DISTINGUISHED_NAME, distinguishedName);
            } else {
                ValidationException.expectedProxyPropertyNotFound("OracleConnection.PROXYTYPE_DISTINGUISHED_NAME", OracleConnection.PROXY_DISTINGUISHED_NAME);
            }
        } else if(proxyType == OracleConnection.PROXYTYPE_CERTIFICATE) {
            Object certificate = session.getProperty(OracleConnection.PROXY_CERTIFICATE);
            if(certificate != null) {
                proxyProperties.put(OracleConnection.PROXY_CERTIFICATE, certificate);
            } else {
                ValidationException.expectedProxyPropertyNotFound("OracleConnection.PROXYTYPE_CERTIFICATE", OracleConnection.PROXY_CERTIFICATE);
            }
        } else {
            ValidationException.unknownProxyType(proxyType, "OracleConnection.PROXYTYPE_USER_NAME", "OracleConnection.PROXYTYPE_DISTINGUISHED_NAME", "OracleConnection.PROXYTYPE_CERTIFICATE");
        }
        String proxyUserPassword = (String)session.getProperty(OracleConnection.PROXY_USER_PASSWORD);
        // set the value if it's not null and not an empty String
        if(proxyUserPassword != null && proxyUserPassword.length() > 0) {
            proxyProperties.setProperty(OracleConnection.PROXY_USER_PASSWORD, proxyUserPassword);
        }
        Object proxyRoles = session.getProperty(OracleConnection.PROXY_ROLES);
        // set the value if it's not null and not an empty String
        if(proxyRoles != null && !((proxyRoles instanceof String) && (((String)proxyRoles).length() == 0))) {
            proxyProperties.put(OracleConnection.PROXY_ROLES, proxyRoles);
        }
    }

    /**
     * INTERNAL:
     * Clears connection's both implicit and explicit caches.
     */
    protected void clearConnectionCache() {
        this.getSession().getServerPlatform().clearStatementCache(this.getAccessor().getConnection());
        ((DatabaseAccessor)this.getAccessor()).clearStatementCache((AbstractSession)this.getSession());
    }
}
