/*
 * Copyright (c) 1998, 2018 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:
//     Oracle - initial API and implementation from Oracle TopLink
//     17/10/2008-1.1  Michael O'Brien
//       - 251005: The default JNDI InitialContextFactory is modified from
//       OC4J: oracle.j2ee.rmi.RMIInitialContextFactory to
//       WebLogic: weblogic.jndi.WLInitialContextFactory
package org.eclipse.persistence.sessions.coordination;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Map;

import javax.naming.Context;
import javax.naming.NamingException;

import org.eclipse.persistence.exceptions.CommunicationException;
import org.eclipse.persistence.exceptions.RemoteCommandManagerException;
import org.eclipse.persistence.internal.security.SecurableObjectHolder;
import org.eclipse.persistence.internal.sessions.coordination.ConnectToHostCommand;
import org.eclipse.persistence.internal.sessions.coordination.RemoteConnection;

/**
 * <p>
 * <b>Purpose</b>: Provide an abstract class that offers a common API to handling
 * remote command connections.
 * <p>
 * <b>Description</b>: This class manages the remote connections to other RCM service
 * instances and posts the local connection to this service instance in a name
 * service so that other RCM service instances can connect to it.
 * <p>
 * @author Steven Vo
 * @since OracleAS TopLink 10<i>g</i> (9.0.4)
 */
public abstract class TransportManager {

    /** The remote command connection to this transport */
    protected RemoteConnection localConnection;

    /** The RCM that manages this transport */
    protected RemoteCommandManager rcm;

    /** The type of naming service used to look up other connections */
    protected int namingServiceType;

    /** Properties to obtain the context used for local JNDI access */
    protected Hashtable localContextProperties;

    /** Properties to obtain the context used for remote JNDI access */
    protected Hashtable remoteContextProperties;

    /** Determines whether a connection should be discarded if an error occurs on it */
    protected boolean shouldRemoveConnectionOnError;

    /** Connections to other services */
    protected Hashtable connectionsToExternalServices;

    /** Security util that is used to decrypt and encrypt password */
    protected SecurableObjectHolder securableObjectHolder;

    public static final boolean DEFAULT_REMOVE_CONNECTION_ON_ERROR_MODE = true;

    /** Valid values for naming service type */
    public static final int JNDI_NAMING_SERVICE = 0;
    public static final int REGISTRY_NAMING_SERVICE = 1;

    /** Defaults for RMI applications */
    public static final String DEFAULT_URL_PROTOCOL = "rmi";
    public static final String DEFAULT_IIOP_URL_PROTOCOL = "corbaname";
    public static final String DEFAULT_URL_PORT = "23791";
    public static final String DEFAULT_IIOP_URL_PORT = "5555#";
    public static final int DEFAULT_NAMING_SERVICE = JNDI_NAMING_SERVICE;

    /** Default JNDI properties for remote access */
    public static final String DEFAULT_CONTEXT_FACTORY = "weblogic.jndi.WLInitialContextFactory";
    public static final String DEFAULT_DEDICATED_CONNECTION_KEY = "dedicated.connection";
    public static final String DEFAULT_DEDICATED_CONNECTION_VALUE = "true";
    public static final String DEFAULT_USER_NAME = "admin";

    /**
     * INTERNAL:
     * Return a remote connection to the specified service
     */
    public abstract RemoteConnection createConnection(ServiceId serviceId);

    /**
     * INTERNAL:
     * Does nothing by default. In case TransportManager doesn't use DiscoveryManager
     * (createDiscoveryManager method retuns null)
     * this method called during RCM initialization to create all the necessary connections.
     * TransportManager ancestors that override createDiscoveryManager method to return null
     * must override this method, too.
     */
    public void createConnections() {
    }

