From 45c2dc143f599e417a0225adc28cdb573fb5bf9b Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Fri, 1 Jun 2001 19:56:13 +0000 Subject: [PATCH] More progress on the distributed object stuff. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@19 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- .../presents/dobj/AttributeChangedEvent.java | 23 +- .../com/threerings/presents/dobj/DEvent.java | 20 +- .../threerings/presents/dobj/DEventUtil.java | 66 +++++ .../com/threerings/presents/dobj/DObject.java | 80 +++++- .../presents/server/PresentsDObjectMgr.java | 272 ++++++++++++++++++ .../com/threerings/presents/util/IntMap.java | 221 ++++++++++++++ 6 files changed, 666 insertions(+), 16 deletions(-) create mode 100644 src/java/com/threerings/presents/dobj/DEventUtil.java create mode 100644 src/java/com/threerings/presents/server/PresentsDObjectMgr.java create mode 100644 src/java/com/threerings/presents/util/IntMap.java 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; +}