/**
 *
 * Copyright 2004 Protique Ltd
 *
 * Licensed 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 org.activemq.broker.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.jms.InvalidClientIDException;
import javax.jms.InvalidDestinationException;
import javax.jms.JMSException;
import javax.jms.JMSSecurityException;
import javax.transaction.xa.XAException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.activemq.ActiveMQConnectionMetaData;
import org.activemq.broker.Broker;
import org.activemq.broker.BrokerClient;
import org.activemq.broker.BrokerConnector;
import org.activemq.broker.BrokerContainer;
import org.activemq.broker.BrokerContext;
import org.activemq.capacity.CapacityMonitorEvent;
import org.activemq.capacity.CapacityMonitorEventListener;
import org.activemq.io.WireFormat;
import org.activemq.io.impl.DefaultWireFormat;
import org.activemq.io.util.MemoryBoundedObjectManager;
import org.activemq.message.ActiveMQDestination;
import org.activemq.message.ActiveMQMessage;
import org.activemq.message.ActiveMQXid;
import org.activemq.message.ConnectionInfo;
import org.activemq.message.ConsumerInfo;
import org.activemq.message.DurableUnsubscribe;
import org.activemq.message.MessageAck;
import org.activemq.message.ProducerInfo;
import org.activemq.message.SessionInfo;
import org.activemq.security.SecurityAdapter;
import org.activemq.service.RedeliveryPolicy;
import org.activemq.service.Service;
import org.activemq.store.PersistenceAdapter;
import org.activemq.transport.DiscoveryAgent;
import org.activemq.transport.NetworkConnector;
import org.activemq.transport.TransportServerChannel;
import org.activemq.util.IdGenerator;
import EDU.oswego.cs.dl.util.concurrent.ConcurrentHashMap;
import EDU.oswego.cs.dl.util.concurrent.CopyOnWriteArrayList;
import EDU.oswego.cs.dl.util.concurrent.CopyOnWriteArraySet;
import java.util.Set;

/**
 * Represents the ActiveMQ JMS Broker which typically has one or many connectors
 *
 * @version $Revision: 1.1.1.1 $
 */
public class BrokerContainerImpl implements BrokerContainer, CapacityMonitorEventListener {
    public static final String DISABLE_CLEAN_SHUTDOWN_PROPERTY = "activemq.broker.disable-clean-shutdown";
    private static final Log log = LogFactory.getLog(BrokerContainerImpl.class);
    private static final boolean useLoggingForShutdownErrors = false;

    private BrokerContext context;
    private Broker broker;
    private Map clientIds;
    private Map consumerInfos;
    private Map producerInfos;
    private List transportConnectors;
    private Thread shutdownHook;
    private boolean stopped;
    private List networkConnectors;
    private DiscoveryAgent discoveryAgent;
    private Map localDiscoveryDetails;
    private Set remoteClientIds;


    public BrokerContainerImpl() {
        this(new IdGenerator().generateId());
    }

    public BrokerContainerImpl(String brokerName) {
        this(brokerName, BrokerContext.getInstance());
    }

    public BrokerContainerImpl(String brokerName, MemoryBoundedObjectManager memoryManager) {
        this(brokerName, BrokerContext.getInstance());
    }
    
    public BrokerContainerImpl(String brokerName,String clusterName) {
        this(brokerName, clusterName,BrokerContext.getInstance());
        
    }

    public BrokerContainerImpl(String brokerName, PersistenceAdapter persistenceAdapter) {
        this(brokerName, persistenceAdapter, BrokerContext.getInstance());
    }

    public BrokerContainerImpl(String brokerName, BrokerContext context) {
        this(new DefaultBroker(brokerName), context);
    }
    
    public BrokerContainerImpl(String brokerName, BrokerContext context, MemoryBoundedObjectManager memoryManager) {
        this(new DefaultBroker(brokerName,memoryManager), context);
    }
    
