diff --git a/src/java/com/threerings/presents/io/FieldMarshallerRegistry.java b/src/java/com/threerings/presents/io/FieldMarshallerRegistry.java index d7d48feeb..688c34951 100644 --- a/src/java/com/threerings/presents/io/FieldMarshallerRegistry.java +++ b/src/java/com/threerings/presents/io/FieldMarshallerRegistry.java @@ -1,5 +1,5 @@ // -// $Id: FieldMarshallerRegistry.java,v 1.13 2002/02/07 05:27:07 mdb Exp $ +// $Id: FieldMarshallerRegistry.java,v 1.14 2002/03/20 22:58:26 mdb Exp $ package com.threerings.presents.io; @@ -35,6 +35,9 @@ public class FieldMarshallerRegistry if (Streamable.class.isAssignableFrom(clazz)) { marsh = _streamableMarsh; _registry.put(clazz, marsh); + } else if (STREAMARRAY_CLASS.isAssignableFrom(clazz)) { + marsh = _streamarrayMarsh; + _registry.put(clazz, marsh); } } @@ -75,6 +78,14 @@ public class FieldMarshallerRegistry protected static FieldMarshaller _streamableMarsh = new StreamableFieldMarshaller(); + /** Used for marshalling arrays of {@link Streamable}. */ + protected static FieldMarshaller _streamarrayMarsh = + new StreamableArrayFieldMarshaller(); + + /** Used to identify streamable array instances. */ + protected static final Class STREAMARRAY_CLASS = + (new Streamable[0]).getClass(); + static { // register our field marshallers registerMarshaller(BooleanFieldMarshaller.class); diff --git a/src/java/com/threerings/presents/io/PolyStreamableList.java b/src/java/com/threerings/presents/io/PolyStreamableList.java deleted file mode 100644 index 3bdd4e470..000000000 --- a/src/java/com/threerings/presents/io/PolyStreamableList.java +++ /dev/null @@ -1,132 +0,0 @@ -// -// $Id: PolyStreamableList.java,v 1.1 2002/02/12 01:54:51 shaper Exp $ - -package com.threerings.presents.io; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -import java.util.ArrayList; - -import com.samskivert.util.ListUtil; - -import com.threerings.presents.Log; - -/** - * A poly streamable list is a wrapper around an {@link ArrayList} that - * knows how to efficiently stream {@link Streamable} objects of - * potentially heterogenous classes. Accordingly, all objects inserted - * into the list must implement the {@link Streamable} interface. The - * list ordering is properly maintained when sending data over the wire. - */ -public class PolyStreamableList extends ArrayList - implements Streamable -{ - // documentation inherited from interface - public void writeTo (DataOutputStream out) - throws IOException - { - // create a list used to map class names to class ids - Object[] cnames = new String[DEFAULT_SIZE]; - - // write out the size of the list - int size = size(); - out.writeInt(size); - - // write out each of the list elements - int nextIdx = 0; - for (int ii = 0; ii < size; ii++) { - // get the next streamable to write out - Object value = get(ii); - if (!(value instanceof Streamable)) { - throw new RuntimeException( - "Requested to serialize invalid type [value=" + value + - ", type=" + value.getClass().getName() + "]"); - } - Streamable s = (Streamable)value; - - // look up the class id - String cname = s.getClass().getName(); - int cidx = ListUtil.indexOfEqual(cnames, cname); - if (cidx == -1) { - // store this class name to class id mapping - cidx = nextIdx++; - cnames = ListUtil.add(cnames, cidx, cname); - - // write out the class id and class name - out.writeInt(cidx); - out.writeUTF(cname); - - } else { - // we need only write out the class id - out.writeInt(cidx); - } - - // write out the object itself - s.writeTo(out); - } - } - - // documentation inherited from interface - public void readFrom (DataInputStream in) - throws IOException - { - // create a list used to map class ids to class names - Object[] cnames = new String[DEFAULT_SIZE]; - - // read in the size of the list - int size = in.readInt(); - - // read in each of the list elements - for (int ii = 0; ii < size; ii++) { - // read the class id number - int cidx = in.readInt(); - - // look up the class name - String cname = (cidx > cnames.length - 1) ? null : - (String)cnames[cidx]; - if (cname == null) { - // store this class id to class name mapping - cname = in.readUTF(); - cnames = ListUtil.add(cnames, cidx, cname); - } - - try { - // instantiate the streamable object - Class clazz = Class.forName(cname); - if (!Streamable.class.isAssignableFrom(clazz)) { - throw new RuntimeException( - "Requested to unserialize invalid type " + - "[type=" + cname + "]"); - } - Streamable s = (Streamable)clazz.newInstance(); - - // read in the object itself - s.readFrom(in); - - // and add it to the list - add(s); - - } catch (ClassNotFoundException cnfe) { - throw new IOException( - "Unable to instantiate class [cname=" + cname + - ", cnfe=" + cnfe + "]"); - - } catch (InstantiationException ie) { - throw new IOException( - "Unable to instantiate class [cname=" + cname + - ", ie=" + ie + "]"); - - } catch (IllegalAccessException iae) { - throw new IOException( - "Unable to instantiate class [cname=" + cname + - ", iae=" + iae + "]"); - } - } - } - - /** The default size of the class name lists used when serializing or - * unserializing the list. */ - protected static final int DEFAULT_SIZE = 8; -} diff --git a/src/java/com/threerings/presents/io/StreamableArrayFieldMarshaller.java b/src/java/com/threerings/presents/io/StreamableArrayFieldMarshaller.java new file mode 100644 index 000000000..3f10190c0 --- /dev/null +++ b/src/java/com/threerings/presents/io/StreamableArrayFieldMarshaller.java @@ -0,0 +1,39 @@ +// +// $Id: StreamableArrayFieldMarshaller.java,v 1.1 2002/03/20 22:58:26 mdb Exp $ + +package com.threerings.presents.io; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; + +import com.threerings.presents.io.Streamable; + +public class StreamableArrayFieldMarshaller implements FieldMarshaller +{ + public void writeTo (DataOutputStream out, Field field, Object obj) + throws IOException, IllegalAccessException + { + Streamable[] values = (Streamable[])field.get(obj); + + // in order to make use of the general purpose streamable + // serialization routines, we have to have a non-null array, so we + // create a zero length array if our field value is null + if (values == null) { + values = (Streamable[])Array.newInstance( + field.getType().getComponentType(), 0); + } + + StreamableUtil.writeStreamables(out, values); + } + + public void readFrom (DataInputStream in, Field field, Object obj) + throws IOException, IllegalAccessException + { + // read and set + field.set(obj, StreamableUtil.readStreamables(in)); + } +} diff --git a/src/java/com/threerings/presents/io/StreamableUtil.java b/src/java/com/threerings/presents/io/StreamableUtil.java index 8b0058167..2a1cd6d21 100644 --- a/src/java/com/threerings/presents/io/StreamableUtil.java +++ b/src/java/com/threerings/presents/io/StreamableUtil.java @@ -1,5 +1,5 @@ // -// $Id: StreamableUtil.java,v 1.1 2001/12/03 23:48:38 mdb Exp $ +// $Id: StreamableUtil.java,v 1.2 2002/03/20 22:58:26 mdb Exp $ package com.threerings.presents.io; @@ -7,6 +7,11 @@ import java.io.IOException; import java.io.DataInputStream; import java.io.DataOutputStream; +import java.lang.reflect.Array; + +import com.samskivert.io.NestableIOException; +import com.samskivert.util.ListUtil; + /** * Utility functions useful for {@link Streamable} implementations. */ @@ -67,4 +72,118 @@ public class StreamableUtil } return values; } + + /** + * Writes the supplied streamable array to the data output stream. The + * array cannot be null, but can be of zero length. + */ + public static void writeStreamables ( + DataOutputStream out, Streamable[] values) + throws IOException + { + // create a list used to map class names to class ids + Object[] cnames = new Object[DEFAULT_SIZE]; + + // seed it with the component type of the array (which we write + // out first so that we can recreate the array on the other side) + String cname = values.getClass().getComponentType().getName(); + cnames[0] = cname; + out.writeUTF(cname); + + // write out the size of the list + int size = (values == null) ? 0 : values.length; + out.writeInt(size); + + // write out each of the list elements + short nextIdx = 1; + for (int ii = 0; ii < size; ii++) { + // get the next streamable to write out + Streamable s = values[ii]; + + // look up the class id + cname = s.getClass().getName(); + short cidx = (short)ListUtil.indexOfEqual(cnames, cname); + if (cidx == -1) { + // store this class name to class id mapping + cidx = nextIdx++; + cnames = ListUtil.add(cnames, cidx, cname); + + // write out the class id and class name + out.writeShort(cidx); + out.writeUTF(cname); + + } else { + // we need only write out the class id + out.writeShort(cidx); + } + + // write out the object itself + s.writeTo(out); + } + } + + /** + * Reads an array of streamables from the data input stream that was + * previously written via {@link #writeStreamables}. + */ + public static Streamable[] readStreamables (DataInputStream in) + throws IOException + { + // read in the component type and array size + String ctype = in.readUTF(); + int size = in.readInt(); + Streamable[] values = null; + + // create the target array + try { + values = (Streamable[]) + Array.newInstance(Class.forName(ctype), size); + } catch (Exception e) { + String errmsg = "Unable to instantiate streamable array " + + "[ctype=" + ctype + "]"; + throw new NestableIOException(errmsg, e); + } + + // create a list used to map class ids to class names + Object[] cnames = new Object[DEFAULT_SIZE]; + // seed our class names with the component type + cnames[0] = ctype; + + // read in each of the list elements + for (int ii = 0; ii < size; ii++) { + // read the class id number + short cidx = in.readShort(); + + // look up the class name + String cname = (cidx > cnames.length - 1) ? null : + (String)cnames[cidx]; + if (cname == null) { + // store this class id to class name mapping + cname = in.readUTF(); + cnames = ListUtil.add(cnames, cidx, cname); + } + + try { + // instantiate the streamable object + Class clazz = Class.forName(cname); + Streamable s = (Streamable)clazz.newInstance(); + // read in the object itself + s.readFrom(in); + // and add it to the list + values[ii] = s; + + } catch (Exception e) { + String errmsg = "Error instantiating streamable array " + + "element [ctype=" + ctype + ", index=" + ii + + ", etype=" + cname + "]"; + throw new NestableIOException(errmsg, e); + } + } + + return values; + } + + /** The default size of the class name lists used when serializing or + * unserializing the list. */ + protected static final int DEFAULT_SIZE = 8; } diff --git a/src/java/com/threerings/presents/util/StreamableArrayList.java b/src/java/com/threerings/presents/util/StreamableArrayList.java index 9666ba81d..f099c5c29 100644 --- a/src/java/com/threerings/presents/util/StreamableArrayList.java +++ b/src/java/com/threerings/presents/util/StreamableArrayList.java @@ -1,5 +1,5 @@ // -// $Id: StreamableArrayList.java,v 1.3 2002/03/20 19:07:15 mdb Exp $ +// $Id: StreamableArrayList.java,v 1.4 2002/03/20 22:58:26 mdb Exp $ package com.threerings.presents.util; @@ -10,15 +10,18 @@ import java.io.DataOutputStream; import java.util.ArrayList; import com.threerings.presents.dobj.DSet; import com.threerings.presents.io.Streamable; +import com.threerings.presents.io.StreamableUtil; /** * Provides a means by which an ordered collection of streamable instances - * (all of the exact same class) can be delivered over the network. A - * streamable array list can be supplied anywhere that a distributed - * object value can be supplied, but bear in mind that once the list is - * created, it's elements cannot be changed without rebroadcasting the - * entire list. It is not like a {@link DSet} which allows individual - * elements to be added or removed. + * (of potentially heterogenous derived classes) can be delivered over the + * network. A streamable array list can be supplied anywhere that a + * distributed object value can be supplied, but bear in mind that once + * the list is created, it's elements cannot be changed without + * rebroadcasting the entire list. It is not like a {@link DSet} which + * allows individual elements to be added or removed, or + * Streamable[] distributed object fields for which elements + * can be individually updated. */ public class StreamableArrayList extends ArrayList implements Streamable @@ -27,48 +30,22 @@ public class StreamableArrayList public void writeTo (DataOutputStream out) throws IOException { - int count = size(); - out.writeInt(count); - - // only write element info if we have elements - if (count > 0) { - Streamable first = (Streamable)get(0); - // write out the classname of our elements - out.writeUTF(first.getClass().getName()); - - // now write out our elements - for (int i = 0; i < count; i++) { - Streamable s = (Streamable)get(i); - s.writeTo(out); - } - } + // convert ourselves into an array and use streamable util to + // write it out + Streamable[] values = new Streamable[size()]; + toArray(values); + StreamableUtil.writeStreamables(out, values); } // documentation inherited public void readFrom (DataInputStream in) throws IOException { - // read in our element count - int count = in.readInt(); - - // only read in elements if we have some - if (count > 0) { - // read in the element class and load it up - String cname = in.readUTF(); - try { - Class clazz = Class.forName(cname); - - // now read in the elements - for (int i = 0; i < count; i++) { - Streamable s = (Streamable)clazz.newInstance(); - s.readFrom(in); - add(s); - } - - } catch (Exception e) { - throw new IOException("Error reading streamable " + - "[class=" + cname + ", err=" + e + "]"); - } + // use streamable util to read in our values + Streamable[] values = StreamableUtil.readStreamables(in); + int vcount = values.length; + for (int i = 0; i < vcount; i++) { + add(values[i]); } } } diff --git a/tests/src/java/com/threerings/io/StreamableUtilTest.java b/tests/src/java/com/threerings/io/StreamableUtilTest.java new file mode 100644 index 000000000..d981d904f --- /dev/null +++ b/tests/src/java/com/threerings/io/StreamableUtilTest.java @@ -0,0 +1,128 @@ +// +// $Id: StreamableUtilTest.java,v 1.1 2002/03/20 22:58:26 mdb Exp $ + +package com.threerings.presents.io; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +import java.util.Arrays; + +import junit.framework.Test; +import junit.framework.TestCase; + +import com.samskivert.util.StringUtil; + +/** + * Tests the {@link StreamableUtil} class. + */ +public class StreamableUtilTest extends TestCase +{ + public StreamableUtilTest () + { + super(StreamableUtilTest.class.getName()); + } + + public static class Boink extends SimpleStreamableObject + { + public String foo; + public int bar; + public float baz; + + public Boink () + { + } + + public Boink (String foo, int bar, float baz) + { + this.foo = foo; + this.bar = bar; + this.baz = baz; + } + + public boolean equals (Object other) + { + Boink bo = (Boink)other; + return (bo.foo.equals(foo) && + bo.bar == bar && + bo.baz == baz); + } + } + + public static class Blink extends Boink + { + public int bizz; + + public Blink () + { + } + + public Blink (String foo, int bar, float baz, int bizz ) + { + super(foo, bar, baz); + this.bizz = bizz; + } + + public boolean equals (Object other) + { + Blink bo = (Blink)other; + return (bo.foo.equals(foo) && + bo.bar == bar && + bo.baz == baz && + bo.bizz == bizz); + } + } + + public void runTest () + { + int bcount = 15; + Boink[] boinks = new Boink[bcount]; + for (int i = 0; i < bcount; i++) { + if (i % 2 == 0) { + boinks[i] = new Boink(Integer.toString(i), i, + (float)i + (float)0.5); + } else { + boinks[i] = new Blink(Integer.toString(i), i, + (float)i + (float)0.5, 100-i); + } + } + // System.out.println("boinks: " + StringUtil.toString(boinks)); + + try { + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + DataOutputStream dout = new DataOutputStream(bout); + StreamableUtil.writeStreamables(dout, boinks); + dout.flush(); + + byte[] data = bout.toByteArray(); + // System.out.println(bcount + " boinks takes up " + + // data.length + " bytes."); + + ByteArrayInputStream bin = new ByteArrayInputStream(data); + DataInputStream din = new DataInputStream(bin); + Boink[] nboinks = (Boink[])StreamableUtil.readStreamables(din); + + // make sure all went well + assert("boinks == nboinks", Arrays.equals(boinks, nboinks)); + + // System.out.println("nboinks: " + StringUtil.toString(nboinks)); + + } catch (Exception e) { + e.printStackTrace(System.err); + fail("exception: " + e); + } + } + + public static Test suite () + { + return new StreamableUtilTest(); + } + + public static void main (String[] args) + { + StreamableUtilTest test = new StreamableUtilTest(); + test.runTest(); + } +}