1 /*
   2  * Copyright (c) 2003, 2013, 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  * @summary Tests behavior when client or server gets object of unknown class
  28  * @author Eamonn McManus
  29  * @run clean MissingClassTest SingleClassLoader
  30  * @run build MissingClassTest SingleClassLoader
  31  * @run main MissingClassTest

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