// // $Id: PresentsClient.java,v 1.26 2001/12/03 22:01:57 mdb Exp $ package com.threerings.presents.server; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import com.samskivert.util.HashIntMap; import com.threerings.presents.Log; import com.threerings.presents.data.ClientObject; import com.threerings.presents.dobj.DEvent; import com.threerings.presents.dobj.DObject; import com.threerings.presents.dobj.EventListener; import com.threerings.presents.dobj.ObjectAccessException; import com.threerings.presents.dobj.Subscriber; import com.threerings.presents.dobj.TypedEvent; import com.threerings.presents.net.BootstrapData; import com.threerings.presents.net.BootstrapNotification; import com.threerings.presents.net.EventNotification; import com.threerings.presents.net.UpstreamMessage; import com.threerings.presents.net.ForwardEventRequest; import com.threerings.presents.net.LogoffRequest; import com.threerings.presents.net.PingRequest; import com.threerings.presents.net.SubscribeRequest; import com.threerings.presents.net.UnsubscribeRequest; import com.threerings.presents.net.FailureResponse; import com.threerings.presents.net.ObjectResponse; import com.threerings.presents.net.PongResponse; import com.threerings.presents.server.net.Connection; import com.threerings.presents.server.net.MessageHandler; /** * A client object represents a client session in the server. It is * associated with a connection instance (while the client is connected) * and acts as the intermediary for the remote client in terms of passing * along events forwarded by the client, ensuring that subscriptions are * maintained on behalf of the client and that events are forwarded to the * client. * *

