From b1d75775998ccd8c04b92cd475a8c7eea5fb9761 Mon Sep 17 00:00:00 2001 From: Ray Greenwell Date: Tue, 8 Mar 2011 00:48:39 +0000 Subject: [PATCH] - Cleaned up streamers. - Allow configuring of two new streaming policies for enums. - Changed the alphabetical fields policy to be checked once on class init. Let me elaborate on the first two changes: 1) The base Streamer class had grown to handle a variety of things. There were two different ways to handle enums, a whole section of code for arrays (two ways of handling those), and then custom reader/writer methods were supported with fallback for regular old streaming classes. A lot of this was checked by introspection every single time an object was written/created/read. In addition, there were 6 different fields in the base class, many of which would be unused by default. All the BasicStreamers also inherited these fields and never did anything with them. So my change makes Streamer an abstract class with no non-static fields. I pulled all the logic for different streaming strategies into different classes and do the tests to determine which class to use once when first setting up the streamer for a particular target class. If our target class is an array with a final element type, why should we be re-checking those two things each and every time an object is written? Or if it's not an array and we've got field marshallers set up, why are we wondering if it's an array or enum each time? In addition, reading/writing would freak out if a field marshaller was never found for a particular field. Let's fail faster and freak out when the marshallers are first initialized, which is fine now that (in a recentish commit) we don't initialize the marshallers if there appears to be no reason to do so (if a Streamable class has both a custom reader and a custom writer method, then the marshallers are never used. The class could have non-transient fields that don't stream, but it takes care of them itself in the custom methods.) 2) There are now three enum-streaming policies that can be configured. - OLD, the default, will try to stream by byte if the enum is a ByteEnum, and fall back to using the enum's name. - NEW will always stream an enum by ordinal, using the smallest integer type possible, given the enum's size. ByteEnum is ignored. - NEW_WITH_BYTE_ENUM will do the ByteEnum thing for ByteEnums, and fall back to the new ordinal thing for other enums. The idea here is that narya streaming is designed specifically such that the server and client are running the same code. There are some edge cases, and some games persist streamed objects, but in general streaming by ordinal should be fine. But, it's opt-in for now. I can't go breaking everything. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@6522 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- .../com/threerings/io/BasicStreamers.java | 22 +- src/main/java/com/threerings/io/Streamer.java | 1020 +++++++++++------ 2 files changed, 698 insertions(+), 344 deletions(-) diff --git a/src/main/java/com/threerings/io/BasicStreamers.java b/src/main/java/com/threerings/io/BasicStreamers.java index 8d27a12ba..bb9e5a9eb 100644 --- a/src/main/java/com/threerings/io/BasicStreamers.java +++ b/src/main/java/com/threerings/io/BasicStreamers.java @@ -74,6 +74,17 @@ public class BasicStreamers .put(Iterable.class, new IterableStreamer()) .build(); + /** Abstract base class for basic streamers. */ + public abstract static class BasicStreamer extends Streamer + { + @Override + public void readObject (Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + // nothing to do here + } + } + /** Streams {@link Boolean} instances. */ public static class BooleanStreamer extends BasicStreamer { @@ -588,17 +599,6 @@ public class BasicStreamers } } - /** Streams {@link String} instances. */ - public static class BasicStreamer extends Streamer - { - @Override - public void readObject (Object object, ObjectInputStream in, boolean useReader) - throws IOException, ClassNotFoundException - { - // nothing to do here - } - } - public static boolean[] readBooleanArray (ObjectInputStream ins) throws IOException { diff --git a/src/main/java/com/threerings/io/Streamer.java b/src/main/java/com/threerings/io/Streamer.java index a10c542be..7a23b6dba 100644 --- a/src/main/java/com/threerings/io/Streamer.java +++ b/src/main/java/com/threerings/io/Streamer.java @@ -30,14 +30,15 @@ import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.io.IOException; +import com.google.common.base.Objects; import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -45,6 +46,7 @@ import com.google.common.collect.Maps; import com.samskivert.util.ByteEnum; import com.samskivert.util.ByteEnumUtil; import com.samskivert.util.ClassUtil; +import com.samskivert.util.QuickSort; import static com.threerings.NaryaLog.log; @@ -55,7 +57,7 @@ import static com.threerings.NaryaLog.log; * and caches the information necessary to efficiently read and write objects of the class in * question. */ -public class Streamer +public abstract class Streamer { /** * Returns true if the supplied target class can be streamed using a streamer. @@ -159,7 +161,7 @@ public class Streamer stream = AccessController.doPrivileged( new PrivilegedExceptionAction() { public Streamer run () throws IOException { - return new Streamer(target); + return create(target); } }); } catch (PrivilegedActionException pae) { @@ -180,153 +182,16 @@ public class Streamer * @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 { - if (ObjectInputStream.STREAM_DEBUG) { - 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 (IOException) new IOException(errmsg).initCause(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 (Modifier.isFinal(cmods)) { - // 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) { - out.writeBareObject(element, _delegate, 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; - } - - // if we're writing an enum; write its string value (to avoid future compatibility issues - // if someone serializes an enum to a file and then adds a value to the enum, changing the - // ordinal assignments) - if (_target.isEnum()) { - if (object instanceof ByteEnum) { - out.writeByte(((ByteEnum) object).toByte()); - } else { - out.writeUTF(((Enum)object).name()); - } - return; - } - - // otherwise simply write out the fields via our field marshallers - if (_marshallers == null) { - initMarshallers(); - } - 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 { - if (ObjectInputStream.STREAM_DEBUG) { - 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 (IOException) new IOException(errmsg).initCause(e); - } - } - } + public abstract void writeObject (Object object, ObjectOutputStream out, boolean useWriter) + throws IOException; /** * 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, ClassNotFoundException - { - 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()) { - int length = in.readInt(); - if (ObjectInputStream.STREAM_DEBUG) { - log.info(in.hashCode() + ": Creating array '" + - _target.getComponentType().getName() + "[" + length + "]'."); - } - return Array.newInstance(_target.getComponentType(), length); - - } else if (_target.isEnum()) { - if (ObjectInputStream.STREAM_DEBUG) { - log.info(in.hashCode() + ": Creating enum '" + _target.getName() + "'."); - } - @SuppressWarnings("unchecked") Class eclass = - (Class)_target; - if (ByteEnum.class.isAssignableFrom(_target)) { - return ByteEnumUtil.fromByte(eclass, in.readByte()); - } else { - return Enum.valueOf(eclass, in.readUTF()); - } - - } else { - if (ObjectInputStream.STREAM_DEBUG) { - log.info(in.hashCode() + ": Creating object '" + _target.getName() + "'."); - } - return _target.newInstance(); - } - - } catch (InstantiationException ie) { - String errmsg = "Error instantiating object [type=" + _target.getName() + "]"; - throw (IOException) new IOException(errmsg).initCause(ie); - - } catch (IllegalAccessException iae) { - String errmsg = "Error instantiating object [type=" + _target.getName() + "]"; - throw (IOException) new IOException(errmsg).initCause(iae); - } - } + public abstract Object createObject (ObjectInputStream in) + throws IOException, ClassNotFoundException; /** * Reads and populates the fields of the supplied object from the specified stream. @@ -335,122 +200,24 @@ public class Streamer * @param in the stream from which to read the instance. * @param useReader whether or not to use the custom readObject if one exists. */ - public void readObject (Object object, ObjectInputStream in, boolean useReader) - throws IOException, ClassNotFoundException + public abstract 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 { - if (ObjectInputStream.STREAM_DEBUG) { - log.info(in.hashCode() + ": Reading with reader '" + _target.getName() + "." + - _reader.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 (IOException) new IOException(errmsg).initCause(t); - } - return; - } - - if (ObjectInputStream.STREAM_DEBUG) { - log.info(in.hashCode() + ": Reading '" + _target.getName() + "'."); - } - - // 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 (Modifier.isFinal(cmods)) { - // 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)) { - if (ObjectInputStream.STREAM_DEBUG) { - log.info(in.hashCode() + ": Reading fixed element '" + ii + "'."); - } - Object element = _delegate.createObject(in); - in.readBareObject(element, _delegate, useReader); - Array.set(object, ii, element); - } else if (ObjectInputStream.STREAM_DEBUG) { - log.info(in.hashCode() + ": Skipping null element '" + ii + "'."); - } - } - - } else { - // otherwise we had to write each object out individually - for (int ii = 0; ii < length; ii++) { - if (ObjectInputStream.STREAM_DEBUG) { - log.info(in.hashCode() + ": Reading free element '" + ii + "'."); - } - Array.set(object, ii, in.readObject()); - } - } - return; - } - - // an enum has no additional state - if (_target.isEnum()) { - return; - } - - // otherwise simply read the fields via our field marshallers - if (_marshallers == null) { - initMarshallers(); - } - 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 { - if (ObjectInputStream.STREAM_DEBUG) { - log.info(in.hashCode() + ": Reading field '" + field.getName() + "' " + - "with " + fm + "."); - } - // gracefully deal with objects that have had new fields added to their class - // definition - if (in.available() > 0) { - fm.readField(field, object, in); - } else { - log.info("Streamed instance missing field (probably newly added)", - "class", _target.getName(), "field", field.getName()); - } - } catch (Exception e) { - String errmsg = "Failure reading streamable field [class=" + _target.getName() + - ", field=" + field.getName() + ", error=" + e + "]"; - throw (IOException) new IOException(errmsg).initCause(e); - } - } - - if (ObjectInputStream.STREAM_DEBUG) { - log.info(in.hashCode() + ": Read object '" + object + "'."); - } } @Override public String toString () { - return (_target == null) ? getClass().getName() : - "[target=" + _target.getName() + ", delegate=" + _delegate + - ", fcount=" + (_fields == null ? 0 : _fields.length) + - ", reader=" + _reader + ", writer=" + _writer + "]"; + return toStringHelper(Objects.toStringHelper(this)).toString(); + } + + /** + * Overrideable to add more information to this class' toString() representation. + */ + protected Objects.ToStringHelper toStringHelper (Objects.ToStringHelper otsh) + { + // nada in the base class + return otsh; } /** @@ -461,93 +228,677 @@ public class Streamer } /** - * Constructs a streamer for the specified target class. + * Create the appropriate Streamer for a newly-seen class. */ - protected Streamer (Class target) + protected static Streamer create (Class target) throws IOException { - // keep a handle on the class - _target = target; - - // if this is a non-static inner class, freak out because we cannot stream those - boolean isInner = false, isStatic = Modifier.isStatic(_target.getModifiers()); + // validate that the class is really streamable + boolean isInner = false, isStatic = Modifier.isStatic(target.getModifiers()); try { - isInner = (_target.getDeclaringClass() != null); + isInner = (target.getDeclaringClass() != null); } catch (Throwable t) { - log.warning("Failure checking innerness of class", "class", _target.getName(), - "error", t); + log.warning("Failure checking innerness of class", + "class", target.getName(), "error", t); } if (isInner && !isStatic) { throw new IllegalArgumentException( - "Cannot stream non-static inner class: " + _target.getName()); + "Cannot stream non-static inner class: " + target.getName()); } - // 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()); + // create streamers for array types + if (target.isArray()) { + Class componentType = target.getComponentType(); + Streamer delegate = Streamer.getStreamer(componentType); // sanity check - if (_delegate == null) { + if (delegate == null) { String errmsg = "Aiya! Streamer created for array type but we have no registered " + - "streamer for the element type [type=" + _target.getName() + "]"; + "streamer for the element type [type=" + target.getName() + "]"; throw new RuntimeException(errmsg); } - // and that's all we'll need - return; + if (Modifier.isFinal(componentType.getModifiers())) { + return new FinalArrayStreamer(componentType, delegate); - } else if (_target.isEnum()) { - // nothing extra for enums - return; + } else { + return new ArrayStreamer(componentType, delegate); + } } - // look up the reader and writer methods + // create streamers for enum types + if (target.isEnum()) { + // if not NEW and we have a ByteEnum, stream using that + if (ENUM_POLICY != EnumPolicy.NEW) { + if (ByteEnum.class.isAssignableFrom(target)) { + return new ByteEnumStreamer(target); + } + } + // otherwise, if OLD and not a ByteEnum, stream by name + if (ENUM_POLICY == EnumPolicy.OLD) { + return new NameEnumStreamer(target); + + } else { + // if NEW, or NEW_WITH_BYTE_ENUM and not a ByteEnum, stream by ordinal (optimized!) + List universe = ImmutableList.copyOf(target.getEnumConstants()); + int size = universe.size(); + if (size <= (1 << (Byte.SIZE - 1))) { + return new ByteOrdEnumStreamer(target, universe); + + } else if (size <= (1 << (Short.SIZE - 1))) { + return new ShortOrdEnumStreamer(target, universe); + + } else { + return new IntOrdEnumStreamer(target, universe); + } + } + } + + // create Streamers for other types + Method reader = null; + Method writer = null; try { - _reader = target.getMethod(READER_METHOD_NAME, READER_ARGS); + 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); + writer = target.getMethod(WRITER_METHOD_NAME, WRITER_ARGS); } catch (NoSuchMethodException nsme) { // nothing to worry about, we just don't have one } - // let's try to fail fast and initialize the marshallers now if we know - // we're going to need them. - if ((_reader == null) || (_writer == null)) { - initMarshallers(); + // if there is no reader and no writer, we can do a simpler thing + if ((reader == null) && (writer == null)) { + return new ClassStreamer(target); + + } else { + return new CustomClassStreamer(target, reader, writer); } } /** - * Initialize the marshallers. + * A streamer that streams the fields of a class. */ - protected void initMarshallers () + protected static class ClassStreamer extends Streamer { - // reflect on all the object's fields and remove all marked with NotStreamable - List fields = Lists.newArrayList(); - ClassUtil.getFields(_target, fields); - - // Checks whether or not we should stream the fields in alphabetical order. This ensures - // cross-JVM compatibility since Class.getDeclaredFields() does not define an order. Due - // to legacy issues, this is false by default. - if (Boolean.getBoolean("com.threerings.io.streamFieldsAlphabetically")) { - Collections.sort(fields, FIELD_ALPHA_COMPARATOR); + /** Constructor. */ + protected ClassStreamer (Class target) + { + this(target, true); // initialize now, we'll need them and we want to fail fast } - _fields = Iterables.toArray(Iterables.filter(fields, _isStreamableFieldPred), Field.class); - int fcount = _fields.length; - - // 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]); - if (ObjectInputStream.STREAM_DEBUG) { - log.info("Using " + _marshallers[ii] + " for " + _target.getName() + "." + - _fields[ii].getName() + "."); + /** Constructor. */ + protected ClassStreamer (Class target, boolean initMarshallers) + { + _target = target; + if (initMarshallers) { + initMarshallers(); } } - } + + @Override + public void writeObject (Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + int fcount = _fields.length; + for (int ii = 0; ii < fcount; ii++) { + Field field = _fields[ii]; + FieldMarshaller fm = _marshallers[ii]; + try { + if (ObjectInputStream.STREAM_DEBUG) { + 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 (IOException) new IOException(errmsg).initCause(e); + } + } + } + + @Override + public Object createObject (ObjectInputStream in) + throws IOException, ClassNotFoundException + { + try { + if (ObjectInputStream.STREAM_DEBUG) { + log.info(in.hashCode() + ": Creating object '" + _target.getName() + "'."); + } + return _target.newInstance(); + + } catch (InstantiationException ie) { + String errmsg = "Error instantiating object [type=" + _target.getName() + "]"; + throw (IOException) new IOException(errmsg).initCause(ie); + + } catch (IllegalAccessException iae) { + String errmsg = "Error instantiating object [type=" + _target.getName() + "]"; + throw (IOException) new IOException(errmsg).initCause(iae); + } + } + + @Override + public void readObject (Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + int fcount = _fields.length; + for (int ii = 0; ii < fcount; ii++) { + Field field = _fields[ii]; + FieldMarshaller fm = _marshallers[ii]; + try { + if (ObjectInputStream.STREAM_DEBUG) { + log.info(in.hashCode() + ": Reading field '" + field.getName() + "' " + + "with " + fm + "."); + } + // gracefully deal with objects that have had new fields added to their class + // definition + if (in.available() > 0) { + fm.readField(field, object, in); + } else { + log.info("Streamed instance missing field (probably newly added)", + "class", _target.getName(), "field", field.getName()); + } + } catch (Exception e) { + String errmsg = "Failure reading streamable field [class=" + _target.getName() + + ", field=" + field.getName() + ", error=" + e + "]"; + throw (IOException) new IOException(errmsg).initCause(e); + } + } + + if (ObjectInputStream.STREAM_DEBUG) { + log.info(in.hashCode() + ": Read object '" + object + "'."); + } + } + + /** + * Initialize the marshallers. + */ + protected void initMarshallers () + { + // reflect on all the object's fields and remove all marked with NotStreamable + List fields = Lists.newArrayList(); + ClassUtil.getFields(_target, fields); + + // Checks whether or not we should stream the fields in alphabetical order. + // This ensures cross-JVM compatibility since Class.getDeclaredFields() does not + // define an order. Due to legacy issues, this is not used by default. + if (SORT_FIELDS) { + QuickSort.sort(fields, FIELD_NAME_ORDER); + } + + _fields = Iterables.toArray( + Iterables.filter(fields, _isStreamableFieldPred), Field.class); + int fcount = _fields.length; + + // 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]); + if (_marshallers[ii] == null) { + String errmsg = "Unable to marshall field [class=" + _target.getName() + + ", field=" + _fields[ii].getName() + + ", type=" + _fields[ii].getType().getName() + "]"; + throw new RuntimeException(errmsg); + } + if (ObjectInputStream.STREAM_DEBUG) { + log.info("Using " + _marshallers[ii] + " for " + _target.getName() + "." + + _fields[ii].getName() + "."); + } + } + } + + @Override + protected Objects.ToStringHelper toStringHelper (Objects.ToStringHelper otsh) + { + return super.toStringHelper(otsh) + .add("target", _target.getName()) + .add("fcount", (_fields == null) ? 0 : _fields.length); + } + + /** The class for which this streamer instance is configured. */ + protected Class _target; + + /** 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; + } // end: static class ClassStreamer + + /** + * Extends basic class streaming with support for customized streaming. + */ + protected static class CustomClassStreamer extends ClassStreamer + { + /** Constructor. */ + protected CustomClassStreamer (Class target, Method reader, Method writer) + { + super(target, false); // we will lazy-initialize the marshallers only if needed + _reader = reader; + _writer = writer; + } + + @Override + 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 { + if (ObjectInputStream.STREAM_DEBUG) { + 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 (IOException) new IOException(errmsg).initCause(t); + } + return; + } + + // otherwise, ensure the marshallers are initialized and call super + if (_marshallers == null) { + initMarshallers(); + } + super.writeObject(object, out, useWriter); + } + + @Override + 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 { + if (ObjectInputStream.STREAM_DEBUG) { + log.info(in.hashCode() + ": Reading with reader '" + _target.getName() + + "." + _reader.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 (IOException) new IOException(errmsg).initCause(t); + } + return; + } + + if (ObjectInputStream.STREAM_DEBUG) { + log.info(in.hashCode() + ": Reading '" + _target.getName() + "'."); + } + + // otherwise, ensure the marshallers are iniitalized and call super + if (_marshallers == null) { + initMarshallers(); + } + super.readObject(object, in, useReader); + } + + @Override + protected Objects.ToStringHelper toStringHelper (Objects.ToStringHelper otsh) + { + return super.toStringHelper(otsh) + .add("reader", _reader) + .add("writer", _writer); + } + + /** A reference to the readObject method if one is defined by our target. */ + protected Method _reader; + + /** A reference to the writeObject method if one is defined by our target. */ + protected Method _writer; + } // end: static class CustomClassStreamer + + /** + * A streamer for array types. + */ + protected static class ArrayStreamer extends Streamer + { + /** Constructor. */ + protected ArrayStreamer (Class componentType, Streamer delegate) + { + _componentType = componentType; + _delegate = delegate; + } + + @Override + public void writeObject (Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + int length = Array.getLength(object); + out.writeInt(length); + // 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)); + } + } + + @Override + public Object createObject (ObjectInputStream in) + throws IOException, ClassNotFoundException + { + int length = in.readInt(); + if (ObjectInputStream.STREAM_DEBUG) { + log.info(in.hashCode() + ": Creating array '" + + _componentType.getName() + "[" + length + "]'."); + } + return Array.newInstance(_componentType, length); + } + + @Override + public void readObject (Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + int length = Array.getLength(object); + for (int ii = 0; ii < length; ii++) { + if (ObjectInputStream.STREAM_DEBUG) { + log.info(in.hashCode() + ": Reading free element '" + ii + "'."); + } + Array.set(object, ii, in.readObject()); + } + } + + @Override + protected Objects.ToStringHelper toStringHelper (Objects.ToStringHelper otsh) + { + return super.toStringHelper(otsh) + .add("componentType", _componentType.getName()) + .add("delegate", _delegate); + } + + /** The class of our component type. */ + protected Class _componentType; + + /** Our delegate streamer. */ + protected Streamer _delegate; + } // end: static class ArrayStreamer + + /** + * A streamer for arrays with a final component type. + */ + protected static class FinalArrayStreamer extends ArrayStreamer + { + /** Constructor. */ + protected FinalArrayStreamer (Class componentType, Streamer delegate) + { + super(componentType, delegate); + } + + @Override + public void writeObject (Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + int length = Array.getLength(object); + out.writeInt(length); + // 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. + // 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) { + out.writeBareObject(element, _delegate, useWriter); + } + } + } + + @Override + public void readObject (Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + int length = Array.getLength(object); + // 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 + // 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)) { + if (ObjectInputStream.STREAM_DEBUG) { + log.info(in.hashCode() + ": Reading fixed element '" + ii + "'."); + } + Object element = _delegate.createObject(in); + in.readBareObject(element, _delegate, useReader); + Array.set(object, ii, element); + } else if (ObjectInputStream.STREAM_DEBUG) { + log.info(in.hashCode() + ": Skipping null element '" + ii + "'."); + } + } + } + } // end: static class FinalArrayStreamer + + /** + * Base class for Enum streamers. + */ + protected static abstract class EnumStreamer extends Streamer + { + /** Constructor. */ + protected EnumStreamer (Class target) + { + @SuppressWarnings("unchecked") + Class eclass = (Class)target; + _eclass = eclass; + } + + @Override + public void readObject (Object object, ObjectInputStream in, boolean useReader) + throws IOException, ClassNotFoundException + { + // nothing here: handled in createObject + } + + @Override + public Objects.ToStringHelper toStringHelper (Objects.ToStringHelper otsh) + { + return super.toStringHelper(otsh) + .add("eclass", _eclass.getName()); + } + + /** Used to coerce the type system into quietude when reading enums from the wire. */ + protected static enum EnumReader implements ByteEnum { + NOT_USED; + public byte toByte () { return 0; } + } + + /** Our enum class, not actually an EnumReader. */ + protected Class _eclass; + } // end: static abstract class EnumStreamer + + /** + * Streams ByteEnums, if that's what's desired. + */ + protected static class ByteEnumStreamer extends EnumStreamer + { + /** Constructor. */ + protected ByteEnumStreamer (Class target) + { + super(target); + } + + @Override + public void writeObject (Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + out.writeByte(((ByteEnum) object).toByte()); + } + + @Override + public Object createObject (ObjectInputStream in) + throws IOException, ClassNotFoundException + { + return ByteEnumUtil.fromByte(_eclass, in.readByte()); + } + } // end: static class ByteEnumStreamer + + /** + * Streams enums by name. + */ + protected static class NameEnumStreamer extends EnumStreamer + { + /** Constructor. */ + protected NameEnumStreamer (Class target) + { + super(target); + } + + @Override + public void writeObject (Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + out.writeUTF(((Enum)object).name()); + } + + @Override + public Object createObject (ObjectInputStream in) + throws IOException, ClassNotFoundException + { + return Enum.valueOf(_eclass, in.readUTF()); + } + } // end: static class NameEnumStreamer + + /** + * Base class for enum streamers that stream by ordinal. + */ + protected static abstract class OrdEnumStreamer extends EnumStreamer + { + /** Constructor. */ + protected OrdEnumStreamer (Class target, List universe) + { + super(target); + _universe = universe; + } + + @Override + public void writeObject (Object object, ObjectOutputStream out, boolean useWriter) + throws IOException + { + int code = (object == null) ? -1 : ((Enum)object).ordinal(); + writeCode(out, code); + } + + @Override + public Object createObject (ObjectInputStream in) + throws IOException, ClassNotFoundException + { + int code = readCode(in); + return (code == -1) ? null : _universe.get(code); + } + + /** Write the ordinal code. */ + protected abstract void writeCode (ObjectOutputStream out, int code) + throws IOException; + + /** Read the ordinal code. */ + protected abstract int readCode (ObjectInputStream in) + throws IOException; + + /** The universe of this enum. */ + protected List _universe; + } // end: static abstract class OrdEnumStreamer + + /** + * Streams enums by the byte value of their ordinal. + */ + protected static class ByteOrdEnumStreamer extends OrdEnumStreamer + { + /** Constructor. */ + protected ByteOrdEnumStreamer (Class target, List universe) + { + super(target, universe); + } + + @Override + protected void writeCode (ObjectOutputStream out, int code) + throws IOException + { + out.writeByte((byte)code); + } + + @Override + protected int readCode (ObjectInputStream in) + throws IOException + { + return in.readByte(); + } + } // end: static class ByteOrdEnumStreamer + + /** + * Streams enums by the short value of their ordinal. + */ + protected static class ShortOrdEnumStreamer extends OrdEnumStreamer + { + /** Constructor. */ + protected ShortOrdEnumStreamer (Class target, List universe) + { + super(target, universe); + } + + @Override + protected void writeCode (ObjectOutputStream out, int code) + throws IOException + { + out.writeShort((short)code); + } + + @Override + protected int readCode (ObjectInputStream in) + throws IOException + { + return in.readShort(); + } + } // end: static class ShortOrdEnumStreamer + + /** + * Streams enums by the int value of their ordinal. + */ + protected static class IntOrdEnumStreamer extends OrdEnumStreamer + { + /** Constructor. */ + protected IntOrdEnumStreamer (Class target, List universe) + { + super(target, universe); + } + + @Override + protected void writeCode (ObjectOutputStream out, int code) + throws IOException + { + out.writeInt(code); + } + + @Override + protected int readCode (ObjectInputStream in) + throws IOException + { + return in.readInt(); + } + } // end: static class IntOrdEnumStreamer /** * Initializes static state if necessary. @@ -559,42 +910,45 @@ public class Streamer } } - protected Comparator FIELD_ALPHA_COMPARATOR = new Comparator() { + /** Contains the mapping from class names to configured streamer instances. */ + protected static Map, Streamer> _streamers; + + /** Should we sort fields in streamable classes? */ + protected static final boolean SORT_FIELDS = + Boolean.getBoolean("com.threerings.io.streamFieldsAlphabetically"); + + /** Our policy on handling enum classes. */ + protected static final EnumPolicy ENUM_POLICY = EnumPolicy.create(); + + /** Compares fields by name. */ + protected static final Comparator FIELD_NAME_ORDER = new Comparator() { public int compare (Field arg0, Field arg1) { return arg0.getName().compareTo(arg1.getName()); } }; - /** Used to coerce the type system into quietude when reading enums from the wire. */ - protected static enum EnumReader implements ByteEnum { - NOT_USED; - public byte toByte () { return 0; } + /** + * The enum policy of this streamer, determined at start time by examining + * a system property. + */ + protected enum EnumPolicy + { + OLD, NEW, NEW_WITH_BYTE_ENUM; + + /** + * Create the static enum policy by checking the com.threerings.io.enumPolicy system prop. + */ + public static EnumPolicy create () + { + try { + return valueOf(System.getProperty("com.threerings.io.enumPolicy", OLD.name())); + } catch (Exception e) { + return OLD; + } + } } - /** 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 Map, Streamer> _streamers; - /** A simple predicate to filter "NotStreamable" members from a Streamable object's fields. */ protected static final Predicate _isStreamableFieldPred = new Predicate() { public boolean apply (Field obj) {