/*
 * Copyright (c) 2007, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.tools.visualvm.jmx.impl;

import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.visualvm.application.Application;
import com.sun.tools.visualvm.application.jvm.HeapHistogram;
import com.sun.tools.visualvm.application.type.ApplicationType;
import com.sun.tools.visualvm.core.datasource.Storage;
import com.sun.tools.visualvm.core.datasource.descriptor.DataSourceDescriptor;
import com.sun.tools.visualvm.core.datasupport.DataRemovedListener;
import com.sun.tools.visualvm.core.datasupport.Stateful;
import com.sun.tools.visualvm.host.Host;
import com.sun.tools.visualvm.jmx.EnvironmentProvider;
import com.sun.tools.visualvm.tools.attach.AttachModelFactory;
import com.sun.tools.visualvm.tools.jmx.CachedMBeanServerConnection;
import com.sun.tools.visualvm.tools.jmx.CachedMBeanServerConnectionFactory;
import com.sun.tools.visualvm.tools.jmx.JmxModel;
import com.sun.tools.visualvm.tools.jmx.JmxModelFactory;
import com.sun.tools.visualvm.tools.jvmstat.JvmJvmstatModel;
import com.sun.tools.visualvm.tools.jvmstat.JvmJvmstatModelFactory;
import com.sun.tools.visualvm.tools.jvmstat.JvmstatModel;
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.management.ManagementFactory;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.management.ListenerNotFoundException;
import javax.management.MBeanServerConnection;
import javax.management.Notification;
import javax.management.NotificationListener;
import javax.management.remote.JMXConnectionNotification;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import javax.rmi.ssl.SslRMIClientSocketFactory;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.awt.Mnemonics;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import sun.tools.attach.HotSpotVirtualMachine;

/**
 * This class encapsulates the JMX functionality of the target Java application.
 *
 * Call {@link JmxModelFactory#getJmxModelFor()} to get an instance of the
 * {@link JmxModel} class.
 *
 * Usually this class will be used as follows:
 *
 * <pre>
 * JmxModel jmx = JmxModelFactory.getJmxModelFor(application);
 * MBeanServerConnection mbsc = jmx.getMBeanServerConnection();
 * if (mbsc != null) {
 *    // Invoke JMX operations...
 * }
 * </pre>
 *
 * Several factory methods are available in {@link CachedMBeanServerConnectionFactory}
 * that can be used to work with a {@link CachedMBeanServerConnection} instead of a
 * plain {@link MBeanServerConnection}.
 *
 * In case the JMX connection is not established yet, you could register
 * a listener on the {@code JmxModel} for ConnectionState property changes.
 * The JmxModel notifies any PropertyChangeListeners about the ConnectionState
 * property change to CONNECTED and DISCONNECTED. The JmxModel instance will
 * be the source for any generated events.
 *
 * Polling for the ConnectionState is also possible by calling
 * {@link JmxModel#getConnectionState()}.
 *
 * @author Luis-Miguel Alventosa
 * @author Jiri Sedlacek
 * @author Tomas Hurka
 */
class JmxModelImpl extends JmxModel {
//    private static final String PROPERTY_USERNAME = "prop_username";    // NOI18N
//    private static final String PROPERTY_PASSWORD = "prop_password";    // NOI18N
    private final static Logger LOGGER = Logger.getLogger(JmxModelImpl.class.getName());
    private ProxyClient client;
    private ApplicationRemovedListener removedListener;
    private ApplicationAvailabilityListener availabilityListener;
    private JmxSupport jmxSupport;
    private final Object jmxSupportLock = new Object();
    
