/*******************************************************************************
 * Copyright (c) 1998, 2008 Oracle. 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:
 *     Oracle - initial API and implementation from Oracle TopLink
 ******************************************************************************/  
package org.eclipse.persistence.sessions.coordination.jms;

import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.jms.Topic;
import javax.jms.TopicConnection;
import javax.jms.TopicConnectionFactory;
import org.eclipse.persistence.exceptions.RemoteCommandManagerException;
import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.internal.sessions.coordination.jms.JMSTopicRemoteConnection;
import org.eclipse.persistence.sessions.coordination.broadcast.BroadcastTransportManager;
import org.eclipse.persistence.sessions.coordination.RemoteCommandManager;
import org.eclipse.persistence.sessions.coordination.TransportManager;

/**
 * <p>
 * <b>Purpose</b>: Provide a JMS transport implementation for the Remote Command Module (RCM).
 * <p>
 * <b>Description</b>: This class manages two connections to the same known JMS Topic:
 * external connection for publishing, local connection for receiving messages.
 * <p>
 * @author Steven Vo
 * @since OracleAS TopLink 10<i>g</i> (10.0.3)
 */
public class JMSTopicTransportManager extends BroadcastTransportManager {
    protected String connectionFactoryName;

    public static final String DEFAULT_TOPIC = "jms/TopLinkTopic";
    public static final String DEFAULT_CONNECTION_FACTORY = "jms/TopLinkTopicConnectionFactory";

    public JMSTopicTransportManager(RemoteCommandManager rcm) {
        super(rcm);
    }

    /**
     * INTERNAL:
     * JMSTopicTransportManager has maximum one external connection.
     * Verify there are no external connections,
     * create a new external connection, 
     * add it to external connections' map.
     */
    public void createExternalConnection() {
        synchronized(connectionsToExternalServices) {
            if(connectionsToExternalServices.isEmpty()) {
                try {
                    connectionsToExternalServices.put(rcm.getServiceId().getId(), createConnection(false));
                } catch (RemoteCommandManagerException rcmException) {
                    // to recover handle RemoteCommandManagerException.ERROR_CREATING_JMS_CONNECTION:
                    // after changing something (for instance jmsHostUrl)
                    // call createExternalConnection method again.
                    rcm.handleException(rcmException);
                }
            }            
        }
    }

    /**
     * INTERNAL:
     * JMSTopicTransportManager may have only two connections: one local and one external.
     * In case the local connection doesn't exist, this method creates it.
     */
    public synchronized void createLocalConnection() {
        if(localConnection == null) {
            try {
                localConnection = createConnection(true);
            } catch (RemoteCommandManagerException rcmException) {
                // to recover handle RemoteCommandManagerException.ERROR_CREATING_LOCAL_JMS_CONNECTION:
                // after changing something (for instance jmsHostUrl)
                // call createLocalConnection method again.
                rcm.handleException(rcmException);
            }
        }
    }

    /**
     * INTERNAL:
     * This method creates JMSTopicRemoteConnection to be used by this TransportManager.
     * Don't confuse this method with no-op createConnection(ServiceId serviceId).
     */
    protected JMSTopicRemoteConnection createConnection(boolean isLocalConnectionBeingCreated) throws RemoteCommandManagerException {
        Context remoteHostContext = null;
        try {
            remoteHostContext = getRemoteHostContext(getTopicHostUrl());
            TopicConnectionFactory connectionFactory = getTopicConnectionFactory(remoteHostContext);
            Topic topic = getTopic(remoteHostContext);    
            TopicConnection topicConnection = connectionFactory.createTopicConnection();
            // external connection is a puiblisher; local connection is a subscriber
            return new JMSTopicRemoteConnection(rcm, topicConnection, topic, isLocalConnectionBeingCreated);
        } catch (Exception ex) {
            RemoteCommandManagerException rcmException;
            if(isLocalConnectionBeingCreated) {
                rcmException = RemoteCommandManagerException.errorCreatingLocalJMSConnection(topicName, connectionFactoryName, getRemoteContextProperties(), ex);
            } else {
                rcmException = RemoteCommandManagerException.errorCreatingJMSConnection(topicName, connectionFactoryName, getRemoteContextProperties(), ex);
            }
            throw rcmException;
        } finally {
            if(remoteHostContext != null) {
                try {
                    remoteHostContext.close();
                } catch (NamingException namingException) {
                    // ignore
                }
            }
        }
    }
    
