/**
 * JaDOrT: JASMINe Deployment Orchestration Tool
 * Copyright (C) 2008-2009 Bull S.A.S.
 * Copyright (C) 2008-2009 France Telecom R&D
 * Contact: jasmine@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id: AbstractJMXAction.java 3643 2009-05-07 11:28:30Z alitokmen $
 * --------------------------------------------------------------------------
 */
package org.ow2.jasmine.jadort.service.action;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketTimeoutException;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

import javax.management.MBeanServerConnection;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

/**
 * Action for any JMX-based action.
 * 
 * @author Malek Chahine
 * @author S. Ali Tokmen
 */
public abstract class AbstractJMXAction extends AbstractAction {

    /**
     * Timeout for establishing JMX connections, in seconds.<br/>
     * <br/>
     * If 0 or negative, no timeout is ever used.
     */
    public static final long JMX_CONNECT_TIMEOUT = -1;

    /**
     * Timeout for checking JMX connections, in seconds.<br/>
     * <br/>
     * If 0 or negative, no timeout is ever used.
     */
    public static final long JMX_CHECK_TIMEOUT = -1;

    protected MBeanServerConnection mbscnx = null;

    protected JMXConnector connector = null;

    /**
     * Establish a JMX connection, either with a timeout or not.
     * 
     * @param url URL to connect to.
     * @param env JMX environment.
     * @throws Exception If any exception occurs.
     */
    protected void establishJMXConnection(final JMXServiceURL url, final Map<String, Object> env) throws Exception {
        this.connector = null;
        this.mbscnx = null;

        if (AbstractJMXAction.JMX_CONNECT_TIMEOUT > 0) {
            final BlockingQueue<Object> mailbox = new ArrayBlockingQueue<Object>(1);

            ExecutorService executor = Executors.newSingleThreadExecutor(AbstractJMXAction.daemonThreadFactory);
            executor.submit(new Runnable() {
                public void run() {
                    try {
                        JMXConnector connector = JMXConnectorFactory.connect(url);
                        if (!mailbox.offer(connector)) {
                            connector.close();
                        }
                    } catch (Exception e) {
                        mailbox.offer(e);
                    }
                }
            });
            Object result;
            try {
                result = mailbox.poll(AbstractJMXAction.JMX_CONNECT_TIMEOUT, TimeUnit.SECONDS);
                if (result == null) {
                    if (!mailbox.offer("")) {
                        result = mailbox.take();
                    }
                }
            } catch (InterruptedException e) {
                throw AbstractJMXAction.initCause(new InterruptedIOException(e.getMessage()), e);
            } finally {
                executor.shutdown();
            }
            if (result == null) {
                throw new SocketTimeoutException("JMX connection to URL \"" + url + "\" timed out");
            }
            if (result instanceof JMXConnector) {
                this.connector = (JMXConnector) result;
                this.mbscnx = this.connector.getMBeanServerConnection();
            } else {
                throw (Exception) result;
            }
        } else {
            this.connector = JMXConnectorFactory.connect(url);
            this.mbscnx = this.connector.getMBeanServerConnection();
        }
    }

    /**
     * @throws Exception If JMX connection is down.
     */
    private void checkJMXConnectivity() throws Exception {
        if (AbstractJMXAction.JMX_CHECK_TIMEOUT > 0) {
            final BlockingQueue<Object> mailbox = new ArrayBlockingQueue<Object>(1);
            final MBeanServerConnection mbscnx = this.mbscnx;

            ExecutorService executor = Executors.newSingleThreadExecutor(AbstractJMXAction.daemonThreadFactory);
            executor.submit(new Runnable() {
                public void run() {
                    try {
                        Integer mBeanCount = mbscnx.getMBeanCount();
                        mailbox.offer(mBeanCount);
                    } catch (Exception e) {
                        mailbox.offer(e);
                    }
                }
            });
            Object result;
            try {
                result = mailbox.poll(AbstractJMXAction.JMX_CHECK_TIMEOUT, TimeUnit.SECONDS);
                if (result == null) {
                    if (!mailbox.offer("")) {
                        result = mailbox.take();
                    }
                }
            } catch (InterruptedException e) {
                throw AbstractJMXAction.initCause(new InterruptedIOException(e.getMessage()), e);
            } finally {
                executor.shutdown();
            }
            if (result == null) {
                throw new SocketTimeoutException("Check JMX connection timed out");
            }
            if (result instanceof Exception) {
                throw (Exception) result;
            }
        } else {
            this.mbscnx.getMBeanCount();
        }
    }

    /**
     * Checks if the JMX connection is still alive and reconnects if necessary.
     * 
     * @throws Exception If any error occurs when reconnecting.
     */
    public synchronized void checkJMXConnection() throws Exception {
        if (this.mbscnx != null) {
            try {
                this.checkJMXConnectivity();
            } catch (IOException e) {
                try {
                    // Connection is down, deallocate all associated resources
                    JMXConnector connector = this.connector;
                    this.connector = null;
                    this.mbscnx = null;

                    connector.close();
                    System.gc();
                } catch (Exception ignored) {
                    // Ignored
                }

                this.appendToLog("Connection dropped, reconnecting to JMX server");
            }
        }

        if (this.mbscnx == null) {
            this.connectViaJMX();
        }
    }

    /**
     * Connects via JMX. This is expected to call
     * {@link AbstractJMXAction#establishJMXConnection(JMXServiceURL, Map)} at
     * some point.
     * 
     * @throws Exception If any exception occurs.
     */
    protected abstract void connectViaJMX() throws Exception;

    /**
     * Disconnects all JMX-related resources.
     */
    public void disconnectJMX() {
        if (this.connector != null) {
            try {
                this.connector.close();
            } catch (IOException ignored) {
                // Ignored
            }
            this.connector = null;
            this.mbscnx = null;
            System.gc();
        }
    }

    private static <T extends Exception> T initCause(final T wrapper, final Exception wrapped) {
        wrapper.initCause(wrapped);
        return wrapper;
    }

    private static class DaemonThreadFactory implements ThreadFactory {
        public Thread newThread(final Runnable r) {
            Thread t = Executors.defaultThreadFactory().newThread(r);
            t.setDaemon(true);
            return t;
        }
    }

    private static final ThreadFactory daemonThreadFactory = new DaemonThreadFactory();

}
