diff --git a/src/java/com/threerings/presents/dobj/DObject.java b/src/java/com/threerings/presents/dobj/DObject.java index 290943895..c160181ff 100644 --- a/src/java/com/threerings/presents/dobj/DObject.java +++ b/src/java/com/threerings/presents/dobj/DObject.java @@ -1,5 +1,5 @@ // -// $Id: DObject.java,v 1.23 2001/08/15 18:38:54 mdb Exp $ +// $Id: DObject.java,v 1.24 2001/08/16 03:31:08 mdb Exp $ package com.threerings.cocktail.cher.dobj; @@ -412,6 +412,28 @@ public class DObject _mgr.postEvent(event); } + /** + * Calls by derived instances when a set adder method was called. + */ + protected void requestElementAdd (String name, DSet.Element elem) + { + // generate an element added event + DEvent event = new ElementAddedEvent(_oid, name, elem); + // and dispatch it to our dobjmgr + _mgr.postEvent(event); + } + + /** + * Calls by derived instances when a set remover method was called. + */ + protected void requestElementRemove (String name, Object key) + { + // generate an element removed event + DEvent event = new ElementRemovedEvent(_oid, name, key); + // and dispatch it to our dobjmgr + _mgr.postEvent(event); + } + /** Our object id. */ protected int _oid; diff --git a/src/java/com/threerings/presents/dobj/DSet.java b/src/java/com/threerings/presents/dobj/DSet.java new file mode 100644 index 000000000..1965587b8 --- /dev/null +++ b/src/java/com/threerings/presents/dobj/DSet.java @@ -0,0 +1,224 @@ +// +// $Id: DSet.java,v 1.1 2001/08/16 03:31:09 mdb Exp $ + +package com.threerings.cocktail.cher.dobj; + +import com.threerings.cocktail.cher.Log; +import com.threerings.cocktail.cher.io.Streamable; + +/** + * The distributed set class provides a means by which an unordered set of + * objects can be maintained as a distributed object field. Elements can + * be added to and removed from the set, requests for which will generate + * events much like other distributed object fields. + * + *

The sets must contain a homogenous set of objects (all of exactly + * the same type) and the master copy of the set (the one in the master + * copy of the distributed object, ie. the server copy), must be + * configured with the class object of the type of object that this set + * will contain. This allows the set to distributed updates without + * communicating the class of the object contained therein. That need only + * be communicated once when the entire containing distributed object is + * sent over the wire. + * + *