    public BrokerContainerImpl(String brokerName, String clusterName, BrokerContext context) {
        this(new DefaultBroker(brokerName,clusterName), context);
    }
    
    public BrokerContainerImpl(String brokerName, PersistenceAdapter persistenceAdapter, BrokerContext context) {
        this(new DefaultBroker(brokerName, persistenceAdapter), context);
    }

    public BrokerContainerImpl(String brokerName,String clusterName, PersistenceAdapter persistenceAdapter, BrokerContext context) {
        this(new DefaultBroker(brokerName,clusterName, persistenceAdapter), context);
    }

    /**
     * @param broker
     */
    public BrokerContainerImpl(Broker broker, BrokerContext context) {
        this.broker = broker;
        this.context = context;
        this.clientIds = new ConcurrentHashMap();
        this.consumerInfos = new ConcurrentHashMap();
        this.producerInfos = new ConcurrentHashMap();
        this.transportConnectors = new CopyOnWriteArrayList();
        this.networkConnectors = new CopyOnWriteArrayList();
        this.remoteClientIds = new CopyOnWriteArraySet();
        this.broker.addCapacityEventListener(this);

        // lets register ourselves with the context
        context.registerContainer(broker.getBrokerName(), this);
        //register ourselves for vm:// transports
        context.registerContainer("vm://" + broker.getBrokerName(), this);
    }

    /**
     * start the Container
     *
     * @throws JMSException
     */
    public void start() throws JMSException {
        log.info("ActiveMQ "+ActiveMQConnectionMetaData.PROVIDER_VERSION+" JMS Message Broker (" + broker.getBrokerName() + ") is starting");
        log.info("For help or more information please see: www.protique.com");
        broker.start();
        addShutdownHook();

        // TODO we might not need to copy the collections, as maybe the List might not
        // throw concurrent modification exception? Couldn't tell from the docs
        // but I don't think it does

        for (Iterator iter = new ArrayList(networkConnectors).iterator(); iter.hasNext();) {
            Service connector = (Service) iter.next();
            connector.start();
        }

        for (Iterator iter = new ArrayList(transportConnectors).iterator(); iter.hasNext();) {
            Service connector = (Service) iter.next();
            connector.start();
        }

        if (discoveryAgent != null) {
            discoveryAgent.start();

            localDiscoveryDetails = createLocalDiscoveryDetails();
            discoveryAgent.registerService(getLocalBrokerName(), localDiscoveryDetails);
        }

        log.info("ActiveMQ JMS Message Broker (" + broker.getBrokerName() + ") has started");
    }

    /**
     * Stop the Container
     *
     * @throws JMSException
     */
    public synchronized void stop() throws JMSException {
        if (!stopped) {
            stopped = true;

            log.info("ActiveMQ Message Broker (" + broker.getBrokerName() + ") is shutting down");

            context.deregisterContainer(broker.getBrokerName(), this);

            try {
                Runtime.getRuntime().removeShutdownHook(shutdownHook);
            }
            catch (Exception e) {
                log.debug("Caught exception, must be shutting down: " + e);
            }

            JMSException firstException = null;
            if (discoveryAgent != null) {
                try {
                    discoveryAgent.stop();
                } catch (JMSException e) {
                    firstException = e;
                    log.warn("Could not close discovery agent: " + discoveryAgent + " due to: " + e, e);
                }
            }

            for (Iterator iter = new ArrayList(transportConnectors).iterator(); iter.hasNext();) {
                Service connector = (Service) iter.next();
                try {
                    connector.stop();
                }
                catch (JMSException e) {
                    if (firstException == null) {
                        firstException = e;
                    }
                    log.warn("Could not close transport connector: " + connector + " due to: " + e, e);
                }
            }
            transportConnectors.clear();

            for (Iterator iter = new ArrayList(networkConnectors).iterator(); iter.hasNext();) {
                Service connector = (Service) iter.next();
                try {
                    connector.stop();
                }
                catch (JMSException e) {
                    if (firstException == null) {
                        firstException = e;
                    }
                    log.warn("Could not close network connector: " + connector + " due to: " + e, e);
                }
            }
            networkConnectors.clear();

            

            // lets close all the channels
            // note that this Map implementation does not throw concurrent modification exception
            for (Iterator iter = clientIds.values().iterator();iter.hasNext();) {
                // should remove clients from parent container?
                BrokerClient client = (BrokerClient) iter.next();
                if (client != null) {
                    try {
                        client.stop();
                    }
                    catch (JMSException e) {
                        if (firstException == null) {
                            firstException = e;
                        }
                        log.warn("Could not close client: " + client + " due to: " + e, e);
                    }
                }
            }
            clientIds.clear();

            broker.removeCapacityEventListener(this);
            broker.stop();

            log.info("ActiveMQ JMS Message Broker (" + broker.getBrokerName() + ") stopped");

            if (firstException != null) {
                throw firstException;
            }
        }
    }

