diff --git a/build.xml b/build.xml
index bf21bc3b3..da5c66cab 100644
--- a/build.xml
+++ b/build.xml
@@ -255,9 +255,6 @@
-
A note to the reader: the Presents system is a complex
+ one and though a great deal of code is provided in explaining the
+ services it provides, it is not the intent that one should start from
+ only these examples and build a working system. A better approach is
+ to read through this documentation to come to an understanding of the
+ concepts and mechanisms that define the system and then take a look at
+ some working sample code which is provided in the tests
+ directory of this distribution.
+
+
Clients cannot modify their proxy distributed objects directly, + instead they make use of setter methods which package up the requested + change into an event and send that event to the server for processing. + After performing access control checks, the server will apply the + event to the primary distributed object instance and then dispatch + that event to all subscribed clients. Those clients (including the + original change requesting client) then apply the event to their proxy + copy of the object and in this way all clients maintain an up to date + copy of the object's data. + +
+
+
+ public class CageObject extends DObject
+ {
+ /** The number of monkeys in the cage. */
+ public int monkeys;
+
+ /** The name of the owner of this cage. */
+ public String owner;
+ }
+
+ Note that all distributed fields, or attributes (fields in a
+ distributed object are frequently referred to as attributes in
+ this documentation and elsewhere in the system), are public fields in
+ our distributed object. Non-public fields will be ignored by the
+ system and not transmitted when a proxy object is delivered over the
+ network to a subscriber. Further, fields marked transient
+ will also be ignored by the system.
+
+ We then run our class definition through a post-processor which + turns it into the following: + +
+ public class CageObject extends DObject
+ {
+ // AUTO-GENERATED: FIELDS START
+ /** The field name of the monkeys field. */
+ public static final String MONKEYS = "monkeys";
+
+ /** The field name of the owner field. */
+ public static final String OWNER = "owner";
+ // AUTO-GENERATED: FIELDS END
+
+ /** The number of monkeys in the cage. */
+ public int monkeys;
+
+ /** The name of the owner of this cage. */
+ public String owner;
+
+ // AUTO-GENERATED: METHODS START
+ /**
+ * Requests that the monkeys field be set to the
+ * specified value. The local value will be updated immediately and an
+ * event will be propagated through the system to notify all listeners
+ * that the attribute did change. Proxied copies of this object (on
+ * clients) will apply the value change when they received the
+ * attribute changed notification.
+ */
+ public void setMonkeys (int value)
+ {
+ int ovalue = this.monkeys;
+ requestAttributeChange(
+ EVEN_BASE, new Integer(value), new Integer(ovalue));
+ this.monkeys = value;
+ }
+
+ /**
+ * Requests that the owner field be set to the
+ * specified value. The local value will be updated immediately and an
+ * event will be propagated through the system to notify all listeners
+ * that the attribute did change. Proxied copies of this object (on
+ * clients) will apply the value change when they received the
+ * attribute changed notification.
+ */
+ public void setOwner (String value)
+ {
+ String ovalue = this.owner;
+ requestAttributeChange(
+ ODD_BASE, value, ovalue);
+ this.owner = value;
+ }
+ // AUTO-GENERATED: METHODS END
+ }
+
+ The contents of the methods are not too important, the main things to
+ note are that setter methods for the two attributes were generated and
+ constants were defined that will be used to identify which attribute
+ changed if we choose to inspect an event notifying us of such a
+ change. Note also that additional methods may be added to a
+ distributed object class as long as nothing is modified in the
+ AUTO-GENERATED section. As new fields are added and the
+ post-processing tool re-run, everything outside the auto-generated
+ section will be preserved.
+
+ One may also notice that attribute change requests result in the + new value of the attribute being immediately written to the local copy + of the object. This is a convention that was decided upon after + repeatedly running into trouble when users of the system would set a + value in an object and immediately assume it held the new value rather + than realizing that an event would have to propagate back from the + server before the value was in fact updated. By setting the value + immediately, these problems are avoided and the opposite assumption is + almost never made. This is further justified by the fact that, in + general, attribute changes never originate on a client but instead + originate on the server after processing a request from the client + (via the below documented invocation + services) to do something application-specific that results in one + or more attribute changes taking place. + +
See the section on Ant Tasks for + information on how to configure and run this post-processor. + +
+ public class ServerEntity implements Subscriber {
+ public void init (DObjectManager omgr) {
+ omgr.createObject(CageObject.class, this);
+ }
+
+ // inherited from interface Subscriber
+ public void objectAvailable (DObject object) {
+ // yay! we created our object
+ _object = (CageObject)object;
+ }
+
+ // inherited from interface Subscriber
+ public void requestFailed (int oid, ObjectAccessException cause) {
+ // oh the humanity, we failed to create our object; in
+ // general this would only happen if we did something silly like
+ // passed in a DObject class that didn't extend DObject
+ }
+
+ protected CageObject _object;
+ }
+
+ You'll notice that we provide an instance of a Subscriber
+ when creating our object. This subscriber instance is in fact
+ subscribed to the newly created object in the same manner as is
+ described below for all additional subscribers to the object. It is
+ possible to instruct an object to automatically destroy itself when
+ all subscribers have unsubscribed. (See the not very terse {@link
+ com.threerings.presents.dobj.DObject}.setDestroyOnLastSubscriberRemoved()).
+
+ The client obtains a proxy of the object by a process called + subscription, which is accomplished via {@link + com.threerings.presents.dobj.DObjectManager}.subscribeToObject(): + +
+ public class ObjectUser implements Subscriber {
+ public void init (Client client, int objectId) {
+ client.getDObjectManager().subscribeToObject(objectId, this);
+ }
+
+ // inherited from interface Subscriber
+ public void objectAvailable (DObject object) {
+ // yay! we got our object
+ _object = (CageObject)object;
+ }
+
+ // inherited from interface Subscriber
+ public void requestFailed (int oid, ObjectAccessException cause) {
+ // oh the humanity, we failed to subscribe
+ }
+
+ protected CageObject _object;
+ }
+
+ Later a client would relinquish its subscription to the object + using a similar mechanism: + +
+ public class ObjectUser implements Subscriber {
+ // ...
+ public void shutdown (Client client) {
+ client.getDObjectManager().unsubscribeFromObject(
+ _object.getOid(), this);
+ _object = null;
+ }
+ // ...
+ }
+
+ However, this is a fine time to point out the dangers of working in an
+ asynchronous distributed environment. There is no guarantee that your
+ object subscription request will be completed before the client
+ decides to call shutdown() on its ObjectUser. Thus, in
+ the previous code, we could get a null pointer exception, and even
+ worse, we could remain subscribed to the object even though we didn't
+ want to be. To avoid these sorts of problems, the {@link
+ com.threerings.presents.util.SafeSubscriber} class is provided:
+
+
+ public class ObjectUser implements Subscriber {
+ public void init (Client client, int objectId) {
+ _safesub = new SafeSubscriber(objectId, this);
+ _safesub.subcribe(client.getDObjectManager());
+ }
+
+ // inherited from interface Subscriber
+ public void objectAvailable (DObject object) {
+ // yay! we got our object
+ _object = (CageObject)object;
+ }
+
+ // inherited from interface Subscriber
+ public void requestFailed (int oid, ObjectAccessException cause) {
+ // oh the humanity, we failed to subscribe
+ }
+
+ public void shutdown (Client client) {
+ _safesub.unsubscribe(client.getDObjectManager());
+ _object = null;
+ }
+
+ protected SafeSubscriber _safesub;
+ protected CageObject _object;
+ }
+
+ The safe subscriber will pass the object availability on to your
+ subscriber and when the time comes to unsubscribe, it will cope with
+ the case where the original subscription was not fully processed and
+ stick around long enough to ensure that once it is, the request to
+ unsubscribe is also dispatched. It will also cope with a request to
+ unsubscribe() even if the original subscription request
+ failed.
+
+ The basic listener is the {@link + com.threerings.presents.dobj.AttributeChangeListener} which is + informed of all simple attribute changes (setting a primitive field to + a new value is called an attribute change). We return to our trusty + example: + +
+ public class ObjectUser
+ implements Subscriber, AttributeChangeListener {
+ // ...
+ public void init (Client client, int objectId) {
+ _safesub = new SafeSubscriber(_subscriber, objectId);
+ _safesub.subcribe(client.getDObjectManager());
+ }
+
+ // inherited from interface Subscriber
+ public void objectAvailable (DObject object) {
+ // yay! we got our object
+ _object = (CageObject)object;
+ _object.addListener(this);
+ }
+
+ // inherited from interface Subscriber
+ public void requestFailed (int oid, ObjectAccessException cause) {
+ // oh the humanity, we failed to subscribe
+ }
+
+ // inherited from interface AttributeChangeListener
+ public void attributeChanged (AttributeChangedEvent event)
+ {
+ System.out.println("Wow! The " + event.getName() +
+ " field changed to " + event.getValue() + ".");
+ }
+
+ public void shutdown (Client client) {
+ _safesub.unsubscribe(client.getDObjectManager());
+ if (_object != null) {
+ // removing our listener not necessary as we are
+ // unsubscribing, but it's a good habit to develop as
+ // frequently listeners will come and go during the
+ // lifetime of an object subscription
+ _object.removeListener(this);
+ _object = null;
+ }
+ }
+
+ protected SafeSubscriber _safesub;
+ protected CageObject _object;
+ }
+
+ The attributeChanged() method of our registered listener
+ will be called whenever an event is received as a result of one of the
+ setter methods being called on the CageObject by
+ any participant in the distributed system. The setter creates
+ an event which is sent to the server, the server dispatches the event
+ to all subscribers of the object and the Presents system dispatches
+ the event notification to all registered listeners when the event is
+ received on the client. Note that listeners are also used on the
+ server as entities on the server also frequently need to respond to
+ attribute changes. They are notified immediately after the server has
+ dispatched the event (over the network) to all subscribed clients.
+
+ It is useful to note that listeners are notified of a changed + attribute after the change has been applied to the object. The + previous value of the attribute is available through the {@link + com.threerings.presents.dobj.AttributeChangedEvent#getOldValue} + method, though in spite of many years of experience using this system + in a variety of circumstances, we have rarely found that we cared to + know the previous value. + +
Distributed Arrays
+
+ Arrays of primitive types can be used in a distributed object and the
+ system will detect their use and provide a mechanism for updating the
+ entire array and an additional mechanism for updating a single element
+ at a time:
+
+
+ public class ChessObject extends DObject
+ {
+ // AUTO-GENERATED: FIELDS START
+ /** The field name of the state field. */
+ public static final String STATE = "state";
+ // AUTO-GENERATED: FIELDS END
+
+ /** Used to track our board state. */
+ public int[] state;
+
+ // AUTO-GENERATED: METHODS START
+ /**
+ * Requests that the state field be set to the
+ * specified value. The local value will be updated immediately and an
+ * event will be propagated through the system to notify all listeners
+ * that the attribute did change. Proxied copies of this object (on
+ * clients) will apply the value change when they received the
+ * attribute changed notification.
+ */
+ public void setState (int[] value)
+ {
+ int[] ovalue = this.state;
+ requestAttributeChange(
+ STATE, value, ovalue);
+ this.state = (value == null) ? null : (int[])value.clone();
+ }
+
+ /**
+ * Requests that the indexth element of
+ * state field be set to the specified value.
+ * The local value will be updated immediately and an event will be
+ * propagated through the system to notify all listeners that the
+ * attribute did change. Proxied copies of this object (on clients)
+ * will apply the value change when they received the attribute
+ * changed notification.
+ */
+ public void setStateAt (int value, int index)
+ {
+ int ovalue = this.state[index];
+ requestElementUpdate(
+ STATE, index, new Integer(value), new Integer(ovalue));
+ this.state[index] = value;
+ }
+ // AUTO-GENERATED: METHODS END
+ }
+
+ To correspond with what is called an "element update" (the
+ modification of a single element in an array), there is the {@link
+ com.threerings.presents.dobj.ElementUpdateListener}. When an element
+ is updated, listeners implementing that interface will be notified.
+ Remember that if the whole array is changed using
+ setState(), the normal {@link
+ com.threerings.presents.dobj.AttributeChangeListener} is the interface
+ one uses to hear about it.
+
+ Note that distributed arrays are not + automatically resized. If a request is made to update the element at + index 9 of an array, the array must be of at least size 10 or an array + index out of bounds exception will be thrown (as should be evident + from inspecting the code above). For more dynamic collections of + objects, see the documentation below about distributed sets. + +
This mechanism is not actually limited to arrays of primitive + types. It also works for arrays of objects that implement the {@link + com.threerings.io.Streamable} interface which is documented next. + +
Streamable and its good friend SimpleStreamableObject
+
+ The {@link com.threerings.io.Streamable} interface is used to mark
+ objects that can be sent over the network by using them in distributed
+ object fields by using arrays of such objects as a field. This
+ interface functions in much the same way that {@link
+ java.io.Serializable} does in that it simply marks the class and an
+ underlying mechanism uses reflection to actually marshall and
+ unmarshall the object on the network. In fact, all
+ non-transient fields of a streamable object are included
+ during the marhsalling process. Here's an example:
+
+
+ public class Player implements Streamable
+ {
+ /** This player's name. */
+ public String name;
+
+ /** This player's rating. */
+ public int rating;
+ }
+
+ public class ChessObject extends DObject
+ {
+ /** A record for each player in the game. */
+ public Player[] players;
+ }
+
+ The generated methods are ommitted for the sake of brevity, but as you
+ would expect, both a setPlayers(Player[] value) and a
+ setPlayersAt(Player value, int index) method will be
+ generated and do just what you expect.
+
+ It should be pointed out that streamable objects sent over the + network are sent in their entirety. No mechanism is provided for + updating just a single field in a streamable instance both because + that would increase the complexity of the system tremendously and + because it is generally not very useful. If conservation of bandwidth + is of extreme importance, special {@link + com.threerings.presents.dobj.DEvent} derived classes can be created to + transmit precisely what is desired and nothing more. Doing so is + beyond the scope of this introduction, but will hopefully be covered + in an additional tutorial. + +
The {@link com.threerings.io.SimpleStreamableObject} class is a
+ convenient way to create a simple record like the Player
+ record above that implements Streamable and provides a
+ default implementation of toString() that uses reflection
+ to print out the actual values of the fields in the object (a boon
+ when logging and debugging).
+
+
Distributed Sets
+
+ In developing a distributed system, one frequently encounters
+ situations where one wants distributed collection of objects where
+ order is generally not important but the ability to fluidly add and
+ remove elements is. For such occasions we provide the distributed set
+ or {@link com.threerings.presents.dobj.DSet}.
+
+
A DSet contains entries (called entries rather than
+ elements to avoid confusion with array "element updating") which must
+ implement the {@link com.threerings.presents.dobj.DSet.Entry}
+ interface. This automatically makes them {@link
+ com.threerings.io.Streamable} and requires that they provide a {@link
+ java.lang.Comparable} key which is used to distinguish them from other
+ entries in the set (and look them up via an efficient binary search).
+
+
When using a DSet one is provided with three new
+ operations: addToFoo(), updateFoo() and
+ removeFromFoo(). Once again an example is in order:
+
+
+ public class Monkey implements DSet.Entry
+ {
+ /** The monkey's name. */
+ public String name;
+
+ /** The monkey's age. */
+ public int age;
+
+ // documentation inherited from interface DSet.Entry
+ public Comparable getKey ()
+ {
+ return name;
+ }
+ }
+
+ public class CageObject extends DObject
+ {
+ // AUTO-GENERATED: FIELDS START
+ /** The field name of the monkeys field. */
+ public static final String MONKEYS = "monkeys";
+ // AUTO-GENERATED: FIELDS END
+
+ /** A collection of monkeys. */
+ public DSet monkeys;
+
+ // AUTO-GENERATED: METHODS START
+ /**
+ * Requests that the specified entry be added to the
+ * monkeys set.
+ */
+ public void addToMonkeys (DSet.Entry elem)
+ {
+ requestEntryAdd(MONKEYS, monkeys, elem);
+ }
+
+ /**
+ * Requests that the entry matching the supplied key be removed from
+ * the monkeys set.
+ */
+ public void removeFromMonkeys (Comparable key)
+ {
+ requestEntryRemove(MONKEYS, monkeys, key);
+ }
+
+ /**
+ * Requests that the specified entry be updated in the
+ * monkeys set.
+ */
+ public void updateMonkeys (DSet.Entry elem)
+ {
+ requestEntryUpdate(MONKEYS, monkeys, elem);
+ }
+
+ /**
+ * Requests that the monkeys field be set to the
+ * specified value.
+ */
+ public void setMonkeys (DSet value)
+ {
+ requestAttributeChange(MONKEYS, value, this.monkeys);
+ this.monkeys = (value == null) ? null : (DSet)value.clone();
+ }
+ // AUTO-GENERATED: METHODS END
+ }
+
+ It is possible to set the entire set (which is necessary to establish
+ its original value even if one decides to set it to the empty set),
+ but more commonly one will simply add entries to the set, update those
+ entries and remove them using the provided methods.
+
+ In conjunction with the DSet there exists the {@link
+ com.threerings.presents.dobj.SetListener} which is notified when
+ changes are made to a distributed set. This functions in the same was
+ as the previously documented listeners, so I will refrain from boring
+ you with yet more sample code.
+
+
The main Crowd concepts are that of Body and Place and the notion of a + PlaceManager on the server and a pairing of PlaceController and PlaceView + on the client.
+ +Crowd also provides a mechanism for Chat in Places and between + individuals that are logged on.
+ +Crowd also integrates with the Presents peer system to make its Chat + services work in a peer environment.
+ +For libraries that are build on the distributed object services as well + as most code in a system using the distributed object services, everything + you need are in these packages:
A note to the reader: the Presents system is a complex
- one and though a great deal of code is provided in explaining the
- services it provides, it is not the intent that one should start from
- only these examples and build a working system. A better approach is
- to read through this documentation to come to an understanding of the
- concepts and mechanisms that define the system and then take a look at
- some working sample code which is provided in the tests
- directory of this distribution.
-
-
Clients cannot modify their proxy distributed objects directly, - instead they make use of setter methods which package up the requested - change into an event and send that event to the server for processing. - After performing access control checks, the server will apply the - event to the primary distributed object instance and then dispatch - that event to all subscribed clients. Those clients (including the - original change requesting client) then apply the event to their proxy - copy of the object and in this way all clients maintain an up to date - copy of the object's data. - -
-
-
- public class CageObject extends DObject
- {
- /** The number of monkeys in the cage. */
- public int monkeys;
-
- /** The name of the owner of this cage. */
- public String owner;
- }
-
- Note that all distributed fields, or attributes (fields in a
- distributed object are frequently referred to as attributes in
- this documentation and elsewhere in the system), are public fields in
- our distributed object. Non-public fields will be ignored by the
- system and not transmitted when a proxy object is delivered over the
- network to a subscriber. Further, fields marked transient
- will also be ignored by the system.
-
- We then run our class definition through a post-processor which - turns it into the following: - -
- public class CageObject extends DObject
- {
- // AUTO-GENERATED: FIELDS START
- /** The field name of the monkeys field. */
- public static final String MONKEYS = "monkeys";
-
- /** The field name of the owner field. */
- public static final String OWNER = "owner";
- // AUTO-GENERATED: FIELDS END
-
- /** The number of monkeys in the cage. */
- public int monkeys;
-
- /** The name of the owner of this cage. */
- public String owner;
-
- // AUTO-GENERATED: METHODS START
- /**
- * Requests that the monkeys field be set to the
- * specified value. The local value will be updated immediately and an
- * event will be propagated through the system to notify all listeners
- * that the attribute did change. Proxied copies of this object (on
- * clients) will apply the value change when they received the
- * attribute changed notification.
- */
- public void setMonkeys (int value)
- {
- int ovalue = this.monkeys;
- requestAttributeChange(
- EVEN_BASE, new Integer(value), new Integer(ovalue));
- this.monkeys = value;
- }
-
- /**
- * Requests that the owner field be set to the
- * specified value. The local value will be updated immediately and an
- * event will be propagated through the system to notify all listeners
- * that the attribute did change. Proxied copies of this object (on
- * clients) will apply the value change when they received the
- * attribute changed notification.
- */
- public void setOwner (String value)
- {
- String ovalue = this.owner;
- requestAttributeChange(
- ODD_BASE, value, ovalue);
- this.owner = value;
- }
- // AUTO-GENERATED: METHODS END
- }
-
- The contents of the methods are not too important, the main things to
- note are that setter methods for the two attributes were generated and
- constants were defined that will be used to identify which attribute
- changed if we choose to inspect an event notifying us of such a
- change. Note also that additional methods may be added to a
- distributed object class as long as nothing is modified in the
- AUTO-GENERATED section. As new fields are added and the
- post-processing tool re-run, everything outside the auto-generated
- section will be preserved.
-
- One may also notice that attribute change requests result in the - new value of the attribute being immediately written to the local copy - of the object. This is a convention that was decided upon after - repeatedly running into trouble when users of the system would set a - value in an object and immediately assume it held the new value rather - than realizing that an event would have to propagate back from the - server before the value was in fact updated. By setting the value - immediately, these problems are avoided and the opposite assumption is - almost never made. This is further justified by the fact that, in - general, attribute changes never originate on a client but instead - originate on the server after processing a request from the client - (via the below documented invocation - services) to do something application-specific that results in one - or more attribute changes taking place. - -
See the section on Ant Tasks for - information on how to configure and run this post-processor. - -
- public class ServerEntity implements Subscriber {
- public void init (DObjectManager omgr) {
- omgr.createObject(CageObject.class, this);
- }
-
- // inherited from interface Subscriber
- public void objectAvailable (DObject object) {
- // yay! we created our object
- _object = (CageObject)object;
- }
-
- // inherited from interface Subscriber
- public void requestFailed (int oid, ObjectAccessException cause) {
- // oh the humanity, we failed to create our object; in
- // general this would only happen if we did something silly like
- // passed in a DObject class that didn't extend DObject
- }
-
- protected CageObject _object;
- }
-
- You'll notice that we provide an instance of a Subscriber
- when creating our object. This subscriber instance is in fact
- subscribed to the newly created object in the same manner as is
- described below for all additional subscribers to the object. It is
- possible to instruct an object to automatically destroy itself when
- all subscribers have unsubscribed. (See the not very terse {@link
- com.threerings.presents.dobj.DObject}.setDestroyOnLastSubscriberRemoved()).
-
- The client obtains a proxy of the object by a process called - subscription, which is accomplished via {@link - com.threerings.presents.dobj.DObjectManager}.subscribeToObject(): - -
- public class ObjectUser implements Subscriber {
- public void init (Client client, int objectId) {
- client.getDObjectManager().subscribeToObject(objectId, this);
- }
-
- // inherited from interface Subscriber
- public void objectAvailable (DObject object) {
- // yay! we got our object
- _object = (CageObject)object;
- }
-
- // inherited from interface Subscriber
- public void requestFailed (int oid, ObjectAccessException cause) {
- // oh the humanity, we failed to subscribe
- }
-
- protected CageObject _object;
- }
-
- Later a client would relinquish its subscription to the object - using a similar mechanism: - -
- public class ObjectUser implements Subscriber {
- // ...
- public void shutdown (Client client) {
- client.getDObjectManager().unsubscribeFromObject(
- _object.getOid(), this);
- _object = null;
- }
- // ...
- }
-
- However, this is a fine time to point out the dangers of working in an
- asynchronous distributed environment. There is no guarantee that your
- object subscription request will be completed before the client
- decides to call shutdown() on its ObjectUser. Thus, in
- the previous code, we could get a null pointer exception, and even
- worse, we could remain subscribed to the object even though we didn't
- want to be. To avoid these sorts of problems, the {@link
- com.threerings.presents.util.SafeSubscriber} class is provided:
-
-
- public class ObjectUser implements Subscriber {
- public void init (Client client, int objectId) {
- _safesub = new SafeSubscriber(objectId, this);
- _safesub.subcribe(client.getDObjectManager());
- }
-
- // inherited from interface Subscriber
- public void objectAvailable (DObject object) {
- // yay! we got our object
- _object = (CageObject)object;
- }
-
- // inherited from interface Subscriber
- public void requestFailed (int oid, ObjectAccessException cause) {
- // oh the humanity, we failed to subscribe
- }
-
- public void shutdown (Client client) {
- _safesub.unsubscribe(client.getDObjectManager());
- _object = null;
- }
-
- protected SafeSubscriber _safesub;
- protected CageObject _object;
- }
-
- The safe subscriber will pass the object availability on to your
- subscriber and when the time comes to unsubscribe, it will cope with
- the case where the original subscription was not fully processed and
- stick around long enough to ensure that once it is, the request to
- unsubscribe is also dispatched. It will also cope with a request to
- unsubscribe() even if the original subscription request
- failed.
-
- The basic listener is the {@link - com.threerings.presents.dobj.AttributeChangeListener} which is - informed of all simple attribute changes (setting a primitive field to - a new value is called an attribute change). We return to our trusty - example: - -
- public class ObjectUser
- implements Subscriber, AttributeChangeListener {
- // ...
- public void init (Client client, int objectId) {
- _safesub = new SafeSubscriber(_subscriber, objectId);
- _safesub.subcribe(client.getDObjectManager());
- }
-
- // inherited from interface Subscriber
- public void objectAvailable (DObject object) {
- // yay! we got our object
- _object = (CageObject)object;
- _object.addListener(this);
- }
-
- // inherited from interface Subscriber
- public void requestFailed (int oid, ObjectAccessException cause) {
- // oh the humanity, we failed to subscribe
- }
-
- // inherited from interface AttributeChangeListener
- public void attributeChanged (AttributeChangedEvent event)
- {
- System.out.println("Wow! The " + event.getName() +
- " field changed to " + event.getValue() + ".");
- }
-
- public void shutdown (Client client) {
- _safesub.unsubscribe(client.getDObjectManager());
- if (_object != null) {
- // removing our listener not necessary as we are
- // unsubscribing, but it's a good habit to develop as
- // frequently listeners will come and go during the
- // lifetime of an object subscription
- _object.removeListener(this);
- _object = null;
- }
- }
-
- protected SafeSubscriber _safesub;
- protected CageObject _object;
- }
-
- The attributeChanged() method of our registered listener
- will be called whenever an event is received as a result of one of the
- setter methods being called on the CageObject by
- any participant in the distributed system. The setter creates
- an event which is sent to the server, the server dispatches the event
- to all subscribers of the object and the Presents system dispatches
- the event notification to all registered listeners when the event is
- received on the client. Note that listeners are also used on the
- server as entities on the server also frequently need to respond to
- attribute changes. They are notified immediately after the server has
- dispatched the event (over the network) to all subscribed clients.
-
- It is useful to note that listeners are notified of a changed - attribute after the change has been applied to the object. The - previous value of the attribute is available through the {@link - com.threerings.presents.dobj.AttributeChangedEvent#getOldValue} - method, though in spite of many years of experience using this system - in a variety of circumstances, we have rarely found that we cared to - know the previous value. - -
Distributed Arrays
-
- Arrays of primitive types can be used in a distributed object and the
- system will detect their use and provide a mechanism for updating the
- entire array and an additional mechanism for updating a single element
- at a time:
-
-
- public class ChessObject extends DObject
- {
- // AUTO-GENERATED: FIELDS START
- /** The field name of the state field. */
- public static final String STATE = "state";
- // AUTO-GENERATED: FIELDS END
-
- /** Used to track our board state. */
- public int[] state;
-
- // AUTO-GENERATED: METHODS START
- /**
- * Requests that the state field be set to the
- * specified value. The local value will be updated immediately and an
- * event will be propagated through the system to notify all listeners
- * that the attribute did change. Proxied copies of this object (on
- * clients) will apply the value change when they received the
- * attribute changed notification.
- */
- public void setState (int[] value)
- {
- int[] ovalue = this.state;
- requestAttributeChange(
- STATE, value, ovalue);
- this.state = (value == null) ? null : (int[])value.clone();
- }
-
- /**
- * Requests that the indexth element of
- * state field be set to the specified value.
- * The local value will be updated immediately and an event will be
- * propagated through the system to notify all listeners that the
- * attribute did change. Proxied copies of this object (on clients)
- * will apply the value change when they received the attribute
- * changed notification.
- */
- public void setStateAt (int value, int index)
- {
- int ovalue = this.state[index];
- requestElementUpdate(
- STATE, index, new Integer(value), new Integer(ovalue));
- this.state[index] = value;
- }
- // AUTO-GENERATED: METHODS END
- }
-
- To correspond with what is called an "element update" (the
- modification of a single element in an array), there is the {@link
- com.threerings.presents.dobj.ElementUpdateListener}. When an element
- is updated, listeners implementing that interface will be notified.
- Remember that if the whole array is changed using
- setState(), the normal {@link
- com.threerings.presents.dobj.AttributeChangeListener} is the interface
- one uses to hear about it.
-
- Note that distributed arrays are not - automatically resized. If a request is made to update the element at - index 9 of an array, the array must be of at least size 10 or an array - index out of bounds exception will be thrown (as should be evident - from inspecting the code above). For more dynamic collections of - objects, see the documentation below about distributed sets. - -
This mechanism is not actually limited to arrays of primitive - types. It also works for arrays of objects that implement the {@link - com.threerings.io.Streamable} interface which is documented next. - -
Streamable and its good friend SimpleStreamableObject
-
- The {@link com.threerings.io.Streamable} interface is used to mark
- objects that can be sent over the network by using them in distributed
- object fields by using arrays of such objects as a field. This
- interface functions in much the same way that {@link
- java.io.Serializable} does in that it simply marks the class and an
- underlying mechanism uses reflection to actually marshall and
- unmarshall the object on the network. In fact, all
- non-transient fields of a streamable object are included
- during the marhsalling process. Here's an example:
-
-
- public class Player implements Streamable
- {
- /** This player's name. */
- public String name;
-
- /** This player's rating. */
- public int rating;
- }
-
- public class ChessObject extends DObject
- {
- /** A record for each player in the game. */
- public Player[] players;
- }
-
- The generated methods are ommitted for the sake of brevity, but as you
- would expect, both a setPlayers(Player[] value) and a
- setPlayersAt(Player value, int index) method will be
- generated and do just what you expect.
-
- It should be pointed out that streamable objects sent over the - network are sent in their entirety. No mechanism is provided for - updating just a single field in a streamable instance both because - that would increase the complexity of the system tremendously and - because it is generally not very useful. If conservation of bandwidth - is of extreme importance, special {@link - com.threerings.presents.dobj.DEvent} derived classes can be created to - transmit precisely what is desired and nothing more. Doing so is - beyond the scope of this introduction, but will hopefully be covered - in an additional tutorial. - -
The {@link com.threerings.io.SimpleStreamableObject} class is a
- convenient way to create a simple record like the Player
- record above that implements Streamable and provides a
- default implementation of toString() that uses reflection
- to print out the actual values of the fields in the object (a boon
- when logging and debugging).
-
-
Distributed Sets
-
- In developing a distributed system, one frequently encounters
- situations where one wants distributed collection of objects where
- order is generally not important but the ability to fluidly add and
- remove elements is. For such occasions we provide the distributed set
- or {@link com.threerings.presents.dobj.DSet}.
-
-
A DSet contains entries (called entries rather than
- elements to avoid confusion with array "element updating") which must
- implement the {@link com.threerings.presents.dobj.DSet.Entry}
- interface. This automatically makes them {@link
- com.threerings.io.Streamable} and requires that they provide a {@link
- java.lang.Comparable} key which is used to distinguish them from other
- entries in the set (and look them up via an efficient binary search).
-
-
When using a DSet one is provided with three new
- operations: addToFoo(), updateFoo() and
- removeFromFoo(). Once again an example is in order:
-
-
- public class Monkey implements DSet.Entry
- {
- /** The monkey's name. */
- public String name;
-
- /** The monkey's age. */
- public int age;
-
- // documentation inherited from interface DSet.Entry
- public Comparable getKey ()
- {
- return name;
- }
- }
-
- public class CageObject extends DObject
- {
- // AUTO-GENERATED: FIELDS START
- /** The field name of the monkeys field. */
- public static final String MONKEYS = "monkeys";
- // AUTO-GENERATED: FIELDS END
-
- /** A collection of monkeys. */
- public DSet monkeys;
-
- // AUTO-GENERATED: METHODS START
- /**
- * Requests that the specified entry be added to the
- * monkeys set.
- */
- public void addToMonkeys (DSet.Entry elem)
- {
- requestEntryAdd(MONKEYS, monkeys, elem);
- }
-
- /**
- * Requests that the entry matching the supplied key be removed from
- * the monkeys set.
- */
- public void removeFromMonkeys (Comparable key)
- {
- requestEntryRemove(MONKEYS, monkeys, key);
- }
-
- /**
- * Requests that the specified entry be updated in the
- * monkeys set.
- */
- public void updateMonkeys (DSet.Entry elem)
- {
- requestEntryUpdate(MONKEYS, monkeys, elem);
- }
-
- /**
- * Requests that the monkeys field be set to the
- * specified value.
- */
- public void setMonkeys (DSet value)
- {
- requestAttributeChange(MONKEYS, value, this.monkeys);
- this.monkeys = (value == null) ? null : (DSet)value.clone();
- }
- // AUTO-GENERATED: METHODS END
- }
-
- It is possible to set the entire set (which is necessary to establish
- its original value even if one decides to set it to the empty set),
- but more commonly one will simply add entries to the set, update those
- entries and remove them using the provided methods.
-
- In conjunction with the DSet there exists the {@link
- com.threerings.presents.dobj.SetListener} which is notified when
- changes are made to a distributed set. This functions in the same was
- as the previously documented listeners, so I will refrain from boring
- you with yet more sample code.
-
-
Systems making use of the distributed object services will need to set + up a client and server. The following packages contain those services and + their implementation:
+ +Systems that need to scale beyond a single server will need to make use + of services in the peer package which provides a mechanism by which + multiple servers can operate in concert.