Classes that wish to act as set elements must implement the {@link + * com.threerings.cocktail.cher.dobj.DSet.Element} interface which extends + * {@link com.threerings.cocktail.cher.io.Streamable} and adds the + * requirement that the object provide a key which will be used to + * identify element equality. Thus an element is declared to be in a set + * of the object returned by that element's geyKey() method + * is equal (using equal()) to the element returned by the + * getKey() method of some other element in the set. + * Additionally, in the case of element removal, only the key for the + * element to be removed will be transmitted with the removal event to + * save network bandwidth. Lastly, the object returned by + * getKey() must be a valid distributed object type. + */ +public class DSet +{ + /** + * Elements of the set must implement this interface. + */ + public static interface Element extends Streamable + { + /** + * Each element provide an associated key which is used to + * determine its uniqueness in the set. See the DSet + * class documentation for further information. + */ + public Object getKey (); + } + + /** + * Constructs a distributed set that will contain the specified + * element type. + */ + public DSet (Class elementType) + { + setElementType(elementType); + } + + /** + * Constructs a distributed set without specifying the element + * type. This is valid if the distributed set will soon be + * unserialized or if one plans to set the element type by hand before + * using the set. + */ + public DSet () + { + } + + /** + * Indicates what type of elements will be stored in this set. This + * can be called multiple times before the set is used (in the event + * that one wishes to further specialize the contents of a set that + * has already been configured to use a particular element type), but + * once the set goes into use, it must not be changed. Also bear in + * mind that the class of elements added to the set are not checked at + * runtime, and adding elements of invalid class will simply result in + * the serialization mechanism failing when an event is dispatched to + * broadcast the addition of an element. + */ + public void setElementType (Class elementType) + { + _elementType = elementType; + } + + /** + * Instantiates a blank element of the type managed by this set. This + * is used during the process of unserializing elements from the + * network. + */ + public DSet.Element newElement () + { + try { + return (Element)_elementType.newInstance(); + } catch (Exception e) { + Log.warning("Unable to instantiate element! We're hosed! " + + "[eclass=" + _elementType + "]."); + return null; + } + } + + /** + * Returns true if the set contains an element whose + * getKey() method returns a key that + * equals() the key returned by getKey() of + * the supplied element. Returns false otherwise. + */ + public boolean contains (Element elem) + { + return containsKey(elem.getKey()); + } + + /** + * Returns true if an element in the set has a key that + * equals() the supplied key. Returns false otherwise. + */ + public boolean containsKey (Object key) + { + return get(key) != null; + } + + /** + * Returns the element that matches + * (getKey().equals(key)) the specified key or null if no + * element could be found that matches the key. + */ + public Element get (Object key) + { + // scan the array looking for a matching element + int elength = _elements.length; + for (int i = 0; i < elength; i++) { + // the array may be sparse + if (_elements[i] != null) { + Element elem = _elements[i]; + if (elem.getKey().equals(key)) { + return elem; + } + } + } + return null; + } + + /** + * Adds the specified element to the set. This should not be called + * directly, instead the associated addTo{Set}() method + * should be called on the distributed object that contains the set in + * question. + * + * @return true if the element was added, false if it was already in + * the set. + */ + protected boolean add (Element elem) + { + Object key = elem.getKey(); + int elength = _elements.length; + int index = elength; + + // scan the array looking for a slot and/or the element already in + // the set + for (int i = 0; i < elength; i++) { + Element el = _elements[i]; + // the array may be sparse + if (el == null) { + if (index == elength) { + index = i; + } + } else if (el.getKey().equals(key)) { + return false; + } + } + + // expand the array if necessary + if (index >= elength) { + Element[] elems = new Element[elength*2]; + System.arraycopy(_elements, 0, elems, 0, elength); + _elements = elems; + } + + // insert the item + _elements[index] = elem; + return true; + } + + /** + * Removes the specified element from the set. This should not be + * called directly, instead the associated + * removeFrom{Set}() method should be called on the + * distributed object that contains the set in question. + * + * @return true if the element was removed, false if it was not in the + * set. + */ + protected boolean remove (Element elem) + { + return removeKey(elem.getKey()); + } + + /** + * Removes from the set the element whose key matches the supplied + * key. This should not be called directly, instead the associated + * removeFrom{Set}() method should be called on the + * distributed object that contains the set in question. + * + * @return true if a matching element was removed, false if no element + * in the set matched the key. + */ + protected boolean removeKey (Object key) + { + // scan the array looking for a matching element + int elength = _elements.length; + for (int i = 0; i < elength; i++) { + Element el = _elements[i]; + if (el != null && el.getKey().equals(key)) { + _elements[i] = null; + return true; + } + } + return false; + } + + protected Class _elementType; + protected Element[] _elements; +} diff --git a/src/java/com/threerings/presents/dobj/EntryAddedEvent.java b/src/java/com/threerings/presents/dobj/EntryAddedEvent.java new file mode 100644 index 000000000..2e9b43572 --- /dev/null +++ b/src/java/com/threerings/presents/dobj/EntryAddedEvent.java @@ -0,0 +1,199 @@ +// +// $Id: EntryAddedEvent.java,v 1.1 2001/08/16 03:31:09 mdb Exp $ + +package com.threerings.cocktail.cher.dobj; + +import java.io.*; + +import com.threerings.cocktail.cher.Log; + +/** + * An element added event is dispatched when an element is added to a + * DSet attribute of a distributed element. It can also be + * constructed to request the addition of an element to a + * DSet attribute of an element and posted to the dobjmgr. + * + * @see DObjectManager#postEvent + */ +public class ElementAddedEvent extends TypedEvent +{ + /** The typed object code for this event. */ + public static final short TYPE = TYPE_BASE + 8; + + /** + * Constructs a new element added event on the specified target object + * with the supplied set attribute name and element to add. + * + * @param targetOid the object id of the object to whose set we will + * add an element. + * @param name the name of the attribute to which to add the specified + * element. + * @param elem the element to add to the set attribute. + */ + public ElementAddedEvent (int targetOid, String name, DSet.Element elem) + { + super(targetOid); + _name = name; + _elem = elem; + } + + /** + * Constructs a blank instance of this event in preparation for + * unserialization from the network. + */ + public ElementAddedEvent () + { + } + + /** + * Returns the name of the set attribute to which an element has been + * added. + */ + public String getName () + { + return _name; + } + + /** + * Returns the element that has been added. + */ + public DSet.Element getElement () + { + return _elem; + } + + /** + * Applies this event to the object. + */ + public boolean applyToObject (DObject target) + throws ObjectAccessException + { + DSet set = (DSet)target.getAttribute(_name); + + // now that we have access to our target set, we can unflatten our + // element (if need be) + if (_elem == null) { + try { + _elem = unflatten(set, _bytes); + } catch (Exception e) { + Log.warning("Error unflattening element " + this + "."); + Log.logStackTrace(e); + return false; + } + } + + set.add(_elem); + return true; + } + + public short getType () + { + return TYPE; + } + + public void writeTo (DataOutputStream out) + throws IOException + { + super.writeTo(out); + out.writeUTF(_name); + flatten(out, _elem); + } + + public void readFrom (DataInputStream in) + throws IOException + { + super.readFrom(in); + _name = in.readUTF(); + + // we read in the raw element data now and decode it later when we + // have access to the object and the DSet instance that knows what + // type of element we need to decode + int bcount = in.readInt(); + _bytes = new byte[bcount]; + in.readFully(_bytes, 0, bcount); + } + + protected void toString (StringBuffer buf) + { + buf.append("ELADD:"); + super.toString(buf); + buf.append(", name=").append(_name); + buf.append(", elem=").append(_elem); + } + + protected String _name; + protected byte[] _bytes; + protected DSet.Element _elem; + + /** + * Because we don't know the type of the element at the time that the + * event is read from the network (we only know it once the event is + * being applied to the object and we can ask the target + * DSet instance what type it manages), we flatten the + * element before writing it to the wire so that we can prepend it by + * a byte count. The receiver will read in the raw bytes and decode + * them only later when it has access to the DSet + * instance that knows what element type to use. This method should + * really only be called by the conmgr thread, but we synchronize just + * in case someone decides to write an event out in some other + * peculiar context. Uncontested syncs are pretty fast. + */ + protected static synchronized void flatten ( + DataOutputStream out, DSet.Element elem) + throws IOException + { + elem.writeTo(_dout); + _dout.flush(); + out.writeInt(_bout.size()); + _bout.writeTo(out); + } + + /** + * Unflattens an element given the serialized element data. We know + * this will always be called on the dobjmgr thread, so we need not + * synchronize. + */ + protected static DSet.Element unflatten (DSet set, byte[] data) + throws IOException + { + _bin.setBytes(data); + DSet.Element elem = set.newElement(); + elem.readFrom(_din); + return elem; + } + + /** + * We extend byte array input stream to avoid having to create a new + * input stream every time we unserialize an element. Our extensions + * allow us to repurpose this input stream to read from a new byte + * array each time we unserialize. + */ + protected static class ReByteArrayInputStream extends ByteArrayInputStream + { + public ReByteArrayInputStream () + { + super(new byte[0]); + } + + public void setBytes (byte[] bytes) + { + buf = bytes; + pos = 0; + count = buf.length; + mark = 0; + } + } + + /** Used when serializing elements. */ + protected static ByteArrayOutputStream _bout = new ByteArrayOutputStream(); + + /** Used when serializing elements. */ + protected static DataOutputStream _dout = new DataOutputStream(_bout); + + /** Used when unserializing elements. */ + protected static ReByteArrayInputStream _bin = + new ReByteArrayInputStream(); + + /** Used when unserializing elements. */ + protected static DataInputStream _din = new DataInputStream(_bin); +} diff --git a/src/java/com/threerings/presents/dobj/EntryRemovedEvent.java b/src/java/com/threerings/presents/dobj/EntryRemovedEvent.java new file mode 100644 index 000000000..31f61d6f9 --- /dev/null +++ b/src/java/com/threerings/presents/dobj/EntryRemovedEvent.java @@ -0,0 +1,111 @@ +// +// $Id: EntryRemovedEvent.java,v 1.1 2001/08/16 03:31:09 mdb Exp $ + +package com.threerings.cocktail.cher.dobj; + +import java.io.IOException; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +import com.threerings.cocktail.cher.dobj.io.ValueMarshaller; + +/** + * An element removed event is dispatched when an element is removed from + * a DSet attribute of a distributed object. It can also be + * constructed to request the removal of an element from a + * DSet attribute of an object and posted to the dobjmgr. + * + * @see DObjectManager#postEvent + */ +public class ElementRemovedEvent extends TypedEvent +{ + /** The typed object code for this event. */ + public static final short TYPE = TYPE_BASE + 9; + + /** + * Constructs a new element removed event on the specified target + * object with the supplied set attribute name and element key to + * remove. + * + * @param targetOid the object id of the object from whose set we will + * remove an element. + * @param name the name of the attribute from which to remove the + * specified element. + * @param key the element key that identifies the element to remove. + */ + public ElementRemovedEvent (int targetOid, String name, Object key) + { + super(targetOid); + _name = name; + _key = key; + } + + /** + * Constructs a blank instance of this event in preparation for + * unserialization from the network. + */ + public ElementRemovedEvent () + { + } + + /** + * Returns the name of the oid list attribute from which an oid has + * been removed. + */ + public String getName () + { + return _name; + } + + /** + * Returns the key that identifies the element that has been removed. + */ + public Object getKey () + { + return _key; + } + + /** + * Applies this event to the object. + */ + public boolean applyToObject (DObject target) + throws ObjectAccessException + { + DSet set = (DSet)target.getAttribute(_name); + set.removeKey(_key); + return true; + + } + + public short getType () + { + return TYPE; + } + + public void writeTo (DataOutputStream out) + throws IOException + { + super.writeTo(out); + out.writeUTF(_name); + ValueMarshaller.writeTo(out, _key); + } + + public void readExternal (DataInputStream in) + throws IOException + { + super.readFrom(in); + _name = in.readUTF(); + _key = ValueMarshaller.readFrom(in); + } + + protected void toString (StringBuffer buf) + { + buf.append("ELREM:"); + super.toString(buf); + buf.append(", name=").append(_name); + buf.append(", key=").append(_key); + } + + protected String _name; + protected Object _key; +} diff --git a/src/java/com/threerings/presents/io/TypedObjectRegistry.java b/src/java/com/threerings/presents/io/TypedObjectRegistry.java index 4d1ce1a5f..083a718b5 100644 --- a/src/java/com/threerings/presents/io/TypedObjectRegistry.java +++ b/src/java/com/threerings/presents/io/TypedObjectRegistry.java @@ -1,5 +1,5 @@ // -// $Id: TypedObjectRegistry.java,v 1.5 2001/08/07 20:38:58 mdb Exp $ +// $Id: TypedObjectRegistry.java,v 1.6 2001/08/16 03:31:09 mdb Exp $ package com.threerings.cocktail.cher.io; @@ -66,5 +66,9 @@ public class TypedObjectRegistry ReleaseLockEvent.class); TypedObjectFactory.registerClass(ObjectDestroyedEvent.TYPE, ObjectDestroyedEvent.class); + TypedObjectFactory.registerClass(ElementAddedEvent.TYPE, + ElementAddedEvent.class); + TypedObjectFactory.registerClass(ElementRemovedEvent.TYPE, + ElementRemovedEvent.class); } }