    /**
     * INTERNAL:
     * This method is called by the remote command manager when this service should
     * connect back ('handshake') to the service from which this remote connection came.
     */
    public void connectBackToRemote(RemoteConnection connection) throws Exception {
        ConnectToHostCommand command = new ConnectToHostCommand();
        command.setServiceId(rcm.getServiceId());
        connection.executeCommand(command);
    }

    /**
     * INTERNAL:
     * Return a remote connection to this service
     */
    public RemoteConnection getConnectionToLocalHost() {
        return localConnection;
    }

    /**
     * INTERNAL:
     * Put the remote connection to local host in naming service and return the  of the created remote connection
     */
    public abstract void createLocalConnection();

    /**
     * PUBLIC:
     * Return the type of naming service used to look up remote connections to other
     * service instances.
     *
     * @return The type of naming service used.
     */
    public int getNamingServiceType() {
        return namingServiceType;
    }

    /**
     * ADVANCED:
     * Set the type of naming service used to look up remote connections to other
     * service instances. The service type must be one of JNDI_NAMING_SERVICE or
     * REGISTRY_NAMING_SERVICE.
     */
    public void setNamingServiceType(int serviceType) {
        namingServiceType = serviceType;
    }

    /**
     * PUBLIC:
     * Return the user name used as the value to the SECURITY_PRINCIPAL key in the
     * cached context properties.
     */
    public String getUserName() {
        return (String)getRemoteContextProperties().get(Context.SECURITY_PRINCIPAL);
    }

    /**
     * ADVANCED:
     * Set the user name used as the value to the SECURITY_PRINCIPAL key in the
     * cached context properties.
     */
    public void setUserName(String userName) {
        getRemoteContextProperties().put(Context.SECURITY_PRINCIPAL, userName);
    }

    /**
     * PUBLIC:
     * Return the password used as the value to the SECURITY_CREDENTIALS key in
     * the cached context properties.
     */
    public String getPassword() {
        return decrypt(getEncryptedPassword());
    }

    /**
     * PUBLIC:
     * Return the encrypted (assumed) password used as the value to the
     * SECURITY_CREDENTIALS key in the cached context properties.
     */
    public String getEncryptedPassword() {
        return (String) getRemoteContextProperties().get(Context.SECURITY_CREDENTIALS);
    }

    /**
     * ADVANCED:
     * Set the password used as the value to the SECURITY_CREDENTIALS key in the
     * cached context properties.
     */
    public void setPassword(String password) {
        if (password != null) {
            getRemoteContextProperties().put(Context.SECURITY_CREDENTIALS, encrypt(password));
        }
    }

    /**
     * ADVANCED:
     * Set the encrypted password used as the value to the SECURITY_CREDENTIALS key in the
     * cached context properties.
     */
    public void setEncryptedPassword(String encryptedPassword) {
        getRemoteContextProperties().put(Context.SECURITY_CREDENTIALS, encryptedPassword);
    }

    /**
     * PUBLIC:
     * Return the context factory name used as the value to the INITIAL_CONTEXT_FACTORY
     * key in the cached context properties.
     */
    public String getInitialContextFactoryName() {
        return (String)getRemoteContextProperties().get(Context.INITIAL_CONTEXT_FACTORY);
    }

    /**
     * ADVANCED:
     * Set the context factory name used as the value to the INITIAL_CONTEXT_FACTORY
     * key in the cached context properties.
     */
    public void setInitialContextFactoryName(String contextFactoryName) {
        getRemoteContextProperties().put(Context.INITIAL_CONTEXT_FACTORY, contextFactoryName);
    }

    /**
     * INTERNAL:
     * Helper method to get a naming context.
     */
    public Context getContext(Hashtable contextProperties) {
        try {
            return new javax.naming.InitialContext(contextProperties);
        } catch (NamingException exception) {
            RemoteCommandManagerException rcmException = RemoteCommandManagerException.errorObtainingContext(exception);
            rcm.handleException(RemoteCommandManagerException.errorObtainingContext(exception));
            throw rcmException;
        }
    }