    /**
     * Creates an instance of {@code JmxModel} for a {@link JvmstatApplication}.
     *
     * @param application the {@link JvmstatApplication}.
     */
    public JmxModelImpl(Application application, JvmstatModel jvmstat) {
        try {
            JvmJvmstatModel jvmstatModel = JvmJvmstatModelFactory.getJvmstatModelFor(application);
            // Create ProxyClient (i.e. create the JMX connection to the JMX agent)
            ProxyClient proxyClient = null;
            if (Application.CURRENT_APPLICATION.equals(application)) {
                // Monitor self
                proxyClient = new ProxyClient(this);
            } else if (application.isLocalApplication()) {
                // Create a ProxyClient from local pid
                String connectorAddress = jvmstat.findByName("sun.management.JMXConnectorServer.address"); // NOI18N
                String javaHome = jvmstat.findByName("java.property.java.home");    // NOI18N
                LocalVirtualMachine lvm = new LocalVirtualMachine(application.getPid(), AttachModelFactory.getAttachFor(application) != null, connectorAddress, javaHome);
                if (!lvm.isManageable()) {
                    if (lvm.isAttachable()) {
                        proxyClient = new ProxyClient(this, lvm);
                    } else {
                        if (LOGGER.isLoggable(Level.WARNING)) {
                            LOGGER.warning("The JMX management agent " +    // NOI18N
                                    "cannot be enabled in this application (pid " + // NOI18N
                                    application.getPid() + ")");  // NOI18N
                        }
                    }
                } else {
                    proxyClient = new ProxyClient(this, lvm);
                }
            }
            if (proxyClient == null) {
                // Create a ProxyClient for the remote out-of-the-box
                // JMX management agent using the port and security
                // related information retrieved through jvmstat.
                List<String> urls = jvmstat.findByPattern("sun.management.JMXConnectorServer.[0-9]+.remoteAddress"); // NOI18N
                if (urls.size() != 0) {
                    List<String> auths = jvmstat.findByPattern("sun.management.JMXConnectorServer.[0-9]+.authenticate"); // NOI18N
                    proxyClient = new ProxyClient(this, urls.get(0));
                    if ("true".equals(auths.get(0))) {  // NOI18N
                        supplyCredentials(application, proxyClient);
                    }
                } else {
                    // Create a ProxyClient for the remote out-of-the-box
                    // JMX management agent using the port specified in
                    // the -Dcom.sun.management.jmxremote.port=<port>
                    // system property
                    String jvmArgs = jvmstatModel.getJvmArgs();
                    StringTokenizer st = new StringTokenizer(jvmArgs);
                    int port = -1;
                    boolean authenticate = false;
                    while (st.hasMoreTokens()) {
                        String token = st.nextToken();
                        if (token.startsWith("-Dcom.sun.management.jmxremote.port=")) { // NOI18N
                            port = Integer.parseInt(token.substring(token.indexOf("=") + 1)); // NOI18N
                        } else if (token.equals("-Dcom.sun.management.jmxremote.authenticate=true")) { // NOI18N
                            authenticate = true;
                        }
                    }
                    if (port != -1) {
                        proxyClient = new ProxyClient(this, application.getHost(), port);
                        if (authenticate) {
                            supplyCredentials(application, proxyClient);
                        }
                    }
                }
            }
            if (proxyClient != null) {
                client = proxyClient;
                removedListener = new ApplicationRemovedListener();
                availabilityListener = new ApplicationAvailabilityListener();
                connect(application, proxyClient, removedListener, availabilityListener);
            }
        } catch (Exception e) {
            LOGGER.throwing(JmxModelImpl.class.getName(), "<init>", e); // NOI18N
            client = null;
        }
    }

    /**
     * Creates an instance of {@code JmxModel} for a {@link JmxApplication}.
     *
     * @param application the {@link JmxApplication}.
     */
    public JmxModelImpl(JmxApplication application) {
        try {
            final ProxyClient proxyClient = new ProxyClient(this, application);
            client = proxyClient;
            removedListener = new ApplicationRemovedListener();
            availabilityListener = new ApplicationAvailabilityListener();
            connect(application, proxyClient, removedListener, availabilityListener);
        } catch (Exception e) {
            LOGGER.throwing(JmxModelImpl.class.getName(), "<init>", e); // NOI18N
            client = null;
        }
    }

