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" +