/* * Copyright (c) 2007, 2011, 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.visualvm.jmx.CredentialsProvider; import com.sun.tools.visualvm.jmx.EnvironmentProvider; import com.sun.tools.visualvm.application.jvm.JvmFactory; import com.sun.tools.visualvm.application.type.ApplicationType; import com.sun.tools.visualvm.core.datasource.DataSourceRepository; import com.sun.tools.visualvm.core.datasource.Storage; import com.sun.tools.visualvm.core.datasupport.DataChangeEvent; import com.sun.tools.visualvm.host.Host; import com.sun.tools.visualvm.host.HostsSupport; import com.sun.tools.visualvm.core.datasource.descriptor.DataSourceDescriptor; import com.sun.tools.visualvm.core.datasource.descriptor.DataSourceDescriptorFactory; import com.sun.tools.visualvm.core.datasupport.DataChangeListener; import com.sun.tools.visualvm.core.datasupport.Stateful; import com.sun.tools.visualvm.core.datasupport.Utils; import com.sun.tools.visualvm.core.options.GlobalPreferences; import com.sun.tools.visualvm.jmx.JmxApplicationException; import com.sun.tools.visualvm.jmx.JmxApplicationsSupport; import com.sun.tools.visualvm.tools.jmx.JmxModel; import com.sun.tools.visualvm.tools.jmx.JmxModel.ConnectionState; import com.sun.tools.visualvm.tools.jmx.JmxModelFactory; import java.awt.BorderLayout; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.NetworkInterface; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.management.remote.JMXServiceURL; import javax.swing.BorderFactory; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import org.openide.DialogDisplayer; import org.openide.NotifyDescriptor; import org.openide.awt.Mnemonics; import org.openide.util.NbBundle; import org.openide.util.RequestProcessor; import org.openide.windows.WindowManager; /** * A provider for Applications added as JMX connections. * * @author Jiri Sedlacek * @author Luis-Miguel Alventosa */ public class JmxApplicationProvider { // private static final Logger LOGGER = Logger.getLogger(JmxApplicationProvider.class.getName()); // --- Snapshot format history --------------------------------------------- // // 1.0: initial snapshot version // 1.1: added PROPERTY_ENV_PROVIDER_ID // 1.2: added PROPERTY_RETRY_WITHOUT_SSL // // ------------------------------------------------------------------------- private static final String SNAPSHOT_VERSION = "snapshot_version"; // NOI18N private static final String SNAPSHOT_VERSION_DIVIDER = "."; // NOI18N private static final String CURRENT_SNAPSHOT_VERSION_MAJOR = "1"; // NOI18N private static final String CURRENT_SNAPSHOT_VERSION_MINOR = "2"; // NOI18N private static final String CURRENT_SNAPSHOT_VERSION = CURRENT_SNAPSHOT_VERSION_MAJOR + SNAPSHOT_VERSION_DIVIDER + CURRENT_SNAPSHOT_VERSION_MINOR; public static final String PROPERTY_RETRY_WITHOUT_SSL = "prop_retry_without_ssl"; // NOI18N private static final String PROPERTY_CONNECTION_STRING = "prop_conn_string"; // NOI18N private static final String PROPERTY_HOSTNAME = "prop_conn_hostname"; // NOI18N private static final String PROPERTY_ENV_PROVIDER_ID = "prop_env_provider_id"; // NOI18N private static final String PROPERTIES_FILE = "jmxapplication" + Storage.DEFAULT_PROPERTIES_EXT; // NOI18N static final String JMX_SUFFIX = ".jmx"; // NOI18N private static final String DNSA_KEY = "JMXApplicationProvider_NotifyUnresolved"; // NOI18N private volatile boolean trackingNewHosts; private Map<String, Set<Storage>> persistedApplications = new HashMap<String, Set<Storage>>(); private static boolean isLocalHost(String hostname) throws IOException { InetAddress remoteAddr = InetAddress.getByName(hostname); // Retrieve all the network interfaces on this host. Enumeration<NetworkInterface> nis = NetworkInterface.getNetworkInterfaces(); // Walk through the network interfaces to see // if any of them matches the client's address. // If true, then the client's address is local. while (nis.hasMoreElements()) { NetworkInterface ni = nis.nextElement(); Enumeration<InetAddress> addrs = ni.getInetAddresses(); while (addrs.hasMoreElements()) { InetAddress localAddr = addrs.nextElement(); if (localAddr.equals(remoteAddr)) { return true; } } } return false; } // Resolves existing host based on hostname, JMXServiceURL // or by using the JMXServiceURL to connect to the agent // and retrieve the hostname information. private Host getHost(String hostname, JMXServiceURL url) throws IOException { // Try to compute the Host instance from hostname if (hostname != null) { if (hostname.isEmpty() || isLocalHost(hostname)) { return Host.LOCALHOST; } else { return HostsSupport.getInstance().getOrCreateHost(hostname, false); } } // TODO: Connect to the agent and try to get the hostname. // app = JmxApplication(Host.UNKNOWN_HOST, url, storage); // JmxModelFactory.getJmxModelFor(app); // WARNING: If a hostname could not be found the JMX application // is added under the <Unknown Host> tree node. return Host.UNKNOWN_HOST; } public static String getConnectionString(JmxApplication application) { return application.getStorage().getCustomProperty(PROPERTY_CONNECTION_STRING); } public static String getSuggestedName(String displayName, String connectionString, String username) { // User-provided displayName always first if (displayName != null) return displayName; // Generated name 'connectionString' or 'user@connectionString' if (username == null) username = ""; // NOI18N return (username.isEmpty() ? "" : username + "@") + connectionString; // NOI18N } public JmxApplication createJmxApplication(String connectionString, String displayName, String suggestedName, EnvironmentProvider provider, boolean persistent, boolean allowsInsecure) throws JmxApplicationException { // Initial check if the provided connectionName can be used for resolving the host/application final String normalizedConnectionName = normalizeConnectionName(connectionString); final JMXServiceURL serviceURL; try { serviceURL = getServiceURL(normalizedConnectionName); } catch (MalformedURLException ex) { throw new JmxApplicationException(NbBundle.getMessage(JmxApplicationProvider.class, "MSG_Invalid_JMX_connection", normalizedConnectionName),ex); // NOI18N } String hostName = getHostName(serviceURL); hostName = hostName == null ? "" : hostName; // NOI18N Storage storage = null; if (persistent) { File storageDirectory = Utils.getUniqueFile(JmxApplicationsSupport.getStorageDirectory(), "" + System.currentTimeMillis(), JMX_SUFFIX); // NOI18N Utils.prepareDirectory(storageDirectory); storage = new Storage(storageDirectory, PROPERTIES_FILE); storage.setCustomProperty(SNAPSHOT_VERSION, CURRENT_SNAPSHOT_VERSION); } try { return addJmxApplication(true, serviceURL, normalizedConnectionName, displayName, suggestedName, hostName, provider, storage, Boolean.toString(allowsInsecure)); } catch (JMXException e) { if (storage != null) { File appStorage = storage.getDirectory(); if (appStorage.isDirectory()) Utils.delete(appStorage, true); } throw new JmxApplicationException(e.getMessage(), e.getCause()); } } private JmxApplication addJmxApplication(boolean newApp, JMXServiceURL serviceURL, String connectionName, String displayName, String suggestedName, String hostName, EnvironmentProvider provider, Storage storage, String allowsInsecure) throws JMXException { // Resolve JMXServiceURL, finish if not resolved if (serviceURL == null) { try { serviceURL = getServiceURL(connectionName); } catch (MalformedURLException ex) { throw new JMXException(true, NbBundle.getMessage(JmxApplicationProvider.class, "MSG_Invalid_JMX_connection", connectionName), ex); // NOI18N } } // Resolve existing Host or create new Host, finish if Host cannot be resolved Set<Host> hosts = DataSourceRepository.sharedInstance().getDataSources(Host.class); Host host = null; try { host = getHost(hostName, serviceURL); } catch (Exception e) { cleanupCreatedHost(hosts, host); throw new JMXException(false, NbBundle.getMessage(JmxApplicationProvider.class, "MSG_Cannot_resolve_host", hostName), e); // NOI18N } // Update persistent storage and EnvironmentProvider if (storage != null) { if (newApp) { storage.setCustomProperty(PROPERTY_HOSTNAME, host.getHostName()); if (provider != null) { storage.setCustomProperty(PROPERTY_ENV_PROVIDER_ID, provider.getId()); provider.saveEnvironment(storage); } } else { if (provider != null) provider.loadEnvironment(storage); } } // Create the JmxApplication final JmxApplication application = new JmxApplication(host, serviceURL, provider, storage); // Update display name and new EnvironmentProvider for non-persistent storage if (newApp) { Storage s = application.getStorage(); String[] keys = new String[] { PROPERTY_CONNECTION_STRING, displayName != null ? DataSourceDescriptor.PROPERTY_NAME : ApplicationType.PROPERTY_SUGGESTED_NAME }; String[] values = new String[] { connectionName, displayName != null ? displayName : suggestedName }; s.setCustomProperties(keys, values); if (provider != null) provider.saveEnvironment(s); } // Check if the given JmxApplication has been already added to the application tree final Set<JmxApplication> jmxapps = host.getRepository().getDataSources(JmxApplication.class); if (jmxapps.contains(application)) { JmxApplication tempapp = null; for (JmxApplication jmxapp : jmxapps) { if (jmxapp.equals(application)) { tempapp = jmxapp; break; } } cleanupCreatedHost(hosts, host); throw new JMXException(true, NbBundle.getMessage(JmxApplicationProvider.class, "MSG_connection_already_exists", new Object[] { // NOI18N application.getId(), DataSourceDescriptorFactory. getDescriptor(tempapp).getName() })); } // Setup whether the SSL connection is required or not application.getStorage().setCustomProperty(PROPERTY_RETRY_WITHOUT_SSL, allowsInsecure); // Connect to the JMX agent JmxModel model = JmxModelFactory.getJmxModelFor(application); if (model == null || model.getConnectionState() != JmxModel.ConnectionState.CONNECTED) { application.setStateImpl(Stateful.STATE_UNAVAILABLE); cleanupCreatedHost(hosts, host); throw new JMXException(false, NbBundle.getMessage(JmxApplicationProvider.class, "MSG_Cannot_connect_using", new Object[] { // NOI18N displayName != null ? displayName : suggestedName, connectionName })); } // Update application state according to the connection state model.addPropertyChangeListener(new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { if (evt.getNewValue() == ConnectionState.CONNECTED) { application.setStateImpl(Stateful.STATE_AVAILABLE); } else { application.setStateImpl(Stateful.STATE_UNAVAILABLE); } } }); // precompute JVM application.jvm = JvmFactory.getJVMFor(application); // If everything succeeded, add datasource to application tree host.getRepository().addDataSource(application); return application; } private void cleanupCreatedHost(Set<Host> hosts, Host host) { // NOTE: this is not absolutely failsafe, if resolving the JMX application // took a long time and its host has been added by the user/plugin, it may // be removed by this call. Hopefully just a hypothetical case... if (host != null && !Host.LOCALHOST.equals(host) && !hosts.contains(host)) host.getOwner().getRepository().removeDataSource(host); } private String normalizeConnectionName(String connectionName) { if (connectionName.startsWith("service:jmx:")) return connectionName; // NOI18N return "service:jmx:rmi:///jndi/rmi://" + connectionName + "/jmxrmi"; // NOI18N hostname:port } private String getHostName(JMXServiceURL serviceURL) { // Try to compute the hostname instance // from the host in the JMXServiceURL. String hostname = serviceURL.getHost(); if (hostname == null || hostname.isEmpty()) { hostname = null; // Try to compute the Host instance from the JNDI/RMI // Registry Service urlPath in the JMXServiceURL. if ("rmi".equals(serviceURL.getProtocol()) && // NOI18N serviceURL.getURLPath().startsWith("/jndi/rmi://")) { // NOI18N String urlPath = serviceURL.getURLPath().substring("/jndi/rmi://".length()); // NOI18N if ('/' == urlPath.charAt(0)) { // NOI18N hostname = "localhost"; // NOI18N } else if ('[' == urlPath.charAt(0)) { // IPv6 address // NOI18N int closingSquareBracketIndex = urlPath.indexOf("]"); // NOI18N if (closingSquareBracketIndex == -1) { hostname = null; } else { hostname = urlPath.substring(0, closingSquareBracketIndex + 1); } } else { int colonIndex = urlPath.indexOf(":"); // NOI18N int slashIndex = urlPath.indexOf("/"); // NOI18N int min = Math.min(colonIndex, slashIndex); // NOTE: can be -1!!! if (min == -1) { min = 0; } hostname = urlPath.substring(0, min); if (hostname.isEmpty()) { hostname = "localhost"; // NOI18N } } } } return hostname; } private JMXServiceURL getServiceURL(String connectionString) throws MalformedURLException { return new JMXServiceURL(connectionString); } private void initPersistedApplications() { if (!JmxApplicationsSupport.storageDirectoryExists()) return; File[] files = JmxApplicationsSupport.getStorageDirectory().listFiles( new FilenameFilter() { public boolean accept(File dir, String name) { return name.endsWith(JMX_SUFFIX); } }); final int[] persistedAppsCount = new int[1]; for (File file : files) { if (file.isDirectory()) { persistedAppsCount[0]++; Storage storage = new Storage(file, PROPERTIES_FILE); Set<Storage> storageSet = persistedApplications.get(storage.getCustomProperty(PROPERTY_HOSTNAME)); if (storageSet == null) { storageSet = new HashSet<Storage>(); persistedApplications.put(storage.getCustomProperty(PROPERTY_HOSTNAME), storageSet); } storageSet.add(storage); } } DataChangeListener<Host> dataChangeListener = new DataChangeListener<Host>() { public synchronized void dataChanged(DataChangeEvent<Host> event) { final Set<String> failedAppsN = Collections.synchronizedSet(new HashSet()); final Set<Storage> failedAppsS = Collections.synchronizedSet(new HashSet()); Set<Host> hosts = event.getAdded(); for (Host host : hosts) { String hostName = host.getHostName(); Set<Storage> storageSet = persistedApplications.get(hostName); if (storageSet != null) { persistedApplications.remove(hostName); String[] keys = new String[] { PROPERTY_CONNECTION_STRING, PROPERTY_HOSTNAME, DataSourceDescriptor.PROPERTY_NAME, ApplicationType.PROPERTY_SUGGESTED_NAME, PROPERTY_ENV_PROVIDER_ID, PROPERTY_RETRY_WITHOUT_SSL }; for (final Storage storage : storageSet) { final String[] values = storage.getCustomProperties(keys); RequestProcessor.getDefault().post(new Runnable() { public void run() { try { String epid = values[4]; if (epid == null) { // Check for ver 1.0 which didn't support PROPERTY_ENVIRONMENT_PROVIDER String sv = storage.getCustomProperty(SNAPSHOT_VERSION); if ("1.0".equals(sv)) epid = CredentialsProvider.class.getName(); // NOI18N } EnvironmentProvider ep = epid == null ? null : JmxConnectionSupportImpl. getProvider(epid); addJmxApplication(false, null, values[0], values[2], values[3], values[1], ep, storage, values[5]); } catch (final JMXException e) { if (e.isConfig()) { DialogDisplayer.getDefault().notifyLater( new NotifyDescriptor.Message(e. getMessage(), NotifyDescriptor. ERROR_MESSAGE)); } else { String name = values[2]; if (name == null || name.trim().isEmpty()) name = values[3]; failedAppsN.add(name); failedAppsS.add(storage); } } synchronized (persistedAppsCount) { persistedAppsCount[0]--; if (persistedAppsCount[0] == 0 && !failedAppsN.isEmpty()) notifyUnresolvedApplications(failedAppsN, failedAppsS); } } }); } } } if (trackingNewHosts && persistedApplications.isEmpty()) { trackingNewHosts = false; DataSourceRepository.sharedInstance().removeDataChangeListener(this); } } }; if (!persistedApplications.isEmpty()) { trackingNewHosts = true; DataSourceRepository.sharedInstance().addDataChangeListener(dataChangeListener, Host.class); } } private static void notifyUnresolvedApplications(final Set<String> failedHostsN, final Set<Storage> failedHostsS) { RequestProcessor.getDefault().post(new Runnable() { public void run() { String s = GlobalPreferences.sharedInstance().getDoNotShowAgain(DNSA_KEY); Boolean b = s == null ? null : Boolean.parseBoolean(s); if (b == null) { JPanel messagePanel = new JPanel(new BorderLayout(5, 5)); messagePanel.add(new JLabel(NbBundle.getMessage(JmxApplicationProvider.class, "MSG_Unresolved_JMX")), BorderLayout.NORTH); // NOI18N JList list = new JList(failedHostsN.toArray()); list.setVisibleRowCount(4); messagePanel.add(new JScrollPane(list), BorderLayout.CENTER); JCheckBox dnsa = new JCheckBox(); Mnemonics.setLocalizedText(dnsa, NbBundle.getMessage(JmxApplicationProvider.class, "LBL_RememberAction")); // NOI18N dnsa.setToolTipText(NbBundle.getMessage(JmxApplicationProvider.class, "TTP_RememberAction")); // NOI18N JPanel p = new JPanel(new BorderLayout()); p.setBorder(BorderFactory.createEmptyBorder(10, 0, 0, 20)); p.add(dnsa, BorderLayout.WEST); messagePanel.add(p, BorderLayout.SOUTH); NotifyDescriptor dd = new NotifyDescriptor( messagePanel, NbBundle.getMessage(JmxApplicationProvider.class, "Title_Unresolved_JMX"), // NOI18N NotifyDescriptor.YES_NO_OPTION, NotifyDescriptor.ERROR_MESSAGE, null, NotifyDescriptor.YES_OPTION); Object ret = DialogDisplayer.getDefault().notify(dd); if (ret == NotifyDescriptor.NO_OPTION) b = Boolean.FALSE; else if (ret == NotifyDescriptor.YES_OPTION) b = Boolean.TRUE; if (dnsa.isSelected() && b != null) GlobalPreferences.sharedInstance().setDoNotShowAgain(DNSA_KEY, b.toString()); } if (Boolean.FALSE.equals(b)) for (Storage storage : failedHostsS) { File appStorage = storage.getDirectory(); if (appStorage.isDirectory()) Utils.delete(appStorage, true); } failedHostsS.clear(); } }, 1000); } public void initialize() { WindowManager.getDefault().invokeWhenUIReady(new Runnable() { public void run() { RequestProcessor.getDefault().post(new Runnable() { public void run() { initPersistedApplications(); } }); } }); } private static class JMXException extends Exception { private final boolean isConfig; public JMXException(boolean config, String message) { super(message); isConfig = config; } public JMXException(boolean config, String message, Throwable cause) { super(message,cause); isConfig = config; } public boolean isConfig() { return isConfig; } } }