    /**
     * registers a new Connection
     *
     * @param client
     * @param info   infomation about the client-side Connection
     * @throws InvalidClientIDException if the ClientID of the Connection is a duplicate
     */
	public void registerConnection(BrokerClient client, ConnectionInfo info) throws JMSException {
		String clientId = info.getClientId();
		if (clientIds.containsKey(clientId)) {
			int timeout = 5000;
			log.info("Got duplicate client with id: " + clientId + ". Giving the existing client " + timeout + " millis to prove it's alive.");

			// Assert that the existing client is alive
			BrokerClient existingClient = (BrokerClient) clientIds.get(clientId);
			JMSException ex = null;
			boolean isValid = true;
			try {
				existingClient.validateConnection(timeout);
			} catch (JMSException e) {
				isValid = false;
				ex = e;
			}
			if (isValid) {
				// The existing client is valid, so kick the new client
				log.info("Client: " + clientId + " on transport: " + existingClient.getChannel()
						+ "' is alive, rejecting new client on transport: " + client.getChannel());
				throw new InvalidClientIDException("Duplicate clientId: " + info);
			} else {
				// A transport error occured or the existing client did not
				// respond in time, so kick it and let the new client connect.
				log.info("Replacing client: " + clientId + " on transport: " + existingClient.getChannel() + " ("
						+ ex.getMessage() + ") with client on transport: " + client.getChannel());
				
				// @TODO: Not sure this is the proper way to close the existing client
				existingClient.cleanUp(); 
				existingClient.stop();
			}

		}
		getBroker().addClient(client, info);
		log.info("Adding new client: " + clientId + " on transport: " + client.getChannel());
		clientIds.put(clientId, client);
	}

    /**
     * un-registers a Connection
     *
     * @param client
     * @param info   infomation about the client-side Connection
     * @throws JMSException
     */
    public void deregisterConnection(BrokerClient client, ConnectionInfo info) throws JMSException {
        String clientId = client.getClientID();
        if (clientId != null) {
            Object answer = clientIds.remove(clientId);
            if (answer != null) {
                log.info("Removing client: " + clientId + " on transport: " + client.getChannel());
                getBroker().removeClient(client, info);
            }
            else {
                log.warn("Got duplicate deregisterConnection for client: " + clientId);
            }
        }
        else {
            log.warn("No clientID available for client: " + client);
        }
    }

    /**
     * Registers a MessageConsumer
     *
     * @param client
     * @param info
     * @throws JMSException
     * @throws JMSSecurityException if client authentication fails for the Destination the Consumer applies for
     */
    public void registerMessageConsumer(BrokerClient client, ConsumerInfo info) throws JMSException {
        consumerInfos.put(info, client);
        getBroker().addMessageConsumer(client, info);
    }