    private void connect(Application application, ProxyClient proxyClient,
            ApplicationRemovedListener listener, ApplicationAvailabilityListener aListener) {
        while (true) {
            try {
                proxyClient.connect();
                application.notifyWhenRemoved(listener);
                application.addPropertyChangeListener(Stateful.PROPERTY_STATE, aListener);
                break;
            } catch (SecurityException e) {
                LOGGER.log(Level.INFO, "connect", e);   // NOI18N
                if (proxyClient.hasSSLStubCheck()) {
                    Storage storage = application.getStorage();
                    String noSSLProp = JmxApplicationProvider.PROPERTY_RETRY_WITHOUT_SSL;
                    String noSSL = storage.getCustomProperty(noSSLProp);
                    if (noSSL != null && Boolean.parseBoolean(noSSL)) { // NOI18N
                        proxyClient.setInsecure();
                        continue;
                    } else {
                        String conn = storage.getCustomProperty(DataSourceDescriptor.PROPERTY_NAME);
                        if (conn == null) conn = storage.getCustomProperty(ApplicationType.PROPERTY_SUGGESTED_NAME);
                        if (conn == null) conn = proxyClient.getUrl().toString();
                        String msg = NbBundle.getMessage(JmxModelImpl.class, "MSG_Insecure_SSL", conn);  // NOI18N
                        String title = NbBundle.getMessage(JmxModelImpl.class, "Title_Insecure_SSL");   // NOI18N
                        String retry = NbBundle.getMessage(JmxModelImpl.class, "Retry_Insecure_SSL");   // NOI18N
                        JLabel l = new JLabel(msg);
                        JCheckBox c = new JCheckBox();
                        Mnemonics.setLocalizedText(c, retry);
                        c.setSelected(noSSL == null);
                        JPanel p = new JPanel(new BorderLayout(0, 20));
                        p.add(l, BorderLayout.CENTER);
                        p.add(c, BorderLayout.SOUTH);
                        NotifyDescriptor dd = new NotifyDescriptor.Confirmation(p, title, NotifyDescriptor.YES_NO_OPTION);
                        if (DialogDisplayer.getDefault().notify(dd) == NotifyDescriptor.YES_OPTION) {
                            storage.setCustomProperty(noSSLProp, Boolean.toString(c.isSelected()));
                            proxyClient.setInsecure();
                            continue;
                        } else {
                            break;
                        }
                    }
                }
                if (supplyCredentials(application, proxyClient) == null) {
                    break;
                }
            }
        }
    }

    /**
     *  Ask for security credentials.
     */
    private CredentialsConfigurator supplyCredentials(
            Application application, ProxyClient proxyClient) {
        String displayName = application.getStorage().getCustomProperty(DataSourceDescriptor.PROPERTY_NAME);
        if (displayName == null) displayName = proxyClient.getUrl().toString();
        CredentialsConfigurator jsc =
                CredentialsConfigurator.supplyCredentials(displayName);
        if (jsc != null)
            proxyClient.setCredentials(jsc.getUsername(), jsc.getPassword());
        return jsc;
    }

    /**
     * Returns the current connection state.
     *
     * @return the current connection state.
     */
    public ConnectionState getConnectionState() {
        if (client != null) {
            return client.getConnectionState();
        }
        return ConnectionState.DISCONNECTED;
    }

    /**
     * Returns the {@link MBeanServerConnection} for the connection to
     * an application. The returned {@code MBeanServerConnection} object
     * becomes invalid when the connection state is changed to the
     * {@link ConnectionState#DISCONNECTED DISCONNECTED} state.
     *
     * @return the {@code MBeanServerConnection} for the
     * connection to an application. It returns {@code null}
     * if the JMX connection couldn't be established.
     */
    public MBeanServerConnection getMBeanServerConnection() {
        if (client != null) {
            return client.getMBeanServerConnection();
        }
        return null;
    }