    /**
     * ADVANCED:
     * Return the cached properties that will be used to create the initial context
     * when doing remote JNDI lookups.
     */
    public Hashtable getRemoteContextProperties() {
        return remoteContextProperties;
    }

    /**
     * ADVANCED:
     * Set the cached properties that will be used to create the initial context
     * when doing remote JNDI lookups.
     */
    public void setRemoteContextProperties(Hashtable properties) {
        remoteContextProperties = properties;
    }

    /**
     * ADVANCED:
     * Return the properties that will be used to create the initial context
     * for local JNDI access.
     */
    public Hashtable getLocalContextProperties() {
        if (localContextProperties == null) {
            localContextProperties = new Hashtable();
        }
        return localContextProperties;
    }

    /**
     * ADVANCED:
     * Set the properties that will be used to create the initial context
     * for local JNDI access.
     */
    public void setLocalContextProperties(Hashtable properties) {
        localContextProperties = properties;
    }

    /**
     * INTERNAL:
     */
    public RemoteCommandManager getRemoteCommandManager() {
        return rcm;
    }

    /**
     * INTERNAL:
     */
    public void setRemoteCommandManager(RemoteCommandManager rcm) {
        this.rcm = rcm;
    }

    /**
     * INTERNAL:
     * Add a remote Connection to a remote service.
     */
    public void addConnectionToExternalService(RemoteConnection connection) {
        if (connection == null) {
            return;
        }
        try {
            connectBackToRemote(connection);
            connectionsToExternalServices.put(connection.getServiceId().getId(), connection);
            Object[] args = { connection.getServiceId() };
            rcm.logDebug("received_connection_from", args);
        } catch (Exception exception) {
            try {
                rcm.handleException(CommunicationException.errorSendingConnectionService(connection.toString(), exception));
            } catch (RuntimeException reThrownException) {
                Object[] args = { connection.getServiceId(), reThrownException };
                rcm.logWarning("problem_adding_connection", args);
                if (!shouldRemoveConnectionOnError) {
                    throw reThrownException;
                }
            }
        }
    }

    /**
     * INTERNAL:
     * Remove a remote connection from the list of connections to receive remote commands.
     */
    public void removeConnectionToExternalService(RemoteConnection connection) {
        synchronized (this) {
            connectionsToExternalServices.remove(connection.getServiceId().getId());
            connection.close();
        }
    }

    /**
     * INTERNAL:
     * Remove all remote connections from the list.
     */
    public void removeAllConnectionsToExternalServices() {
        synchronized (this) {
            Enumeration connections = connectionsToExternalServices.elements();
            connectionsToExternalServices = new Hashtable(3);

            while (connections.hasMoreElements()) {
                ((RemoteConnection)connections.nextElement()).close();
            }
        }
    }

    /**
     * INTERNAL:
     */
    public Map<String, RemoteConnection> getConnectionsToExternalServices() {
        return connectionsToExternalServices;
    }

    /**
     * INTERNAL:
     * Returns clone of the original map.
     */
    public Map<String, RemoteConnection> getConnectionsToExternalServicesForCommandPropagation() {
        return (Map)connectionsToExternalServices.clone();
    }

    /**
     * PUBLIC:
     * Set whether connections to remote services should be disconnected when an
     * error occurs.
     */
    public void setShouldRemoveConnectionOnError(boolean shouldRemoveConnectionOnError) {
        this.shouldRemoveConnectionOnError = shouldRemoveConnectionOnError;
    }

    /**
     * PUBLIC:
     * Return whether connections to remote services should be disconnected when an
     * error occurs.
     */
    public boolean shouldRemoveConnectionOnError() {
        return shouldRemoveConnectionOnError;
    }

