From 6663aae772fc419a673f1a66ab6f38b48b70a9ec Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Thu, 19 Jul 2001 05:56:20 +0000 Subject: [PATCH] Beginnings of invocation services implementation. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@67 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- .../threerings/presents/client/Client.java | 27 ++- .../presents/client/Communicator.java | 29 ++- .../presents/client/InvocationDirector.java | 208 ++++++++++++++++++ .../presents/data/ClientObject.java | 16 ++ .../presents/data/InvocationObject.java | 21 ++ .../com/threerings/presents/dobj/DObject.java | 15 +- .../presents/dobj/MessageEvent.java | 120 ++++++++++ .../presents/net/AuthResponseData.java | 8 +- .../presents/server/InvocationManager.java | 77 +++++++ .../presents/server/TestObject.dobj | 14 +- 10 files changed, 518 insertions(+), 17 deletions(-) create mode 100644 src/java/com/threerings/presents/client/InvocationDirector.java create mode 100644 src/java/com/threerings/presents/data/ClientObject.java create mode 100644 src/java/com/threerings/presents/data/InvocationObject.java create mode 100644 src/java/com/threerings/presents/dobj/MessageEvent.java create mode 100644 src/java/com/threerings/presents/server/InvocationManager.java diff --git a/src/java/com/threerings/presents/client/Client.java b/src/java/com/threerings/presents/client/Client.java index 40ce632cb..5938035df 100644 --- a/src/java/com/threerings/presents/client/Client.java +++ b/src/java/com/threerings/presents/client/Client.java @@ -1,5 +1,5 @@ // -// $Id: Client.java,v 1.6 2001/06/11 17:44:03 mdb Exp $ +// $Id: Client.java,v 1.7 2001/07/19 05:56:20 mdb Exp $ package com.threerings.cocktail.cher.client; @@ -141,6 +141,31 @@ public class Client return (_comm != null) ? _comm.getDObjectManager() : null; } + /** + * Every client has an associated ClientObject instance. + * + * @return the oid of the client object or -1 if we are not currently + * connected to the server. + */ + public int getClientOid () + { + return (_comm != null) ? _comm.getClientOid() : -1; + } + + /** + * Returns the invocation manager associated with this session. This + * reference is only valid for the duration of the session and a new + * reference must be obtained if the client disconnects and reconnects + * to the server. + * + * @return the invocation manager in effect or null if we have no + * established connection to the server. + */ + public InvocationManager getInvocationManager () + { + return (_comm != null) ? _comm.getInvocationManager() : null; + } + /** * Requests that this client connect and logon to the server with * which it was previously configured. diff --git a/src/java/com/threerings/presents/client/Communicator.java b/src/java/com/threerings/presents/client/Communicator.java index 1bfdcd2e0..fbff0e83a 100644 --- a/src/java/com/threerings/presents/client/Communicator.java +++ b/src/java/com/threerings/presents/client/Communicator.java @@ -1,5 +1,5 @@ // -// $Id: Communicator.java,v 1.7 2001/06/09 23:39:03 mdb Exp $ +// $Id: Communicator.java,v 1.8 2001/07/19 05:56:20 mdb Exp $ package com.threerings.cocktail.cher.client; @@ -54,14 +54,31 @@ public class Communicator /** * Returns the distributed object manager in effect for this session. * This instance is only valid while the client is connected to the - * server. If we become disconnected and have to reconnect, a new omgr - * instance should be obtained. + * server. */ public DObjectManager getDObjectManager () { return _omgr; } + /** + * Returns the oid of the client object associated with this session. + * It is only valid for the duration of the session. + */ + public int getClientOid () + { + return _cloid; + } + + /** + * Returns the invocation manager associated with this session. This + * reference is only valid for the duration of the session. + */ + public InvocationManager getInvocationManager () + { + return _invmgr; + } + /** * Logs on to the server and initiates our full-duplex message * exchange. @@ -155,10 +172,14 @@ public class Communicator Log.info("Logon succeeded: " + data); // extract bootstrap information + _cloid = data.clientOid; // create our distributed object manager _omgr = new ClientDObjectMgr(this, _client); + // create our invocation manager + _invmgr = new InvocationManager(_client, data.invOid); + // create a new writer thread and start it up if (_writer != null) { throw new RuntimeException("Writer already started!?"); @@ -481,4 +502,6 @@ public class Communicator protected DataInputStream _din; protected ClientDObjectMgr _omgr; + protected int _cloid; + protected InvocationManager _invmgr; } diff --git a/src/java/com/threerings/presents/client/InvocationDirector.java b/src/java/com/threerings/presents/client/InvocationDirector.java new file mode 100644 index 000000000..516611bf5 --- /dev/null +++ b/src/java/com/threerings/presents/client/InvocationDirector.java @@ -0,0 +1,208 @@ +// +// $Id: InvocationDirector.java,v 1.1 2001/07/19 05:56:20 mdb Exp $ + +package com.threerings.cocktail.cher.client; + +import java.lang.reflect.Method; +import java.util.HashMap; + +import com.threerings.cocktail.cher.Log; +import com.threerings.cocktail.cher.data.*; +import com.threerings.cocktail.cher.dobj.*; +import com.threerings.cocktail.cher.util.IntMap; + +/** + * The invocation services provide client to server invocations (service + * requests) and server to client invocations (responses and + * notifications). Via this mechanism, the client can make requests of the + * server, be notified of its response and the server can asynchronously + * invoke code on the client. + * + *

