diff --git a/build.xml b/build.xml index 73322b3fd..e7da8186c 100644 --- a/build.xml +++ b/build.xml @@ -120,7 +120,8 @@ + link="http://www.threerings.net/code/narya/narya/docs/api" + stylesheetfile="docs/stylesheet.css"> diff --git a/docs/stylesheet.css b/docs/stylesheet.css new file mode 100644 index 000000000..47ae720bd --- /dev/null +++ b/docs/stylesheet.css @@ -0,0 +1,48 @@ +/* Javadoc style sheet */ + +/* Page background color */ +body { + background-color: #FFFFFF; + font-family: Arial, Helvetica, sans-serif; +} + +pre { + font-family: Courier New, monospace +} + +/* Headings */ +h1 { font-size: 145% } + +/* Table colors */ +.TableHeadingColor { background: #CCCCFF } /* Dark mauve */ +.TableSubHeadingColor { background: #EEEEFF } /* Light mauve */ +.TableRowColor { background: #FFFFFF } /* White */ + +/* Font used in left-hand frame lists */ +.FrameTitleFont { font-size: 100%; font-family: Helvetica, Arial, sans-serif } +.FrameHeadingFont { font-size: 90%; font-family: Helvetica, Arial, sans-serif } +.FrameItemFont { font-size: 90%; font-family: Helvetica, Arial, sans-serif } + +/* Navigation bar fonts and colors */ +.NavBarCell1 { background-color:#EEEEFF;} /* Light mauve */ +.NavBarCell1Rev { background-color:#00008B;} /* Dark Blue */ +.NavBarFont1 { font-family: Arial, Helvetica, sans-serif; color:#000000;} +.NavBarFont1Rev { font-family: Arial, Helvetica, sans-serif; color:#FFFFFF;} + +.NavBarCell2 { + font-family: Arial, Helvetica, sans-serif; background-color:#FFFFFF; +} +.NavBarCell3 { + font-family: Arial, Helvetica, sans-serif; background-color:#FFFFFF; +} + +.example { + padding-top: 5px; + padding-bottom: 5px; + padding-left: 10px; + padding-right: 10px; + margin-left: 20px; + margin-right: 20px; + border: 1px solid black; + background-color: #FFFF99 +} diff --git a/src/java/com/threerings/presents/images/dobject.dia b/src/java/com/threerings/presents/images/dobject.dia new file mode 100644 index 000000000..0c1735354 Binary files /dev/null and b/src/java/com/threerings/presents/images/dobject.dia differ diff --git a/src/java/com/threerings/presents/images/dobject.png b/src/java/com/threerings/presents/images/dobject.png new file mode 100644 index 000000000..5890da6e8 Binary files /dev/null and b/src/java/com/threerings/presents/images/dobject.png differ diff --git a/src/java/com/threerings/presents/package.html b/src/java/com/threerings/presents/package.html new file mode 100644 index 000000000..a80c62d4b --- /dev/null +++ b/src/java/com/threerings/presents/package.html @@ -0,0 +1,321 @@ + + + + + + + + + Defines a framework for distributing information between multiple + separate applications (over a network) and for coordinating control + flow between those applications in the form of remote procedure call + services. The normal configuration of the Presents system is + client/server; generally with many clients connecting to a single + server. All information transfer takes place through the server using + the distributed object system documented below. + +

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. + +

Distributed Objects

+ + The Presents services allow applications to access and update shared + information through a mechanism known as distributed objects. + Distributed objects are maintainedon the server and clients + "subscribe" to the objects and are provided with proxy copies which + are updated by a stream of events sent by the server when any state + changes in the objects. + +

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. + +

+ +

Defining an object

+ + A distributed object is defined just like a regular Java object and is + then run through a post-processor which inserts methods and constants + into the object definition which are needed by the distributed object + system. Here is a distributed object as originally defined: + +
+    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;
+    }
+ + This class definition is then run 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 (fields in a + distributed object are referred to as attributes in this + documentation and elsewhere in the system) 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. + +

Creating an object

+ + Generally, some entity on the server will choose to create a new + instance of a distributed object. Rather than simply instantiate the + object directly, one must create the object through the {@link + com.threerings.presents.dobj.DObjectManager}: + +
+    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 {@link + com.threerings.presents.dobj.DObject}.setDestroyOnLastSubscriberRemoved()). + +

Subscribing to an object

+ +

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 would get a null pointer exception, and even + worse, we would 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 = 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 DObject _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. + +

Listeners

+ + Once a client has subscribed to a distributed object, all events + pertaining to that object will be delivered to the client. Frequently, + it is useful to respond dynamically to changes in distributed object + values and this is accomplished using listeners. A client can register + any number of listeners on an object and when the object is finally + unsubscribed from and garbage collected, the listener registrations + all go away as well. + +

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 in this distributed object + system). 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 = 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 DObject _object;
+    }
+ + 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 collections

+ +

Invocation Services

+ + +