    /**
     * De-register a MessageConsumer from the Broker
     *
     * @param client
     * @param info
     * @throws JMSException
     */
    public void deregisterMessageConsumer(BrokerClient client, ConsumerInfo info) throws JMSException {
        consumerInfos.remove(info);
        getBroker().removeMessageConsumer(client, info);
    }

    /**
     * Registers a MessageProducer
     *
     * @param client
     * @param info
     * @throws JMSException
     * @throws JMSSecurityException if client authentication fails for the Destination the Consumer applies for
     */
    public void registerMessageProducer(BrokerClient client, ProducerInfo info) throws JMSException {
        ActiveMQDestination dest = info.getDestination();
        checkTempDestinationExistance(dest);
        getBroker().addMessageProducer(client, info);

        producerInfos.put(info, client);
    }

    /**
     * De-register a MessageProducer from the Broker
     *
     * @param client
     * @param info
     * @throws JMSException
     */
    public void deregisterMessageProducer(BrokerClient client, ProducerInfo info) throws JMSException {
        getBroker().removeMessageProducer(client, info);

        producerInfos.remove(info);
    }

    /**
     * Register a client-side Session (used for Monitoring)
     *
     * @param client
     * @param info
     * @throws JMSException
     */
    public void registerSession(BrokerClient client, SessionInfo info) throws JMSException {
    }

    /**
     * De-register a client-side Session from the Broker (used for monitoring)
     *
     * @param client
     * @param info
     * @throws JMSException
     */
    public void deregisterSession(BrokerClient client, SessionInfo info) throws JMSException {
    }

    /**
     * Start a transaction from the Client session
     *
     * @param client
     * @param transactionId
     * @throws JMSException
     */
    public void startTransaction(BrokerClient client, String transactionId) throws JMSException {
        getBroker().startTransaction(client, transactionId);
    }

    /**
     * Rollback a transacton
     *
     * @param client
     * @param transactionId
     * @throws JMSException
     */
    public void rollbackTransaction(BrokerClient client, String transactionId) throws JMSException {
        getBroker().rollbackTransaction(client, transactionId);
    }

    /**
     * Commit a transaction
     *
     * @param client
     * @param transactionId
     * @throws JMSException
     */
    public void commitTransaction(BrokerClient client, String transactionId) throws JMSException {
        getBroker().commitTransaction(client, transactionId);
    }

    /**
     * Send a non-transacted message to the Broker
     *
     * @param client
     * @param message
     * @throws JMSException
     */
    public void sendMessage(BrokerClient client, ActiveMQMessage message) throws JMSException {
        ActiveMQDestination dest = message.getJMSActiveMQDestination();        
        checkTempDestinationExistance(dest);
        broker.sendMessage(client, message);
    }
    
    /**
     * register a remote clientID
     * @param remoteClientID
     */
    
    public void registerRemoteClientID(String remoteClientID){
        remoteClientIds.add(remoteClientID);
    }
    
    /**
     * deregister a remote clientID
     * @param remoteClientID
     */
    public void deregisterRemoteClientID(String remoteClientID){
        remoteClientIds.remove(remoteClientID);
    }

    /**
     * @param dest
     * @throws InvalidDestinationException
     */
    private void checkTempDestinationExistance(ActiveMQDestination dest) throws InvalidDestinationException {
        if (dest != null && dest.isTemporary()) {
            //check to see if the client that is the target for the temporary destination still exists
            String clientId = ActiveMQDestination.getClientId(dest);
            if (clientId == null) {
                throw new InvalidDestinationException("Destination " + dest.getPhysicalName()
                        + " is a temporary destination with null clientId");
            }
            if (!clientIds.containsKey(clientId) && !remoteClientIds.contains(clientId)) {
                throw new InvalidDestinationException("Destination " + dest.getPhysicalName()
                        + " is no longer valid because the client " + clientId + " no longer exists");
            }
        }
    }