    /**
     * Returns the {@link JMXServiceURL} associated to this (@code JmxModel}.
     *
     * @return the {@link JMXServiceURL} associated to this (@code JmxModel}.
     */
    public JMXServiceURL getJMXServiceURL() {
        if (client != null) {
            return client.getUrl();
        }
        return null;        
    }

    public Properties getSystemProperties() {
        return getJmxSupport().getSystemProperties();
    }

    public boolean takeHeapDump(String fileName) {
        return getJmxSupport().takeHeapDump(fileName);
    }

    public String takeThreadDump() {
        return getJmxSupport().takeThreadDump();
    }

    public String takeThreadDump(long[] threadIds) {
        return getJmxSupport().takeThreadDump(threadIds);
    }    

    public HeapHistogram takeHeapHistogram() {
        return getJmxSupport().takeHeapHistogram();
    }

    public String getFlagValue(String name) {
        return getJmxSupport().getFlagValue(name);
    }

    public void setFlagValue(String name, String value) {
        getJmxSupport().setFlagValue(name,value);
    }

    public boolean isTakeHeapDumpSupported() {
        JmxSupport support = getJmxSupport();
        return support.getHotSpotDiagnostic() != null && !support.isReadOnlyConnection();
    }
    
    public boolean isTakeThreadDumpSupported() {
        JmxSupport support = getJmxSupport();
        return support.getThreadBean() != null && !support.isReadOnlyConnection();
    }

    private JmxSupport getJmxSupport() {
        synchronized (jmxSupportLock) {
            if (jmxSupport == null) {
                jmxSupport = new JmxSupport(this);
            }
            return jmxSupport;
        }
    }

    /**
     * Disconnect from JMX agent when the application is removed.
     */
    private class ApplicationRemovedListener implements DataRemovedListener<Application> {

        public void dataRemoved(Application application) {
            RequestProcessor.getDefault().post(new Runnable() {
                public void run() {
                    client.markAsDead();
                    removedListener = null;
                }
            });
        }
    }
    
    private class ApplicationAvailabilityListener implements PropertyChangeListener {

        public void propertyChange(PropertyChangeEvent evt) {
            if (!evt.getNewValue().equals(Stateful.STATE_AVAILABLE)) {
                ((Application)evt.getSource()).removePropertyChangeListener(
                        Stateful.PROPERTY_STATE, this);
                client.disconnectImpl(false);
                availabilityListener = null;
            }
        }
    }
    
    private static class ProxyClient implements NotificationListener {

        private static final int MODE_SELF = 0;
        private static final int MODE_LOCAL = 1;
        private static final int MODE_GENERIC = 2;

        private final int mode;

        private ConnectionState connectionState = ConnectionState.DISCONNECTED;
        private volatile boolean isDead = true;
        private String userName = null;
        private String password = null;
        private LocalVirtualMachine lvm;
        private JMXServiceURL jmxUrl = null;
        private Application app;
        private EnvironmentProvider envProvider = null;
        private MBeanServerConnection conn = null;
        private JMXConnector jmxc = null;
        private static final SslRMIClientSocketFactory sslRMIClientSocketFactory =
                new SslRMIClientSocketFactory();
        private final JmxModelImpl model;
        private boolean insecure;   // do not check for SSL-protected RMI registry
        private boolean checkSSLStub;


        // Self attach
        public ProxyClient(JmxModelImpl model) throws IOException {
            this.mode = MODE_SELF;
            this.model = model;
        }

        // Local attach
        public ProxyClient(JmxModelImpl model, LocalVirtualMachine lvm) throws IOException {
            this.mode = MODE_LOCAL;
            this.model = model;
            this.lvm = lvm;
        }

