/*
 * 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 flex.messaging.services.messaging.adapters;

import java.util.Hashtable;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Session;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import flex.messaging.config.ConfigurationException;
import flex.messaging.log.Log;

/**
 * The JMSProxy is the superclass for all producers and consumers
 * on both topics and queues. This class contains shared behavior
 * between producers and consumers.
 */
public abstract class JMSProxy {
    /* JMS related variables */
    protected Connection connection;
    protected ConnectionCredentials connectionCredentials;
    protected ConnectionFactory connectionFactory;
    protected Session session;
    protected Destination destination;
    protected Context jndiContext;

    protected int acknowledgeMode;
    protected String connectionFactoryName;
    protected String destinationJndiName;
    protected Hashtable initialContextEnvironment;

    //--------------------------------------------------------------------------
    //
    // Constructor
    //
    //--------------------------------------------------------------------------

    /**
     * Creates a new <code>JMSProxy</code> default default acknowledge mode of
     * <code>javax.jms.Session.AUTO_ACKNOWLEDGE</code>.
     */
    public JMSProxy() {
        acknowledgeMode = javax.jms.Session.AUTO_ACKNOWLEDGE;
    }

    //--------------------------------------------------------------------------
    //
    // Initialize, validate, start, and stop methods.
    //
    //--------------------------------------------------------------------------

    /**
     * Initialize with settings from the JMS adapter.
     *
     * @param settings JMS settings to use for initialization.
     */
    public void initialize(JMSSettings settings) {
        String ackString = settings.getAcknowledgeMode();
        if (ackString.equals(JMSConfigConstants.AUTO_ACKNOWLEDGE))
            acknowledgeMode = Session.AUTO_ACKNOWLEDGE;
        else if (ackString.equals(JMSConfigConstants.CLIENT_ACKNOWLEDGE))
            acknowledgeMode = Session.CLIENT_ACKNOWLEDGE;
        else if (ackString.equals(JMSConfigConstants.DUPS_OK_ACKNOWLEDGE))
            acknowledgeMode = Session.DUPS_OK_ACKNOWLEDGE;

        connectionFactoryName = settings.getConnectionFactory();
        String username = settings.getConnectionUsername();
        String password = settings.getConnectionPassword();
        if (username != null || password != null) {
            connectionCredentials = new ConnectionCredentials(username, password);
        }
        destinationJndiName = settings.getDestinationJNDIName();
        initialContextEnvironment = settings.getInitialContextEnvironment();
    }

    /**
     * Verifies that the <code>JMSProxy</code> is in valid state before
     * it is started. For <code>JMSProxy</code> to be in valid state, it needs
     * to have a connection factory name and destination jndi name assigned.
     */
    protected void validate() {
        if (connectionFactoryName == null) {
            // JMS connection factory of message destinations with JMS Adapters must be specified.
            ConfigurationException ce = new ConfigurationException();
            ce.setMessage(JMSConfigConstants.MISSING_CONNECTION_FACTORY);
            throw ce;
        }

        if (destinationJndiName == null) {
            // JNDI names for message destinations with JMS Adapters must be specified.
            ConfigurationException ce = new ConfigurationException();
            ce.setMessage(JMSConfigConstants.MISSING_DESTINATION_JNDI_NAME);
            throw ce;
        }
    }

    /**
     * Starts the <code>JMSProxy</code>. The default implementation verifies
     * that <code>JMSProxy</code> is in a valid state to be started and then
     * initializes JNDI context and connection factory for JMS. Subclasses
     * should call <code>super.start</code>.
     *
     * @throws NamingException The thrown naming exception.
     * @throws JMSException    The thrown JMS exception.
     */
    public void start() throws NamingException, JMSException {
        validate();
        initializeJndiContext();
        initializeConnectionFactory();
        initializeDestination();
    }

    /**
     * Stops the <code>JMSProxy</code> by stopping its associated session
     * and connection.
     */
    public void stop() {
        try {
            if (session != null)
                session.close();
        } catch (JMSException e) {
            if (Log.isWarn())
                Log.getLogger(JMSAdapter.LOG_CATEGORY).warn("JMS proxy for JMS destination '"
                        + destinationJndiName + "' received an error while closing"
                        + " its underlying Session: " + e.getMessage());
        }

        try {
            if (connection != null)
                connection.close();
        } catch (JMSException e) {
            if (Log.isWarn())
                Log.getLogger(JMSAdapter.LOG_CATEGORY).warn("JMS proxy for JMS destination '"
                        + destinationJndiName + "' received an error while closing"
                        + " its underlying Connection: " + e.getMessage());
        }

        try {
            if (jndiContext != null)
                jndiContext.close();
        } catch (NamingException e) {
            if (Log.isWarn())
                Log.getLogger(JMSAdapter.LOG_CATEGORY).warn("JMS proxy for JMS destination '"
                        + destinationJndiName + "' received an error while closing"
                        + " its underlying JNDI context: " + e.getMessage());
        }
    }

    //--------------------------------------------------------------------------
    //
    // Public Methods
    //
    //--------------------------------------------------------------------------