    /**
     * Acknowledge reciept of a message
     *
     * @param client
     * @param ack
     * @throws JMSException
     */
    public void acknowledgeMessage(BrokerClient client, MessageAck ack) throws JMSException {
        getBroker().acknowledgeMessage(client, ack);
    }

    /**
     * Command to delete a durable topic subscription
     *
     * @param client
     * @param ds
     * @throws JMSException
     */
    public void durableUnsubscribe(BrokerClient client, DurableUnsubscribe ds) throws JMSException {
        getBroker().deleteSubscription(ds.getClientId(), ds.getSubscriberName());
    }

    /**
     * Start an XA transaction.
     *
     * @param client
     * @param xid
     */
    public void startTransaction(BrokerClient client, ActiveMQXid xid) throws XAException {
        getBroker().startTransaction(client, xid);
    }

    /**
     * Gets the prepared XA transactions.
     *
     * @param client
     * @return
     */
    public ActiveMQXid[] getPreparedTransactions(BrokerClient client) throws XAException {
        return getBroker().getPreparedTransactions(client);
    }

    /**
     * Prepare an XA transaction.
     *
     * @param client
     * @param xid
     */
    public int prepareTransaction(BrokerClient client, ActiveMQXid xid) throws XAException {
        return getBroker().prepareTransaction(client, xid);
    }

    /**
     * Rollback an XA transaction.
     *
     * @param client
     * @param xid
     */
    public void rollbackTransaction(BrokerClient client, ActiveMQXid xid) throws XAException {
        getBroker().rollbackTransaction(client, xid);
    }

    /**
     * Commit an XA transaction.
     *
     * @param client
     * @param xid
     * @param onePhase
     */
    public void commitTransaction(BrokerClient client, ActiveMQXid xid, boolean onePhase) throws XAException {
        getBroker().commitTransaction(client, xid, onePhase);
    }


    /**
     * Update any message producers about our capacity to handle messages
     *
     * @param event
     */
    public void capacityChanged(CapacityMonitorEvent event) {
        //only send to producers
        for (Iterator i = producerInfos.values().iterator(); i.hasNext();) {
            BrokerClient client = (BrokerClient) i.next();
            client.updateBrokerCapacity(event.getCapacity());
        }
    }


    // Properties
    //-------------------------------------------------------------------------

    public List getTransportConnectors() {
        return transportConnectors;
    }

    public void setTransportConnectors(List transportConnectors) {
        this.transportConnectors = new CopyOnWriteArrayList(transportConnectors);
    }

    public void addConnector(BrokerConnector connector) {
        if( !transportConnectors.contains(connector) ) {
            transportConnectors.add(connector);
            context.registerConnector(connector.getServerChannel().getUrl(), connector);
        }
    }

    public void removeConnector(BrokerConnector connector) {
        transportConnectors.remove(connector);
        context.deregisterConnector(connector.getServerChannel().getUrl());
    }


    public void addConnector(String bindAddress) throws JMSException {
        addConnector(bindAddress, new DefaultWireFormat());
    }

    public void addConnector(String bindAddress, WireFormat wireFormat) throws JMSException {
        addConnector(new BrokerConnectorImpl(this, bindAddress, wireFormat));
    }

    public void addConnector(TransportServerChannel transportConnector) {
        addConnector(new BrokerConnectorImpl(this, transportConnector));
    }

    public List getNetworkConnectors() {
        return networkConnectors;
    }

    public void setNetworkConnectors(List networkConnectors) {
        this.networkConnectors = new CopyOnWriteArrayList(networkConnectors);
    }

    public NetworkConnector addNetworkConnector(String uri) throws JMSException {
        NetworkConnector connector = addNetworkConnector();
        connector.addNetworkChannel(uri);
        return connector;
    }

    public NetworkConnector addNetworkConnector() {
        NetworkConnector connector = new NetworkConnector(this);
        addNetworkConnector(connector);
        return connector;
    }

    public void addNetworkConnector(NetworkConnector connector) {
        networkConnectors.add(connector);
    }