A note on synchronization: the client object is structured * so that its Subscriber implementation (which is called * from the dobjmgr thread) can proceed without synchronization. This does * not overlap with its other client duties which are called from the * conmgr thread and therefore also need not be synchronized. */ public class PresentsClient implements Subscriber, EventListener, MessageHandler { /** * Returns the username with which this client instance is associated. */ public String getUsername () { return _username; } /** * Returns the client object that is associated with this client. */ public ClientObject getClientObject () { return _clobj; } /** * Initializes this client instance with the specified username and * connection instance and begins a client session. */ protected void startSession ( ClientManager cmgr, String username, Connection conn, final Object authInfo) { _cmgr = cmgr; _username = username; setConnection(conn); // create our client object and bootstrap the client once we've // got it Subscriber sub = new Subscriber () { public void objectAvailable (DObject object) { setClientObject((ClientObject)object); sessionWillStart(authInfo); sendBootstrap(); } public void requestFailed (int oid, ObjectAccessException cause) { Log.warning("Unable to create client object " + "[client=" + PresentsClient.this + ", error=" + cause + "]."); } }; Class clobjClass = _cmgr.getClientObjectClass(); PresentsServer.omgr.createObject(clobjClass, sub); } /** * Called when we receive our client object from the dobjmgr. We go * through this synchronized method to ensure that the client object * is visible to the conmgr. */ protected synchronized void setClientObject (ClientObject clobj) { _clobj = clobj; } /** * Called by the client manager when a new connection arrives that * authenticates as this already established client. This must only be * called from the congmr thread. */ protected void resumeSession (Connection conn, final Object authInfo) { Connection oldconn = getConnection(); // check to see if we've already got a connection object, in which // case it's probably stale if (oldconn != null) { Log.info("Closing stale connection [old=" + oldconn + ", new=" + conn + "]."); // close the old connection (which results in everything being // properly unregistered) oldconn.close(); } // start using the new connection setConnection(conn); // we need to get onto the distributed object thread so that we // can finalize the resumption of the session. we do so by // posting a special event DEvent event = new DEvent(0) { public boolean applyToObject (DObject target) { // now that we're on the dobjmgr thread we can resume our // session resumption finishResumeSession(authInfo); return false; } }; PresentsServer.omgr.postEvent(event); } /** * This is called from the dobjmgr thread to complete the session * resumption. We call some call backs and send the bootstrap info to * the client. */ protected void finishResumeSession (Object authInfo) { // let derived classes do any session resuming sessionWillResume(authInfo); // send off a bootstrap notification immediately because we've // already got our client object sendBootstrap(); Log.info("Session resumed [client=" + this + "]."); } /** * Requests that the client session be terminated. This is normally * invoked as a result of a logoff request by the client. It should * only be invoked from the conmgr thread. */ public void endSession () { // close our connection (which results in everything being // properly unregistered) Connection conn = getConnection(); if (conn != null) { conn.close(); } else { Log.info("Unable to close connection for logoff request " + "[client=" + this + "]."); } // then let the client manager know what's up _cmgr.clientDidEndSession(this); // we need to get onto the distributed object thread so that we // can finalize the ending of the session. we do so by posting a // special event DEvent event = new DEvent(0) { public boolean applyToObject (DObject target) { // now that we're on the dobjmgr thread we can call into // our derived classes to finish the session termination sessionDidTerminate(); return false; } }; PresentsServer.omgr.postEvent(event); } /** * Makes a note that this client is subscribed to this object so that * we can clean up after ourselves if and when the client goes * away. This is called by the client internals and needn't be called * by code outside the client. */ public synchronized void mapSubscrip (DObject object) { _subscrips.put(object.getOid(), object); } /** * Makes a note that this client is no longer subscribed to this * object. The subscription map is used to clean up after the client * when it goes away. This is called by the client internals and * needn't be called by code outside the client. */ public synchronized void unmapSubscrip (int oid) { DObject object = (DObject)_subscrips.remove(oid); if (object != null) { object.removeListener(this); object.removeSubscriber(this); } else { Log.warning("Requested to unmap non-existent subscription " + "[oid=" + oid + "]."); } } /** * Clears out the tracked client subscriptions. Called when the client * goes away and shouldn't be called otherwise. */ protected synchronized void clearSubscrips () { Iterator enum = _subscrips.elements(); while (enum.hasNext()) { DObject object = (DObject)enum.next(); // Log.info("Clearing subscription [client=" + this + // ", obj=" + object.getOid() + "]."); object.removeListener(this); object.removeSubscriber(this); } } /** * Called when the client session is first started. The client object * has been created at this point and after this method is executed, * the bootstrap information will be sent to the client which will * trigger the start of the session. Derived classes that override * this method should be sure to call * super.sessionWillStart. * *

Note: This function will be called on the dobjmgr * thread which means that object manipulations are OK, but client * instance manipulations must done carefully. * * @param authInfo optional information provided by the authenticator * that it obtained during the authentication phase. */ protected void sessionWillStart (Object authInfo) { } /** * Called when the client resumes a session (after having disconnected * and reconnected). After this method is executed, the bootstrap * information will be sent to the client which will trigger the * resumption of the session. Derived classes that override this * method should be sure to call super.sessionWillResume. * *

Note: This function will be called on the dobjmgr * thread which means that object manipulations are OK, but client * instance manipulations must done carefully. * * @param authInfo optional information provided by the authenticator * that it obtained during the authentication phase. */ protected void sessionWillResume (Object authInfo) { } /** * Called when the client session ends (either because the client * logged off or because the server forcibly terminated the session). * Derived classes that override this method should be sure to call * super.sessionDidTerminate. * *

Note: This function will be called on the dobjmgr * thread which means that object manipulations are OK, but client * instance manipulations must done carefully. */ protected void sessionDidTerminate () { // destroy the client object PresentsServer.omgr.destroyObject(_clobj.getOid()); } /** * This is called once we have a handle on the client distributed * object. It sends a bootstrap notification to the client with all * the information it will need to interact with the server. */ protected void sendBootstrap () { Log.info("Sending bootstrap [client=" + this + "]."); // create and populate our bootstrap data BootstrapData data = createBootstrapData(); populateBootstrapData(data); // create a send bootstrap notification _conn.postMessage(new BootstrapNotification(data)); } /** * Derived client classes can override this member to create derived * bootstrap data classes that contain extra bootstrap information, if * desired. */ protected BootstrapData createBootstrapData () { return new BootstrapData(); } /** * Derived client classes can override this member to populate the * bootstrap data with additional information. They should be sure to * call super.populateBootstrapData before doing their * own populating, however. * *

Note: This function will be called on the dobjmgr * thread which means that object manipulations are OK, but client * instance manipulations must done carefully. */ protected void populateBootstrapData (BootstrapData data) { // give them the client object id data.clientOid = _clobj.getOid(); // give them the invocation oid data.invOid = PresentsServer.invmgr.getOid(); } /** * Called by the connection manager when this client's connection is * unmapped. That may be because of a connection failure (in which * case this call will be followed up by a call to * connectionFailed) or it may be because of an orderly * closing of the connection. In either case, the client can deal with * its lack of a connection in this method. This is invoked by the * conmgr thread and should behave accordingly. */ protected void wasUnmapped () { // clear out our connection reference setConnection(null); // clear out our subscriptions. we need to do this on the dobjmgr // thread. so we do so by posting a special event. it is important // that we do this *after* we clear out our connection reference. // once the connection ref is null, no more subscriptions will be // processed (even those that were queued up before the connection // went away) DEvent event = new DEvent(0) { public boolean applyToObject (DObject target) { clearSubscrips(); return false; } }; PresentsServer.omgr.postEvent(event); } /** * Called by the connection manager when this client's connection * fails. This is invoked on the conmgr thread and should behave * accordingly. */ protected void connectionFailed (IOException fault) { // nothing to do here presently. the client manager already // complained about the failed connection } /** * Sets our connection reference in a thread safe way. Also * establishes the back reference to us as the connection's message * handler. */ protected synchronized void setConnection (Connection conn) { // keep a handle to the new connection _conn = conn; // tell the connection to pass messages on to us (if we're setting // a connection rather than clearing one out) if (_conn != null) { _conn.setMessageHandler(this); } } /** * The connection instance must be accessed via this member function * because it is read from both the dobjmgr and conmgr threads and is * modified by the conmgr thread. * * @return The connection instance associated with this client or null * if the client is not currently connected. */ protected synchronized Connection getConnection () { return _conn; } // documentation inherited from interface public void handleMessage (UpstreamMessage message) { // we dispatch to a message dispatcher that is specialized for the // particular class of message that we received MessageDispatcher disp = (MessageDispatcher) _disps.get(message.getClass()); if (disp == null) { Log.warning("No dispacther for message [msg=" + message + "]."); return; } // otherwise pass the message on to the dispatcher disp.dispatch(this, message); } // documentation inherited from interface public void objectAvailable (DObject object) { // queue up an object response Connection conn = getConnection(); if (conn != null) { // add ourselves as an event listener object.addListener(this); // pass the successful subscrip on to the client conn.postMessage(new ObjectResponse(object)); // make a note of this new subscription mapSubscrip(object); } else { Log.info("Dropped object available notification " + "[client=" + this + ", oid=" + object.getOid() + "]."); } } // documentation inherited from interface public void requestFailed (int oid, ObjectAccessException cause) { Connection conn = getConnection(); if (conn != null) { conn.postMessage(new FailureResponse(oid)); } else { Log.info("Dropped failure notification " + "[client=" + this + ", oid=" + oid + ", cause=" + cause + "]."); } } // documentation inherited from interface public void eventReceived (DEvent event) { // forward the event to the client Connection conn = getConnection(); if (conn != null) { // only typed events will be forwarded to the client, so we // need not worry that a non-typed event would make it here TypedEvent tevent = (TypedEvent)event; conn.postMessage(new EventNotification(tevent)); } else { Log.info("Dropped event forward notification " + "[client=" + this + ", event=" + event + "]."); } } public String toString () { return "[username=" + _username + ", conn=" + _conn + ", cloid=" + ((_clobj == null) ? -1 : _clobj.getOid()) + "]"; } /** * Message dispatchers are used to dispatch each different type of * upstream message. We can look the dispatcher up in a table and * invoke it through an overloaded member which is faster (so we * think) than doing a bunch of instanceofs. */ protected static interface MessageDispatcher { /** * Dispatch the supplied message for the specified client. */ public void dispatch (PresentsClient client, UpstreamMessage mge); } /** * Processes subscribe requests. */ protected static class SubscribeDispatcher implements MessageDispatcher { public void dispatch (PresentsClient client, UpstreamMessage msg) { SubscribeRequest req = (SubscribeRequest)msg; // Log.info("Subscribing [client=" + client + // ", oid=" + req.getOid() + "]."); // forward the subscribe request to the omgr for processing PresentsServer.omgr.subscribeToObject(req.getOid(), client); } } /** * Processes unsubscribe requests. */ protected static class UnsubscribeDispatcher implements MessageDispatcher { public void dispatch (PresentsClient client, UpstreamMessage msg) { UnsubscribeRequest req = (UnsubscribeRequest)msg; // Log.info("Unsubscribing [client=" + client + // ", oid=" + req.getOid() + "]."); // forward the unsubscribe request to the omgr for processing PresentsServer.omgr.unsubscribeFromObject(req.getOid(), client); // update our subscription tracking table client.unmapSubscrip(req.getOid()); } } /** * Processes forward event requests. */ protected static class ForwardEventDispatcher implements MessageDispatcher { public void dispatch (PresentsClient client, UpstreamMessage msg) { ForwardEventRequest req = (ForwardEventRequest)msg; DEvent fevt = req.getEvent(); // fill in the proper source oid fevt.setSourceOid(client.getClientObject().getOid()); // Log.info("Forwarding event [client=" + client + // ", event=" + fevt + "]."); // forward the event to the omgr for processing PresentsServer.omgr.postEvent(fevt); } } /** * Processes ping requests. */ protected static class PingDispatcher implements MessageDispatcher { public void dispatch (PresentsClient client, UpstreamMessage msg) { Log.info("Received client ping [client=" + client + "]."); // send a pong response Connection conn = client.getConnection(); if (conn != null) { conn.postMessage(new PongResponse()); } else { Log.info("Dropped pong response [client=" + client + "]."); } } } /** * Processes logoff requests. */ protected static class LogoffDispatcher implements MessageDispatcher { public void dispatch (PresentsClient client, UpstreamMessage msg) { Log.info("Client requested logoff " + "[client=" + client + "]."); // request that the client end the session client.endSession(); } } protected ClientManager _cmgr; protected String _username; protected Connection _conn; protected ClientObject _clobj; protected HashIntMap _subscrips = new HashIntMap(); protected static HashMap _disps = new HashMap(); // register our message dispatchers static { _disps.put(SubscribeRequest.class, new SubscribeDispatcher()); _disps.put(UnsubscribeRequest.class, new UnsubscribeDispatcher()); _disps.put(ForwardEventRequest.class, new ForwardEventDispatcher()); _disps.put(PingRequest.class, new PingDispatcher()); _disps.put(LogoffRequest.class, new LogoffDispatcher()); } }