    /**
     * INTERNAL:
     * caches local connection, set localConnection to null, closes the cached connection in a new thread.
     */
    public void removeLocalConnection() {
        JMSTopicRemoteConnection connectionToRemove = (JMSTopicRemoteConnection)localConnection;
        synchronized(this) {
            if(connectionToRemove == localConnection) {
                localConnection = null;
            } else {
                connectionToRemove = null;
            }
        }
        // closing connection may take time - do it outside of the synchronzed block
        if(connectionToRemove != null) {
            connectionToRemove.close();
        }
    }

    /**
     * INTERNAL:
     * JMTopicTransportManager doesn't use DiscoveryManager, therefore
     * this method is called during RCM initialization to create all the necessary connections.
     */
    public void createConnections() {
        createExternalConnection();
        createLocalConnection();
    }

    /**
     * PUBLIC:
     * Configure the JMS Topic Connection Factory Name for the JMS Topic connections.
     */
    public void setTopicConnectionFactoryName(String newTopicConnectionFactoryName) {
        connectionFactoryName = newTopicConnectionFactoryName;
    }

    /**
     * PUBLIC:
     * Return the JMS Topic Connection Factory Name for the JMS Topic connections.
     */
    public String getTopicConnectionFactoryName() {
        return connectionFactoryName;
    }

    /**
     * INTERNAL:
     * Initialize default properties.
     */
    public void initialize() {
        super.initialize();
        topicName = DEFAULT_TOPIC;
        connectionFactoryName = DEFAULT_CONNECTION_FACTORY;
    }

    /**
     * PUBLIC:
     * Return the URL of the machine on the network that hosts the JMS Topic.  This is a reqired property and must be configured.
     */
    public String getTopicHostUrl() {
        return (String)getRemoteContextProperties().get(Context.PROVIDER_URL);
    }

    /**
     * PUBLIC:
     * Configure the URL of the machine on the network that hosts the JMS Topic. This is a required property and must be configured.
     */
    public void setTopicHostUrl(String jmsHostUrl) {
        getRemoteContextProperties().put(Context.PROVIDER_URL, jmsHostUrl);
        rcm.getServiceId().setURL(jmsHostUrl);
    }

    /**
     * ADVANCED:
     * This funcation is not supported for naming service other than JNDI or TransportManager.JNDI_NAMING_SERVICE.
     */
    public void setNamingServiceType(int serviceType) {
        if (serviceType != TransportManager.JNDI_NAMING_SERVICE) {
            throw ValidationException.operationNotSupported("setNamingServiceType");
        }
    }

    /**
     * INTERNAL:
     */
    protected Topic getTopic(Context remoteHostContext) {
        try {
            return (Topic)remoteHostContext.lookup(topicName);
        } catch (NamingException e) {
            RemoteCommandManagerException rcmException = RemoteCommandManagerException.errorLookingUpRemoteConnection(topicName, rcm.getUrl(), e);
            rcm.handleException(rcmException);
            // If the handler hasn't thrown the exception rethrow it here - it's impossible to recover.
            throw rcmException;
        }
    }

    /**
     * INTERNAL:
     */
    protected TopicConnectionFactory getTopicConnectionFactory(Context remoteHostContext) {
        try {
            return (TopicConnectionFactory)remoteHostContext.lookup(connectionFactoryName);
        } catch (NamingException e) {
            RemoteCommandManagerException rcmException = RemoteCommandManagerException.errorLookingUpRemoteConnection(connectionFactoryName, rcm.getUrl(), e);
            rcm.handleException(rcmException);
            // If the handler hasn't thrown the exception rethrow it here - it's impossible to recover.
            throw rcmException;
        }
    }

    /**
     * INTERNAL:
     * In case there's no external connection attempts to create one,
     * if that's successful then (in case there is no local connection, too)
     * attempts to create local connection in a separate thread.
     * Returns clone of the original map.
     */
    public Hashtable getConnectionsToExternalServicesForCommandPropagation() {
        if(this.getConnectionsToExternalServices().isEmpty() && !this.rcm.isStopped()) {
            this.createExternalConnection();
            if(this.localConnection == null) {
                // It's a good time to create localConnection,
                // in a new thread - to return externalConnections promptly.
                this.rcm.getServerPlatform().launchContainerRunnable(new Runnable() {
                    public void run() {
                        try {
                            createLocalConnection();
                        } catch (RemoteCommandManagerException ex) {
                            // Ignore exception - user had a chance to handle it in createLocalConnection method:
                            // for instance to change host url and create a new local connection.
                        }
                    }
                });
            }
        }
        return super.getConnectionsToExternalServicesForCommandPropagation();
    }
}