        // Generic attach - host/port
        public ProxyClient(JmxModelImpl model, Host host, int port) throws IOException {
            this(model, new JMXServiceURL("rmi", "", 0, createUrl(host.getHostName(), // NOI18N
                                          port)), null, null);
        }

        // Generic attach - connection string
        public ProxyClient(JmxModelImpl model, String url) throws IOException {
            this(model, new JMXServiceURL(url), null, null);
        }

        // Generic attach - JmxApplication
        public ProxyClient(JmxModelImpl model, JmxApplication jmxApp) throws IOException {
            this(model, jmxApp.getJMXServiceURL(), jmxApp, jmxApp.getEnvironmentProvider());
        }

        // Generic attach - JMXServiceURL
        private ProxyClient(JmxModelImpl model, JMXServiceURL url, Application app,
                           EnvironmentProvider envProvider) throws IOException {
            this.mode = MODE_GENERIC;
            this.model = model;
            this.jmxUrl = url;
            this.app = app;
            this.envProvider = envProvider;
        }

        public void setCredentials(String userName, String password) {
            this.userName = userName;
            this.password = password;
        }

        boolean hasSSLStubCheck() {
            return checkSSLStub;
        }
        
        void setInsecure() {
            insecure = true;
        }
        
        boolean isInsecure() {
            return insecure;
        }
        
        private static String createUrl(String hostName, int port) {
            return "/jndi/rmi://" + hostName + ":" + port + "/jmxrmi";  // NOI18N
        }

        private void setConnectionState(ConnectionState state) {
            ConnectionState oldState = connectionState;
            connectionState = state;
            model.propertyChangeSupport.firePropertyChange(
                    JmxModelImpl.CONNECTION_STATE_PROPERTY, oldState, state);
        }

        public ConnectionState getConnectionState() {
            return connectionState;
        }

        void connect() {
            setConnectionState(ConnectionState.CONNECTING);
            try {
                tryConnect();
                setConnectionState(ConnectionState.CONNECTED);
            } catch (SecurityException e) {
                setConnectionState(ConnectionState.DISCONNECTED);
                throw e;
            } catch (Exception e) {
                setConnectionState(ConnectionState.DISCONNECTED);
                // Workaround for GlassFish's LoginException class not found
                if (e.toString().contains("com.sun.enterprise.security.LoginException")) {  // NOI18N
                    throw new SecurityException("Authentication failed! Invalid username or password"); // NOI18N
                }
                if (LOGGER.isLoggable(Level.INFO))  {
                    // Try to provide info on the target
                    //    Use PID when attach was used to connect,
                    //    Use JMXServiceURL otherwise...
                    final String param = 
                            (lvm != null) ? String.valueOf(lvm.vmid())
                            : ((jmxUrl != null) ? jmxUrl.toString() : ""); // NOI18N
                    LOGGER.log(Level.INFO, "connect(" + param + ")", e); // NOI18N
                }
            }
        }

        private void tryConnect() throws IOException {
            if (mode == MODE_SELF) {
                jmxc = null;
                conn = ManagementFactory.getPlatformMBeanServer();
            } else {
                if (mode == MODE_LOCAL) {
                    if (!lvm.isManageable()) {
                        lvm.startManagementAgent();
                        if (!lvm.isManageable()) {
                            // FIXME: what to throw
                            throw new IOException(lvm + " not manageable"); // NOI18N
                        }
                    }
                    if (jmxUrl == null) {
                        jmxUrl = new JMXServiceURL(lvm.connectorAddress());
                    }
                }

                Map<String, Object> env = new HashMap();
                if (envProvider != null)
                    env.putAll(envProvider.getEnvironment(app, app.getStorage()));
                if (userName != null || password != null)
                    env.put(JMXConnector.CREDENTIALS,
                            new String[] { userName, password });

                if (!insecure && mode != MODE_LOCAL && env.get(JMXConnector.CREDENTIALS) != null) {
                    env.put("jmx.remote.x.check.stub", "true");     // NOI18N
                    checkSSLStub = true;
                } else {
                    checkSSLStub = false;
                }

                jmxc = JMXConnectorFactory.newJMXConnector(jmxUrl, env);
                jmxc.addConnectionNotificationListener(this, null, null);
                try {
                    jmxc.connect(env);
                } catch (java.io.IOException e) {
                    // Likely a SSL-protected RMI registry
                    if ("rmi".equals(jmxUrl.getProtocol())) { // NOI18N
                        env.put("com.sun.jndi.rmi.factory.socket", sslRMIClientSocketFactory); // NOI18N
                        jmxc.connect(env);
                    } else {
                        throw e;
                    }
                }
                
                MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();
                conn = Checker.newChecker(this, mbsc);
            }
            isDead = false;
        }

