jdk/test/javax/management/remote/mandatory/loading/MissingClassTest.java
changeset 2 90ce3da70b43
child 1004 5ba8217eb504
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/javax/management/remote/mandatory/loading/MissingClassTest.java	Sat Dec 01 00:00:00 2007 +0000
@@ -0,0 +1,646 @@
+/*
+ * Copyright 2003-2004 Sun Microsystems, Inc.  All Rights Reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+
+/*
+ * @test
+ * @bug 4915825 4921009 4934965 4977469
+ * @summary Tests behavior when client or server gets object of unknown class
+ * @author Eamonn McManus
+ * @run clean MissingClassTest SingleClassLoader
+ * @run build MissingClassTest SingleClassLoader
+ * @run main MissingClassTest
+ */
+
+/*
+  Tests that clients and servers react correctly when they receive
+  objects of unknown classes.  This can happen easily due to version
+  skew or missing jar files on one end or the other.  The default
+  behaviour of causing a connection to die because of the resultant
+  IOException is not acceptable!  We try sending attributes and invoke
+  parameters to the server of classes it doesn't know, and we try
+  sending attributes, exceptions and notifications to the client of
+  classes it doesn't know.
+
+  We also test objects that are of known class but not serializable.
+  The test cases are similar.
+ */
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import javax.management.*;
+import javax.management.loading.*;
+import javax.management.remote.*;
+import javax.management.remote.rmi.RMIConnectorServer;
+
+public class MissingClassTest {
+    private static final int NNOTIFS = 50;
+
+    private static ClassLoader clientLoader, serverLoader;
+    private static Object serverUnknown;
+    private static Exception clientUnknown;
+    private static ObjectName on;
+    private static final Object[] NO_OBJECTS = new Object[0];
+    private static final String[] NO_STRINGS = new String[0];
+
+    private static final Object unserializableObject = Thread.currentThread();
+
+    public static void main(String[] args) throws Exception {
+        System.out.println("Test that the client or server end of a " +
+                           "connection does not fail if sent an object " +
+                           "it cannot deserialize");
+
+        on = new ObjectName("test:type=Test");
+
+        ClassLoader testLoader = MissingClassTest.class.getClassLoader();
+        clientLoader =
+            new SingleClassLoader("$ServerUnknown$", HashMap.class,
+                                  testLoader);
+        serverLoader =
+            new SingleClassLoader("$ClientUnknown$", Exception.class,
+                                  testLoader);
+        serverUnknown =
+            clientLoader.loadClass("$ServerUnknown$").newInstance();
+        clientUnknown = (Exception)
+            serverLoader.loadClass("$ClientUnknown$").newInstance();
+
+        final String[] protos = {"rmi", /*"iiop",*/ "jmxmp"};
+        // iiop commented out until bug 4935098 is fixed
+        boolean ok = true;
+        for (int i = 0; i < protos.length; i++) {
+            try {
+                ok &= test(protos[i]);
+            } catch (Exception e) {
+                System.out.println("TEST FAILED WITH EXCEPTION:");
+                e.printStackTrace(System.out);
+                ok = false;
+            }
+        }
+
+        if (ok)
+            System.out.println("Test passed");
+        else {
+            System.out.println("TEST FAILED");
+            System.exit(1);
+        }
+    }
+
+    private static boolean test(String proto) throws Exception {
+        System.out.println("Testing for proto " + proto);
+
+        boolean ok = true;
+
+        MBeanServer mbs = MBeanServerFactory.newMBeanServer();
+        mbs.createMBean(Test.class.getName(), on);
+
+        JMXConnectorServer cs;
+        JMXServiceURL url = new JMXServiceURL(proto, null, 0);
+        Map serverMap = new HashMap();
+        serverMap.put(JMXConnectorServerFactory.DEFAULT_CLASS_LOADER,
+                      serverLoader);
+
+        // make sure no auto-close at server side
+        serverMap.put("jmx.remote.x.server.connection.timeout", "888888888");
+
+        try {
+            cs = JMXConnectorServerFactory.newJMXConnectorServer(url,
+                                                                 serverMap,
+                                                                 mbs);
+        } catch (MalformedURLException e) {
+            System.out.println("System does not recognize URL: " + url +
+                               "; ignoring");
+            return true;
+        }
+        cs.start();
+        JMXServiceURL addr = cs.getAddress();
+        Map clientMap = new HashMap();
+        clientMap.put(JMXConnectorFactory.DEFAULT_CLASS_LOADER,
+                      clientLoader);
+
+        System.out.println("Connecting for client-unknown test");
+
+        JMXConnector client = JMXConnectorFactory.connect(addr, clientMap);
+
+        // add a listener to verify no failed notif
+        CNListener cnListener = new CNListener();
+        client.addConnectionNotificationListener(cnListener, null, null);
+
+        MBeanServerConnection mbsc = client.getMBeanServerConnection();
+
+        System.out.println("Getting attribute with class unknown to client");
+        try {
+            Object result = mbsc.getAttribute(on, "ClientUnknown");
+            System.out.println("TEST FAILS: getAttribute for class " +
+                               "unknown to client should fail, returned: " +
+                               result);
+            ok = false;
+        } catch (IOException e) {
+            Throwable cause = e.getCause();
+            if (cause instanceof ClassNotFoundException) {
+                System.out.println("Success: got an IOException wrapping " +
+                                   "a ClassNotFoundException");
+            } else {
+                System.out.println("TEST FAILS: Caught IOException (" + e +
+                                   ") but cause should be " +
+                                   "ClassNotFoundException: " + cause);
+                ok = false;
+            }
+        }
+
+        System.out.println("Doing queryNames to ensure connection alive");
+        Set names = mbsc.queryNames(null, null);
+        System.out.println("queryNames returned " + names);
+
+        System.out.println("Provoke exception of unknown class");
+        try {
+            mbsc.invoke(on, "throwClientUnknown", NO_OBJECTS, NO_STRINGS);
+            System.out.println("TEST FAILS: did not get exception");
+            ok = false;
+        } catch (IOException e) {
+            Throwable wrapped = e.getCause();
+            if (wrapped instanceof ClassNotFoundException) {
+                System.out.println("Success: got an IOException wrapping " +
+                                   "a ClassNotFoundException: " +
+                                   wrapped);
+            } else {
+                System.out.println("TEST FAILS: Got IOException but cause " +
+                                   "should be ClassNotFoundException: ");
+                if (wrapped == null)
+                    System.out.println("(null)");
+                else
+                    wrapped.printStackTrace(System.out);
+                ok = false;
+            }
+        } catch (Exception e) {
+            System.out.println("TEST FAILS: Got wrong exception: " +
+                               "should be IOException with cause " +
+                               "ClassNotFoundException:");
+            e.printStackTrace(System.out);
+            ok = false;
+        }
+
+        System.out.println("Doing queryNames to ensure connection alive");
+        names = mbsc.queryNames(null, null);
+        System.out.println("queryNames returned " + names);
+
+        ok &= notifyTest(client, mbsc);
+
+        System.out.println("Doing queryNames to ensure connection alive");
+        names = mbsc.queryNames(null, null);
+        System.out.println("queryNames returned " + names);
+
+        for (int i = 0; i < 2; i++) {
+            boolean setAttribute = (i == 0); // else invoke
+            String what = setAttribute ? "setAttribute" : "invoke";
+            System.out.println("Trying " + what +
+                               " with class unknown to server");
+            try {
+                if (setAttribute) {
+                    mbsc.setAttribute(on, new Attribute("ServerUnknown",
+                                                        serverUnknown));
+                } else {
+                    mbsc.invoke(on, "useServerUnknown",
+                                new Object[] {serverUnknown},
+                                new String[] {"java.lang.Object"});
+                }
+                System.out.println("TEST FAILS: " + what + " with " +
+                                   "class unknown to server should fail " +
+                                   "but did not");
+                ok = false;
+            } catch (IOException e) {
+                Throwable cause = e.getCause();
+                if (cause instanceof ClassNotFoundException) {
+                    System.out.println("Success: got an IOException " +
+                                       "wrapping a ClassNotFoundException");
+                } else {
+                    System.out.println("TEST FAILS: Caught IOException (" + e +
+                                       ") but cause should be " +
+                                       "ClassNotFoundException: " + cause);
+                    e.printStackTrace(System.out); // XXX
+                    ok = false;
+                }
+            }
+        }
+
+        System.out.println("Doing queryNames to ensure connection alive");
+        names = mbsc.queryNames(null, null);
+        System.out.println("queryNames returned " + names);
+
+        System.out.println("Trying to get unserializable attribute");
+        try {
+            mbsc.getAttribute(on, "Unserializable");
+            System.out.println("TEST FAILS: get unserializable worked " +
+                               "but should not");
+            ok = false;
+        } catch (IOException e) {
+            System.out.println("Success: got an IOException: " + e +
+                               " (cause: " + e.getCause() + ")");
+        }
+
+        System.out.println("Doing queryNames to ensure connection alive");
+        names = mbsc.queryNames(null, null);
+        System.out.println("queryNames returned " + names);
+
+        System.out.println("Trying to set unserializable attribute");
+        try {
+            Attribute attr =
+                new Attribute("Unserializable", unserializableObject);
+            mbsc.setAttribute(on, attr);
+            System.out.println("TEST FAILS: set unserializable worked " +
+                               "but should not");
+            ok = false;
+        } catch (IOException e) {
+            System.out.println("Success: got an IOException: " + e +
+                               " (cause: " + e.getCause() + ")");
+        }
+
+        System.out.println("Doing queryNames to ensure connection alive");
+        names = mbsc.queryNames(null, null);
+        System.out.println("queryNames returned " + names);
+
+        System.out.println("Trying to throw unserializable exception");
+        try {
+            mbsc.invoke(on, "throwUnserializable", NO_OBJECTS, NO_STRINGS);
+            System.out.println("TEST FAILS: throw unserializable worked " +
+                               "but should not");
+            ok = false;
+        } catch (IOException e) {
+            System.out.println("Success: got an IOException: " + e +
+                               " (cause: " + e.getCause() + ")");
+        }
+
+        client.removeConnectionNotificationListener(cnListener);
+        ok = ok && !cnListener.failed;
+
+        client.close();
+        cs.stop();
+
+        if (ok)
+            System.out.println("Test passed for protocol " + proto);
+
+        System.out.println();
+        return ok;
+    }
+
+    private static class TestListener implements NotificationListener {
+        TestListener(LostListener ll) {
+            this.lostListener = ll;
+        }
+
+        public void handleNotification(Notification n, Object h) {
+            /* Connectors can handle unserializable notifications in
+               one of two ways.  Either they can arrange for the
+               client to get a NotSerializableException from its
+               fetchNotifications call (RMI connector), or they can
+               replace the unserializable notification by a
+               JMXConnectionNotification.NOTIFS_LOST (JMXMP
+               connector).  The former case is handled by code within
+               the connector client which will end up sending a
+               NOTIFS_LOST to our LostListener.  The logic here
+               handles the latter case by converting it into the
+               former.
+             */
+            if (n instanceof JMXConnectionNotification
+                && n.getType().equals(JMXConnectionNotification.NOTIFS_LOST)) {
+                lostListener.handleNotification(n, h);
+                return;
+            }
+
+            synchronized (result) {
+                if (!n.getType().equals("interesting")
+                    || !n.getUserData().equals("known")) {
+                    System.out.println("TestListener received strange notif: "
+                                       + notificationString(n));
+                    result.failed = true;
+                    result.notifyAll();
+                } else {
+                    result.knownCount++;
+                    if (result.knownCount == NNOTIFS)
+                        result.notifyAll();
+                }
+            }
+        }
+
+        private LostListener lostListener;
+    }
+
+    private static class LostListener implements NotificationListener {
+        public void handleNotification(Notification n, Object h) {
+            synchronized (result) {
+                handle(n, h);
+            }
+        }
+
+        private void handle(Notification n, Object h) {
+            if (!(n instanceof JMXConnectionNotification)) {
+                System.out.println("LostListener received strange notif: " +
+                                   notificationString(n));
+                result.failed = true;
+                result.notifyAll();
+                return;
+            }
+
+            JMXConnectionNotification jn = (JMXConnectionNotification) n;
+            if (!jn.getType().equals(jn.NOTIFS_LOST)) {
+                System.out.println("Ignoring JMXConnectionNotification: " +
+                                   notificationString(jn));
+                return;
+            }
+            final String msg = jn.getMessage();
+            if ((!msg.startsWith("Dropped ")
+                 || !msg.endsWith("classes were missing locally"))
+                && (!msg.startsWith("Not serializable: "))) {
+                System.out.println("Surprising NOTIFS_LOST getMessage: " +
+                                   msg);
+            }
+            if (!(jn.getUserData() instanceof Long)) {
+                System.out.println("JMXConnectionNotification userData " +
+                                   "not a Long: " + jn.getUserData());
+                result.failed = true;
+            } else {
+                int lost = ((Long) jn.getUserData()).intValue();
+                result.lostCount += lost;
+                if (result.lostCount == NNOTIFS*2)
+                    result.notifyAll();
+            }
+        }
+    }
+
+    private static class TestFilter implements NotificationFilter {
+        public boolean isNotificationEnabled(Notification n) {
+            return (n.getType().equals("interesting"));
+        }
+    }
+
+    private static class Result {
+        int knownCount, lostCount;
+        boolean failed;
+    }
+    private static Result result;
+
+    /* Send a bunch of notifications to exercise the logic to recover
+       from unknown notification classes.  We have four kinds of
+       notifications: "known" ones are of a class known to the client
+       and which match its filters; "unknown" ones are of a class that
+       match the client's filters but that the client can't load;
+       "tricky" ones are unserializable; and "boring" notifications
+       are of a class that the client knows but that doesn't match its
+       filters.  We emit NNOTIFS notifications of each kind.  We do a
+       random shuffle on these 4*NNOTIFS notifications so it is likely
+       that we will cover the various different cases in the logic.
+
+       Specifically, what we are testing here is the logic that
+       recovers from a fetchNotifications request that gets a
+       ClassNotFoundException.  Because the request can contain
+       several notifications, the client doesn't know which of them
+       generated the exception.  So it redoes a request that asks for
+       just one notification.  We need to be sure that this works when
+       that one notification is of an unknown class and when it is of
+       a known class, and in both cases when there are filtered
+       notifications that are skipped.
+
+       We are also testing the behaviour in the server when it tries
+       to include an unserializable notification in the response to a
+       fetchNotifications, and in the client when that happens.
+
+       If the test succeeds, the listener should receive the NNOTIFS
+       "known" notifications, and the connection listener should
+       receive an indication of NNOTIFS lost notifications
+       representing the "unknown" notifications.
+
+       We depend on some implementation-specific features here:
+
+       1. The buffer size is sufficient to contain the 4*NNOTIFS
+       notifications which are all sent at once, before the client
+       gets a chance to start receiving them.
+
+       2. When one or more notifications are dropped because they are
+       of unknown classes, the NOTIFS_LOST notification contains a
+       userData that is a Long with a count of the number dropped.
+
+       3. If a notification is not serializable on the server, the
+       client gets told about it somehow, rather than having it just
+       dropped on the floor.  The somehow might be through an RMI
+       exception, or it might be by the server replacing the
+       unserializable notif by a JMXConnectionNotification.NOTIFS_LOST.
+    */
+    private static boolean notifyTest(JMXConnector client,
+                                      MBeanServerConnection mbsc)
+            throws Exception {
+        System.out.println("Send notifications including unknown ones");
+        result = new Result();
+        LostListener ll = new LostListener();
+        client.addConnectionNotificationListener(ll, null, null);
+        TestListener nl = new TestListener(ll);
+        mbsc.addNotificationListener(on, nl, new TestFilter(), null);
+        mbsc.invoke(on, "sendNotifs", NO_OBJECTS, NO_STRINGS);
+
+        // wait for the listeners to receive all their notifs
+        // or to fail
+        long deadline = System.currentTimeMillis() + 60000;
+        long remain;
+        while ((remain = deadline - System.currentTimeMillis()) >= 0) {
+            synchronized (result) {
+                if (result.failed
+                    || (result.knownCount == NNOTIFS
+                        && result.lostCount == NNOTIFS*2))
+                    break;
+                result.wait(remain);
+            }
+        }
+        if (result.failed) {
+            System.out.println("TEST FAILS: Notification strangeness");
+            return false;
+        } else if (result.knownCount == NNOTIFS
+                   && result.lostCount == NNOTIFS*2) {
+            System.out.println("Success: received known notifications and " +
+                               "got NOTIFS_LOST for unknown and " +
+                               "unserializable ones");
+            return true;
+        } else {
+            System.out.println("TEST FAILS: Timed out without receiving " +
+                               "all notifs: known=" + result.knownCount +
+                               "; lost=" + result.lostCount);
+            return false;
+        }
+    }
+
+    public static interface TestMBean {
+        public Object getClientUnknown() throws Exception;
+        public void throwClientUnknown() throws Exception;
+        public void setServerUnknown(Object o) throws Exception;
+        public void useServerUnknown(Object o) throws Exception;
+        public Object getUnserializable() throws Exception;
+        public void setUnserializable(Object un) throws Exception;
+        public void throwUnserializable() throws Exception;
+        public void sendNotifs() throws Exception;
+    }
+
+    public static class Test extends NotificationBroadcasterSupport
+            implements TestMBean {
+
+        public Object getClientUnknown() {
+            return clientUnknown;
+        }
+
+        public void throwClientUnknown() throws Exception {
+            throw clientUnknown;
+        }
+
+        public void setServerUnknown(Object o) {
+            throw new IllegalArgumentException("setServerUnknown succeeded "+
+                                               "but should not have");
+        }
+
+        public void useServerUnknown(Object o) {
+            throw new IllegalArgumentException("useServerUnknown succeeded "+
+                                               "but should not have");
+        }
+
+        public Object getUnserializable() {
+            return unserializableObject;
+        }
+
+        public void setUnserializable(Object un) {
+            throw new IllegalArgumentException("setUnserializable succeeded " +
+                                               "but should not have");
+        }
+
+        public void throwUnserializable() throws Exception {
+            throw new Exception() {
+                private Object unserializable = unserializableObject;
+            };
+        }
+
+        public void sendNotifs() {
+            /* We actually send the same four notification objects
+               NNOTIFS times each.  This doesn't particularly matter,
+               but note that the MBeanServer will replace "this" by
+               the sender's ObjectName the first time.  Since that's
+               always the same, no problem.  */
+            Notification known =
+                new Notification("interesting", this, 1L, 1L, "known");
+            known.setUserData("known");
+            Notification unknown =
+                new Notification("interesting", this, 1L, 1L, "unknown");
+            unknown.setUserData(clientUnknown);
+            Notification boring =
+                new Notification("boring", this, 1L, 1L, "boring");
+            Notification tricky =
+                new Notification("interesting", this, 1L, 1L, "tricky");
+            tricky.setUserData(unserializableObject);
+
+            // check that the tricky notif is indeed unserializable
+            try {
+                new ObjectOutputStream(new ByteArrayOutputStream())
+                    .writeObject(tricky);
+                System.out.println("TEST INCORRECT: tricky notif is " +
+                                   "serializable");
+                System.exit(1);
+            } catch (NotSerializableException e) {
+                // OK: tricky notif is not serializable
+            } catch (IOException e) {
+                System.out.println("TEST INCORRECT: tricky notif " +
+                                   "serialization check failed");
+                System.exit(1);
+            }
+
+            /* Now shuffle an imaginary deck of cards where K, U, T, and
+               B (known, unknown, tricky, boring) each appear NNOTIFS times.
+               We explicitly seed the random number generator so we
+               can reproduce rare test failures if necessary.  We only
+               use a StringBuffer so we can print the shuffled deck --
+               otherwise we could just emit the notifications as the
+               cards are placed.  */
+            long seed = System.currentTimeMillis();
+            System.out.println("Random number seed is " + seed);
+            Random r = new Random(seed);
+            int knownCount = NNOTIFS;   // remaining K cards
+            int unknownCount = NNOTIFS; // remaining U cards
+            int trickyCount = NNOTIFS;  // remaining T cards
+            int boringCount = NNOTIFS;  // remaining B cards
+            StringBuffer notifList = new StringBuffer();
+            for (int i = NNOTIFS * 4; i > 0; i--) {
+                int rand = r.nextInt(i);
+                // use rand to pick a card from the remaining ones
+                if ((rand -= knownCount) < 0) {
+                    notifList.append('k');
+                    knownCount--;
+                } else if ((rand -= unknownCount) < 0) {
+                    notifList.append('u');
+                    unknownCount--;
+                } else if ((rand -= trickyCount) < 0) {
+                    notifList.append('t');
+                    trickyCount--;
+                } else {
+                    notifList.append('b');
+                    boringCount--;
+                }
+            }
+            if (knownCount != 0 || unknownCount != 0
+                || trickyCount != 0 || boringCount != 0) {
+                System.out.println("TEST INCORRECT: Shuffle failed: " +
+                                   "known=" + knownCount +" unknown=" +
+                                   unknownCount + " tricky=" + trickyCount +
+                                   " boring=" + boringCount +
+                                   " deal=" + notifList);
+                System.exit(1);
+            }
+            String notifs = notifList.toString();
+            System.out.println("Shuffle: " + notifs);
+            for (int i = 0; i < NNOTIFS * 4; i++) {
+                Notification n;
+                switch (notifs.charAt(i)) {
+                case 'k': n = known; break;
+                case 'u': n = unknown; break;
+                case 't': n = tricky; break;
+                case 'b': n = boring; break;
+                default:
+                    System.out.println("TEST INCORRECT: Bad shuffle char: " +
+                                       notifs.charAt(i));
+                    System.exit(1);
+                    throw new Error();
+                }
+                sendNotification(n);
+            }
+        }
+    }
+
+    private static String notificationString(Notification n) {
+        return n.getClass().getName() + "/" + n.getType() + " \"" +
+            n.getMessage() + "\" <" + n.getUserData() + ">";
+    }
+
+    //
+    private static class CNListener implements NotificationListener {
+        public void handleNotification(Notification n, Object o) {
+            if (n instanceof JMXConnectionNotification) {
+                JMXConnectionNotification jn = (JMXConnectionNotification)n;
+                if (JMXConnectionNotification.FAILED.equals(jn.getType())) {
+                    failed = true;
+                }
+            }
+        }
+
+        public boolean failed = false;
+    }
+}