/*
* 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; }
}
}