diff --git a/src/java/com/threerings/io/ArrayMask.java b/src/java/com/threerings/io/ArrayMask.java new file mode 100644 index 000000000..f373453d7 --- /dev/null +++ b/src/java/com/threerings/io/ArrayMask.java @@ -0,0 +1,73 @@ +// +// $Id: ArrayMask.java,v 1.1 2002/07/23 05:42:34 mdb Exp $ + +package com.threerings.io; + +import java.io.IOException; + +/** + * Used to keep track of which entries in an array are null and which are + * not. Note: only arrays up to 262,144 elements in length can be + * handled by this class. + */ +public class ArrayMask +{ + /** + * Creates an array mask suitable for unserializing. + */ + public ArrayMask () + { + } + + /** + * Creates an array mask for an array of the specified length. + */ + public ArrayMask (int length) + { + int mlength = length/8; + if (length % 8 != 0) { + mlength++; + } + _mask = new byte[mlength]; + } + + /** + * Sets the bit indicating that the specified array index is non-null. + */ + public void set (int index) + { + _mask[index/8] |= (1 << (index%8)); + } + + /** + * Returns true if the specified array index should be non-null. + */ + public boolean isSet (int index) + { + return (_mask[index/8] & (1 << (index%8))) != 0; + } + + /** + * Writes this mask to the specified output stream. + */ + public void writeTo (ObjectOutputStream out) + throws IOException + { + out.writeShort(_mask.length); + out.write(_mask); + } + + /** + * Reads this mask from the specified input stream. + */ + public void readFrom (ObjectInputStream in) + throws IOException + { + int length = in.readShort(); + _mask = new byte[length]; + in.read(_mask); + } + + /** A byte array with bits for every entry in the source array. */ + protected byte[] _mask; +} diff --git a/src/java/com/threerings/io/BasicStreamers.java b/src/java/com/threerings/io/BasicStreamers.java new file mode 100644 index 000000000..a62ffdf88 --- /dev/null +++ b/src/java/com/threerings/io/BasicStreamers.java @@ -0,0 +1,629 @@ +// +// $Id: BasicStreamers.java,v 1.1 2002/07/23 05:42:34 mdb Exp $ + +package com.threerings.io; + +import java.io.EOFException; +import java.io.IOException; +import java.util.HashMap; + +/** + * Code to read and write basic object types (like arrays of primitives, + * {@link Integer} instances, {@link Double} instances, etc.). + */ +public class BasicStreamers +{ + /** An array of types for all of the basic streamers. */ + public static Class[] BSTREAMER_TYPES = { + Boolean.class, + Byte.class, + Short.class, + Character.class, + Integer.class, + Long.class, + Float.class, + Double.class, + String.class, + (new boolean[0]).getClass(), + (new byte[0]).getClass(), + (new short[0]).getClass(), + (new char[0]).getClass(), + (new int[0]).getClass(), + (new long[0]).getClass(), + (new float[0]).getClass(), + (new double[0]).getClass(), + (new Object[0]).getClass(), + }; + + /** An array of instances of all of the basic streamers. */ + public static Streamer[] BSTREAMER_INSTANCES = { + new BooleanStreamer(), + new ByteStreamer(), + new ShortStreamer(), + new CharacterStreamer(), + new IntegerStreamer(), + new LongStreamer(), + new FloatStreamer(), + new DoubleStreamer(), + new StringStreamer(), + new BooleanArrayStreamer(), + new ByteArrayStreamer(), + new ShortArrayStreamer(), + new CharArrayStreamer(), + new IntArrayStreamer(), + new LongArrayStreamer(), + new FloatArrayStreamer(), + new DoubleArrayStreamer(), + new ObjectArrayStreamer(), + }; + + /** Streams {@link Boolean} instances. */ + public static class BooleanStreamer extends Streamer + { + // documentation inherited + public Object createObject (ObjectInputStream in) + throws IOException + { + return new Boolean(in.readBoolean()); + } + + // documentation inherited + public void writeObject ( + Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + out.writeBoolean(((Boolean)object).booleanValue()); + } + + // documentation inherited + public void readObject ( + Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + // nothing to do here + } + } + + /** Streams {@link Byte} instances. */ + public static class ByteStreamer extends Streamer + { + // documentation inherited + public Object createObject (ObjectInputStream in) + throws IOException + { + return new Byte(in.readByte()); + } + + // documentation inherited + public void writeObject ( + Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + out.writeByte(((Byte)object).byteValue()); + } + + // documentation inherited + public void readObject ( + Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + // nothing to do here + } + } + + /** Streams {@link Short} instances. */ + public static class ShortStreamer extends Streamer + { + // documentation inherited + public Object createObject (ObjectInputStream in) + throws IOException + { + return new Short(in.readShort()); + } + + // documentation inherited + public void writeObject ( + Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + out.writeShort(((Short)object).shortValue()); + } + + // documentation inherited + public void readObject ( + Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + // nothing to do here + } + } + + /** Streams {@link Character} instances. */ + public static class CharacterStreamer extends Streamer + { + // documentation inherited + public Object createObject (ObjectInputStream in) + throws IOException + { + return new Character(in.readChar()); + } + + // documentation inherited + public void writeObject ( + Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + out.writeChar(((Character)object).charValue()); + } + + // documentation inherited + public void readObject ( + Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + // nothing to do here + } + } + + /** Streams {@link Integer} instances. */ + public static class IntegerStreamer extends Streamer + { + // documentation inherited + public Object createObject (ObjectInputStream in) + throws IOException + { + return new Integer(in.readInt()); + } + + // documentation inherited + public void writeObject ( + Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + out.writeInt(((Integer)object).intValue()); + } + + // documentation inherited + public void readObject ( + Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + // nothing to do here + } + } + + /** Streams {@link Long} instances. */ + public static class LongStreamer extends Streamer + { + // documentation inherited + public Object createObject (ObjectInputStream in) + throws IOException + { + return new Long(in.readLong()); + } + + // documentation inherited + public void writeObject ( + Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + out.writeLong(((Long)object).longValue()); + } + + // documentation inherited + public void readObject ( + Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + // nothing to do here + } + } + + /** Streams {@link Float} instances. */ + public static class FloatStreamer extends Streamer + { + // documentation inherited + public Object createObject (ObjectInputStream in) + throws IOException + { + return new Float(in.readFloat()); + } + + // documentation inherited + public void writeObject ( + Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + out.writeFloat(((Float)object).floatValue()); + } + + // documentation inherited + public void readObject ( + Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + // nothing to do here + } + } + + /** Streams {@link Double} instances. */ + public static class DoubleStreamer extends Streamer + { + // documentation inherited + public Object createObject (ObjectInputStream in) + throws IOException + { + return new Double(in.readDouble()); + } + + // documentation inherited + public void writeObject ( + Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + out.writeDouble(((Double)object).doubleValue()); + } + + // documentation inherited + public void readObject ( + Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + // nothing to do here + } + } + + /** Streams {@link String} instances. */ + public static class StringStreamer extends Streamer + { + // documentation inherited + public Object createObject (ObjectInputStream in) + throws IOException + { + return in.readUTF(); + } + + // documentation inherited + public void writeObject ( + Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + out.writeUTF((String)object); + } + + // documentation inherited + public void readObject ( + Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + // nothing to do here + } + } + + /** Streams arrays of booleans. */ + public static class BooleanArrayStreamer extends Streamer + { + // documentation inherited + public Object createObject (ObjectInputStream in) + throws IOException + { + return new boolean[in.readInt()]; + } + + // documentation inherited + public void writeObject ( + Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + boolean[] value = (boolean[])object; + int ecount = value.length; + out.writeInt(ecount); + for (int ii = 0; ii < ecount; ii++) { + out.writeBoolean(value[ii]); + } + } + + // documentation inherited + public void readObject ( + Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + boolean[] value = (boolean[])object; + int ecount = value.length; + for (int ii = 0; ii < ecount; ii++) { + value[ii] = in.readBoolean(); + } + } + } + + /** Streams arrays of bytes. */ + public static class ByteArrayStreamer extends Streamer + { + // documentation inherited + public Object createObject (ObjectInputStream in) + throws IOException + { + return new byte[in.readInt()]; + } + + // documentation inherited + public void writeObject ( + Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + byte[] value = (byte[])object; + int ecount = value.length; + out.writeInt(ecount); + out.write(value); + } + + // documentation inherited + public void readObject ( + Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + byte[] value = (byte[])object; + int remain = value.length, offset = 0, read; + while (remain > 0) { + if ((read = in.read(value, offset, remain)) > 0) { + remain -= read; + offset += read; + } else { + throw new EOFException(); + } + } + } + } + + /** Streams arrays of shorts. */ + public static class ShortArrayStreamer extends Streamer + { + // documentation inherited + public Object createObject (ObjectInputStream in) + throws IOException + { + return new short[in.readInt()]; + } + + // documentation inherited + public void writeObject ( + Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + short[] value = (short[])object; + int ecount = value.length; + out.writeInt(ecount); + for (int ii = 0; ii < ecount; ii++) { + out.writeShort(value[ii]); + } + } + + // documentation inherited + public void readObject ( + Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + short[] value = (short[])object; + int ecount = value.length; + for (int ii = 0; ii < ecount; ii++) { + value[ii] = in.readShort(); + } + } + } + + /** Streams arrays of chars. */ + public static class CharArrayStreamer extends Streamer + { + // documentation inherited + public Object createObject (ObjectInputStream in) + throws IOException + { + return new char[in.readInt()]; + } + + // documentation inherited + public void writeObject ( + Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + char[] value = (char[])object; + int ecount = value.length; + out.writeInt(ecount); + for (int ii = 0; ii < ecount; ii++) { + out.writeChar(value[ii]); + } + } + + // documentation inherited + public void readObject ( + Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + char[] value = (char[])object; + int ecount = value.length; + for (int ii = 0; ii < ecount; ii++) { + value[ii] = in.readChar(); + } + } + } + + /** Streams arrays of ints. */ + public static class IntArrayStreamer extends Streamer + { + // documentation inherited + public Object createObject (ObjectInputStream in) + throws IOException + { + return new int[in.readInt()]; + } + + // documentation inherited + public void writeObject ( + Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + int[] value = (int[])object; + int ecount = value.length; + out.writeInt(ecount); + for (int ii = 0; ii < ecount; ii++) { + out.writeInt(value[ii]); + } + } + + // documentation inherited + public void readObject ( + Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + int[] value = (int[])object; + int ecount = value.length; + for (int ii = 0; ii < ecount; ii++) { + value[ii] = in.readInt(); + } + } + } + + /** Streams arrays of longs. */ + public static class LongArrayStreamer extends Streamer + { + // documentation inherited + public Object createObject (ObjectInputStream in) + throws IOException + { + return new long[in.readInt()]; + } + + // documentation inherited + public void writeObject ( + Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + long[] value = (long[])object; + int ecount = value.length; + out.writeInt(ecount); + for (int ii = 0; ii < ecount; ii++) { + out.writeLong(value[ii]); + } + } + + // documentation inherited + public void readObject ( + Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + long[] value = (long[])object; + int ecount = value.length; + for (int ii = 0; ii < ecount; ii++) { + value[ii] = in.readLong(); + } + } + } + + /** Streams arrays of floats. */ + public static class FloatArrayStreamer extends Streamer + { + // documentation inherited + public Object createObject (ObjectInputStream in) + throws IOException + { + return new float[in.readInt()]; + } + + // documentation inherited + public void writeObject ( + Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + float[] value = (float[])object; + int ecount = value.length; + out.writeInt(ecount); + for (int ii = 0; ii < ecount; ii++) { + out.writeFloat(value[ii]); + } + } + + // documentation inherited + public void readObject ( + Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + float[] value = (float[])object; + int ecount = value.length; + for (int ii = 0; ii < ecount; ii++) { + value[ii] = in.readFloat(); + } + } + } + + /** Streams arrays of doubles. */ + public static class DoubleArrayStreamer extends Streamer + { + // documentation inherited + public Object createObject (ObjectInputStream in) + throws IOException + { + return new double[in.readInt()]; + } + + // documentation inherited + public void writeObject ( + Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + double[] value = (double[])object; + int ecount = value.length; + out.writeInt(ecount); + for (int ii = 0; ii < ecount; ii++) { + out.writeDouble(value[ii]); + } + } + + // documentation inherited + public void readObject ( + Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + double[] value = (double[])object; + int ecount = value.length; + for (int ii = 0; ii < ecount; ii++) { + value[ii] = in.readDouble(); + } + } + } + + /** Streams arrays of Object instances. */ + public static class ObjectArrayStreamer extends Streamer + { + // documentation inherited + public Object createObject (ObjectInputStream in) + throws IOException + { + return new Object[in.readInt()]; + } + + // documentation inherited + public void writeObject ( + Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + Object[] value = (Object[])object; + int ecount = value.length; + out.writeInt(ecount); + for (int ii = 0; ii < ecount; ii++) { + out.writeObject(value[ii]); + } + } + + // documentation inherited + public void readObject ( + Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + Object[] value = (Object[])object; + int ecount = value.length; + for (int ii = 0; ii < ecount; ii++) { + value[ii] = in.readObject(); + } + } + } +} diff --git a/src/java/com/threerings/io/ClassMapping.java b/src/java/com/threerings/io/ClassMapping.java new file mode 100644 index 000000000..a3c7caecb --- /dev/null +++ b/src/java/com/threerings/io/ClassMapping.java @@ -0,0 +1,27 @@ +// +// $Id: ClassMapping.java,v 1.1 2002/07/23 05:42:34 mdb Exp $ + +package com.threerings.io; + +/** + * Used by the {@link ObjectOutputStream} and {@link ObjectInputStream} to + * map classes to codes during a session. + */ +class ClassMapping +{ + public short code; + public Class sclass; + public Streamer streamer; + + public ClassMapping (short code, Class sclass, Streamer streamer) + { + this.code = code; + this.sclass = sclass; + this.streamer = streamer; + } + + public String toString () + { + return "[code=" + code + ", class=" + sclass.getName() + "]"; + } +} diff --git a/src/java/com/threerings/io/FieldMarshaller.java b/src/java/com/threerings/io/FieldMarshaller.java new file mode 100644 index 000000000..000f6e774 --- /dev/null +++ b/src/java/com/threerings/io/FieldMarshaller.java @@ -0,0 +1,244 @@ +// +// $Id: FieldMarshaller.java,v 1.1 2002/07/23 05:42:34 mdb Exp $ + +package com.threerings.io; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.HashMap; + +import com.threerings.presents.Log; + +/** + * Used to read and write a single field of a {@link Streamable} instance. + */ +public abstract class FieldMarshaller +{ + /** + * Reads the contents of the supplied field from the supplied stream + * and sets it in the supplied object. + */ + public abstract void readField ( + Field field, Object target, ObjectInputStream in) throws Exception; + + /** + * Writes the contents of the supplied field in the supplied object to + * the supplied stream. + */ + public abstract void writeField ( + Field field, Object source, ObjectOutputStream out) throws Exception; + + /** + * Returns a field marshaller appropriate for the supplied field or + * null if no marshaller exists for the type contained by the field in + * question. + */ + public static FieldMarshaller getFieldMarshaller (Field field) + { + if (_marshallers == null) { + createMarshallers(); + } + + // if we have an exact match, use that + Class ftype = field.getType(); + FieldMarshaller fm = (FieldMarshaller)_marshallers.get(ftype); + + // otherwise if the class is a streamable, use the streamable + // marshaller + if (fm == null && Streamer.isStreamable(ftype)) { + fm = (FieldMarshaller)_marshallers.get(Streamable.class); + } + + return fm; + } + + /** + * Used to marshall and unmarshall classes for which we have a basic + * {@link Streamer}. + */ + protected static class StreamerMarshaller extends FieldMarshaller + { + public StreamerMarshaller (Streamer streamer) + { + _streamer = streamer; + } + + // documentation inherited + public void readField ( + Field field, Object target, ObjectInputStream in) + throws Exception + { + if (in.readBoolean()) { + Object value = _streamer.createObject(in); + _streamer.readObject(value, in, true); + field.set(target, value); + } else { + field.set(target, null); + } + } + + // documentation inherited + public void writeField ( + Field field, Object source, ObjectOutputStream out) + throws Exception + { + Object value = field.get(source); + if (value == null) { + out.writeBoolean(false); + } else { + out.writeBoolean(true); + _streamer.writeObject(value, out, true); + } + } + + /** The streamer we use to read and write our field. */ + protected Streamer _streamer; + } + + /** + * Creates instances of all known field marshaller types and populates + * the {@link _marshallers} table with them. This is called the first + * time a marshaller is requested. + */ + protected static void createMarshallers () + { + // create our table + _marshallers = new HashMap(); + + // create a marshaller for streamable instances + _marshallers.put(Streamable.class, new FieldMarshaller() { + public void readField ( + Field field, Object target, ObjectInputStream in) + throws Exception { + field.set(target, in.readObject()); + } + public void writeField ( + Field field, Object source, ObjectOutputStream out) + throws Exception { + out.writeObject(field.get(source)); + } + }); + + // create marshallers for the primitive types + _marshallers.put(Boolean.TYPE, new FieldMarshaller() { + public void readField ( + Field field, Object target, ObjectInputStream in) + throws Exception { + field.setBoolean(target, in.readBoolean()); + } + public void writeField ( + Field field, Object source, ObjectOutputStream out) + throws Exception { + out.writeBoolean(field.getBoolean(source)); + } + }); + _marshallers.put(Byte.TYPE, new FieldMarshaller() { + public void readField ( + Field field, Object target, ObjectInputStream in) + throws Exception { + field.setByte(target, in.readByte()); + } + public void writeField ( + Field field, Object source, ObjectOutputStream out) + throws Exception { + out.writeByte(field.getByte(source)); + } + }); + _marshallers.put(Character.TYPE, new FieldMarshaller() { + public void readField ( + Field field, Object target, ObjectInputStream in) + throws Exception { + field.setChar(target, in.readChar()); + } + public void writeField ( + Field field, Object source, ObjectOutputStream out) + throws Exception { + out.writeChar(field.getChar(source)); + } + }); + _marshallers.put(Short.TYPE, new FieldMarshaller() { + public void readField ( + Field field, Object target, ObjectInputStream in) + throws Exception { + field.setShort(target, in.readShort()); + } + public void writeField ( + Field field, Object source, ObjectOutputStream out) + throws Exception { + out.writeShort(field.getShort(source)); + } + }); + _marshallers.put(Integer.TYPE, new FieldMarshaller() { + public void readField ( + Field field, Object target, ObjectInputStream in) + throws Exception { + field.setInt(target, in.readInt()); + } + public void writeField ( + Field field, Object source, ObjectOutputStream out) + throws Exception { + out.writeInt(field.getInt(source)); + } + }); + _marshallers.put(Long.TYPE, new FieldMarshaller() { + public void readField ( + Field field, Object target, ObjectInputStream in) + throws Exception { + field.setLong(target, in.readLong()); + } + public void writeField ( + Field field, Object source, ObjectOutputStream out) + throws Exception { + out.writeLong(field.getLong(source)); + } + }); + _marshallers.put(Float.TYPE, new FieldMarshaller() { + public void readField ( + Field field, Object target, ObjectInputStream in) + throws Exception { + field.setFloat(target, in.readFloat()); + } + public void writeField ( + Field field, Object source, ObjectOutputStream out) + throws Exception { + out.writeFloat(field.getFloat(source)); + } + }); + _marshallers.put(Double.TYPE, new FieldMarshaller() { + public void readField ( + Field field, Object target, ObjectInputStream in) + throws Exception { + field.setDouble(target, in.readDouble()); + } + public void writeField ( + Field field, Object source, ObjectOutputStream out) + throws Exception { + out.writeDouble(field.getDouble(source)); + } + }); + _marshallers.put(String.class, new FieldMarshaller() { + public void readField ( + Field field, Object target, ObjectInputStream in) + throws Exception { + field.set(target, in.readUTF()); + } + public void writeField ( + Field field, Object source, ObjectOutputStream out) + throws Exception { + out.writeUTF((String)field.get(source)); + } + }); + + // create field marshallers for all of the basic types + int bscount = BasicStreamers.BSTREAMER_TYPES.length; + for (int ii = 0; ii < bscount; ii++) { + _marshallers.put(BasicStreamers.BSTREAMER_TYPES[ii], + new StreamerMarshaller( + BasicStreamers.BSTREAMER_INSTANCES[ii])); + } + } + + /** Contains a mapping from field type to field marshaller instance + * for that type. */ + protected static HashMap _marshallers; +} diff --git a/src/java/com/threerings/io/FramedInputStream.java b/src/java/com/threerings/io/FramedInputStream.java new file mode 100644 index 000000000..08bffa030 --- /dev/null +++ b/src/java/com/threerings/io/FramedInputStream.java @@ -0,0 +1,261 @@ +// +// $Id: FramedInputStream.java,v 1.1 2002/07/23 05:42:34 mdb Exp $ + +package com.threerings.io; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +import com.samskivert.util.StringUtil; +import com.threerings.presents.Log; + +/** + * The framed input stream reads input that was framed by a framing output + * stream. Framing in this case simply means writing the length of the + * frame followed by the data associated with the frame so that an entire + * frame can be loaded from the network layer before any higher layer + * attempts to process it. Additionally, any failure in decoding a frame + * won't result in the entire stream being skewed due to the remainder of + * the undecoded frame remaining in the input stream. + * + *
The framed input stream reads an entire frame worth of data into its
+ * internal buffer when readFrame() is called. It then
+ * behaves as if this is the only data available on the stream (meaning
+ * that when the data in the frame is exhausted, it will behave as if the
+ * end of the stream has been reached). The buffer can only contain a
+ * single frame at a time, so any data left over from a previous frame
+ * will disappear when readFrame() is called again.
+ *
+ *
Note: The framing input stream does not synchronize reads + * from its internal buffer. It is intended to only be accessed from a + * single thread. + * + *
Implementation note: maybe this should derive from
+ * FilterInputStream and be tied to a single
+ * InputStream for its lifetime.
+ */
+public class FramedInputStream extends InputStream
+{
+ public FramedInputStream ()
+ {
+ _header = new byte[HEADER_SIZE];
+ _buffer = new byte[INITIAL_BUFFER_SIZE];
+ }
+
+ /**
+ * Reads a frame from the provided input stream, or appends to a
+ * partially read frame. Appends the read data to the existing data
+ * available via the framed input stream's read methods. If the entire
+ * frame data is not yet available, readFrame will return
+ * false, otherwise true.
+ *
+ *
The code assumes that it will be able to read the entire frame
+ * header in a single read. The header is only four bytes and should
+ * always arrive at the beginning of a packet, so unless something is
+ * very funky with the networking layer, this should be a safe
+ * assumption.
+ *
+ * @return true if the entire frame has been read, false if the buffer
+ * contains only a partial frame.
+ */
+ public boolean readFrame (InputStream source)
+ throws IOException
+ {
+ // if the buffer currently contains a complete frame, that means
+ // we're not halfway through reading a frame and that we can start
+ // anew.
+ if (_count == _length) {
+ // read in the frame length
+ int got = source.read(_header, 0, HEADER_SIZE);
+ if (got < 0) {
+ throw new EOFException();
+
+ } else if (got == 0) {
+ // TBD: don't log this for now, but look into it later
+ // Log.info("Woke up to read data, but there ain't none. Sigh.");
+ return false;
+
+ } else if (got < HEADER_SIZE) {
+ String errmsg = "FramedInputStream does not support " +
+ "partially reading the header. Needed " + HEADER_SIZE +
+ " bytes, got " + got + " bytes.";
+ throw new RuntimeException(errmsg);
+ }
+
+ // now that we've read our new frame length, we can clear out
+ // any prior data
+ _pos = 0;
+ _count = 0;
+
+ // decode the frame length
+ _length = (_header[0] & 0xFF) << 24;
+ _length += (_header[1] & 0xFF) << 16;
+ _length += (_header[2] & 0xFF) << 8;
+ _length += (_header[3] & 0xFF);
+
+ // if necessary, expand our buffer to accomodate the frame
+ if (_length > _buffer.length) {
+ // increase the buffer size in large increments
+ _buffer = new byte[Math.max(_buffer.length << 1, _length)];
+ }
+ }
+
+ // read the data into the buffer
+ int got = source.read(_buffer, _count, _length-_count);
+ if (got < 0) {
+ throw new EOFException();
+ }
+ _count += got;
+
+// System.err.println("Read frame " + _count +
+// " (want " + _length + " pos " + _pos + ")");
+
+ return (_count == _length);
+ }
+
+ /**
+ * Reads the next byte of data from this input stream. The value byte
+ * is returned as an int in the range 0 to
+ * 255. If no byte is available because the end of the
+ * stream has been reached, the value -1 is returned.
+ *
+ *
This read method cannot block.
+ *
+ * @return the next byte of data, or -1 if the end of the
+ * stream has been reached.
+ */
+ public int read ()
+ {
+ return (_pos < _count) ? (_buffer[_pos++] & 0xFF) : -1;
+ }
+
+ /**
+ * Reads up to len bytes of data into an array of bytes
+ * from this input stream. If pos equals
+ * count, then -1 is returned to indicate
+ * end of file. Otherwise, the number k of bytes read is
+ * equal to the smaller of len and
+ * count-pos. If k is positive, then bytes
+ * buf[pos] through buf[pos+k-1] are copied
+ * into b[off] through b[off+k-1] in the
+ * manner performed by System.arraycopy. The value
+ * k is added into pos and k is
+ * returned.
+ *
+ *
This read method cannot block.
+ *
+ * @param b the buffer into which the data is read.
+ * @param off the start offset of the data.
+ * @param len the maximum number of bytes read.
+ *
+ * @return the total number of bytes read into the buffer, or
+ * -1 if there is no more data because the end of the
+ * stream has been reached.
+ */
+ public int read (byte[] b, int off, int len)
+ {
+ // sanity check the arguments
+ if (b == null) {
+ throw new NullPointerException();
+ } else if ((off < 0) || (off > b.length) || (len < 0) ||
+ ((off + len) > b.length) || ((off + len) < 0)) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ // figure out how much data we'll return
+ if (_pos >= _count) {
+ // if they asked to read zero bytes and we have no bytes
+ // remaining; we're supposed to return 0 rather than EOF
+ return (len == 0) ? 0 : -1;
+ }
+ if (_pos + len > _count) {
+ len = _count - _pos;
+ }
+ if (len <= 0) {
+ return 0;
+ }
+
+ // copy and advance
+ System.arraycopy(_buffer, _pos, b, off, len);
+ _pos += len;
+
+ return len;
+ }
+
+ /**
+ * Skips n bytes of input from this input stream. Fewer
+ * bytes might be skipped if the end of the input stream is reached.
+ * The actual number k of bytes to be skipped is equal to
+ * the smaller of n and count-pos. The value
+ * k is added into pos and k is
+ * returned.
+ *
+ * @param n the number of bytes to be skipped.
+ *
+ * @return the actual number of bytes skipped.
+ */
+ public long skip (long n)
+ {
+ if (_pos + n > _count) {
+ n = _count - _pos;
+ }
+ if (n <= 0) {
+ return 0;
+ }
+ _pos += n;
+ return n;
+ }
+
+ /**
+ * Returns the number of bytes that can be read from this input stream
+ * without blocking. The value returned is count - pos,
+ * which is the number of bytes remaining to be read from the input
+ * buffer.
+ *
+ * @return the number of bytes remaining to be read from the buffered
+ * frames.
+ */
+ public int available ()
+ {
+ return _count - _pos;
+ }
+
+ /**
+ * Always returns false as framed input streams do not support
+ * marking.
+ */
+ public boolean markSupported ()
+ {
+ return false;
+ }
+
+ /**
+ * Does nothing, as marking is not supported.
+ */
+ public void mark (int readAheadLimit)
+ {
+ // not supported; do nothing
+ }
+
+ /**
+ * Resets the buffer to the beginning of the buffered frames.
+ */
+ public void reset ()
+ {
+ _pos = 0;
+ }
+
+ protected byte[] _header;
+ protected int _length;
+
+ protected byte[] _buffer;
+ protected int _pos;
+ protected int _count;
+
+ /** The size of the frame header (a 32-bit integer). */
+ protected static final int HEADER_SIZE = 4;
+
+ /** The default initial size of the internal buffer. */
+ protected static final int INITIAL_BUFFER_SIZE = 32;
+}
diff --git a/src/java/com/threerings/io/FramingOutputStream.java b/src/java/com/threerings/io/FramingOutputStream.java
new file mode 100644
index 000000000..f3725ea1c
--- /dev/null
+++ b/src/java/com/threerings/io/FramingOutputStream.java
@@ -0,0 +1,125 @@
+//
+// $Id: FramingOutputStream.java,v 1.1 2002/07/23 05:42:34 mdb Exp $
+
+package com.threerings.io;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * The framing output stream accumulates output into a byte array just
+ * like the byte array output stream, but can then be instructed to send
+ * its contents down another output stream, prefixed by the length
+ * (written as an integer) of those contents. It does this efficiently so
+ * that data is copied as little as possible and so that the output stream
+ * to which the data is written need not be buffered because the framed
+ * output is written in a single call to write().
+ *
+ *
Note: The framing output stream does not synchronize writes + * to its internal buffer. It is intended to only be accessed from a + * single thread. + * + *
Implementation note: maybe this should derive from
+ * FilterOutputStream and be tied to a single
+ * OutputStream for its lifetime.
+ */
+public class FramingOutputStream extends OutputStream
+{
+ public FramingOutputStream ()
+ {
+ _buffer = new byte[INITIAL_BUFFER_SIZE];
+ _count = 4; // leave room for the frame size at the beginning
+ }
+
+ /**
+ * Writes the specified byte to this framing output stream.
+ *
+ * @param b the byte to be written.
+ */
+ public void write (int b)
+ {
+ // expand our buffer if necessary
+ int newcount = _count + 1;
+ if (newcount > _buffer.length) {
+ // increase the buffer size in large increments
+ byte[] newbuf = new byte[Math.max(_buffer.length << 1, newcount)];
+ System.arraycopy(_buffer, 0, newbuf, 0, _count);
+ _buffer = newbuf;
+ }
+
+ // copy and advance
+ _buffer[_count] = (byte)b;
+ _count = newcount;
+ }
+
+ /**
+ * Writes len bytes from the specified byte array
+ * starting at offset off to this framing output stream.
+ *
+ * @param b the data.
+ * @param off the start offset in the data.
+ * @param len the number of bytes to write.
+ */
+ public void write (byte[] b, int off, int len)
+ {
+ // sanity check the arguments
+ if ((off < 0) || (off > b.length) || (len < 0) ||
+ ((off + len) > b.length) || ((off + len) < 0)) {
+ throw new IndexOutOfBoundsException();
+ } else if (len == 0) {
+ return;
+ }
+
+ // expand the buffer if necessary
+ int newcount = _count + len;
+ if (newcount > _buffer.length) {
+ // increase the buffer size in large increments
+ byte[] newbuf = new byte[Math.max(_buffer.length << 1, newcount)];
+ System.arraycopy(_buffer, 0, newbuf, 0, _count);
+ _buffer = newbuf;
+ }
+
+ // copy and advance
+ System.arraycopy(b, off, _buffer, _count, len);
+ _count = newcount;
+ }
+
+ /**
+ * Writes the contents of this framing output stream to the target
+ * output stream, prefixed by an integer with value equal to the
+ * number of bytes written folling that integer. It then resets the
+ * framing output stream to prepare for another framed message.
+ */
+ public void writeFrameAndReset (OutputStream target)
+ throws IOException
+ {
+ // prefix the frame with the byte count in network byte order (the
+ // format used by DataOutputStream)
+ int count = _count - 4;
+ _buffer[0] = (byte)((count >>> 24) & 0xFF);
+ _buffer[1] = (byte)((count >>> 16) & 0xFF);
+ _buffer[2] = (byte)((count >>> 8) & 0xFF);
+ _buffer[3] = (byte)((count >>> 0) & 0xFF);
+
+ // write the data
+ target.write(_buffer, 0, _count);
+
+// System.err.println("Wrote frame " + (_count-4));
+
+ // reset our internal buffer
+ reset();
+ }
+
+ public void reset ()
+ {
+ // leave room for the frame size at the beginning
+ _count = 4;
+ }
+
+ protected byte[] _buffer;
+ protected int _count;
+
+ /** The default initial size of the internal buffer. */
+ protected static final int INITIAL_BUFFER_SIZE = 32;
+}
diff --git a/src/java/com/threerings/io/ObjectInputStream.java b/src/java/com/threerings/io/ObjectInputStream.java
new file mode 100644
index 000000000..6cb8d39e4
--- /dev/null
+++ b/src/java/com/threerings/io/ObjectInputStream.java
@@ -0,0 +1,169 @@
+//
+// $Id: ObjectInputStream.java,v 1.1 2002/07/23 05:42:34 mdb Exp $
+
+package com.threerings.io;
+
+import java.io.InputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+
+import com.samskivert.util.HashIntMap;
+
+import com.threerings.presents.Log;
+
+/**
+ * Used to read {@link Streamable} objects from an {@link InputStream}.
+ * Other common object types are supported as well (@see {@link
+ * ObjectOutputStream}).
+ *
+ * @see Streamable
+ */
+public class ObjectInputStream extends DataInputStream
+{
+ /**
+ * Constructs an object input stream which will read its data from the
+ * supplied source stream.
+ */
+ public ObjectInputStream (InputStream source)
+ {
+ super(source);
+ }
+
+ /**
+ * Reads a {@link Streamable} instance or one of the supported object
+ * types from the input stream.
+ */
+ public Object readObject ()
+ throws IOException, ClassNotFoundException
+ {
+ ClassMapping cmap;
+
+ // create our classmap if necessary
+ if (_classmap == null) {
+ _classmap = new HashIntMap();
+ }
+
+ try {
+ // read in the class code for this instance
+ short code = readShort();
+
+ // a zero code indicates a null value
+ if (code == 0) {
+ return null;
+
+ // if the code is negative, that means that we've never
+ // seen it before and class metadata follows
+ } else if (code < 0) {
+ // first swap the code into positive-land
+ code *= -1;
+
+ // read in the class metadata, create a class mapping record
+ // for the class, and cache it
+ String cname = readUTF();
+ Class sclass = Class.forName(cname);
+ Streamer streamer = Streamer.getStreamer(sclass);
+
+ // sanity check
+ if (streamer == null) {
+ String errmsg = "Aiya! Unable to create streamer for " +
+ "newly seen class [code=" + code +
+ ", class=" + cname + "]";
+ throw new RuntimeException(errmsg);
+ }
+
+ cmap = new ClassMapping(code, sclass, streamer);
+ _classmap.put(code, cmap);
+
+ } else {
+ cmap = (ClassMapping)_classmap.get(code);
+
+ // sanity check
+ if (cmap == null) {
+ String errmsg = "Aiya! Read object code for which we " +
+ "have no registered class metadata [code=" + code + "]";
+ throw new RuntimeException(errmsg);
+ }
+ }
+
+ // create an instance of the appropriate object
+ _streamer = cmap.streamer;
+ Object target = _streamer.createObject(this);
+ _current = target;
+
+ // now read the instance data
+ _streamer.readObject(target, this, true);
+
+ // and return the newly read object
+ return target;
+
+ } finally {
+ // clear out our current object references
+ _current = null;
+ _streamer = null;
+ }
+ }
+
+ /**
+ * Reads an object from the input stream that was previously written
+ * with {@link ObjectOutputStream#writeBareObject}.
+ *
+ * @param object the object to be populated from data on the stream.
+ * It cannot be null.
+ */
+ public void readBareObject (Object object)
+ throws IOException, ClassNotFoundException
+ {
+ try {
+ // obtain the streamer for objects of this class
+ _streamer = Streamer.getStreamer(object.getClass());
+ _current = object;
+
+ // now read the instance data
+ _streamer.readObject(object, this, true);
+
+ } finally {
+ // clear out our current object references
+ _current = null;
+ _streamer = null;
+ }
+ }
+
+ /**
+ * Reads the fields of the specified {@link Streamable} instance from
+ * the input stream using the default object streaming mechanisms (a
+ * call is not made to readObject(), even if such a
+ * method exists).
+ */
+ public void defaultReadObject ()
+ throws IOException, ClassNotFoundException
+ {
+ // sanity check
+ if (_current == null) {
+ throw new RuntimeException(
+ "defaultReadObject() called illegally.");
+ }
+
+ // read the instance data
+ _streamer.readObject(_current, this, false);
+ }
+
+ /**
+ * Used by a {@link Streamer} that is reading an array of {@link
+ * Streamable} instances.
+ */
+ protected void setCurrent (Streamer streamer, Object current)
+ {
+ _streamer = streamer;
+ _current = current;
+ }
+
+ /** Used to map classes to numeric codes and the {@link Streamer}
+ * instance used to write them. */
+ protected HashIntMap _classmap;
+
+ /** The object currently being read from the stream. */
+ protected Object _current;
+
+ /** The streamer being used currently. */
+ protected Streamer _streamer;
+}
diff --git a/src/java/com/threerings/io/ObjectOutputStream.java b/src/java/com/threerings/io/ObjectOutputStream.java
new file mode 100644
index 000000000..b111f68fd
--- /dev/null
+++ b/src/java/com/threerings/io/ObjectOutputStream.java
@@ -0,0 +1,183 @@
+//
+// $Id: ObjectOutputStream.java,v 1.1 2002/07/23 05:42:34 mdb Exp $
+
+package com.threerings.io;
+
+import java.io.OutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.HashMap;
+
+import com.threerings.presents.Log;
+
+/**
+ * Used to write {@link Streamable} objects to an {@link OutputStream}.
+ * Other common object types are supported as well:
+ *
+ *
+ * Boolean + * Byte + * Character + * Short + * Integer + * Long + * Float + * Double + * boolean[] + * byte[] + * char[] + * short[] + * int[] + * long[] + * float[] + * double[] + * Object[] + *+ * + * @see Streamable + */ +public class ObjectOutputStream extends DataOutputStream +{ + /** + * Constructs an object output stream which will write its data to the + * supplied target stream. + */ + public ObjectOutputStream (OutputStream target) + { + super(target); + } + + /** + * Writes a {@link Streamable} instance or one of the support object + * types to the output stream. + */ + public void writeObject (Object object) + throws IOException + { + // if the object to be written is null, simply write a zero + if (object == null) { + writeShort(0); + return; + } + + // create our classmap if necessary + if (_classmap == null) { + _classmap = new HashMap(); + } + + try { + // otherwise, look up the class mapping record + Class sclass = object.getClass(); + ClassMapping cmap = (ClassMapping)_classmap.get(sclass); + _current = object; + + // create a class mapping for this class if we've not got one + if (cmap == null) { + // create a streamer instance and assign a code to this class + Streamer streamer = Streamer.getStreamer(sclass); + // we specifically do not inline the getStreamer() call + // into the ClassMapping constructor because we want to be + // sure not to call _nextCode++ if getStreamer() throws an + // exception + cmap = new ClassMapping(_nextCode++, sclass, streamer); + _classmap.put(sclass, cmap); + + // make sure we didn't blow past our maximum class count + if (_nextCode <= 0) { + throw new RuntimeException("Too many unique classes " + + "written to ObjectOutputStream"); + } + + // writing a negative class code indicates that the class + // name will follow + writeShort(-cmap.code); + writeUTF(sclass.getName()); + + } else { + writeShort(cmap.code); + } + + // now write the instance data + _streamer = cmap.streamer; + _streamer.writeObject(object, this, true); + + } finally { + // clear out our current object references + _current = null; + _streamer = null; + } + } + + /** + * Writes a {@link Streamable} instance or one of the support object + * types without associated class metadata to the output + * stream. The caller is responsible for knowing the exact class of + * the written object, creating an instance of such and calling {@link + * ObjectInputStream#readBareObject} to read its data from the stream. + * + * @param object the object to be written. It cannot be + *
null.
+ */
+ public void writeBareObject (Object object)
+ throws IOException
+ {
+ try {
+ // get the streamer for objects of this type
+ _streamer = Streamer.getStreamer(object.getClass());
+ _current = object;
+
+ // now write the instance data
+ _streamer.writeObject(object, this, true);
+
+ } finally {
+ // clear out our current object references
+ _current = null;
+ _streamer = null;
+ }
+ }
+
+ /**
+ * Uses the default streamable mechanism to write the contents of the
+ * object currently being streamed. This can only be called from
+ * within a writeObject implementation in a {@link
+ * Streamable} object.
+ */
+ public void defaultWriteObject ()
+ throws IOException
+ {
+ // sanity check
+ if (_current == null) {
+ throw new RuntimeException(
+ "defaultWriteObject() called illegally.");
+ }
+
+// Log.info("Writing default [cmap=" + _streamer +
+// ", current=" + _current + "].");
+
+ // write the instance data
+ _streamer.writeObject(_current, this, false);
+ }
+
+ /**
+ * Used by a {@link Streamer} that is writing an array of {@link
+ * Streamable} instances.
+ */
+ protected void setCurrent (Streamer streamer, Object current)
+ {
+ _streamer = streamer;
+ _current = current;
+ }
+
+ /** Used to map classes to numeric codes and the {@link Streamer}
+ * instance used to write them. */
+ protected HashMap _classmap;
+
+ /** A counter used to assign codes to streamed classes. */
+ protected short _nextCode = 1;
+
+ /** The object currently being written to the stream. */
+ protected Object _current;
+
+ /** The streamer being used currently. */
+ protected Streamer _streamer;
+}
diff --git a/src/java/com/threerings/io/SimpleStreamableObject.java b/src/java/com/threerings/io/SimpleStreamableObject.java
new file mode 100644
index 000000000..0fd1a9adb
--- /dev/null
+++ b/src/java/com/threerings/io/SimpleStreamableObject.java
@@ -0,0 +1,33 @@
+//
+// $Id: SimpleStreamableObject.java,v 1.1 2002/07/23 05:42:34 mdb Exp $
+
+package com.threerings.io;
+
+import com.samskivert.util.StringUtil;
+
+/**
+ * A simple serializable object implements the {@link Streamable}
+ * interface and provides a default {@link #toString} implementation which
+ * outputs all public members.
+ */
+public class SimpleStreamableObject implements Streamable
+{
+ /**
+ * Generates a string representation of this instance.
+ */
+ public String toString ()
+ {
+ StringBuffer buf = new StringBuffer("[");
+ toString(buf);
+ return buf.append("]").toString();
+ }
+
+ /**
+ * Handles the toString-ification of all public members. Derived
+ * classes can override and include non-public members if desired.
+ */
+ protected void toString (StringBuffer buf)
+ {
+ StringUtil.fieldsToString(buf, this);
+ }
+}
diff --git a/src/java/com/threerings/io/Streamable.java b/src/java/com/threerings/io/Streamable.java
new file mode 100644
index 000000000..f4a6bd649
--- /dev/null
+++ b/src/java/com/threerings/io/Streamable.java
@@ -0,0 +1,29 @@
+//
+// $Id: Streamable.java,v 1.1 2002/07/23 05:42:34 mdb Exp $
+
+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 public fields will be automatically
+ * written and restored for a {@link Streamable} instance. Classes that
+ * wish to stream non-public 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.
+ */
+public interface Streamable
+{
+}
diff --git a/src/java/com/threerings/io/Streamer.java b/src/java/com/threerings/io/Streamer.java
new file mode 100644
index 000000000..8cbc786a4
--- /dev/null
+++ b/src/java/com/threerings/io/Streamer.java
@@ -0,0 +1,430 @@
+//
+// $Id: Streamer.java,v 1.1 2002/07/23 05:42:34 mdb Exp $
+
+package com.threerings.io;
+
+import java.lang.NoSuchMethodException;
+import java.lang.reflect.Array;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import com.samskivert.io.NestableIOException;
+
+import com.threerings.presents.Log;
+
+/**
+ * Handles the streaming of {@link Streamable} instances as well as a set
+ * of basic object types (see {@link ObjectOutputStream}). An instance of
+ * {@link Streamer} is created for each distinct class that implements
+ * {@link Streamable}. The {@link Streamer} reflects on the streamed class
+ * and caches the information necessary to efficiently read and write
+ * objects of the class in question.
+ */
+public class Streamer
+{
+ /**
+ * Returns true if the supplied target class can be streamed using a
+ * streamer.
+ */
+ public synchronized static boolean isStreamable (Class target)
+ {
+ // if we have a streamer registered for this class, then it's
+ // definitely streamable
+ if (_streamers.containsKey(target)) {
+ return true;
+ }
+
+ // return true if it is a streamable or already registered type,
+ // or an array of same
+ Class uclass = target.isArray() ? target.getComponentType() : target;
+ return (_streamers.containsKey(uclass) ||
+ Streamable.class.isAssignableFrom(uclass));
+ }
+
+ /**
+ * Obtains a {@link Streamer} that can be used to read and write
+ * objects of the specified target class. {@link Streamer} instances
+ * are shared among all {@link ObjectInputStream}s and {@link
+ * ObjectOutputStream}s.
+ *
+ * @throws IOException when a streamer is requested for an object that
+ * does not implement {@link Streamable} and is not one of the basic
+ * object types (@see {@link ObjectOutputStream}).
+ */
+ public synchronized static Streamer getStreamer (Class target)
+ throws IOException
+ {
+ // if we have not yet initialized ourselves, do so now
+ if (_streamers == null) {
+ createStreamers();
+ }
+
+ Streamer stream = (Streamer)_streamers.get(target);
+ if (stream == null) {
+ // make sure this is a streamable class
+ if (!isStreamable(target)) {
+ throw new IOException("Requested to stream invalid class '" +
+ target.getName() + "'");
+ }
+
+ // create a streamer for this class and cache it
+// Log.info("Creating a streamer for '" + target.getName() + "'.");
+ stream = new Streamer(target);
+ _streamers.put(target, stream);
+ }
+ return stream;
+ }
+
+ /**
+ * Writes the supplied object to the specified stream.
+ *
+ * @param object the instance to be written to the stream.
+ * @param out the stream to which to write the instance.
+ * @param useWriter whether or not to use the custom
+ * writeObject if one exists.
+ */
+ public void writeObject (
+ Object object, ObjectOutputStream out, boolean useWriter)
+ throws IOException
+ {
+ // if we're supposed to and one exists, use the writer method
+ if (useWriter && _writer != null) {
+ try {
+// Log.info("Writing with writer " +
+// "[class=" + _target.getName() + "].");
+ _writer.invoke(object, new Object[] { out });
+
+ } catch (Throwable t) {
+ if (t instanceof InvocationTargetException) {
+ t = ((InvocationTargetException)t).getTargetException();
+ }
+ if (t instanceof IOException) {
+ throw (IOException)t;
+ }
+ String errmsg = "Failure invoking streamable writer " +
+ "[class=" + _target.getName() + "]";
+ throw new NestableIOException(errmsg, t);
+ }
+ return;
+ }
+
+ // if we're writing an array, do some special business
+ if (_target.isArray()) {
+ int length = Array.getLength(object);
+ out.writeInt(length);
+ // if the component class is final, we can be sure that all
+ // instances in the array will be of the same class and thus
+ // can serialize things more efficiently
+ int cmods = _target.getComponentType().getModifiers();
+ if ((cmods & Modifier.FINAL) != 0) {
+ // compute a mask indicating which elements are null and
+ // which are populated
+ ArrayMask mask = new ArrayMask(length);
+ for (int ii = 0; ii < length; ii++) {
+ if (Array.get(object, ii) != null) {
+ mask.set(ii);
+ }
+ }
+ // write that mask out to the stream
+ mask.writeTo(out);
+ // now write out the populated elements
+ for (int ii = 0; ii < length; ii++) {
+ Object element = Array.get(object, ii);
+ if (element == null) {
+ continue;
+ }
+ out.setCurrent(_delegate, element);
+ _delegate.writeObject(element, out, useWriter);
+ }
+
+ } else {
+ // otherwise we've got to write each array element with
+ // its own class identifier because it could be any
+ // derived class of the array element type
+ for (int ii = 0; ii < length; ii++) {
+ out.writeObject(Array.get(object, ii));
+ }
+ }
+ return;
+ }
+
+ // otherwise simply write out the fields via our field marshallers
+ int fcount = _fields.length;
+ for (int ii = 0; ii < fcount; ii++) {
+ Field field = _fields[ii];
+ FieldMarshaller fm = _marshallers[ii];
+ if (fm == null) {
+ String errmsg = "Unable to marshall field " +
+ "[class=" + _target.getName() +
+ ", field=" + field.getName() +
+ ", type=" + field.getType().getName() + "]";
+ throw new IOException(errmsg);
+ }
+ try {
+// Log.info("Writing field [class=" + _target.getName() +
+// ", field=" + field.getName() + "].");
+ fm.writeField(field, object, out);
+ } catch (Exception e) {
+ String errmsg = "Failure writing streamable field " +
+ "[class=" + _target.getName() +
+ ", field=" + field.getName() + "]";
+ throw new NestableIOException(errmsg, e);
+ }
+ }
+ }
+
+ /**
+ * Creates a blank object that can subsequently be read by this
+ * streamer. Data may be read from the input stream as a result of
+ * this method (in the case of arrays, the length of the array must be
+ * read before creating the array).
+ */
+ public Object createObject (ObjectInputStream in)
+ throws IOException
+ {
+ try {
+ // if our target class is an array type, read in the element
+ // count and create an array instance of the appropriate type
+ // and size
+ if (_target.isArray()) {
+ return Array.newInstance(
+ _target.getComponentType(), in.readInt());
+ } else {
+ return _target.newInstance();
+ }
+
+ } catch (InstantiationException ie) {
+ String errmsg = "Error instantiating object " +
+ "[type=" + _target.getName() + "]";
+ throw new NestableIOException(errmsg, ie);
+
+ } catch (IllegalAccessException iae) {
+ String errmsg = "Error instantiating object " +
+ "[type=" + _target.getName() + "]";
+ throw new NestableIOException(errmsg, iae);
+ }
+ }
+
+ /**
+ * Reads and populates the fields of the supplied object from the
+ * specified stream.
+ *
+ * @param object the instance to be read from the stream.
+ * @param out the stream from which to read the instance.
+ * @param useWriter whether or not to use the custom
+ * readObject if one exists.
+ */
+ public void readObject (
+ Object object, ObjectInputStream in, boolean useReader)
+ throws IOException, ClassNotFoundException
+ {
+ // if we're supposed to and one exists, use the reader method
+ if (useReader && _reader != null) {
+ try {
+// Log.info("Reading with reader " +
+// "[class=" + _target.getName() + "].");
+ _reader.invoke(object, new Object[] { in });
+
+ } catch (Throwable t) {
+ if (t instanceof InvocationTargetException) {
+ t = ((InvocationTargetException)t).getTargetException();
+ }
+ if (t instanceof IOException) {
+ throw (IOException)t;
+ }
+ String errmsg = "Failure invoking streamable reader " +
+ "[class=" + _target.getName() + "]";
+ throw new NestableIOException(errmsg, t);
+ }
+ return;
+ }
+
+ // if we're reading in an array, do some special business
+ if (_target.isArray()) {
+ int length = Array.getLength(object);
+ // if the component class is final, we can be sure that all
+ // instances in the array will be of the same class and thus
+ // have serialized things more efficiently
+ int cmods = _target.getComponentType().getModifiers();
+ if ((cmods & Modifier.FINAL) != 0) {
+ // read in the nullness mask
+ ArrayMask mask = new ArrayMask();
+ mask.readFrom(in);
+ // now read in the array elements given that we know which
+ // elements to read
+ for (int ii = 0; ii < length; ii++) {
+ if (mask.isSet(ii)) {
+ Object element = _delegate.createObject(in);
+ in.setCurrent(_delegate, element);
+ _delegate.readObject(element, in, useReader);
+ Array.set(object, ii, element);
+ }
+ }
+
+ } else {
+ // otherwise we had to write each object out individually
+ for (int ii = 0; ii < length; ii++) {
+ Array.set(object, ii, in.readObject());
+ }
+ }
+ return;
+ }
+
+ // otherwise simply read the fields via our field marshallers
+ int fcount = _fields.length;
+ for (int ii = 0; ii < fcount; ii++) {
+ Field field = _fields[ii];
+ FieldMarshaller fm = _marshallers[ii];
+ if (fm == null) {
+ String errmsg = "Unable to marshall field " +
+ "[class=" + _target.getName() +
+ ", field=" + field.getName() +
+ ", type=" + field.getType().getName() + "]";
+ throw new IOException(errmsg);
+ }
+ try {
+ fm.readField(field, object, in);
+ } catch (Exception e) {
+ String errmsg = "Failure reading streamable field " +
+ "[class=" + _target.getName() +
+ ", field=" + field.getName() + "]";
+ throw new NestableIOException(errmsg, e);
+ }
+ }
+ }
+
+ /**
+ * The constructor used by the basic streamers.
+ */
+ protected Streamer ()
+ {
+ }
+
+ /**
+ * Constructs a streamer for the specified target class.
+ */
+ protected Streamer (Class target)
+ throws IOException
+ {
+ // keep a handle on the class
+ _target = target;
+
+ // if our target class is an array, we want to get a handle on a
+ // streamer delegate that we'll use to stream our elements
+ if (_target.isArray()) {
+ _delegate = Streamer.getStreamer(_target.getComponentType());
+ // sanity check
+ if (_delegate == null) {
+ String errmsg = "Aiya! Streamer created for array type " +
+ "but we have no registered streamer for the element " +
+ "type [type=" + _target.getName() + "]";
+ throw new RuntimeException(errmsg);
+ }
+ // and that's all we'll need
+ return;
+ }
+
+ // reflect on all the public fields
+ Field[] fields = target.getFields();
+ ArrayList flist = new ArrayList();
+ int fcount = fields.length;
+ for (int ii = 0; ii < fcount; ii++) {
+ Field field = fields[ii];
+ int mods = field.getModifiers();
+ // skip static, transient and non-public fields
+ if (((mods & Modifier.STATIC) != 0) ||
+ ((mods & Modifier.PUBLIC) == 0) ||
+ ((mods & Modifier.TRANSIENT) != 0)) {
+ continue;
+ }
+ // otherwise add the field to the list
+ flist.add(field);
+ }
+
+ // convert the list of fields into an array
+ fcount = flist.size();
+ _fields = new Field[fcount];
+ flist.toArray(_fields);
+
+ // obtain field marshallers for all of our fields
+ _marshallers = new FieldMarshaller[fcount];
+ for (int ii = 0; ii < fcount; ii++) {
+ _marshallers[ii] = FieldMarshaller.getFieldMarshaller(_fields[ii]);
+// Log.info("Using " + _marshallers[ii] + " for " +
+// _fields[ii].getName() + ".");
+ }
+
+ // look up the reader and writer methods
+ try {
+ _reader = target.getMethod(READER_METHOD_NAME, READER_ARGS);
+ } catch (NoSuchMethodException nsme) {
+ // nothing to worry about, we just don't have one
+ }
+ try {
+ _writer = target.getMethod(WRITER_METHOD_NAME, WRITER_ARGS);
+ } catch (NoSuchMethodException nsme) {
+ // nothing to worry about, we just don't have one
+ }
+ }
+
+ /**
+ * Creates our streamers table and registers streamers for all of the
+ * basic types.
+ */
+ protected static void createStreamers ()
+ {
+ _streamers = new HashMap();
+
+ // register all of the basic streamers
+ int bscount = BasicStreamers.BSTREAMER_TYPES.length;
+ for (int ii = 0; ii < bscount; ii++) {
+ _streamers.put(BasicStreamers.BSTREAMER_TYPES[ii],
+ BasicStreamers.BSTREAMER_INSTANCES[ii]);
+ }
+ }
+
+ /** The class for which this streamer instance is configured. */
+ protected Class _target;
+
+ /** If our target class is an array, this is a reference to a streamer
+ * that can stream our array elements, otherwise it is null. */
+ protected Streamer _delegate;
+
+ /** The non-transient, non-static public fields that we will stream
+ * when requested. */
+ protected Field[] _fields;
+
+ /** Field marshallers for each field that will be read or written in
+ * our objects. */
+ protected FieldMarshaller[] _marshallers;
+
+ /** A reference to the readObject method if one is
+ * defined by our target class. */
+ protected Method _reader;
+
+ /** A reference to the writeObject method if one is
+ * defined by our target class. */
+ protected Method _writer;
+
+ /** Contains the mapping from class names to configured streamer
+ * instances. */
+ protected static HashMap _streamers;
+
+ /** The name of the custom reader method. */
+ protected static final String READER_METHOD_NAME = "readObject";
+
+ /** The argument list for the custom reader method. */
+ protected static final Class[] READER_ARGS = { ObjectInputStream.class };
+
+ /** The name of the custom writer method. */
+ protected static final String WRITER_METHOD_NAME = "writeObject";
+
+ /** The argument list for the custom writer method. */
+ protected static final Class[] WRITER_ARGS = { ObjectOutputStream.class };
+}