    /**
     * Returns the acknowledge mode used by the <code>JMSProxy</code>.
     *
     * @return The acknowledge mode used by the <code>JMSProxy</code>.
     */
    public int getAcknowledgeMode() {
        return acknowledgeMode;
    }

    /**
     * Sets the acknowledge mode used by the <code>JMSProxy</code>. Valid values
     * are javax.jms.Session.AUTO_ACKNOWLEDGE, javax.jms.Session.CLIENT_ACKNOWLEDGE,
     * javax.jms.Session.DUPS_OK_ACNOWLEDGE. This property is optional and
     * defaults to javax.jms.Session.AUTO_ACKNOWLEDGE.
     *
     * @param acknowledgeMode An int representing the acknowledge mode used.
     */
    public void setAcknowledgeMode(int acknowledgeMode) {
        if (acknowledgeMode == Session.AUTO_ACKNOWLEDGE
                || acknowledgeMode == Session.CLIENT_ACKNOWLEDGE
                || acknowledgeMode == Session.DUPS_OK_ACKNOWLEDGE)
            this.acknowledgeMode = acknowledgeMode;
    }

    /**
     * Returns the connection factory name.
     *
     * @return The connection factory name.
     */
    public String getConnectionFactoryName() {
        return connectionFactoryName;
    }

    /**
     * Sets the connection factory name. This property should not changed
     * after startup.
     *
     * @param connectionFactoryName The connection factory name.
     */
    public void setConnectionFactoryName(String connectionFactoryName) {
        this.connectionFactoryName = connectionFactoryName;
    }

    /**
     * Returns the connection credentials used while creating JMS connections.
     *
     * @return The connection credentials used while creating JMS connections.
     */
    public ConnectionCredentials getConnectionCredentials() {
        return connectionCredentials;
    }

    /**
     * Sets the connection credentials. Connections credentials are passed to JMS
     * connection factory when a JMS connection is created.
     *
     * @param connectionCredentials The connection credentials.
     */
    public void setConnectionCredentials(ConnectionCredentials connectionCredentials) {
        this.connectionCredentials = connectionCredentials;
    }

    /**
     * Returns the JNDI name of the JMS destination that <code>JMSProxy</code> talks to.
     *
     * @return The JNDI name of the JMS destination.
     */
    public String getDestinationJndiName() {
        return destinationJndiName;
    }

    /**
     * Sets the JNDI name of the JMS destination that <code>JMSProxy</code> talks to.
     *
     * @param destinationJndiName The JNDI name of the JMS destination.
     */
    public void setDestinationJndiName(String destinationJndiName) {
        this.destinationJndiName = destinationJndiName;
    }

    /**
     * Returns the <code>initial-context-environment</code> property.
     *
     * @return a Hashtable of the <code>initial-context-environment</code>.
     */
    public Hashtable getInitialContextEnvironment() {
        return initialContextEnvironment;
    }

    /**
     * Sets the <code>initial-context-environment</code> property. This property
     * is optional. This property should be change after startup.
     *
     * @param env A Hashtable of the <code>initial-context-environment</code>.
     */
    public void setInitialContextEnvironment(Hashtable env) {
        initialContextEnvironment = env;
    }

    //--------------------------------------------------------------------------
    //
    // Protected and Private Methods
    //
    //--------------------------------------------------------------------------

    /**
     * Initializes the connection factory needed for JMS.
     */
    protected ConnectionFactory initializeConnectionFactory() throws NamingException {
        if (connectionFactory == null)
            connectionFactory = (ConnectionFactory) jndiContext.lookup(connectionFactoryName);

        return connectionFactory;
    }

    /**
     * Initializes the destination (topic and queue) used by JMS.
     */
    protected Destination initializeDestination() throws NamingException {
        if (destination == null)
            destination = (Destination) jndiContext.lookup(destinationJndiName);

        return destination;
    }

    /**
     * Initializes the JNDI context needed by JMS. This should be called before
     * any other initialize methods.
     */
    protected Context initializeJndiContext() throws NamingException {
        if (jndiContext != null)
            stop();

        if (initialContextEnvironment != null)
            jndiContext = new InitialContext(initialContextEnvironment);
        else
            jndiContext = new InitialContext();

        return jndiContext;
    }

    //--------------------------------------------------------------------------
    //
    // Nested Classes
    //
    //--------------------------------------------------------------------------

    /**
     * A static inner class for connection credentials that is passed to JMS
     * connection factory when a JMS connection is created.
     */
    public static class ConnectionCredentials {
        private String username;
        private String password;

        /**
         * Creates a <code>ConnectionCredentials</code> instance with the supplied
         * username and password.
         *
         * @param username Username of the credential.
         * @param password Password of the credential.
         */
        public ConnectionCredentials(String username, String password) {
            this.username = username;
            this.password = password;
        }

        /**
         * Returns the username being used.
         *
         * @return The username being used.
         */
        public String getUsername() {
            return username;
        }

        /**
         * Returns the password being used.
         *
         * @return The password being used.
         */
        public String getPassword() {
            return password;
        }
    }
}
