--- /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;
+ }
+}