/*
 * Copyright (c) 2011, 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:
//     cdelahun - Bug 214534: added JMS Cache Coordination for publishing only
package org.eclipse.persistence.sessions.coordination.jms;

import java.util.Map;

import javax.jms.Topic;
import javax.jms.TopicConnectionFactory;
import javax.naming.Context;
import javax.naming.NamingException;

import org.eclipse.persistence.exceptions.RemoteCommandManagerException;
import org.eclipse.persistence.exceptions.ValidationException;
import org.eclipse.persistence.internal.sessions.coordination.RemoteConnection;
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 transport implementation for the Remote Command Module (RCM) that publishes
 * to a JMS topic.
 * <p>
 * <b>Description</b>: This class manages two connections: an external connection for publishing to JMS,
 *  and a local connection which can be used to process JMS messages received from an application JMS listener.
 * <p>
 * @author Chris Delahunt
 * @since EclipseLink 2.1
 */
public class JMSPublishingTransportManager extends BroadcastTransportManager {
    protected String connectionFactoryName;

    /*
     * flag used to toggle between spec compliant behavior and legacy behavior.  Default false causes external JMSTopicRemoteConnection to cache the
     * TopicConnectionFactory and obtain TopicConnections etc only when executeCommand is called, releasing them immediately.
     * True causes TopicConnection, TopicSession and TopicPublisher to be held and used for every executeCommand call.
     * A value of true has spec implications, as TopicPublishers are not concurrent objects, but was added in for applications
     * that may have worked previously but encounter performance issues with repeatedly creating TopicConnections etc.
     */
    protected boolean reuseJMSTopicPublisher = false;

    public static final String DEFAULT_TOPIC = "jms/EclipseLinkTopic";
    public static final String DEFAULT_CONNECTION_FACTORY = "jms/EclipseLinkTopicConnectionFactory";
    /**
     * PUBLIC:
     * Creates a JMSPublishingOnlyTopicTransportManager
     */
    public JMSPublishingTransportManager(RemoteCommandManager rcm) {
        super(rcm);
    }

    /**
     * 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);
            // external connection is a publisher; local connection is a subscriber
            return new JMSTopicRemoteConnection(rcm, connectionFactory, topic, isLocalConnectionBeingCreated, reuseJMSTopicPublisher);
        } catch (Exception ex) {
            RemoteCommandManagerException rcmException;
            if(isLocalConnectionBeingCreated) {
                rcmException = RemoteCommandManagerException.errorCreatingLocalJMSConnection(topicName, connectionFactoryName, ex);
            } else {
                rcmException = RemoteCommandManagerException.errorCreatingJMSConnection(topicName, connectionFactoryName, ex);
            }
            throw rcmException;
        } finally {
            if(remoteHostContext != null) {
                try {
                    remoteHostContext.close();
                } catch (NamingException namingException) {
                    // ignore
                }
            }
        }
    }

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

    /**
     * INTERNAL:
     * JMSPublishingTransportManager 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:
     * JMSPublishingTransportManager has only two connections: one local and one external.
     * In case the local connection doesn't exist, this method creates it and holds it to be used
     * when processing incoming JMS messages.  The stored local connection on JMSPublishingTransportManager
     * does not connect to topicConnection, and instead must be used from an MDB when a message is received
     */
    @Override
    public void createLocalConnection() {
        if(localConnection == null) {
            localConnection = new JMSTopicRemoteConnection(rcm);
        }
    }


    /**
     * INTERNAL:
     * In case there's no external connection attempts to create one.
     * Returns clone of the original map.
     */
    @Override
    public Map<String, RemoteConnection> getConnectionsToExternalServicesForCommandPropagation() {
        if(this.getConnectionsToExternalServices().isEmpty() && !this.rcm.isStopped()) {
            this.createExternalConnection();
        }
        return super.getConnectionsToExternalServicesForCommandPropagation();
    }

    /**
     * PUBLIC:
     * flag used to toggle between j2EE/JMS spec compliant behavior and legacy behavior.  Default value false causes external
     *  JMSTopicRemoteConnection to cache the TopicConnectionFactory and obtain TopicConnections, TopicSession and TopicPublishers
     *  every time executeCommand is called, and then closing them immediately.  This is JMS and J2EE compliant, as the TopicConnection
     *  is never reused in different threads.
     * True causes TopicConnection, TopicSession and TopicPublisher to be cached within the JMSTopicRemoteConnection and used for
     *  every executeCommand call.  These objects can potentially used concurrently, which the JMS spec does not force
     *  providers to support.
     */
    public boolean getReuseJMSTopicPublisher(){
        return this.reuseJMSTopicPublisher;
    }

    /**
     * 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;
        }
    }

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

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

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

    /**
     * INTERNAL:
     * No-op, as the local connection does not need to be removed from JMSPublishingTransportManager.
     * An application must close the connection directly if it is using the local connection as a listener.
     */
    @Override
    public void removeLocalConnection() {}

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

    /**
     * PUBLIC:
     * flag used to toggle between j2EE/JMS spec compliant behavior and legacy behavior.  Default value false causes external
     *  JMSTopicRemoteConnection to cache the TopicConnectionFactory and obtain TopicConnections, TopicSession and TopicPublishers
     *  every time executeCommand is called, and then closing them immediately.  This is JMS and J2EE compliant, as the TopicConnection
     *  is never reused in different threads.
     * True causes TopicConnection, TopicSession and TopicPublisher to be cached within the JMSTopicRemoteConnection and used for
     *  every executeCommand call.  These objects can potentially used concurrently, which the JMS spec does not force
     *  providers to support.
     *
     *  @param reuseJMSTopicPublisher
     */
    public void setShouldReuseJMSTopicPublisher(boolean reuseJMSTopicPublisher){
        this.reuseJMSTopicPublisher = reuseJMSTopicPublisher;
    }

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

    /**
     * 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);
    }
}
