diff --git a/src/java/com/threerings/presents/dobj/AttributeChangedEvent.java b/src/java/com/threerings/presents/dobj/AttributeChangedEvent.java index 1ffd9581a..412c1d45d 100644 --- a/src/java/com/threerings/presents/dobj/AttributeChangedEvent.java +++ b/src/java/com/threerings/presents/dobj/AttributeChangedEvent.java @@ -1,8 +1,11 @@ // -// $Id: AttributeChangedEvent.java,v 1.1 2001/06/01 07:12:13 mdb Exp $ +// $Id: AttributeChangedEvent.java,v 1.2 2001/06/01 19:56:13 mdb Exp $ package com.threerings.cocktail.cher.dobj; +import java.lang.reflect.Method; +import com.threerings.cocktail.cher.Log; + /** * An attribute changed event is dispatched when a single attribute of a * distributed object has changed. It can also be constructed to request @@ -92,6 +95,24 @@ public class AttributeChangedEvent extends DEvent return ((Double)_value).doubleValue(); } + /** + * Applies this attribute change to the object. + */ + public boolean applyToObject (DObject target) + throws ObjectAccessException + { + // look up the setter for this object + Method setter = DEventUtil.getSetter(target.getClass(), _name); + + try { + setter.invoke(target, new Object[] { _value }); + } catch (Exception e) { + throw new ObjectAccessException("Reflection error: " + e); + } + + return true; + } + protected String _name; protected Object _value; } diff --git a/src/java/com/threerings/presents/dobj/DEvent.java b/src/java/com/threerings/presents/dobj/DEvent.java index 7dc1edb90..c77097b36 100644 --- a/src/java/com/threerings/presents/dobj/DEvent.java +++ b/src/java/com/threerings/presents/dobj/DEvent.java @@ -1,5 +1,5 @@ // -// $Id: DEvent.java,v 1.2 2001/06/01 07:12:13 mdb Exp $ +// $Id: DEvent.java,v 1.3 2001/06/01 19:56:13 mdb Exp $ package com.threerings.cocktail.cher.dobj; @@ -9,7 +9,7 @@ package com.threerings.cocktail.cher.dobj; * notification purposes, without making any modifications to the object * that defines the delivery group (the object's subscribers). */ -public class DEvent +public abstract class DEvent { /** * Returns the oid of the object that is the target of this event. @@ -19,6 +19,22 @@ public class DEvent return _toid; } + /** + * Applies the attribute modifications represented by this event to + * the specified target object. This is called by the distributed + * object manager in the course of dispatching events and should not + * be called directly. + * + * @exception ObjectAccessException thrown if there is any problem + * applying the event to the object (invalid attribute, etc.). + * + * @return true if the object manager should go on to notify the + * object's subscribers of this event, false if the event should be + * treated silently and the subscribers should not be notified. + */ + public abstract boolean applyToObject (DObject target) + throws ObjectAccessException; + /** * Constructs a new distributed object event that pertains to the * specified distributed object. diff --git a/src/java/com/threerings/presents/dobj/DEventUtil.java b/src/java/com/threerings/presents/dobj/DEventUtil.java new file mode 100644 index 000000000..39d2d69e5 --- /dev/null +++ b/src/java/com/threerings/presents/dobj/DEventUtil.java @@ -0,0 +1,66 @@ +// +// $Id: DEventUtil.java,v 1.1 2001/06/01 19:56:13 mdb Exp $ + +package com.threerings.cocktail.cher.dobj; + +import java.lang.reflect.Method; +import java.util.HashMap; + +/** + * A repository for distributed object event related utility functions. + */ +public class DEventUtil +{ + /** + * Looks up and returns the method used to set an attribute of the + * specified name on an instance of the specified distributed object + * class. + */ + public static Method getSetter (Class clazz, String name) + throws ObjectAccessException + { + // first see if it's cached + Method setter = (Method)_setterCache.get(name); + if (setter != null) { + return setter; + } + + // convert the attribute name into a setter name + String sname = setterName(name); + try { + // look up that method (we don't have the parameter types, so + // we have to do our own name matching) + Method[] methods = clazz.getMethods(); + for (int i = 0; i < methods.length; i++) { + if (methods[i].getName().equals(sname)) { + setter = methods[i]; + break; + } + } + + } catch (SecurityException se) { + throw new ObjectAccessException("Reflection error: " + se); + } + + // make sure we found a matching method + if (setter == null) { + String errmsg = "No setter for attribute '" + name + "'."; + throw new ObjectAccessException(errmsg); + } + + // and cache it + _setterCache.put(name, setter); + return setter; + } + + protected static String setterName (String name) + { + StringBuffer sname = new StringBuffer(); + sname.append("get"); + sname.append(Character.toUpperCase(name.charAt(0))); + sname.append(name.substring(1)); + return sname.toString(); + } + + protected static HashMap _setterCache = new HashMap(); +} diff --git a/src/java/com/threerings/presents/dobj/DObject.java b/src/java/com/threerings/presents/dobj/DObject.java index 1e21494f1..68eb517b5 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.6 2001/06/01 05:17:16 mdb Exp $ +// $Id: DObject.java,v 1.7 2001/06/01 19:56:13 mdb Exp $ package com.threerings.cocktail.cher.dobj; @@ -73,17 +73,17 @@ import java.util.ArrayList; public class DObject { /** - * Constructs a new distributed object with the supplied object id. - * This should not be called directly, rather objects should be - * created via the distributed object manager. + * Initializes this distributed object with the supplied object id and + * distributed object manager. This is called by the distributed + * object manager when an object is created and registered with the + * system. Don't call this function yourself. * * @see DObjectManager.createObject */ - public DObject (int oid, DObjectManager manager) + public void init (int oid, DObjectManager mgr) { _oid = oid; - _mgr = manager; - _subscribers = new ArrayList(); + _mgr = mgr; } /** @@ -106,10 +106,10 @@ public class DObject *
If the specified subscriber is already subscribed to this
* object, they will not be added to the list a second time.
*/
- public void addSubscriber (Subscriber subscriber)
+ public void addSubscriber (Subscriber sub)
{
- if (!_subscribers.contains(subscriber)) {
- _subscribers.add(subscriber);
+ if (!_subscribers.contains(sub)) {
+ _subscribers.add(sub);
}
}
@@ -121,12 +121,66 @@ public class DObject
* subscriber is not currently on the list of subscribers for this
* object, nothing happens.
*/
- public void removeSubscriber (Subscriber subscriber)
+ public void removeSubscriber (Subscriber sub)
{
- _subscribers.remove(subscriber);
+ _subscribers.remove(sub);
+ }
+
+ /**
+ * Checks to ensure that the specified subscriber has access to this
+ * object. This will be called before satisfying any fetch or
+ * subscription request. By default objects are accessible to all
+ * subscriber, but certain objects may wish to implement more fine
+ * grained access control.
+ *
+ * @param sub the subscriber that will fetch or subscribe to this
+ * object.
+ *
+ * @return true if the subscriber has access to the object, false if
+ * they do not.
+ */
+ public boolean checkPermissions (Subscriber sub)
+ {
+ return true;
+ }
+
+ /**
+ * Checks to ensure that this event which is about to be processed,
+ * has the appropriate permissions. By default objects accept all
+ * manner of events, but certain objects may wish to implement more
+ * fine grained access control.
+ *
+ * @param event the event that will be dispatched, object permitting.
+ *
+ * @return true if the event is valid and should be dispatched, false
+ * if the event fails the permissions check and should be aborted.
+ */
+ public boolean checkPermissions (DEvent event)
+ {
+ return true;
+ }
+
+ /**
+ * Called by the distributed object manager after it has applied an
+ * event to this object. This dispatches an event notification to all
+ * of the subscribers of this object.
+ *
+ * @param event the event that was just applied.
+ */
+ public void notifySubscribers (DEvent event)
+ {
+ for (int i = 0; i < _subscribers.size(); i++) {
+ Subscriber sub = (Subscriber)_subscribers.get(i);
+ // notify the subscriber
+ if (!sub.handleEvent(event, this)) {
+ // if they return false, we need to remove them from the
+ // subscriber list
+ _subscribers.remove(i--);
+ }
+ }
}
protected int _oid;
protected DObjectManager _mgr;
- protected ArrayList _subscribers;
+ protected ArrayList _subscribers = new ArrayList();
}
diff --git a/src/java/com/threerings/presents/server/PresentsDObjectMgr.java b/src/java/com/threerings/presents/server/PresentsDObjectMgr.java
new file mode 100644
index 000000000..a1e488496
--- /dev/null
+++ b/src/java/com/threerings/presents/server/PresentsDObjectMgr.java
@@ -0,0 +1,272 @@
+//
+// $Id: PresentsDObjectMgr.java,v 1.1 2001/06/01 19:56:13 mdb Exp $
+
+package com.threerings.cocktail.cher.server;
+
+import com.samskivert.util.Queue;
+
+import com.threerings.cocktail.cher.Log;
+import com.threerings.cocktail.cher.dobj.*;
+import com.threerings.cocktail.cher.util.IntMap;
+
+/**
+ * The cher distributed object manager implements the
+ * DObjectManager interface, providing an object manager that
+ * runs on the server. By virtue of running on the server, it manages its
+ * objects directly rather than managing proxies of objects which is what
+ * is done on the client. Thus it simply queues up events and dispatches
+ * them to subscribers.
+ *
+ *
The server object manager is meant to run on the main thread of the
+ * server application and thus provides a method to be invoked by the
+ * application main thread which won't return until the manager has been
+ * requested to shut down.
+ */
+public class CherDObjectMgr implements DObjectManager
+{
+ // inherit documentation from the interface
+ public void createObject (Class dclass, Subscriber target,
+ boolean subscribe)
+ {
+ // queue up a create object event
+ postEvent(new CreateObjectEvent(dclass, target, subscribe));
+ }
+
+ // inherit documentation from the interface
+ public void subscribeToObject (int oid, Subscriber target)
+ {
+ // queue up an access object event
+ postEvent(new AccessObjectEvent(oid, target, true));
+ }
+
+ // inherit documentation from the interface
+ public void fetchObject (int oid, Subscriber target)
+ {
+ // queue up an access object event
+ postEvent(new AccessObjectEvent(oid, target, false));
+ }
+
+ // inherit documentation from the interface
+ public void postEvent (DEvent event)
+ {
+ // just append it to the queue
+ _evqueue.append(event);
+ }
+
+ /**
+ * Initializes the dobjmgr and prepares it for operation.
+ */
+ public void init ()
+ {
+ // we create a dummy object to live as oid zero and we'll use that
+ // for some internal event trickery
+ DObject dummy = new DObject();
+ dummy.init(0, this);
+ _objects.put(0, new DObject());
+ }
+
+ /**
+ * Runs the dobjmgr event loop until it is requested to exit. This
+ * should be called from the main application thread.
+ */
+ public void run ()
+ {
+ Log.info("DOMGR running.");
+
+ while (isRunning()) {
+ // pop the next event off the queue
+ DEvent event = (DEvent)_evqueue.get();
+
+ // look up the target object
+ DObject target = (DObject)_objects.get(event.getTargetOid());
+ if (target == null) {
+ Log.warning("Event target no longer exists " +
+ "[event=" + event + "].");
+ continue;
+ }
+
+ // check the event's permissions
+ if (!target.checkPermissions(event)) {
+ Log.warning("Event failed permissions check " +
+ "[event=" + event + ", target=" + target + "].");
+ continue;
+ }
+
+ try {
+ // everything's good so far, apply the event to the object
+ if (!event.applyToObject(target)) {
+ // if the event returns false from applyToObject, this
+ // means it's a silent event and we shouldn't notify
+ // the subscribers
+ continue;
+ }
+
+ } catch (Exception e) {
+ Log.warning("Failure applying event [event=" + event +
+ ", target=" + target + ", error=" + e + "].");
+ continue;
+ }
+
+ try {
+ // and notify the object's subscribers
+ target.notifySubscribers(event);
+
+ } catch (Exception e) {
+ Log.warning("Failure dispatching event [event=" + event +
+ ", target=" + target + "].");
+ Log.logStackTrace(e);
+ }
+ }
+
+ Log.info("DOMGR exited.");
+ }
+
+ /**
+ * Requests that the dobjmgr shut itself down. It will exit the event
+ * processing loop which cause run() to return.
+ */
+ public void shutdown ()
+ {
+ _running = false;
+ // stick a bogus object on the event queue to ensure that the mgr
+ // wakes up and smells the coffee
+ _evqueue.append(this);
+ }
+
+ protected synchronized boolean isRunning ()
+ {
+ return _running;
+ }
+
+ protected int getNextOid ()
+ {
+ // look for the next unused oid. in theory if we had two billion
+ // objects, this would loop infinitely, but the world would have
+ // come to an end long before we had two billion objects
+ do {
+ _nextOid = (_nextOid + 1) % Integer.MAX_VALUE;
+ } while (_objects.contains(_nextOid));
+
+ return _nextOid;
+ }
+
+ protected boolean _running = true;
+ protected Queue _evqueue = new Queue();
+ protected IntMap _objects = new IntMap();
+ protected int _nextOid = 0;
+
+ /**
+ * Used to create a distributed object and register it with the
+ * system.
+ */
+ protected class CreateObjectEvent extends DEvent
+ {
+ public CreateObjectEvent (Class clazz, Subscriber target,
+ boolean subscribe)
+ {
+ super(0); // target the fake object
+ _class = clazz;
+ _target = target;
+ _subscribe = subscribe;
+ }
+
+ public boolean applyToObject (DObject target)
+ throws ObjectAccessException
+ {
+ try {
+ // create a new instance of this object
+ DObject obj = (DObject)_class.newInstance();
+ int oid = getNextOid();
+
+ // initialize this object
+ obj.init(oid, CherDObjectMgr.this);
+ // insert it into the table
+ _objects.put(oid, obj);
+
+ if (_target != null) {
+ // add the subscriber to this object's subscriber list
+ // if they requested it
+ if (_subscribe) {
+ obj.addSubscriber(_target);
+ }
+
+ // let the target subscriber know that their object is
+ // available
+ _target.objectAvailable(obj);
+ }
+
+ } catch (Exception e) {
+ Log.warning("Object creation failure " +
+ "[class=" + _class.getName() +
+ ", error=" + e + "].");
+
+ // let the subscriber know shit be fucked
+ if (_target != null) {
+ String errmsg = "Object instantiation failed: " + e;
+ _target.requestFailed(new ObjectAccessException(errmsg));
+ }
+ }
+
+ // and return false to ensure that this event is not
+ // dispatched to the fake object's subscriber list (even
+ // though it's empty)
+ return false;
+ }
+
+ protected Class _class;
+ protected Subscriber _target;
+ protected boolean _subscribe;
+ }
+
+ /**
+ * Used to make an object available to a subscriber (with or without
+ * the associated subscription).
+ */
+ protected class AccessObjectEvent extends DEvent
+ {
+ public AccessObjectEvent (int oid, Subscriber target,
+ boolean subscribe)
+ {
+ super(0); // target the bogus object
+ _oid = oid;
+ _target = target;
+ _subscribe = subscribe;
+ }
+
+ public boolean applyToObject (DObject target)
+ throws ObjectAccessException
+ {
+ // look up the target object
+ DObject obj = (DObject)_objects.get(_oid);
+
+ // if it don't exist, let them know
+ if (obj == null) {
+ _target.requestFailed(new NoSuchObjectException(_oid));
+ return false;
+ }
+
+ // check permissions
+ if (!obj.checkPermissions(_target)) {
+ String errmsg = "m.access_denied\t" + _oid;
+ _target.requestFailed(new ObjectAccessException(errmsg));
+ return false;
+ }
+
+ // if they wanted to subscribe, do so
+ if (_subscribe) {
+ obj.addSubscriber(_target);
+ }
+
+ // let them know that things are groovy
+ _target.objectAvailable(obj);
+
+ // return false to ensure that this event is not dispatched to
+ // the fake object's subscriber list (even though it's empty)
+ return false;
+ }
+
+ protected int _oid;
+ protected Subscriber _target;
+ protected boolean _subscribe;
+ }
+}
diff --git a/src/java/com/threerings/presents/util/IntMap.java b/src/java/com/threerings/presents/util/IntMap.java
new file mode 100644
index 000000000..b74fa7b6c
--- /dev/null
+++ b/src/java/com/threerings/presents/util/IntMap.java
@@ -0,0 +1,221 @@
+//
+// $Id: IntMap.java,v 1.1 2001/06/01 19:56:13 mdb Exp $
+
+package com.threerings.cocktail.cher.util;
+
+import java.util.Enumeration;
+import java.util.NoSuchElementException;
+
+/**
+ * An int map is like a hash map, but with ints as keys. We avoid the
+ * annoyance of having to create Integer objects every time
+ * we want to lookup or insert values. Like java.util.HashMap
+ * it is not thread safe.
+ */
+public class IntMap
+{
+ public final static int DEFAULT_BUCKETS = 64;
+
+ public IntMap (int buckets)
+ {
+ _buckets = new Record[buckets];
+ }
+
+ public IntMap ()
+ {
+ this(DEFAULT_BUCKETS);
+ }
+
+ public int size ()
+ {
+ return _size;
+ }
+
+ public void put (int key, Object value)
+ {
+ int index = Math.abs(key)%_buckets.length;
+ Record rec = _buckets[index];
+
+ // either we start a new chain
+ if (rec == null) {
+ _buckets[index] = new Record(key, value);
+ _size++; // we're bigger
+ return;
+ }
+
+ // or we replace an element in an existing chain
+ Record prev = rec;
+ for (; rec != null; rec = rec.next) {
+ if (rec.key == key) {
+ rec.value = value; // we're not bigger
+ return;
+ }
+ prev = rec;
+ }
+
+ // or we append it to this chain
+ prev.next = new Record(key, value);
+ _size++; // we're bigger
+ }
+
+ public Object get (int key)
+ {
+ int index = Math.abs(key)%_buckets.length;
+ for (Record rec = _buckets[index]; rec != null; rec = rec.next) {
+ if (rec.key == key) {
+ return rec.value;
+ }
+ }
+ return null;
+ }
+
+ public boolean contains (int key)
+ {
+ return get(key) != null;
+ }
+
+ public Object remove (int key)
+ {
+ int index = Math.abs(key)%_buckets.length;
+ Record rec = _buckets[index];
+
+ // if there's no chain, there's no object
+ if (rec == null) {
+ return null;
+ }
+
+ // maybe it's the first one in this chain
+ if (rec.key == key) {
+ _buckets[index] = rec.next;
+ _size--;
+ return rec.value;
+ }
+
+ // or maybe it's an element further down the chain
+ for (Record prev = rec; rec != null; rec = rec.next) {
+ if (rec.key == key) {
+ prev.next = rec.next;
+ _size--;
+ return rec.value;
+ }
+ prev = rec;
+ }
+
+ return null;
+ }
+
+ public void clear ()
+ {
+ // abandon all of our hash chains (the joy of garbage collection)
+ for (int i = 0; i < _buckets.length; i++) {
+ _buckets[i] = null;
+ }
+ // zero out our size
+ _size = 0;
+ }
+
+ public Enumeration keys ()
+ {
+ return new Enumerator(_buckets, true);
+ }
+
+ public Enumeration elements ()
+ {
+ return new Enumerator(_buckets, false);
+ }
+
+ protected static class Record
+ {
+ public Record next;
+ public int key;
+ public Object value;
+
+ public Record (int key, Object value)
+ {
+ this.key = key;
+ this.value = value;
+ }
+ }
+
+ class Enumerator implements Enumeration
+ {
+ public Enumerator (Record[] buckets, boolean returnKeys)
+ {
+ _buckets = buckets;
+ _index = buckets.length;
+ _returnKeys = returnKeys;
+ }
+
+ public boolean hasMoreElements ()
+ {
+ // if we're pointing to an entry, we've got more entries
+ if (_record != null) {
+ return true;
+ }
+
+ // search backward through the buckets looking for the next
+ // non-empty hash chain
+ while (_index-- > 0) {
+ if ((_record = _buckets[_index]) != null) {
+ return true;
+ }
+ }
+
+ // found no non-empty hash chains, we're done
+ return false;
+ }
+
+ public Object nextElement ()
+ {
+ // if we're not pointing to an entry, search for the next
+ // non-empty hash chain
+ if (_record == null) {
+ while ((_index-- > 0) &&
+ ((_record = _buckets[_index]) == null));
+ }
+
+ // if we found a record, return it's value and move our record
+ // reference to it's successor
+ if (_record != null) {
+ Record r = _record;
+ _record = r.next;
+ return _returnKeys ? new Integer(r.key) : r.value;
+ }
+
+ throw new NoSuchElementException("IntMapEnumerator");
+ }
+
+ private int _index;
+ private Record _record;
+ private Record[] _buckets;
+ private boolean _returnKeys;
+ }
+
+// public static void main (String[] args)
+// {
+// IntMap table = new IntMap();
+
+// System.out.print("Inserting: ");
+// for (int i = 10; i < 100; i++) {
+// Integer value = new Integer(i);
+// table.put(i, value);
+// System.out.print("(" + i + "," + value + ")");
+// }
+// System.out.println("");
+
+// System.out.print("Looking up: ");
+// for (int i = 10; i < 100; i++) {
+// System.out.print("(" + i + "," + table.get(i) + ")");
+// }
+// System.out.println("");
+
+// System.out.print("Removing: ");
+// for (int i = 10; i < 100; i++) {
+// System.out.print("(" + i + "," + table.remove(i) + ")");
+// }
+// System.out.println("");
+// }
+
+ private Record[] _buckets;
+ private int _size;
+}