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 }; +}