From 064fbe425a56b89a84c9b7e2dea3d05dc813ae0f Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Tue, 29 Mar 2011 23:28:26 +0000 Subject: [PATCH] Added support for "streamable closures". The idea with these are that the streaming mechanism will quietly gloss over the funny little outer class reference stuffed into your closure as long as you promise not to use it (because it's going to be null on the other side of the wire). This will allow us to write: _peermgr.invokeNodeAction(new NodeAction() { ... }); which we presently have a big fat warning against, since it didn't work until about 30 seconds from now. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@6550 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- .../java/com/threerings/io/Streamable.java | 18 ++- src/main/java/com/threerings/io/Streamer.java | 73 ++++++++++--- .../com/threerings/io/StreamableTest.java | 103 +++++++++++++++--- 3 files changed, 159 insertions(+), 35 deletions(-) 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" +