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