Invocations are like remote procedure calls in that they are named + * and take arguments. They are simple in that the arguments can only be + * of a small set of supported types (the set of distributed object field + * types) and there is no special facility provided for referencing + * non-local objects (it is assumed that the distributed object facility + * will already be in use for any objects that should be shared). + * + *

The client invocation manager delivers invocation requests to the + * server invocation manager and maps the responses back to the proper + * response target objects when they arrive. It also maintains the mapping + * of invocation receivers that can receive asynchronous invocation + * notifications at any time from the server. + */ +public class InvocationManager + implements Subscriber +{ + /** + * Constructs a new invocation manager with the specified invocation + * manager oid. It will obtain its distributed object manager and + * client object references from the supplied client instance. The + * invocation manager oid is the oid of the object on the server to + * which to deliver invocation requests. + */ + public InvocationManager (Client client, int imoid) + { + _omgr = client.getDObjectManager(); + _imoid = imoid; + + // add ourselves as a subscriber to the client object + _omgr.subscribeToObject(client.getClientOid(), this); + } + + /** + * Sends an invocation request to the server. If a response target is + * supplied, the response will be delivered to that object by calling + * a member function on it whose name is derived from the invocation + * name. For example, if the caller invoked a procedure named + * SwitchLocation, the response would be delivered via a + * call to the handleSwitchLocationResponse method on the + * response target object. The signature of that method would be + * defined by the arguments provided in the response message. + * + * @param name the name of the invocation. + * @param args the arguments of the invocation. + * @param rsptarget the object that will receive the response, or null + * if no response is desired. + */ + public void invoke (String name, Object[] args, Object rsptarget) + { + int invid = nextInvocationId(); + + // we need an args array for a message that can contain the + // invocation name, an invocation id and the invocation arguments + Object[] iargs = new Object[args.length+2]; + System.arraycopy(args, 0, iargs, 2, args.length); + iargs[0] = name; + iargs[1] = new Integer(invid); + + // create a message event on the invocation manager object + MessageEvent event = new MessageEvent( + _imoid, InvocationObject.MESSAGE_NAME, iargs); + + // if we have a response target, register that for later receipt + // of the response + if (rsptarget != null) { + _targets.put(invid, new Response(name, rsptarget)); + } + + // and finally ship off the invocation message + _omgr.postEvent(event); + } + + public void objectAvailable (DObject object) + { + // nothing doing + } + + public void requestFailed (int oid, ObjectAccessException cause) + { + // nothing doing + } + + /** + * Process incoming message requests on user object. + */ + public boolean handleEvent (DEvent event, DObject target) + { + // we only care about message events + if (!(event instanceof MessageEvent)) { + return true; + } + + MessageEvent mevt = (MessageEvent)event; + + // and only those of proper name + if (!mevt.getName().equals(InvocationObject.MESSAGE_NAME)) { + return true; + } + + // we've got an invocation response, so we extract the args and + // process it + Object[] args = mevt.getArgs(); + int invid = ((Integer)args[0]).intValue(); + String name = (String)args[1]; + + Response rsp = (Response)_targets.get(invid); + if (rsp == null) { + Log.warning("No target for invocation response " + + "[rsp=" + mevt + "]."); + return true; + } + + // prune the method arguments from the full message arguments + Object[] rargs = new Object[args.length-2]; + System.arraycopy(args, 2, rargs, 0, rargs.length); + + // and invoke the response method + Method rspmeth = getResponseMethod(name, target, rargs); + if (rspmeth != null) { + try { + rspmeth.invoke(target, rargs); + } catch (Exception e) { + Log.warning("Error invoking response target method " + + "[target=" + target + ", method=" + rspmeth + + ", error=" + e + "]."); + } + } + + return true; + } + + protected Method getResponseMethod (String name, Object target, + Object[] args) + { + Class tclass = target.getClass(); + String key = tclass.getName() + ":" + name; + Method method = (Method)_methcache.get(key); + + if (method == null) { + String methname = "handle" + name; + method = findMethod(tclass, name); + if (method == null) { + Log.warning("Unable to locate method on response target " + + "[rtclass=" + tclass.getName() + + ", method=" + methname + "]."); + return null; + } + _methcache.put(key, method); + } + + return method; + } + + protected static Method findMethod (Class clazz, String name) + { + Method[] methods = clazz.getMethods(); + for (int i = 0; i < methods.length; i++) { + if (methods[i].getName().equals(name)) { + return methods[i]; + } + } + return null; + } + + protected synchronized int nextInvocationId () + { + return _invocationId++; + } + + protected static class Response + { + public String name; + public Object target; + public Response (String name, Object target) + { + this.name = name; + this.target = target; + } + } + + protected DObjectManager _omgr; + protected int _imoid; + protected ClientObject _clobj; + + protected int _invocationId; + protected IntMap _targets = new IntMap(); + protected HashMap _methcache = new HashMap(); +} diff --git a/src/java/com/threerings/presents/data/ClientObject.java b/src/java/com/threerings/presents/data/ClientObject.java new file mode 100644 index 000000000..e5d8ef6d0 --- /dev/null +++ b/src/java/com/threerings/presents/data/ClientObject.java @@ -0,0 +1,16 @@ +// +// $Id: ClientObject.java,v 1.1 2001/07/19 05:56:20 mdb Exp $ + +package com.threerings.cocktail.cher.data; + +import com.threerings.cocktail.cher.dobj.DObject; + +/** + * Every client in the system has an associated client object to which + * only they subscribe. The client object can be used to deliver messages + * solely to a particular client as well as to publish client-specific + * data. + */ +public class ClientObject extends DObject +{ +} diff --git a/src/java/com/threerings/presents/data/InvocationObject.java b/src/java/com/threerings/presents/data/InvocationObject.java new file mode 100644 index 000000000..63eff4622 --- /dev/null +++ b/src/java/com/threerings/presents/data/InvocationObject.java @@ -0,0 +1,21 @@ +// +// $Id: InvocationObject.java,v 1.1 2001/07/19 05:56:20 mdb Exp $ + +package com.threerings.cocktail.cher.data; + +import com.threerings.cocktail.cher.dobj.DObject; + +/** + * A single invocation object is created by the server invocation manager + * and is used to receive invocation request messages from the client. The + * server presently delivers invocation messages to the client via the + * client object. + */ +public class InvocationObject extends DObject +{ + /** + * This constant is used to identify messages on both ends of the + * invocation services. + */ + public static final String MESSAGE_NAME = "invoke"; +} diff --git a/src/java/com/threerings/presents/dobj/DObject.java b/src/java/com/threerings/presents/dobj/DObject.java index 07fb8ef15..c19053cec 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.11 2001/06/13 05:17:55 mdb Exp $ +// $Id: DObject.java,v 1.12 2001/07/19 05:56:20 mdb Exp $ package com.threerings.cocktail.cher.dobj; @@ -235,6 +235,19 @@ public class DObject _oid = oid; } + /** + * Called by derived instances when an attribute setter method was + * called. + */ + protected void requestAttributeChange (String name, Object value) + { + // generate an attribute changed event + AttributeChangedEvent event = new AttributeChangedEvent( + _oid, name, value); + // and dispatch it to our dobjmgr + _mgr.postEvent(event); + } + protected int _oid; protected DObjectManager _mgr; protected ArrayList _subscribers = new ArrayList(); diff --git a/src/java/com/threerings/presents/dobj/MessageEvent.java b/src/java/com/threerings/presents/dobj/MessageEvent.java new file mode 100644 index 000000000..2e55c0055 --- /dev/null +++ b/src/java/com/threerings/presents/dobj/MessageEvent.java @@ -0,0 +1,120 @@ +// +// $Id: MessageEvent.java,v 1.1 2001/07/19 05:56:20 mdb Exp $ + +package com.threerings.cocktail.cher.dobj; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.reflect.Method; + +import com.threerings.cocktail.cher.dobj.io.ValueMarshaller; + +/** + * A message event is used to dispatch a message to all subscribers of a + * distributed object without actually changing any of the fields of the + * object. A message has a name, by which different subscribers of the + * same object can distinguish their different messages, and an array of + * arguments by which any contents of the message can be delivered. + * + * @see DObjectManager.postEvent + */ +public class MessageEvent extends TypedEvent +{ + /** The typed object code for this event. */ + public static final short TYPE = TYPE_BASE + 3; + + /** + * Constructs a new message event on the specified target object with + * the supplied name and arguments. + * + * @param targetOid the object id of the object whose attribute has + * changed. + * @param name the name of the message event. + * @param args the arguments for this message. This array should + * contain only values of valid distributed object types. + */ + public MessageEvent (int targetOid, String name, Object[] args) + { + super(targetOid); + _name = name; + _args = args; + } + + /** + * Constructs a blank instance of this event in preparation for + * unserialization from the network. + */ + public MessageEvent () + { + } + + /** + * Returns the name of the message. + */ + public String getName () + { + return _name; + } + + /** + * Returns the arguments to this message. + */ + public Object[] getArgs () + { + return _args; + } + + /** + * Applies this attribute change to the object. + */ + public boolean applyToObject (DObject target) + throws ObjectAccessException + { + // nothing to do here + return true; + } + + public short getType () + { + return TYPE; + } + + public void writeTo (DataOutputStream out) + throws IOException + { + super.writeTo(out); + out.writeUTF(_name); + if (_args != null) { + out.writeInt(_args.length); + for (int i = 0; i < _args.length; i++) { + ValueMarshaller.writeTo(out, _args[i]); + } + } else { + out.writeInt(0); + } + } + + public void readFrom (DataInputStream in) + throws IOException + { + super.readFrom(in); + _name = in.readUTF(); + int args = in.readInt(); + if (args > 0) { + _args = new Object[args]; + for (int i = 0; i < args; i++) { + _args[i] = ValueMarshaller.readFrom(in); + } + } + } + + public String toString () + { + return "[MSG:targetOid=" + _toid + ", name=" + _name + + ", args=" + _args + "]"; + } + + protected String _name; + protected Object[] _args; +} diff --git a/src/java/com/threerings/presents/net/AuthResponseData.java b/src/java/com/threerings/presents/net/AuthResponseData.java index 3bc218f4b..7aab65303 100644 --- a/src/java/com/threerings/presents/net/AuthResponseData.java +++ b/src/java/com/threerings/presents/net/AuthResponseData.java @@ -1,5 +1,5 @@ // -// $Id: AuthResponseData.java,v 1.4 2001/05/30 23:58:31 mdb Exp $ +// $Id: AuthResponseData.java,v 1.5 2001/07/19 05:56:20 mdb Exp $ package com.threerings.cocktail.cher.net; @@ -23,6 +23,12 @@ public class AuthResponseData extends DObject */ public String code; + /** The oid of this client's associated distributed object. */ + public int clientOid; + + /** The oid to which to send invocation requests. */ + public int invOid; + public String toString () { return "[code=" + code + "]"; diff --git a/src/java/com/threerings/presents/server/InvocationManager.java b/src/java/com/threerings/presents/server/InvocationManager.java new file mode 100644 index 000000000..2e2b26d68 --- /dev/null +++ b/src/java/com/threerings/presents/server/InvocationManager.java @@ -0,0 +1,77 @@ +// +// $Id: InvocationManager.java,v 1.1 2001/07/19 05:56:20 mdb Exp $ + +package com.threerings.cocktail.cher.server; + +import com.threerings.cocktail.cher.Log; +import com.threerings.cocktail.cher.dobj.*; + +/** + * The invocation services provide client to server invocations (service + * requests) and server to client invocations (responses and + * notifications). Via this mechanism, the client can make requests of the + * server, be notified of its response and the server can asynchronously + * invoke code on the client. + * + *

Invocations are like remote procedure calls in that they are named + * and take arguments. They are simple in that the arguments can only be + * of a small set of supported types (the set of distributed object field + * types) and there is no special facility provided for referencing + * non-local objects (it is assumed that the distributed object facility + * will already be in use for any objects that should be shared). + * + *

The server invocation manager listens for invocation requests from + * the client and passes them on to the invocation provider registered for + * the requested invocation type. It also provides a mechanism by which + * responses and asynchronous notification invocations can be delivered to + * the client. + */ +public class InvocationManager + implements Subscriber +{ + public InvocationManager (DObjectManager omgr) + { + _omgr = omgr; + + // create the object on which we'll listen for invocation requests + omgr.createObject(DObject.class, this, true); + } + + public int getOid () + { + return _invoid; + } + + public void objectAvailable (DObject object) + { + // this must be our invocation object + _invoid = object.getOid(); + } + + public void requestFailed (int oid, ObjectAccessException cause) + { + // if for some reason we were unable to create our invocation + // object, we'll end up here + Log.warning("Unable to create invocation object " + + "[reason=" + cause + "]."); + _invoid = -1; + } + + public boolean handleEvent (DEvent event, DObject target) + { + // we shouldn't be getting non-message events, but check just to + // be sure + if (!(event instanceof MessageEvent)) { + Log.warning("Got non-message event!? [evt=" + event + "]."); + return true; + } + + MessageEvent mevt = (MessageEvent)event; + // make sure the name is proper just for sanities sake + + return true; + } + + protected DObjectManager _omgr; + protected int _invoid; +} diff --git a/tests/src/java/com/threerings/presents/server/TestObject.dobj b/tests/src/java/com/threerings/presents/server/TestObject.dobj index 03c41618d..5e8ac4487 100644 --- a/tests/src/java/com/threerings/presents/server/TestObject.dobj +++ b/tests/src/java/com/threerings/presents/server/TestObject.dobj @@ -1,5 +1,5 @@ // -// $Id: TestObject.dobj,v 1.1 2001/06/09 23:39:42 mdb Exp $ +// $Id: TestObject.dobj,v 1.2 2001/07/19 05:56:20 mdb Exp $ package com.threerings.cocktail.cher.server.test; @@ -21,20 +21,12 @@ public class TestObject extends DObject public void setFoo (int foo) { - // generate an attribute changed event - AttributeChangedEvent event = new AttributeChangedEvent( - _oid, FOO, new Integer(foo)); - // and dispatch it to our dobjmgr - _mgr.postEvent(event); + requestAttributeChange(FOO, new Integer(foo)); } public void setBar (String bar) { - // generate an attribute changed event - AttributeChangedEvent event = new AttributeChangedEvent( - _oid, BAR, bar); - // and dispatch it to our dobjmgr - _mgr.postEvent(event); + requestAttributeChange(BAR, bar); } public String toString ()