diff --git a/src/main/java/com/threerings/io/Streamable.java b/src/main/java/com/threerings/io/Streamable.java
index dea3d43d8..1c51611c6 100644
--- a/src/main/java/com/threerings/io/Streamable.java
+++ b/src/main/java/com/threerings/io/Streamable.java
@@ -25,20 +25,26 @@ package com.threerings.io;
* Marks an object as streamable, meaning that it can be written to {@link ObjectOutputStream}
* instances and read from {@link ObjectInputStream} instances.
*
- *
All non-transient fields will be automatically written and restored for a
- * {@link Streamable} instance. Classes that wish to stream transient fields or customize the
- * streaming process should implement methods with the following signatures:
+ *
All non-{@code transient} fields will be automatically written and restored for a {@link
+ * Streamable} instance. Classes that wish to stream transient fields or customize the streaming
+ * process should implement methods with the following signatures:
*
*
* public void writeObject ({@link ObjectOutputStream} out);
* public void readObject ({@link ObjectInputStream} in);
- *
+ *
*
* They can then handle the entirety of the streaming process, or call {@link
* ObjectOutputStream#defaultWriteObject} and {@link ObjectInputStream#defaultReadObject} from
- * within their writeObject and readObject methods to perform the
- * standard streaming in addition to their customized behavior.
+ * within their {@code writeObject} and {@code readObject} methods to perform the standard
+ * streaming in addition to their customized behavior.
*/
public interface Streamable
{
+ /**
+ * A marker interface for streamable classes that expect to be extended anonymously, but for
+ * which the implicit outer class reference can (and should) be ignored. This allows one to
+ * package up units of code and ship them between peers, or even between client and server.
+ */
+ public interface Closure extends Streamable {}
}
diff --git a/src/main/java/com/threerings/io/Streamer.java b/src/main/java/com/threerings/io/Streamer.java
index cb0aa11f1..9666dd3dc 100644
--- a/src/main/java/com/threerings/io/Streamer.java
+++ b/src/main/java/com/threerings/io/Streamer.java
@@ -22,6 +22,7 @@
package com.threerings.io;
import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -251,7 +252,6 @@ public abstract class Streamer
}
if (Modifier.isFinal(componentType.getModifiers())) {
return new FinalArrayStreamer(componentType, delegate);
-
} else {
return new ArrayStreamer(componentType, delegate);
}
@@ -309,7 +309,6 @@ public abstract class Streamer
// if there is no reader and no writer, we can do a simpler thing
if ((reader == null) && (writer == null)) {
return new ClassStreamer(target);
-
} else {
return new CustomClassStreamer(target, reader, writer);
}
@@ -365,7 +364,11 @@ public abstract class Streamer
if (ObjectInputStream.STREAM_DEBUG) {
log.info(in.hashCode() + ": Creating object '" + _target.getName() + "'.");
}
- return _target.newInstance();
+ return _ctor.newInstance(_ctorArgs);
+
+ } catch (InvocationTargetException ite) {
+ String errmsg = "Error instantiating object [type=" + _target.getName() + "]";
+ throw (IOException) new IOException(errmsg).initCause(ite.getCause());
} catch (InstantiationException ie) {
String errmsg = "Error instantiating object [type=" + _target.getName() + "]";
@@ -415,8 +418,9 @@ public abstract class Streamer
*/
protected void initMarshallers ()
{
- // reflect on all the object's fields and remove all marked with NotStreamable
+ // reflect on all the object's fields
List fields = Lists.newArrayList();
+ // this will read all non-static, non-transient fields into our fields list
ClassUtil.getFields(_target, fields);
// Checks whether or not we should stream the fields in alphabetical order.
@@ -426,8 +430,13 @@ public abstract class Streamer
QuickSort.sort(fields, FIELD_NAME_ORDER);
}
- _fields = Iterables.toArray(
- Iterables.filter(fields, _isStreamableFieldPred), Field.class);
+ // note whether this class is a streamable closure
+ final boolean isClosure = Streamable.Closure.class.isAssignableFrom(_target);
+
+ // remove all marked with NotStreamable, and if we're a streamable closure, remove any
+ // anonymous inner class reference
+ Predicate filter = isClosure ? IS_STREAMCLOSURE : IS_STREAMABLE;
+ _fields = Iterables.toArray(Iterables.filter(fields, filter), Field.class);
int fcount = _fields.length;
// obtain field marshallers for all of our fields
@@ -445,6 +454,25 @@ public abstract class Streamer
_fields[ii].getName() + ".");
}
}
+
+ // obtain the constructor we'll use to create instances
+ _ctorArgs = isClosure ? SINGLE_NULL_ARG : NO_ARGS;
+ for (Constructor> ctor : _target.getDeclaredConstructors()) {
+ if (ctor.getParameterTypes().length == _ctorArgs.length) {
+ if (_ctor != null) {
+ throw new RuntimeException(
+ "Streamable has multiple applicable ctors [class=" + _target.getName() +
+ ", argCount=" + _ctorArgs.length + "]");
+ }
+ _ctor = ctor;
+ // keep going, to be sure we catch conflicting ctors
+ }
+ }
+
+ // if this is a streamable closure, we need to make the constructor accessible
+ if (isClosure) {
+ _ctor.setAccessible(true);
+ }
}
@Override
@@ -458,6 +486,12 @@ public abstract class Streamer
/** The class for which this streamer instance is configured. */
protected Class> _target;
+ /** The constructor we use to create instances. */
+ protected Constructor> _ctor;
+
+ /** The arguments we pass to said constructor (empty or a single null). */
+ protected Object[] _ctorArgs;
+
/** The non-transient, non-static public fields that we will stream when requested. */
protected Field[] _fields;
@@ -953,13 +987,6 @@ public abstract class Streamer
}
}
- /** A simple predicate to filter "NotStreamable" members from a Streamable object's fields. */
- protected static final Predicate _isStreamableFieldPred = new Predicate() {
- public boolean apply (Field obj) {
- return (obj.getAnnotation(NotStreamable.class) == null);
- }
- };
-
/** The name of the custom reader method. */
protected static final String READER_METHOD_NAME = "readObject";
@@ -971,4 +998,24 @@ public abstract class Streamer
/** The argument list for the custom writer method. */
protected static final Class>[] WRITER_ARGS = { ObjectOutputStream.class };
+
+ /** Filters "NotStreamable" members from a field list. */
+ protected static final Predicate IS_STREAMABLE = new Predicate() {
+ public boolean apply (Field obj) {
+ return (obj.getAnnotation(NotStreamable.class) == null);
+ }
+ };
+
+ /** Filters "NotStreamable" members and enclosing class refs from a field list. */
+ protected static final Predicate IS_STREAMCLOSURE = new Predicate() {
+ public boolean apply (Field obj) {
+ return IS_STREAMABLE.apply(obj) && !obj.isSynthetic();
+ }
+ };
+
+ /** Used by {@link ClassStreamer} to create instances. */
+ protected static final Object[] NO_ARGS = new Object[] {};
+
+ /** Used by {@link ClassStreamer} to create instances. */
+ protected static final Object[] SINGLE_NULL_ARG = new Object[] { null };
}
diff --git a/src/test/java/com/threerings/io/StreamableTest.java b/src/test/java/com/threerings/io/StreamableTest.java
index 1b59e091d..77de8cbeb 100644
--- a/src/test/java/com/threerings/io/StreamableTest.java
+++ b/src/test/java/com/threerings/io/StreamableTest.java
@@ -325,12 +325,7 @@ public class StreamableTest
throws IOException, ClassNotFoundException
{
Widget w = new Widget();
- ByteArrayOutputStream bout = new ByteArrayOutputStream();
- ObjectOutputStream oout = new ObjectOutputStream(bout);
- oout.writeObject(w);
- ObjectInputStream oin = new ObjectInputStream(
- new ByteArrayInputStream(bout.toByteArray()));
- assertEquals(w, oin.readObject());
+ assertEquals(w, unflatten(flatten(w)));
}
@Test
@@ -340,10 +335,7 @@ public class StreamableTest
Widget w = new Widget();
// make sure that we serialize to the expected stream of bytes
- ByteArrayOutputStream bout = new ByteArrayOutputStream();
- ObjectOutputStream oout = new ObjectOutputStream(bout);
- oout.writeObject(w);
- byte[] data = bout.toByteArray();
+ byte[] data = flatten(w);
// uncomment this and rerun the tests to generate an updated WIRE_DATA blob
// printWireData(w);
@@ -353,17 +345,13 @@ public class StreamableTest
assertEquals(StringUtil.hexlate(data), StringUtil.hexlate(WIRE_DATA));
// make sure that we unserialize a known stream of bytes to the expected object
- ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(WIRE_DATA));
- assertEquals(w, oin.readObject());
+ assertEquals(w, unflatten(WIRE_DATA));
}
protected void printWireData (Object o)
throws IOException
{
- ByteArrayOutputStream bout = new ByteArrayOutputStream();
- ObjectOutputStream oout = new ObjectOutputStream(bout);
- oout.writeObject(o);
- String dstr = StringUtil.wordWrap(StringUtil.hexlate(bout.toByteArray()), 80);
+ String dstr = StringUtil.wordWrap(StringUtil.hexlate(flatten(o)), 80);
dstr = StringUtil.join(dstr.split("\n"), "\" +\n \"");
System.out.println(" protected static final byte[] WIRE_DATA = "
+ "StringUtil.unhexlate(\n \"" + dstr + "\");");
@@ -405,6 +393,89 @@ public class StreamableTest
assertEquals(tup, otup);
}
+ @Test(expected=RuntimeException.class)
+ public void testUnlabledClosureFail ()
+ throws IOException, ClassNotFoundException
+ {
+ abstract class Action implements Streamable {
+ public int count;
+ public String arg;
+ public abstract String act ();
+ }
+ Action act = new Action() {
+ public String act () {
+ return count + ":" + arg;
+ }
+ };
+ act.count = 3;
+ act.arg = "hello";
+ Action react = (Action)unflatten(flatten(act));
+ assertEquals(act.act(), react.act());
+ }
+
+ @Test
+ public void testClosure ()
+ throws IOException, ClassNotFoundException
+ {
+ abstract class Action implements Streamable.Closure {
+ public int count;
+ public String arg;
+ public abstract String act ();
+ }
+ Action act = new Action() {
+ public String act () {
+ return count + ":" + arg;
+ }
+ };
+ act.count = 3;
+ act.arg = "hello";
+ Action react = (Action)unflatten(flatten(act));
+ assertEquals(act.act(), react.act());
+ }
+
+ // unfortunately we can't warn you if you do something naughty in your closure, but since we
+ // flatten and unflatten closures even when running on the local peer, the programmer should
+ // find about about funny business early enough
+ @Test(expected=NullPointerException.class)
+ public void testNaughtyClosureFail ()
+ throws IOException, ClassNotFoundException
+ {
+ abstract class Action implements Streamable.Closure {
+ public int count;
+ public String arg;
+ public abstract String act ();
+ }
+ Action act = new Action() {
+ public String act () {
+ return count + ":" + arg + ":" + naughtyOuterCall();
+ }
+ };
+ act.count = 3;
+ act.arg = "hello";
+ Action react = (Action)unflatten(flatten(act));
+ assertEquals(act.act(), react.act());
+ }
+
+ protected int naughtyOuterCall ()
+ {
+ return 42;
+ }
+
+ protected static byte[] flatten (Object object)
+ throws IOException
+ {
+ ByteArrayOutputStream bout = new ByteArrayOutputStream();
+ ObjectOutputStream oout = new ObjectOutputStream(bout);
+ oout.writeObject(object);
+ return bout.toByteArray();
+ }
+
+ protected static Object unflatten (byte[] data)
+ throws IOException, ClassNotFoundException
+ {
+ return new ObjectInputStream(new ByteArrayInputStream(data)).readObject();
+ }
+
protected static final byte[] WIRE_DATA = StringUtil.unhexlate(
"ffff0027636f6d2e746872656572696e67732e696f2e53747265616d61626c655465737424576964" +
"676574017f00617fff7fffffff7fffffffffffffff7f7fffff7fefffffffffffff0101017f010061" +