    public void removeNetworkConnector(NetworkConnector connector) {
        networkConnectors.remove(connector);
    }


    public Broker getBroker() {
        return broker;
    }

    public PersistenceAdapter getPersistenceAdapter() {
        return broker != null ? broker.getPersistenceAdapter() : null;
    }

    public void setPersistenceAdapter(PersistenceAdapter persistenceAdapter) {
        checkBrokerSet();
        broker.setPersistenceAdapter(persistenceAdapter);
    }

    public DiscoveryAgent getDiscoveryAgent() {
        return discoveryAgent;
    }

    public void setDiscoveryAgent(DiscoveryAgent discoveryAgent) {
        this.discoveryAgent = discoveryAgent;
    }

    public SecurityAdapter getSecurityAdapter() {
        return broker != null ? broker.getSecurityAdapter() : null;
    }

    public void setSecurityAdapter(SecurityAdapter securityAdapter) {
        checkBrokerSet();
        broker.setSecurityAdapter(securityAdapter);
    }

    public RedeliveryPolicy getRedeliveryPolicy() {
        return broker != null ? broker.getRedeliveryPolicy() : null;
    }

    public void setRedeliveryPolicy(RedeliveryPolicy redeliveryPolicy) {
        checkBrokerSet();
        broker.setRedeliveryPolicy(redeliveryPolicy);
    }

    // Implementation methods
    //-------------------------------------------------------------------------

    protected void checkBrokerSet() {
        if (broker == null) {
            throw new IllegalStateException("Cannot set this property as we don't have a broker yet");
        }
    }

    protected Map createLocalDiscoveryDetails() {
        Map map = new HashMap();
        map.put("brokerName", getLocalBrokerName());
        map.put("connectURL", getLocalConnectionURL());
        return map;
    }

    protected String getLocalBrokerName() {
        return getBroker().getBrokerName();
    }

    protected String getLocalConnectionURL() {
        StringBuffer buffer = new StringBuffer("reliable:");
        List list = getTransportConnectors();
        boolean first = true;
        for (Iterator iter = list.iterator(); iter.hasNext();) {
            BrokerConnector brokerConnector = (BrokerConnector) iter.next();
            TransportServerChannel connector = brokerConnector.getServerChannel();
            String url = connector.getUrl();
            if (first) {
                first = false;
            }
            else {
                buffer.append(",");
            }
            buffer.append(url);
        }
        return buffer.toString();
    }

    protected void addShutdownHook() {
        if(System.getProperty(DISABLE_CLEAN_SHUTDOWN_PROPERTY,"false").equals("true"))
            return;
        
        shutdownHook = new Thread("ActiveMQ ShutdownHook") {
            public void run() {
                containerShutdown();
            }
        };
        Runtime.getRuntime().addShutdownHook(shutdownHook);
    }

    /**
     * Causes a clean shutdown of the container when the VM is being shut down
     */
    protected void containerShutdown() {
        try {
            stop();
        }
        catch (JMSException e) {
            Exception linkedException = e.getLinkedException();
            if (linkedException != null) {
                if (useLoggingForShutdownErrors) {
                    log.error("Failed to shut down: " + e + ". Reason: " + linkedException, linkedException);
                }
                else {
                    System.err.println("Failed to shut down: " + e + ". Reason: " + linkedException);
                }
            }
            else {
                if (useLoggingForShutdownErrors) {
                    log.error("Failed to shut down: " + e);
                }
                else {
                    System.err.println("Failed to shut down: " + e);
                }
            }
            if (!useLoggingForShutdownErrors) {
                e.printStackTrace(System.err);
            }
        }
        catch (Exception e) {
            if (useLoggingForShutdownErrors) {
                log.error("Failed to shut down: " + e, e);
            }
            else {
                System.err.println("Failed to shut down: " + e);
                e.printStackTrace(System.err);
            }
        }
    }
}