diff -r fd16c54261b3 -r 90ce3da70b43 jdk/test/javax/management/remote/mandatory/loading/MissingClassTest.java --- /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; + } +}