1 /*
   2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
   3  * 
   4  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   5  *
   6  * The contents of this file are subject to the terms of either the Universal Permissive License
   7  * v 1.0 as shown at http://oss.oracle.com/licenses/upl
   8  *
   9  * or the following license:
  10  *
  11  * Redistribution and use in source and binary forms, with or without modification, are permitted
  12  * provided that the following conditions are met:
  13  * 
  14  * 1. Redistributions of source code must retain the above copyright notice, this list of conditions
  15  * and the following disclaimer.
  16  * 
  17  * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
  18  * conditions and the following disclaimer in the documentation and/or other materials provided with
  19  * the distribution.
  20  * 
  21  * 3. Neither the name of the copyright holder nor the names of its contributors may be used to
  22  * endorse or promote products derived from this software without specific prior written permission.
  23  * 
  24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
  25  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
  26  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  27  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  28  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  29  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  30  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
  31  * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  32  */
  33 package org.openjdk.jmc.rjmx.internal;
  34 
  35 import java.io.Closeable;
  36 import java.io.IOException;
  37 import java.lang.management.ManagementFactory;
  38 import java.net.MalformedURLException;
  39 import java.rmi.UnmarshalException;
  40 import java.util.Collection;
  41 import java.util.HashMap;
  42 import java.util.HashSet;
  43 import java.util.Iterator;
  44 import java.util.Map;
  45 import java.util.Set;
  46 import java.util.logging.Level;
  47 
  48 import javax.management.AttributeNotFoundException;
  49 import javax.management.InstanceNotFoundException;
  50 import javax.management.IntrospectionException;
  51 import javax.management.JMRuntimeException;
  52 import javax.management.MBeanException;
  53 import javax.management.MBeanInfo;
  54 import javax.management.MBeanServerConnection;
  55 import javax.management.MBeanServerDelegate;
  56 import javax.management.MBeanServerNotification;
  57 import javax.management.MalformedObjectNameException;
  58 import javax.management.Notification;
  59 import javax.management.NotificationListener;
  60 import javax.management.ObjectName;
  61 import javax.management.QueryExp;
  62 import javax.management.ReflectionException;
  63 import javax.management.remote.JMXConnectionNotification;
  64 import javax.management.remote.JMXConnector;
  65 import javax.management.remote.JMXConnectorFactory;
  66 import javax.management.remote.JMXServiceURL;
  67 import javax.rmi.ssl.SslRMIClientSocketFactory;
  68 
  69 import org.eclipse.core.runtime.ListenerList;
  70 import org.openjdk.jmc.common.version.JavaVersion;
  71 import org.openjdk.jmc.common.version.JavaVersionSupport;
  72 import org.openjdk.jmc.rjmx.ConnectionException;
  73 import org.openjdk.jmc.rjmx.ConnectionToolkit;
  74 import org.openjdk.jmc.rjmx.IConnectionDescriptor;
  75 import org.openjdk.jmc.rjmx.IServerDescriptor;
  76 import org.openjdk.jmc.rjmx.RJMXPlugin;
  77 import org.openjdk.jmc.rjmx.services.IOperation;
  78 import org.openjdk.jmc.rjmx.services.internal.MBeanOperationsWrapper;
  79 import org.openjdk.jmc.rjmx.subscription.IMBeanHelperService;
  80 import org.openjdk.jmc.rjmx.subscription.IMBeanServerChangeListener;
  81 import org.openjdk.jmc.rjmx.subscription.IMRIService;
  82 import org.openjdk.jmc.rjmx.subscription.MRI;
  83 import org.openjdk.jmc.rjmx.subscription.internal.AttributeValueToolkit;
  84 import org.openjdk.jmc.rjmx.subscription.internal.InvoluntaryDisconnectException;
  85 import org.openjdk.jmc.rjmx.subscription.internal.MBeanMRIMetadataDB;
  86 import org.openjdk.jmc.ui.common.jvm.JVMDescriptor;
  87 
  88 /**
  89  * This class simplifies and hides some of the complexity of connecting to a JVM (supporting JSR-174
  90  * and JSR-160) using Remote JMX. The RJMXConnection is shared between several
  91  * {@link DefaultConnectionHandle}s, and when the last {@link DefaultConnectionHandle} using the
  92  * JRMXConnection is closed, the RJMXConnection will be automatically closed.
  93  */
  94 public class RJMXConnection implements Closeable, IMBeanHelperService {
  95 
  96         public final static String KEY_SOCKET_FACTORY = "com.sun.jndi.rmi.factory.socket"; //$NON-NLS-1$
  97 
  98         /**
  99          * The default port JMX
 100          */
 101         public static final int VALUE_DEFAULT_REMOTE_PORT_JMX = 7091;
 102 
 103         /**
 104          * Default recalibration interval. The server to client timediff is recalibrated every two
 105          * minutes per default.
 106          */
 107         private static final long VALUE_RECALIBRATION_INTERVAL = 120000;
 108         private static final long REMOTE_START_TIME_UNDEFINED = -1;
 109 
 110         // The ConnectionDescriptor used to create this RJMXConnection
 111         private final IConnectionDescriptor m_connectionDescriptor;
 112 
 113         private final IServerDescriptor m_serverDescriptor;
 114 
 115         // The MBean server connection used for all local and remote communication.
 116         private volatile MCMBeanServerConnection m_server;
 117 
 118         // The underlying JMX connection used when communicating remote.
 119         private JMXConnector m_jmxc;
 120 
 121         private final MBeanMRIMetadataDB m_mbeanDataProvider;
 122 
 123         // Variables used for calibrating the offset to the server clock.
 124         private long m_serverOffset;
 125         private long m_lastRecalibration;
 126         private long m_remoteStartTime = REMOTE_START_TIME_UNDEFINED;
 127 
 128         private boolean m_hasInitializedAllMBeans = false;
 129         private final HashMap<ObjectName, MBeanInfo> m_cachedInfos = new HashMap<>();
 130         private volatile Set<ObjectName> m_cachedMBeanNames = new HashSet<>();
 131         private final Runnable m_onFailCallback;
 132         private final ListenerList<IMBeanServerChangeListener> m_mbeanListeners = new ListenerList<>();
 133         private final NotificationListener m_registrationListener = new NotificationListener() {
 134                 @Override
 135                 public void handleNotification(Notification notification, Object handback) {
 136                         if (notification instanceof MBeanServerNotification) {
 137                                 ObjectName name = ((MBeanServerNotification) notification).getMBeanName();
 138                                 if (notification.getType().equals(MBeanServerNotification.REGISTRATION_NOTIFICATION)) {
 139                                         try {
 140                                                 synchronized (m_cachedInfos) {
 141                                                         getMBeanInfo(name);
 142                                                         if (m_cachedMBeanNames.size() > 0) {
 143                                                                 m_cachedMBeanNames.add(name);
 144                                                         }
 145                                                 }
 146                                                 for (IMBeanServerChangeListener l : m_mbeanListeners) {
 147                                                         l.mbeanRegistered(name);
 148                                                 }
 149                                         } catch (Exception e) {
 150                                                 RJMXPlugin.getDefault().getLogger().log(Level.WARNING,
 151                                                                 "Could not retrieve MBean information for " + name + '!', e); //$NON-NLS-1$
 152                                         }
 153                                 } else if (notification.getType().equals(MBeanServerNotification.UNREGISTRATION_NOTIFICATION)) {
 154                                         synchronized (m_cachedInfos) {
 155                                                 m_cachedInfos.remove(name);
 156                                                 m_cachedMBeanNames.remove(name);
 157                                         }
 158                                         for (IMBeanServerChangeListener l : m_mbeanListeners) {
 159                                                 l.mbeanUnregistered(name);
 160                                         }
 161                                 }
 162                         }
 163                 }
 164         };
 165 
 166         private final NotificationListener m_disconnectListener = new NotificationListener() {
 167 
 168                 @Override
 169                 public void handleNotification(Notification notification, Object handback) {
 170                         if (notification != null && (JMXConnectionNotification.CLOSED.equals(notification.getType())
 171                                         || JMXConnectionNotification.FAILED.equals(notification.getType()))) {
 172                                 close();
 173                                 if (m_onFailCallback != null) {
 174                                         m_onFailCallback.run();
 175                                 }
 176                         }
 177                 }
 178 
 179         };
 180 
 181         private final Object connectionStateLock = new Object();
 182 
 183         /**
 184          * Creates a new remote JMX connection to the specified host, using the supplied credentials. If
 185          * password is null or empty, it will be ignored. Will attempt to set up a connection to the
 186          * server immediately. The Constructor will fail if no connection could be established.
 187          *
 188          * @throws MalformedURLException
 189          */
 190         public RJMXConnection(IConnectionDescriptor connectionDescriptor, IServerDescriptor serverDescriptor,
 191                         Runnable onFailCallback) {
 192                 if (connectionDescriptor == null) {
 193                         throw new IllegalArgumentException("Connection descriptor must not be null!"); //$NON-NLS-1$
 194                 }
 195                 if (serverDescriptor == null) {
 196                         throw new IllegalArgumentException("Server descriptor must not be null!"); //$NON-NLS-1$
 197                 }
 198                 m_onFailCallback = onFailCallback;
 199                 m_connectionDescriptor = connectionDescriptor;
 200                 m_serverDescriptor = serverDescriptor;
 201                 m_mbeanDataProvider = new MBeanMRIMetadataDB(this);
 202                 addMBeanServerChangeListener(m_mbeanDataProvider);
 203         }
 204 
 205         public IServerDescriptor getServerDescriptor() {
 206                 return m_serverDescriptor;
 207         }
 208 
 209         public IConnectionDescriptor getConnectionDescriptor() {
 210                 return m_connectionDescriptor;
 211         }
 212 
 213         /**
 214          * Disconnects the connection from the RJMX server
 215          */
 216         @Override
 217         public void close() {
 218                 synchronized (connectionStateLock) {
 219                         if (isConnected()) {
 220                                 m_server.dispose();
 221                                 tryRemovingListener();
 222                                 clearCollections();
 223                                 m_server = null;
 224                                 if (m_jmxc != null) {
 225                                         try {
 226                                                 m_jmxc.close();
 227                                         } catch (Exception e) {
 228                                                 RJMXPlugin.getDefault().getLogger().log(Level.INFO, "Problem when closing connection.", e); //$NON-NLS-1$
 229                                         }
 230                                         m_jmxc = null;
 231                                 }
 232                         }
 233                 }
 234         }
 235 
 236         /**
 237          * Sometimes we can fail to remove the unregister listeners from the MBeanConnection, causing
 238          * JMX to keep a reference to this instance. To minimize impact if this happens, we clear all
 239          * collections from data.
 240          */
 241         private void clearCollections() {
 242                 clearCache();
 243         }
 244 
 245         private void tryRemovingListener() {
 246                 try {
 247                         ensureConnected().removeNotificationListener(MBeanServerDelegate.DELEGATE_NAME, m_registrationListener);
 248                 } catch (Exception e) {
 249                         RJMXPlugin.getDefault().getLogger().log(Level.WARNING,
 250                                         "Failed to remove unregistration listener! Lost connection?", e); //$NON-NLS-1$
 251                 }
 252         }
 253 
 254         /**
 255          * Returns whether the underlying connector is connected
 256          *
 257          * @return true if the underlying connector is still connected
 258          */
 259         public boolean isConnected() {
 260                 return m_server != null;
 261         }
 262 
 263         @Override
 264         public Set<ObjectName> getMBeanNames() throws IOException {
 265                 synchronized (m_cachedInfos) {
 266                         if (m_cachedMBeanNames.size() == 0) {
 267                                 MBeanServerConnection server = ensureConnected();
 268                                 m_cachedMBeanNames = server.queryNames(null, null);
 269                         }
 270                         return new HashSet<>(m_cachedMBeanNames);
 271                 }
 272         }
 273 
 274         /**
 275          * Returns the bean information for the MBeans matching the domain and query.
 276          *
 277          * @param domain
 278          *            the domain for which to retrieve the information.
 279          * @param query
 280          *            a query to filter for which MBeans to retrieve the information.
 281          * @return a map with the ObjectNames and their associated MBeanInfos.
 282          * @throws IOException
 283          *             if the connection failed or some other IO related problem occurred.
 284          * @throws MalformedObjectNameException
 285          *             if a particularly malign (malformatted) domain was specified.
 286          */
 287         private HashMap<ObjectName, MBeanInfo> getMBeanInfos(String domain, QueryExp query)
 288                         throws MalformedObjectNameException, IOException {
 289                 MBeanServerConnection server = ensureConnected();
 290                 ObjectName objectName = null;
 291                 int skippedMBeanCounter = 0;
 292                 if (domain != null) {
 293                         objectName = new ObjectName(domain + ":*"); //$NON-NLS-1$
 294                 }
 295                 Set<ObjectName> names = server.queryNames(objectName, query);
 296                 HashMap<ObjectName, MBeanInfo> infos = new HashMap<>(names.size());
 297 
 298                 Iterator<ObjectName> iter = names.iterator();
 299                 while (iter.hasNext()) {
 300                         ObjectName name = iter.next();
 301                         try {
 302                                 infos.put(name, getMBeanInfo(name));
 303                         } catch (NullPointerException e) {
 304                                 /*
 305                                  * Skip problematic MBeans when connecting. Workaround implemented so that we can
 306                                  * connect to JBoss 4.2.3.
 307                                  */
 308                                 RJMXPlugin.getDefault().getLogger().log(Level.WARNING, "Skipping " + name.toString() //$NON-NLS-1$
 309                                                 + ". Could not retrieve the MBean info for the MBean. Set log level to fine for stacktrace!"); //$NON-NLS-1$
 310                                 RJMXPlugin.getDefault().getLogger().log(Level.FINE, e.getMessage(), e);
 311                                 skippedMBeanCounter++;
 312                         } catch (UnmarshalException e) {
 313                                 RJMXPlugin.getDefault().getLogger().log(Level.WARNING, "Skipping " //$NON-NLS-1$
 314                                                 + name.toString()
 315                                                 + ". Could not retrieve the MBean info due to marshalling problems. Set log level to fine for stacktrace!"); //$NON-NLS-1$
 316                                 RJMXPlugin.getDefault().getLogger().log(Level.FINE, e.getMessage(), e);
 317                                 skippedMBeanCounter++;
 318                         } catch (InstanceNotFoundException e) {
 319                                 /*
 320                                  * We may end up here if the MBean was unregistered between the call to
 321                                  * getMBeanNames and getMBeanInfo(). Should not be very common though.
 322                                  */
 323                                 RJMXPlugin.getDefault().getLogger().log(Level.WARNING, "Skipping " + name.toString() //$NON-NLS-1$
 324                                                 + ". It could not be found and may have been unregistered very recently. Set log level to fine to fine for stacktrace!"); //$NON-NLS-1$
 325                                 RJMXPlugin.getDefault().getLogger().log(Level.FINE, e.getMessage(), e);
 326                         } catch (IntrospectionException e) {
 327                                 IOException exception = new IOException("Error accessing the bean."); //$NON-NLS-1$
 328                                 exception.initCause(e);
 329                                 throw exception;
 330                         } catch (ReflectionException e) {
 331                                 IOException exception = new IOException("Error accessing the bean."); //$NON-NLS-1$
 332                                 exception.initCause(e);
 333                                 throw exception;
 334                         }
 335                 }
 336                 if (skippedMBeanCounter > 0) {
 337                         RJMXPlugin.getDefault().getLogger().log(Level.WARNING,
 338                                         "Skipped " + skippedMBeanCounter + " MBeans because of marshalling related issues."); //$NON-NLS-1$ //$NON-NLS-2$
 339                 }
 340                 return infos;
 341         }
 342 
 343         /**
 344          * Tries to add a dedicated notification listener that removes unloaded MBeans.
 345          */
 346         private void tryToAddMBeanNotificationListener() {
 347                 try {
 348                         ensureConnected().addNotificationListener(MBeanServerDelegate.DELEGATE_NAME, m_registrationListener, null,
 349                                         null);
 350                 } catch (InstanceNotFoundException e) {
 351                         // Will typically not happen.
 352                 } catch (IOException e) {
 353                         // Will typically not happen.
 354                 }
 355         }
 356 
 357         /**
 358          * Tries to populate the MBean information cache if it is empty.
 359          *
 360          * @throws IOException
 361          *             if the connection failed or some other IO related problem occurred.
 362          */
 363         private void initializeMBeanInfos() throws IOException {
 364                 synchronized (m_cachedInfos) {
 365                         if (!m_hasInitializedAllMBeans) {
 366                                 try {
 367                                         getMBeanInfos(null, null);
 368                                         m_hasInitializedAllMBeans = true;
 369                                 } catch (MalformedObjectNameException e) {
 370                                         assert (false); // Should not be able to get here!
 371                                 }
 372                         }
 373                 }
 374         }
 375 
 376         @Override
 377         public HashMap<ObjectName, MBeanInfo> getMBeanInfos() throws IOException {
 378                 synchronized (m_cachedInfos) {
 379                         initializeMBeanInfos();
 380                         return new HashMap<>(m_cachedInfos);
 381                 }
 382         }
 383 
 384         @Override
 385         public MBeanInfo getMBeanInfo(ObjectName mbean)
 386                         throws InstanceNotFoundException, IntrospectionException, ReflectionException, IOException {
 387                 synchronized (m_cachedInfos) {
 388                         MBeanInfo mbeanInfo = m_cachedInfos.get(mbean);
 389                         if (mbeanInfo == null) {
 390                                 MBeanServerConnection server = ensureConnected();
 391                                 mbeanInfo = server.getMBeanInfo(mbean);
 392                                 if (mbeanInfo != null) {
 393                                         m_cachedInfos.put(mbean, mbeanInfo);
 394                                 }
 395                         }
 396                         return mbeanInfo;
 397                 }
 398         }
 399 
 400         @Override
 401         public Object getAttributeValue(MRI attribute) throws AttributeNotFoundException, MBeanException, IOException,
 402                         InstanceNotFoundException, ReflectionException {
 403                 try {
 404                         MBeanServerConnection server = ensureConnected();
 405                         return AttributeValueToolkit.getAttribute(server, attribute);
 406                 } catch (JMRuntimeException e) {
 407                         throw new MBeanException(e, e.getMessage());
 408                 }
 409         }
 410 
 411         public boolean connect() throws ConnectionException {
 412                 JVMDescriptor jvmInfo = getServerDescriptor().getJvmInfo();
 413                 if (jvmInfo != null && jvmInfo.getJavaVersion() != null
 414                                 && !new JavaVersion(jvmInfo.getJavaVersion()).isGreaterOrEqualThan(JavaVersionSupport.JDK_6)) {
 415                         throw new ConnectionException("Too low JDK Version. JDK 1.6 or higher is supported."); //$NON-NLS-1$
 416                 }
 417                 synchronized (connectionStateLock) {
 418                         if (isConnected()) {
 419                                 return false;
 420                         }
 421                         JMXServiceURL url;
 422                         try {
 423                                 url = m_connectionDescriptor.createJMXServiceURL();
 424                         } catch (IOException e1) {
 425                                 throw new WrappedConnectionException(m_serverDescriptor.getDisplayName(), null, e1);
 426                         }
 427 
 428                         try {
 429                                 // Use same convention as Sun. localhost:0 means "VM, monitor thyself!"
 430                                 String hostName = ConnectionToolkit.getHostName(url);
 431                                 if (hostName != null && (hostName.equals("localhost")) //$NON-NLS-1$
 432                                                 && ConnectionToolkit.getPort(url) == 0) {
 433                                         m_server = new MCMBeanServerConnection(ManagementFactory.getPlatformMBeanServer());
 434                                 } else {
 435                                         establishConnection(url, m_connectionDescriptor.getEnvironment());
 436                                 }
 437                                 tryToAddMBeanNotificationListener();
 438                                 m_remoteStartTime = fetchServerStartTime();
 439                                 return true;
 440                         } catch (Exception e) {
 441                                 m_server = null;
 442                                 throw new WrappedConnectionException(m_serverDescriptor.getDisplayName(), url, e);
 443                         }
 444                 }
 445         }
 446 
 447         private long fetchServerStartTime() throws IOException {
 448                 try {
 449                         return ConnectionToolkit.getRuntimeBean(ensureConnected()).getStartTime();
 450                 } catch (IllegalArgumentException e) {
 451                         RJMXPlugin.getDefault().getLogger().log(Level.WARNING,
 452                                         "Could not find the Runtime MBean. You are probably connecting to a custom MBean server. Functionality will be limited.", //$NON-NLS-1$
 453                                         e);
 454                         return REMOTE_START_TIME_UNDEFINED;
 455                 }
 456         }
 457 
 458         /**
 459          * Attempts to establish a connection. If the connection fails due to symptoms indicating the
 460          * registry using SSL, another attempt to connect will be performed, with the required additions
 461          * to the env.
 462          */
 463         private void establishConnection(JMXServiceURL serviceURL, Map<String, Object> env) throws IOException {
 464                 try {
 465                         connectJmxConnector(serviceURL, env);
 466                 } catch (IOException exception) {
 467                         try {
 468                                 if (env.get(KEY_SOCKET_FACTORY) instanceof SslRMIClientSocketFactory) {
 469                                         env.remove(KEY_SOCKET_FACTORY);
 470                                 } else {
 471                                         env.put(KEY_SOCKET_FACTORY, new SslRMIClientSocketFactory());
 472                                 }
 473                                 connectJmxConnector(serviceURL, env);
 474                         } catch (IOException ioe) {
 475                                 // So we failed even when changing to secure sockets. Original exception was probably spot on...
 476                                 throw exception;
 477                         }
 478                 }
 479                 m_server = new MCMBeanServerConnection(m_jmxc.getMBeanServerConnection());
 480         }
 481 
 482         private void connectJmxConnector(JMXServiceURL serviceURL, Map<String, Object> env) throws IOException {
 483                 m_jmxc = JMXConnectorFactory.newJMXConnector(serviceURL, env);
 484                 m_jmxc.addConnectionNotificationListener(m_disconnectListener, null, null);
 485                 // This is a hack to provide SSL properties to the RMI SSL server socket factory using system properties
 486                 JMXRMISystemPropertiesProvider.setup();
 487                 // According to javadocs, has to pass env here too (which mSA RMI took literally).
 488                 m_jmxc.connect(env);
 489         }
 490 
 491         @Override
 492         public long getApproximateServerTime(long localTime) {
 493                 long startTime = System.currentTimeMillis();
 494                 if ((startTime - m_lastRecalibration) > VALUE_RECALIBRATION_INTERVAL
 495                                 && m_remoteStartTime != REMOTE_START_TIME_UNDEFINED) {
 496                         try {
 497                                 /*
 498                                  * FIXME: JMC-4270 - Server time approximation is not reliable. Since JDK-6523160,
 499                                  * getUptime can no longer be used to derive the current server time. Find some
 500                                  * other way to do this.
 501                                  */
 502                                 long uptime = ConnectionToolkit.getRuntimeBean(ensureConnected()).getUptime();
 503                                 long returnTime = System.currentTimeMillis();
 504                                 long localTimeEstimate = (startTime + returnTime) / 2;
 505                                 m_serverOffset = m_remoteStartTime + uptime - localTimeEstimate;
 506                                 m_lastRecalibration = returnTime;
 507                         } catch (Exception e) {
 508                                 RJMXPlugin.getDefault().getLogger().log(Level.SEVERE, "Could not recalibrate server offset", e); //$NON-NLS-1$
 509                         }
 510                 }
 511                 return localTime + m_serverOffset;
 512         }
 513 
 514         /**
 515          * Returns the MBeanServerConnection. Yes, this breaks abstraction a bit, and should only be
 516          * used by the MBeanBrowser. Everybody else should be using subscriptions anyway.
 517          *
 518          * @return the MBeanServerConnection currently in use by this connection. May be null if none is
 519          *         currently in use.
 520          */
 521         MBeanServerConnection getMBeanServer() {
 522                 return m_server;
 523         }
 524 
 525         /**
 526          * Ok, so this method may not be very useful, from a strict synchronization perspective, but at
 527          * least it is now done in ONE place.
 528          *
 529          * @return a MBeanServerConnection, if connected (or at least non-null).
 530          * @throws IOException
 531          *             if not connected.
 532          */
 533         private MBeanServerConnection ensureConnected() throws IOException {
 534                 MBeanServerConnection server = m_server;
 535                 if (server == null) {
 536                         throw new InvoluntaryDisconnectException("Server is disconnected!"); //$NON-NLS-1$
 537                 }
 538                 return server;
 539         }
 540 
 541         public void clearCache() {
 542                 synchronized (m_cachedInfos) {
 543                         m_cachedInfos.clear();
 544                         m_cachedMBeanNames.clear();
 545                         m_hasInitializedAllMBeans = false;
 546                 }
 547         }
 548 
 549         @Override
 550         public String toString() {
 551                 return "RJMX Connection: " + m_serverDescriptor.getDisplayName(); //$NON-NLS-1$
 552         }
 553 
 554         @Override
 555         public void removeMBeanServerChangeListener(IMBeanServerChangeListener listener) {
 556                 m_mbeanListeners.remove(listener);
 557         }
 558 
 559         @Override
 560         public void addMBeanServerChangeListener(IMBeanServerChangeListener listener) {
 561                 m_mbeanListeners.add(listener);
 562         }
 563 
 564         @Override
 565         public Map<MRI, Map<String, Object>> getMBeanMetadata(ObjectName mbean) {
 566                 return m_mbeanDataProvider.getMBeanData(mbean);
 567         }
 568 
 569         /**
 570          * Returns the IOperations available for the specified MBean.
 571          *
 572          * @param mbean
 573          *            the MBean for which to return the information.
 574          * @return the operations that can be invoked on this mbean.
 575          * @throws Exception
 576          *             if the connection failed or some other problem occurred when trying create
 577          *             operations.
 578          */
 579         public Collection<IOperation> getOperations(ObjectName mbean) throws Exception {
 580                 MBeanServerConnection srv = ensureConnected();
 581                 return new MBeanOperationsWrapper(mbean, srv.getMBeanInfo(mbean).getOperations(), srv);
 582         }
 583 
 584         IMRIService getMRIService() {
 585                 return m_mbeanDataProvider;
 586         }
 587 
 588 }