1 /*
2  * Copyright (c) 2003, 2018, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.
8  *
9  * This code is distributed in the hope that it will be useful, but WITHOUT
10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12  * version 2 for more details (a copy is included in the LICENSE file that
13  * accompanied this code).
14  *
15  * You should have received a copy of the GNU General Public License version
16  * 2 along with this work; if not, write to the Free Software Foundation,
17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18  *
19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20  * or visit www.oracle.com if you need additional information or have any
21  * questions.
22  */
23 
24 /*
25  * @test
26  * @bug 4915825 4921009 4934965 4977469 8019584
27  * @key randomness
28  * @summary Tests behavior when client or server gets object of unknown class
29  * @author Eamonn McManus
30  *
31  * @run clean MissingClassTest SingleClassLoader
32  * @run build MissingClassTest SingleClassLoader
33  * @run main MissingClassTest
34  */
35 
36 /*
37   Tests that clients and servers react correctly when they receive
38   objects of unknown classes.  This can happen easily due to version
39   skew or missing jar files on one end or the other.  The default
40   behaviour of causing a connection to die because of the resultant
41   IOException is not acceptable!  We try sending attributes and invoke
42   parameters to the server of classes it doesn't know, and we try
43   sending attributes, exceptions and notifications to the client of
44   classes it doesn't know.
45 
46   We also test objects that are of known class but not serializable.
47   The test cases are similar.
48  */
49 
50 import java.io.ByteArrayOutputStream;
51 import java.io.IOException;
52 import java.io.NotSerializableException;
53 import java.io.ObjectOutputStream;
54 import java.net.MalformedURLException;
55 import java.util.HashMap;
56 import java.util.Map;
57 import java.util.Random;
58 import java.util.Set;
59 import javax.management.Attribute;
60 import javax.management.MBeanServer;
61 import javax.management.MBeanServerConnection;
62 import javax.management.MBeanServerFactory;
main(String[] args)63 import javax.management.Notification;
64 import javax.management.NotificationBroadcasterSupport;
65 import javax.management.NotificationFilter;
66 import javax.management.NotificationListener;
67 import javax.management.ObjectName;
68 import javax.management.remote.JMXConnectionNotification;
69 import javax.management.remote.JMXConnector;
70 import javax.management.remote.JMXConnectorFactory;
71 import javax.management.remote.JMXConnectorServer;
72 import javax.management.remote.JMXConnectorServerFactory;
73 import javax.management.remote.JMXServiceURL;
74 import javax.management.remote.rmi.RMIConnectorServer;
75 
76 public class MissingClassTest {
77     private static final int NNOTIFS = 50;
78 
79     private static ClassLoader clientLoader, serverLoader;
80     private static Object serverUnknown;
81     private static Exception clientUnknown;
82     private static ObjectName on;
83     private static final Object[] NO_OBJECTS = new Object[0];
84     private static final String[] NO_STRINGS = new String[0];
85 
86     private static final Object unserializableObject = Thread.currentThread();
87 
88     private static boolean isInstance(Object o, String cn) {
89         try {
90             Class<?> c = Class.forName(cn);
91             return c.isInstance(o);
92         } catch (ClassNotFoundException x) {
93             return false;
94         }
95     }
96 
97     public static void main(String[] args) throws Exception {
98         System.out.println("Test that the client or server end of a " +
99                            "connection does not fail if sent an object " +
100                            "it cannot deserialize");
101 
102         on = new ObjectName("test:type=Test");
103 
104         ClassLoader testLoader = MissingClassTest.class.getClassLoader();
105         clientLoader =
106             new SingleClassLoader("$ServerUnknown$", HashMap.class,
107                                   testLoader);
108         serverLoader =
109             new SingleClassLoader("$ClientUnknown$", Exception.class,
110                                   testLoader);
111         serverUnknown =
112             clientLoader.loadClass("$ServerUnknown$").newInstance();
113         clientUnknown = (Exception)
test(String proto, MBeanServer mbs, ObjectName on)114             serverLoader.loadClass("$ClientUnknown$").newInstance();
115 
116         final String[] protos = {"rmi", /*"iiop",*/ "jmxmp"};
117         boolean ok = true;
118         for (int i = 0; i < protos.length; i++) {
119             try {
120                 ok &= test(protos[i]);
121             } catch (Exception e) {
122                 System.out.println("TEST FAILED WITH EXCEPTION:");
123                 e.printStackTrace(System.out);
124                 ok = false;
125             }
126         }
127 
128         if (ok)
129             System.out.println("Test passed");
130         else {
131             throw new RuntimeException("TEST FAILED");
132         }
133     }
134 
135     private static boolean test(String proto) throws Exception {
136         System.out.println("Testing for proto " + proto);
137 
138         boolean ok = true;
139 
140         MBeanServer mbs = MBeanServerFactory.newMBeanServer();
141         mbs.createMBean(Test.class.getName(), on);
142 
143         JMXConnectorServer cs;
144         JMXServiceURL url = new JMXServiceURL(proto, null, 0);
145         Map<String,Object> serverMap = new HashMap<>();
146         serverMap.put(JMXConnectorServerFactory.DEFAULT_CLASS_LOADER,
147                       serverLoader);
148 
149         // make sure no auto-close at server side
150         serverMap.put("jmx.remote.x.server.connection.timeout", "888888888");
151 
152         try {
153             cs = JMXConnectorServerFactory.newJMXConnectorServer(url,
154                                                                  serverMap,
155                                                                  mbs);
156         } catch (MalformedURLException e) {
157             System.out.println("System does not recognize URL: " + url +
158                                "; ignoring");
159             return true;
160         }
161         cs.start();
162         JMXServiceURL addr = cs.getAddress();
163         Map<String,Object> clientMap = new HashMap<>();
164         clientMap.put(JMXConnectorFactory.DEFAULT_CLASS_LOADER,
165                       clientLoader);
166 
167         System.out.println("Connecting for client-unknown test");
168 
169         JMXConnector client = JMXConnectorFactory.connect(addr, clientMap);
170 
171         // add a listener to verify no failed notif
172         CNListener cnListener = new CNListener();
173         client.addConnectionNotificationListener(cnListener, null, null);
174 
175         MBeanServerConnection mbsc = client.getMBeanServerConnection();
176 
177         System.out.println("Getting attribute with class unknown to client");
178         try {
179             Object result = mbsc.getAttribute(on, "ClientUnknown");
180             System.out.println("TEST FAILS: getAttribute for class " +
181                                "unknown to client should fail, returned: " +
182                                result);
183             ok = false;
184         } catch (IOException e) {
185             Throwable cause = e.getCause();
186             if (cause instanceof ClassNotFoundException) {
187                 System.out.println("Success: got an IOException wrapping " +
188                                    "a ClassNotFoundException");
189             } else {
190                 System.out.println("TEST FAILS: Caught IOException (" + e +
191                                    ") but cause should be " +
192                                    "ClassNotFoundException: " + cause);
connect(JMXServiceURL addr)193                 ok = false;
194             }
195         }
196 
197         System.out.println("Doing queryNames to ensure connection alive");
198         Set<ObjectName> names = mbsc.queryNames(null, null);
199         System.out.println("queryNames returned " + names);
200 
201         System.out.println("Provoke exception of unknown class");
202         try {
203             mbsc.invoke(on, "throwClientUnknown", NO_OBJECTS, NO_STRINGS);
204             System.out.println("TEST FAILS: did not get exception");
205             ok = false;
206         } catch (IOException e) {
207             Throwable wrapped = e.getCause();
208             if (wrapped instanceof ClassNotFoundException) {
209                 System.out.println("Success: got an IOException wrapping " +
210                                    "a ClassNotFoundException: " +
delay(long ms)211                                    wrapped);
212             } else {
213                 System.out.println("TEST FAILS: Got IOException but cause " +
214                                    "should be ClassNotFoundException: ");
215                 if (wrapped == null)
216                     System.out.println("(null)");
217                 else
218                     wrapped.printStackTrace(System.out);
noException(String what)219                 ok = false;
220             }
221         } catch (Exception e) {
222             System.out.println("TEST FAILS: Got wrong exception: " +
223                                "should be IOException with cause " +
224                                "ClassNotFoundException:");
attrValue(AttributeList attrs)225             e.printStackTrace(System.out);
226             ok = false;
227         }
228 
checkType(String what, Object object, Class<?> wrongClass)229         System.out.println("Doing queryNames to ensure connection alive");
230         names = mbsc.queryNames(null, null);
231         System.out.println("queryNames returned " + names);
232 
233         ok &= notifyTest(client, mbsc);
checkType(String what, Object object, Class<?> wrongClass, boolean isException)234 
235         System.out.println("Doing queryNames to ensure connection alive");
236         names = mbsc.queryNames(null, null);
237         System.out.println("queryNames returned " + names);
238 
239         for (int i = 0; i < 2; i++) {
240             boolean setAttribute = (i == 0); // else invoke
241             String what = setAttribute ? "setAttribute" : "invoke";
242             System.out.println("Trying " + what +
243                                " with class unknown to server");
244             try {
245                 if (setAttribute) {
246                     mbsc.setAttribute(on, new Attribute("ServerUnknown",
247                                                         serverUnknown));
248                 } else {
249                     mbsc.invoke(on, "useServerUnknown",
250                                 new Object[] {serverUnknown},
251                                 new String[] {"java.lang.Object"});
252                 }
253                 System.out.println("TEST FAILS: " + what + " with " +
254                                    "class unknown to server should fail " +
255                                    "but did not");
checkExceptionType(String what, Exception exception, Class<?> wrongClass)256                 ok = false;
257             } catch (IOException e) {
258                 Throwable cause = e.getCause();
259                 if (cause instanceof ClassNotFoundException) {
260                     System.out.println("Success: got an IOException " +
261                                        "wrapping a ClassNotFoundException");
262                 } else {
263                     System.out.println("TEST FAILS: Caught IOException (" + e +
264                                        ") but cause should be " +
265                                        "ClassNotFoundException: " + cause);
266                     e.printStackTrace(System.out); // XXX
267                     ok = false;
268                 }
269             }
270         }
checkAttrs(String what, AttributeList attrs)271 
272         System.out.println("Doing queryNames to ensure connection alive");
273         names = mbsc.queryNames(null, null);
274         System.out.println("queryNames returned " + names);
275 
276         System.out.println("Trying to get unserializable attribute");
277         try {
278             mbsc.getAttribute(on, "Unserializable");
279             System.out.println("TEST FAILS: get unserializable worked " +
280                                "but should not");
281             ok = false;
282         } catch (IOException e) {
283             System.out.println("Success: got an IOException: " + e +
284                                " (cause: " + e.getCause() + ")");
285         }
286 
287         System.out.println("Doing queryNames to ensure connection alive");
288         names = mbsc.queryNames(null, null);
Thing()289         System.out.println("queryNames returned " + names);
290 
291         System.out.println("Trying to set unserializable attribute");
292         try {
getExotic()293             Attribute attr =
294                 new Attribute("Unserializable", unserializableObject);
295             mbsc.setAttribute(on, attr);
296             System.out.println("TEST FAILS: set unserializable worked " +
297                                "but should not");
298             ok = false;
setExotic(Exotic x)299         } catch (IOException e) {
300             System.out.println("Success: got an IOException: " + e +
301                                " (cause: " + e.getCause() + ")");
302         }
303 
anExotic()304         System.out.println("Doing queryNames to ensure connection alive");
305         names = mbsc.queryNames(null, null);
306         System.out.println("queryNames returned " + names);
307 
308         System.out.println("Trying to throw unserializable exception");
309         try {
cacheMBeanInfo(MBeanInfo mbi)310             mbsc.invoke(on, "throwUnserializable", NO_OBJECTS, NO_STRINGS);
311             System.out.println("TEST FAILS: throw unserializable worked " +
312                                "but should not");
313             ok = false;
314         } catch (IOException e) {
315             System.out.println("Success: got an IOException: " + e +
setException(boolean x)316                                " (cause: " + e.getCause() + ")");
317         }
318 
319         client.removeConnectionNotificationListener(cnListener);
320         ok = ok && !cnListener.failed;
321 
322         client.close();
323         cs.stop();
getExotic()324 
325         if (ok)
326             System.out.println("Test passed for protocol " + proto);
setException(boolean x)327 
328         System.out.println();
329         return ok;
330     }
331 
332     private static class TestListener implements NotificationListener {
333         TestListener(LostListener ll) {
334             this.lostListener = ll;
ExoticMBeanInfo(MBeanInfo mbi)335         }
336 
337         public void handleNotification(Notification n, Object h) {
338             /* Connectors can handle unserializable notifications in
339                one of two ways.  Either they can arrange for the
340                client to get a NotSerializableException from its
341                fetchNotifications call (RMI connector), or they can
342                replace the unserializable notification by a
343                JMXConnectionNotification.NOTIFS_LOST (JMXMP
344                connector).  The former case is handled by code within
345                the connector client which will end up sending a
346                NOTIFS_LOST to our LostListener.  The logic here
347                handles the latter case by converting it into the
348                former.
349              */
350             if (n instanceof JMXConnectionNotification
351                 && n.getType().equals(JMXConnectionNotification.NOTIFS_LOST)) {
352                 lostListener.handleNotification(n, h);
353                 return;
354             }
355 
356             synchronized (result) {
357                 if (!n.getType().equals("interesting")
358                     || !n.getUserData().equals("known")) {
359                     System.out.println("TestListener received strange notif: "
360                                        + notificationString(n));
361                     result.failed = true;
362                     result.notifyAll();
363                 } else {
364                     result.knownCount++;
365                     if (result.knownCount == NNOTIFS)
366                         result.notifyAll();
367                 }
368             }
369         }
370 
371         private LostListener lostListener;
372     }
373 
374     private static class LostListener implements NotificationListener {
375         public void handleNotification(Notification n, Object h) {
376             synchronized (result) {
377                 handle(n, h);
378             }
379         }
380 
381         private void handle(Notification n, Object h) {
382             if (!(n instanceof JMXConnectionNotification)) {
383                 System.out.println("LostListener received strange notif: " +
384                                    notificationString(n));
385                 result.failed = true;
386                 result.notifyAll();
387                 return;
388             }
389 
390             JMXConnectionNotification jn = (JMXConnectionNotification) n;
391             if (!jn.getType().equals(jn.NOTIFS_LOST)) {
392                 System.out.println("Ignoring JMXConnectionNotification: " +
393                                    notificationString(jn));
394                 return;
395             }
396             final String msg = jn.getMessage();
397             if ((!msg.startsWith("Dropped ")
398                  || !msg.endsWith("classes were missing locally"))
399                 && (!msg.startsWith("Not serializable: "))) {
400                 System.out.println("Surprising NOTIFS_LOST getMessage: " +
401                                    msg);
402             }
403             if (!(jn.getUserData() instanceof Long)) {
404                 System.out.println("JMXConnectionNotification userData " +
405                                    "not a Long: " + jn.getUserData());
406                 result.failed = true;
407             } else {
408                 int lost = ((Long) jn.getUserData()).intValue();
409                 result.lostCount += lost;
410                 if (result.lostCount == NNOTIFS*2)
411                     result.notifyAll();
412             }
413         }
414     }
415 
416     private static class TestFilter implements NotificationFilter {
417         public boolean isNotificationEnabled(Notification n) {
418             return (n.getType().equals("interesting"));
419         }
420     }
421 
422     private static class Result {
423         int knownCount, lostCount;
424         boolean failed;
425     }
426     private static Result result;
427 
428     /* Send a bunch of notifications to exercise the logic to recover
429        from unknown notification classes.  We have four kinds of
430        notifications: "known" ones are of a class known to the client
431        and which match its filters; "unknown" ones are of a class that
432        match the client's filters but that the client can't load;
433        "tricky" ones are unserializable; and "boring" notifications
434        are of a class that the client knows but that doesn't match its
435        filters.  We emit NNOTIFS notifications of each kind.  We do a
436        random shuffle on these 4*NNOTIFS notifications so it is likely
437        that we will cover the various different cases in the logic.
438 
439        Specifically, what we are testing here is the logic that
440        recovers from a fetchNotifications request that gets a
441        ClassNotFoundException.  Because the request can contain
442        several notifications, the client doesn't know which of them
443        generated the exception.  So it redoes a request that asks for
444        just one notification.  We need to be sure that this works when
445        that one notification is of an unknown class and when it is of
446        a known class, and in both cases when there are filtered
447        notifications that are skipped.
448 
449        We are also testing the behaviour in the server when it tries
450        to include an unserializable notification in the response to a
451        fetchNotifications, and in the client when that happens.
452 
453        If the test succeeds, the listener should receive the NNOTIFS
454        "known" notifications, and the connection listener should
455        receive an indication of NNOTIFS lost notifications
456        representing the "unknown" notifications.
457 
458        We depend on some implementation-specific features here:
459 
460        1. The buffer size is sufficient to contain the 4*NNOTIFS
461        notifications which are all sent at once, before the client
462        gets a chance to start receiving them.
463 
464        2. When one or more notifications are dropped because they are
465        of unknown classes, the NOTIFS_LOST notification contains a
466        userData that is a Long with a count of the number dropped.
467 
468        3. If a notification is not serializable on the server, the
469        client gets told about it somehow, rather than having it just
470        dropped on the floor.  The somehow might be through an RMI
471        exception, or it might be by the server replacing the
472        unserializable notif by a JMXConnectionNotification.NOTIFS_LOST.
473     */
474     private static boolean notifyTest(JMXConnector client,
475                                       MBeanServerConnection mbsc)
476             throws Exception {
477         System.out.println("Send notifications including unknown ones");
478         result = new Result();
479         LostListener ll = new LostListener();
480         client.addConnectionNotificationListener(ll, null, null);
481         TestListener nl = new TestListener(ll);
482         mbsc.addNotificationListener(on, nl, new TestFilter(), null);
483         mbsc.invoke(on, "sendNotifs", NO_OBJECTS, NO_STRINGS);
484 
485         // wait for the listeners to receive all their notifs
486         // or to fail
487         long deadline = System.currentTimeMillis() + 60000;
488         long remain;
489         while ((remain = deadline - System.currentTimeMillis()) >= 0) {
490             synchronized (result) {
491                 if (result.failed
492                     || (result.knownCount >= NNOTIFS
493                         && result.lostCount >= NNOTIFS*2))
494                     break;
495                 result.wait(remain);
496             }
497         }
498         Thread.sleep(2);  // allow any spurious extra notifs to arrive
499         if (result.failed) {
500             System.out.println("TEST FAILS: Notification strangeness");
501             return false;
502         } else if (result.knownCount == NNOTIFS
503                    && result.lostCount == NNOTIFS*2) {
504             System.out.println("Success: received known notifications and " +
505                                "got NOTIFS_LOST for unknown and " +
506                                "unserializable ones");
507             return true;
508         } else if (result.knownCount >= NNOTIFS
509                 || result.lostCount >= NNOTIFS*2) {
510             System.out.println("TEST FAILS: Received too many notifs: " +
511                     "known=" + result.knownCount + "; lost=" + result.lostCount);
512             return false;
513         } else {
514             System.out.println("TEST FAILS: Timed out without receiving " +
515                                "all notifs: known=" + result.knownCount +
516                                "; lost=" + result.lostCount);
517             return false;
518         }
519     }
520 
521     public static interface TestMBean {
522         public Object getClientUnknown() throws Exception;
523         public void throwClientUnknown() throws Exception;
524         public void setServerUnknown(Object o) throws Exception;
525         public void useServerUnknown(Object o) throws Exception;
526         public Object getUnserializable() throws Exception;
527         public void setUnserializable(Object un) throws Exception;
528         public void throwUnserializable() throws Exception;
529         public void sendNotifs() throws Exception;
530     }
531 
532     public static class Test extends NotificationBroadcasterSupport
533             implements TestMBean {
534 
535         public Object getClientUnknown() {
536             return clientUnknown;
537         }
538 
539         public void throwClientUnknown() throws Exception {
540             throw clientUnknown;
541         }
542 
543         public void setServerUnknown(Object o) {
544             throw new IllegalArgumentException("setServerUnknown succeeded "+
545                                                "but should not have");
546         }
547 
548         public void useServerUnknown(Object o) {
549             throw new IllegalArgumentException("useServerUnknown succeeded "+
550                                                "but should not have");
551         }
552 
553         public Object getUnserializable() {
554             return unserializableObject;
555         }
556 
557         public void setUnserializable(Object un) {
558             throw new IllegalArgumentException("setUnserializable succeeded " +
559                                                "but should not have");
560         }
561 
562         public void throwUnserializable() throws Exception {
563             throw new Exception() {
564                 private Object unserializable = unserializableObject;
565             };
566         }
567 
568         public void sendNotifs() {
569             /* We actually send the same four notification objects
570                NNOTIFS times each.  This doesn't particularly matter,
571                but note that the MBeanServer will replace "this" by
572                the sender's ObjectName the first time.  Since that's
573                always the same, no problem.  */
574             Notification known =
575                 new Notification("interesting", this, 1L, 1L, "known");
576             known.setUserData("known");
577             Notification unknown =
578                 new Notification("interesting", this, 1L, 1L, "unknown");
579             unknown.setUserData(clientUnknown);
580             Notification boring =
581                 new Notification("boring", this, 1L, 1L, "boring");
582             Notification tricky =
583                 new Notification("interesting", this, 1L, 1L, "tricky");
584             tricky.setUserData(unserializableObject);
585 
586             // check that the tricky notif is indeed unserializable
587             try {
588                 new ObjectOutputStream(new ByteArrayOutputStream())
589                     .writeObject(tricky);
590                 throw new RuntimeException("TEST INCORRECT: tricky notif is " +
591                                            "serializable");
592             } catch (NotSerializableException e) {
593                 // OK: tricky notif is not serializable
594             } catch (IOException e) {
595                 throw new RuntimeException("TEST INCORRECT: tricky notif " +
596                                             "serialization check failed");
597             }
598 
599             /* Now shuffle an imaginary deck of cards where K, U, T, and
600                B (known, unknown, tricky, boring) each appear NNOTIFS times.
601                We explicitly seed the random number generator so we
602                can reproduce rare test failures if necessary.  We only
603                use a StringBuffer so we can print the shuffled deck --
604                otherwise we could just emit the notifications as the
605                cards are placed.  */
606             long seed = System.currentTimeMillis();
607             System.out.println("Random number seed is " + seed);
608             Random r = new Random(seed);
609             int knownCount = NNOTIFS;   // remaining K cards
610             int unknownCount = NNOTIFS; // remaining U cards
611             int trickyCount = NNOTIFS;  // remaining T cards
612             int boringCount = NNOTIFS;  // remaining B cards
613             StringBuffer notifList = new StringBuffer();
614             for (int i = NNOTIFS * 4; i > 0; i--) {
615                 int rand = r.nextInt(i);
616                 // use rand to pick a card from the remaining ones
617                 if ((rand -= knownCount) < 0) {
618                     notifList.append('k');
619                     knownCount--;
620                 } else if ((rand -= unknownCount) < 0) {
621                     notifList.append('u');
622                     unknownCount--;
623                 } else if ((rand -= trickyCount) < 0) {
624                     notifList.append('t');
625                     trickyCount--;
626                 } else {
627                     notifList.append('b');
628                     boringCount--;
629                 }
630             }
631             if (knownCount != 0 || unknownCount != 0
632                 || trickyCount != 0 || boringCount != 0) {
633                 throw new RuntimeException("TEST INCORRECT: Shuffle failed: " +
634                                    "known=" + knownCount +" unknown=" +
635                                    unknownCount + " tricky=" + trickyCount +
636                                    " boring=" + boringCount +
637                                    " deal=" + notifList);
638             }
639             String notifs = notifList.toString();
640             System.out.println("Shuffle: " + notifs);
641             for (int i = 0; i < NNOTIFS * 4; i++) {
642                 Notification n;
643                 switch (notifs.charAt(i)) {
644                 case 'k': n = known; break;
645                 case 'u': n = unknown; break;
646                 case 't': n = tricky; break;
647                 case 'b': n = boring; break;
648                 default:
649                     throw new RuntimeException("TEST INCORRECT: Bad shuffle char: " +
650                                                notifs.charAt(i));
651                 }
652                 sendNotification(n);
653             }
654         }
655     }
656 
657     private static String notificationString(Notification n) {
658         return n.getClass().getName() + "/" + n.getType() + " \"" +
659             n.getMessage() + "\" <" + n.getUserData() + ">";
660     }
661 
662     //
663     private static class CNListener implements NotificationListener {
664         public void handleNotification(Notification n, Object o) {
665             if (n instanceof JMXConnectionNotification) {
666                 JMXConnectionNotification jn = (JMXConnectionNotification)n;
667                 if (JMXConnectionNotification.FAILED.equals(jn.getType())) {
668                     failed = true;
669                 }
670             }
671         }
672 
673         public boolean failed = false;
674     }
675 }
676