        public MBeanServerConnection getMBeanServerConnection() {
            return conn;
        }

        public JMXServiceURL getUrl() {
            return jmxUrl;
        }
        
        public void disconnect() {
            disconnectImpl(true);
        }

        private synchronized void disconnectImpl(boolean sendClose) {
            // Close MBeanServer connection
            if (jmxc != null) {
                try {
                    jmxc.removeConnectionNotificationListener(this);
                    if (sendClose) jmxc.close();
                } catch (IOException e) {
                    // Ignore...
                } catch (ListenerNotFoundException e) {
                     LOGGER.log(Level.INFO, "disconnectImpl", e); // NOI18N                   
                }
                jmxc = null;
            }
            // Set connection state to DISCONNECTED
            if (!isDead) {
                isDead = true;
                setConnectionState(ConnectionState.DISCONNECTED);
            }
        }

        public synchronized void markAsDead() {
            disconnect();
        }

        public boolean isDead() {
            return isDead;
        }

        boolean isConnected() {
            return !isDead();
        }

        public void handleNotification(Notification n, Object hb) {
            if (n instanceof JMXConnectionNotification) {
                if (JMXConnectionNotification.FAILED.equals(n.getType()) ||
                        JMXConnectionNotification.CLOSED.equals(n.getType())) {
                    markAsDead();
                }
            }
        }
    }

    private static class Checker {

        private Checker() {
        }

        public static MBeanServerConnection newChecker(
                ProxyClient client, MBeanServerConnection mbsc) {
            final InvocationHandler ih = new CheckerInvocationHandler(mbsc);
            return (MBeanServerConnection) Proxy.newProxyInstance(
                    Checker.class.getClassLoader(),
                    new Class[]{MBeanServerConnection.class},
                    ih);
        }
    }

    private static class CheckerInvocationHandler implements InvocationHandler {

        private final MBeanServerConnection conn;

        CheckerInvocationHandler(MBeanServerConnection conn) {
            this.conn = conn;
        }

        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
            if (LOGGER.isLoggable(Level.FINE)) {
                // Check if MBeanServerConnection call is performed on EDT
                if (EventQueue.isDispatchThread()) {
                    Throwable thrwbl = new Throwable();

                    LOGGER.log(Level.FINE, createTracedMessage("MBeanServerConnection call " +  // NOI18N
                            "performed on Event Dispatch Thread!", thrwbl));    // NOI18N
                }
            }
            // Invoke MBeanServerConnection call
            try {
                return method.invoke(conn, args);
            } catch (InvocationTargetException e) {
                throw e.getCause();
            }
        }