    /**
     * INTERNAL SECURITY:
     * Set encryption class that will be loaded by the SecurableObjectHolder
     */
    public void setEncryptionClassName(String encryptionClassName) {
        SecurableObjectHolder oldHolder = securableObjectHolder;

        // re-initialize encryption mechanism
        securableObjectHolder = new SecurableObjectHolder();
        securableObjectHolder.setEncryptionClassName(encryptionClassName);

        // re-encrypt password
        if (hasPassword()) {
            setPassword(oldHolder.getSecurableObject().decryptPassword(getEncryptedPassword()));
        }
    }

    /**
     * INTERNAL:
     * @return true if a non null password has been set.
     */
    protected boolean hasPassword() {
        return getRemoteContextProperties().containsKey(Context.SECURITY_CREDENTIALS) && getRemoteContextProperties().get(Context.SECURITY_CREDENTIALS) != null;
    }

    /**
     * INTERNAL:
     * Initialize default properties.
     */
    public void initialize() {
        this.shouldRemoveConnectionOnError = DEFAULT_REMOVE_CONNECTION_ON_ERROR_MODE;
        this.connectionsToExternalServices = new Hashtable(2);

        remoteContextProperties = new Hashtable();
        // Factory is not require inside the server, do not default.
        //remoteContextProperties.put(Context.INITIAL_CONTEXT_FACTORY, DEFAULT_CONTEXT_FACTORY);
        remoteContextProperties.put(DEFAULT_DEDICATED_CONNECTION_KEY, DEFAULT_DEDICATED_CONNECTION_VALUE);
        // User/password are not required inside the server, do not default.
        //remoteContextProperties.put(Context.SECURITY_PRINCIPAL, DEFAULT_USER_NAME);

        this.securableObjectHolder = new SecurableObjectHolder();
    }

    /**
     * INTERNAL:
     * Security method.
     */
    protected String encrypt(String pwd) {
        return securableObjectHolder.getSecurableObject().encryptPassword(pwd);
    }

    /**
     * INTERNAL:
     * Security method called by the children classes
     */
    protected String decrypt(String encryptedPwd) {
        return securableObjectHolder.getSecurableObject().decryptPassword(encryptedPwd);
    }

    /**
     * INTERNAL:
     * Return the context used for looking up in the JNDI space of the specified remote URL.
     */
    public Context getRemoteHostContext(String remoteHostURL) {
        Hashtable remoteProperties = (Hashtable)getRemoteContextProperties().clone();
        // Host is only required if JMS connection factory is not accessible from local JNDI.
        if (remoteHostURL != null) {
            remoteProperties.put(Context.PROVIDER_URL, remoteHostURL);
        }
        Object[] args = { remoteProperties };
        rcm.logDebug("context_props_for_remote_lookup", args);

        if (hasPassword()) {
            // decrypt password just before looking up context. Calling
            // getPassword() will decrypt it.
            remoteProperties.put(Context.SECURITY_CREDENTIALS, getPassword());
        }

        return getContext(remoteProperties);
    }

    /**
     * ADVANCED:
     * Factory of new DiscoveryManager for different transports.  The RemoteCommandManger uses this method to create its DicscoveryManager.
     * Sub-class of TransportManager should return special discovery if required.  The default is discovery type is DiscoveryManager;
     * If this method returns null then during initialization RemoteCommandManager
     * calls createConnections method.
     */
    public DiscoveryManager createDiscoveryManager() {
        return new DiscoveryManager(rcm);
    }

    /**
     * INTERNAL:
     * Remove all remote connections for its list and the local connection from JNDI or JMS Subsriber
     */
    public void discardConnections() {
        removeLocalConnection();
        removeAllConnectionsToExternalServices();
    }

    /**
     * ADVANCED:
     * Remove the local connection from remote accesses.  The implementation should set remove the local connection from JNDI or JMS and set it to null.
     * This method is invoked internally by TopLink when the RCM is shutdown and should not be invoked by user's application.
     */
    public abstract void removeLocalConnection();

    /**
     * Generic API to allow config to be set.
     */
    public void setConfig(String config) {

    }
}