        private String createTracedMessage(String message, Throwable thrwbl) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            PrintWriter pw = new PrintWriter(baos);
            pw.println(message);
            thrwbl.printStackTrace(pw);
            pw.flush();
            return baos.toString();
        }
    }

    private static class LocalVirtualMachine {

        private static final String ENABLE_LOCAL_AGENT_JCMD = "ManagementAgent.start_local";  // NOI18N
        
        private int vmid;
        private boolean isAttachSupported;
        private String javaHome;
        
        // @GuardedBy this
        volatile private String address;

        public LocalVirtualMachine(int vmid, boolean canAttach, String connectorAddress, String home) {
            this.vmid = vmid;
            this.address = connectorAddress;
            this.isAttachSupported = canAttach;
            this.javaHome = home;
        }

        public int vmid() {
            return vmid;
        }

        public synchronized boolean isManageable() {
            return (address != null);
        }

        public boolean isAttachable() {
            return isAttachSupported;
        }

        public synchronized void startManagementAgent() throws IOException {
            if (address != null) {
                // already started
                return;
            }

            if (!isAttachable()) {
                throw new IOException("This virtual machine \"" + vmid +    // NOI18N
                        "\" does not support dynamic attach."); // NOI18N
            }

            loadManagementAgent();
            // fails to load or start the management agent
            if (address == null) {
                // should never reach here
                throw new IOException("Fails to find connector address");   // NOI18N
            }
        }

        public synchronized String connectorAddress() {
            // return null if not available or no JMX agent
            return address;
        }
        private static final String LOCAL_CONNECTOR_ADDRESS_PROP =
                "com.sun.management.jmxremote.localConnectorAddress";   // NOI18N

        private synchronized void loadManagementAgent() throws IOException {
            VirtualMachine vm = null;
            String name = String.valueOf(vmid);
            try {
                vm = VirtualMachine.attach(name);
            } catch (AttachNotSupportedException x) {
                throw new IOException(x);
            }
            // try to enable local JMX via jcmd command
            if (!loadManagementAgentViaJcmd(vm)) {
                // load the management agent into the target VM
                loadManagementAgentViaJar(vm);
            }

            // get the connector address
            Properties agentProps = vm.getAgentProperties();
            address = (String) agentProps.get(LOCAL_CONNECTOR_ADDRESS_PROP);

            vm.detach();
        }

        private void loadManagementAgentViaJar(VirtualMachine vm) throws IOException {
            // Normally in ${java.home}/jre/lib/management-agent.jar but might
            // be in ${java.home}/lib in build environments.

            String agent = javaHome + File.separator + "jre" + File.separator + // NOI18N
                    "lib" + File.separator + "management-agent.jar";    // NOI18N
            File f = new File(agent);
            if (!f.exists()) {
                agent = javaHome + File.separator + "lib" + File.separator +    // NOI18N
                        "management-agent.jar"; // NOI18N
                f = new File(agent);
                if (!f.exists()) {
                    throw new IOException("Management agent not found");    // NOI18N
                }
            }

            agent = f.getCanonicalPath();
            try {
                vm.loadAgent(agent, "com.sun.management.jmxremote");    // NOI18N
            } catch (AgentLoadException x) {
                throw new IOException(x);
            } catch (AgentInitializationException x) {
                throw new IOException(x);
            }
        }

        private boolean loadManagementAgentViaJcmd(VirtualMachine vm) throws IOException {
            if (vm instanceof HotSpotVirtualMachine) {
                HotSpotVirtualMachine hsvm = (HotSpotVirtualMachine) vm;
                InputStream in = null;
                try {
                    byte b[] = new byte[256];
                    int n;
                    
                    in = hsvm.executeJCmd(ENABLE_LOCAL_AGENT_JCMD);
                    do {
                        n = in.read(b);
                        if (n > 0) {
                            String s = new String(b, 0, n, "UTF-8");    // NOI18N
                            System.out.print(s);
                        }
                    } while (n > 0);
                    return true;
                } catch (IOException ex) {
                    LOGGER.log(Level.INFO, "jcmd command \""+ENABLE_LOCAL_AGENT_JCMD+"\" for PID "+vmid+" failed", ex); // NOI18N
                } finally {
                    if (in != null) {
                        in.close();
                    }
                }
            }
            return false;
        }
    }
}
