Created ServerCommunicator which does all I/O using the ConnectionManager's
polled socket system. This much more cleanly and efficiently integrates peer (and other server to server) connections into the normal server I/O framework and should also eliminate for good the annoying server hangs that result when our old blocking client I/O threads got their pants wedged. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@5510 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
@@ -183,17 +183,10 @@ public class BlockingCommunicator extends Communicator
|
||||
return _lastWrite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback called by the reader when the authentication process completes successfully. Here
|
||||
* we extract the bootstrap information for the client and start up the writer thread to manage
|
||||
* the other half of our bi-directional message stream.
|
||||
*/
|
||||
@Override // from Communicator
|
||||
protected synchronized void logonSucceeded (AuthResponseData data)
|
||||
{
|
||||
log.debug("Logon succeeded: " + data);
|
||||
|
||||
// create our distributed object manager
|
||||
_omgr = new ClientDObjectMgr(this, _client);
|
||||
super.logonSucceeded(data);
|
||||
|
||||
// create a new writer thread and start it up
|
||||
if (_writer != null) {
|
||||
@@ -201,19 +194,13 @@ public class BlockingCommunicator extends Communicator
|
||||
}
|
||||
_writer = new Writer();
|
||||
_writer.start();
|
||||
|
||||
// fill the auth data into the client's local field so that it can be requested by external
|
||||
// entities
|
||||
_client._authData = data;
|
||||
|
||||
// wait for the bootstrap notification before we claim that we're actually logged on
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback called by the reader or writer thread when something goes awry with our socket
|
||||
* connection to the server.
|
||||
*/
|
||||
protected synchronized void connectionFailed (IOException ioe)
|
||||
protected synchronized void connectionFailed (final IOException ioe)
|
||||
{
|
||||
// make sure the socket isn't already closed down (meaning we've already dealt with the
|
||||
// failed connection)
|
||||
@@ -224,7 +211,11 @@ public class BlockingCommunicator extends Communicator
|
||||
log.info("Connection failed", ioe);
|
||||
|
||||
// let the client know that things went south
|
||||
_client.notifyObservers(Client.CLIENT_CONNECTION_FAILED, ioe);
|
||||
notifyClientObservers(new ObserverOps.Client() {
|
||||
protected void notify (ClientObserver obs) {
|
||||
obs.clientConnectionFailed(_client, ioe);
|
||||
}
|
||||
});
|
||||
|
||||
// and request that we go through the motions of logging off
|
||||
logoff();
|
||||
@@ -260,7 +251,7 @@ public class BlockingCommunicator extends Communicator
|
||||
closeChannel();
|
||||
|
||||
// let the client know when we finally go away
|
||||
_client.cleanup(_logonError);
|
||||
clientCleanup(_logonError);
|
||||
}
|
||||
|
||||
log.debug("Reader thread exited.");
|
||||
@@ -276,7 +267,11 @@ public class BlockingCommunicator extends Communicator
|
||||
log.debug("Writer thread exited.");
|
||||
|
||||
// let the client observers know that we're logged off
|
||||
_client.notifyObservers(Client.CLIENT_DID_LOGOFF, null);
|
||||
notifyClientObservers(new ObserverOps.Session() {
|
||||
protected void notify (SessionObserver obs) {
|
||||
obs.clientDidLogoff(_client);
|
||||
}
|
||||
});
|
||||
|
||||
// now that the writer thread has gone away, we can safely close our socket and let the
|
||||
// client know that the logoff process has completed
|
||||
@@ -284,7 +279,7 @@ public class BlockingCommunicator extends Communicator
|
||||
|
||||
// let the client know when we finally go away
|
||||
if (_reader == null) {
|
||||
_client.cleanup(_logonError);
|
||||
clientCleanup(_logonError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,14 +434,6 @@ public class BlockingCommunicator extends Communicator
|
||||
_datagramChannel.write(buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a note of the time at which we last communicated with the server.
|
||||
*/
|
||||
protected synchronized void updateWriteStamp ()
|
||||
{
|
||||
_lastWrite = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a new message from the socket (blocking until a message has arrived).
|
||||
*/
|
||||
@@ -504,16 +491,6 @@ public class BlockingCommunicator extends Communicator
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback called by the reader thread when it has parsed a new message from the socket and
|
||||
* wishes to have it processed.
|
||||
*/
|
||||
protected void processMessage (DownstreamMessage msg)
|
||||
{
|
||||
// post this message to the dobjmgr queue
|
||||
_omgr.processMessage(msg);
|
||||
}
|
||||
|
||||
protected void openChannel (InetAddress host)
|
||||
throws IOException
|
||||
{
|
||||
@@ -545,13 +522,17 @@ public class BlockingCommunicator extends Communicator
|
||||
@Override
|
||||
protected void willStart ()
|
||||
{
|
||||
// first we connect and authenticate with the server
|
||||
try {
|
||||
// connect to the server
|
||||
connect();
|
||||
|
||||
// then authenticate
|
||||
logon();
|
||||
// construct an auth request and send it
|
||||
sendMessage(new AuthRequest(_client.getCredentials(), _client.getVersion(),
|
||||
_client.getBootGroups()));
|
||||
|
||||
// now wait for the auth response
|
||||
log.debug("Waiting for auth response.");
|
||||
gotAuthResponse((AuthResponse)receiveMessage());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug("Logon failed: " + e);
|
||||
@@ -586,30 +567,6 @@ public class BlockingCommunicator extends Communicator
|
||||
_oout = new ObjectOutputStream(_fout);
|
||||
}
|
||||
|
||||
protected void logon ()
|
||||
throws IOException, LogonException
|
||||
{
|
||||
// construct an auth request and send it
|
||||
AuthRequest req = new AuthRequest(
|
||||
_client.getCredentials(), _client.getVersion(), _client.getBootGroups());
|
||||
sendMessage(req);
|
||||
|
||||
// now wait for the auth response
|
||||
log.debug("Waiting for auth response.");
|
||||
AuthResponse rsp = (AuthResponse)receiveMessage();
|
||||
AuthResponseData data = rsp.getData();
|
||||
log.debug("Got auth response: " + data);
|
||||
|
||||
// if the auth request failed, we want to let the communicator know by throwing a logon
|
||||
// exception
|
||||
if (!data.code.equals(AuthResponseData.SUCCESS)) {
|
||||
throw new LogonException(data.code);
|
||||
}
|
||||
|
||||
// we're all clear. let the communicator know that we're in
|
||||
logonSucceeded(data);
|
||||
}
|
||||
|
||||
// now that we're authenticated, we manage the reading half of things by continuously
|
||||
// reading messages from the socket and processing them
|
||||
@Override
|
||||
@@ -961,7 +918,6 @@ public class BlockingCommunicator extends Communicator
|
||||
protected DatagramChannel _datagramChannel;
|
||||
protected Queue<UpstreamMessage> _dataq = new Queue<UpstreamMessage>();
|
||||
|
||||
protected long _lastWrite;
|
||||
protected Exception _logonError;
|
||||
|
||||
/** We use this to frame our upstream messages. */
|
||||
@@ -983,7 +939,6 @@ public class BlockingCommunicator extends Communicator
|
||||
|
||||
protected DatagramSequencer _sequencer;
|
||||
|
||||
protected ClientDObjectMgr _omgr;
|
||||
protected ClassLoader _loader;
|
||||
|
||||
/** The number of times per port to try to establish a datagram "connection". */
|
||||
|
||||
@@ -418,8 +418,12 @@ public class Client
|
||||
return false;
|
||||
}
|
||||
|
||||
// notify our observers
|
||||
notifyObservers(CLIENT_WILL_LOGON, null);
|
||||
// notify our observers immediately
|
||||
_observers.apply(new ObserverOps.Session() {
|
||||
protected void notify (SessionObserver obs) {
|
||||
obs.clientWillLogon(Client.this);
|
||||
}
|
||||
});
|
||||
|
||||
// otherwise create a new communicator instance and start it up. this will initiate the
|
||||
// logon process
|
||||
@@ -504,7 +508,15 @@ public class Client
|
||||
}
|
||||
|
||||
// if the request is abortable, let's run it past the observers before we act upon it
|
||||
if (abortable && notifyObservers(CLIENT_WILL_LOGOFF, null)) {
|
||||
final boolean[] rejected = new boolean[] { false };
|
||||
_observers.apply(new ObserverOps.Client() {
|
||||
protected void notify (ClientObserver obs) {
|
||||
if (!obs.clientWillLogoff(Client.this)) {
|
||||
rejected[0] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (abortable && rejected[0]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -527,7 +539,12 @@ public class Client
|
||||
public String[] prepareStandaloneLogon ()
|
||||
{
|
||||
_standalone = true;
|
||||
notifyObservers(CLIENT_WILL_LOGON, null);
|
||||
// notify our observers immediately
|
||||
_observers.apply(new ObserverOps.Session() {
|
||||
protected void notify (SessionObserver obs) {
|
||||
obs.clientWillLogon(Client.this);
|
||||
}
|
||||
});
|
||||
return getBootGroups();
|
||||
}
|
||||
|
||||
@@ -548,7 +565,11 @@ public class Client
|
||||
*/
|
||||
public void standaloneLogoff ()
|
||||
{
|
||||
notifyObservers(CLIENT_DID_LOGOFF, null);
|
||||
notifyObservers(new ObserverOps.Session() {
|
||||
protected void notify (SessionObserver obs) {
|
||||
obs.clientDidLogoff(Client.this);
|
||||
}
|
||||
});
|
||||
cleanup(null); // this will set _standalone to false
|
||||
}
|
||||
|
||||
@@ -715,9 +736,9 @@ public class Client
|
||||
*/
|
||||
protected void reportLogonTribulations (final LogonException cause)
|
||||
{
|
||||
_runQueue.postRunnable(new Runnable() {
|
||||
public void run () {
|
||||
notifyObservers(CLIENT_FAILED_TO_LOGON, cause);
|
||||
notifyObservers(new ObserverOps.Client() {
|
||||
protected void notify (ClientObserver obs) {
|
||||
obs.clientFailedToLogon(Client.this, cause);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -732,16 +753,24 @@ public class Client
|
||||
_clobj = clobj;
|
||||
|
||||
// let the client know that logon has now fully succeeded
|
||||
notifyObservers(CLIENT_DID_LOGON, null);
|
||||
notifyObservers(new ObserverOps.Session() {
|
||||
protected void notify (SessionObserver obs) {
|
||||
obs.clientDidLogon(Client.this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the invocation director if it fails to subscribe to the client object after logon.
|
||||
*/
|
||||
protected void getClientObjectFailed (Exception cause)
|
||||
protected void getClientObjectFailed (final Exception cause)
|
||||
{
|
||||
// pass the buck onto the listeners
|
||||
notifyObservers(CLIENT_FAILED_TO_LOGON, cause);
|
||||
notifyObservers(new ObserverOps.Client() {
|
||||
protected void notify (ClientObserver obs) {
|
||||
obs.clientFailedToLogon(Client.this, cause);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -753,32 +782,23 @@ public class Client
|
||||
_cloid = _clobj.getOid();
|
||||
|
||||
// report to our observers
|
||||
notifyObservers(CLIENT_OBJECT_CHANGED, null);
|
||||
notifyObservers(new ObserverOps.Session() {
|
||||
protected void notify (SessionObserver obs) {
|
||||
obs.clientObjectDidChange(Client.this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected boolean notifyObservers (int code, Exception cause)
|
||||
protected void notifyObservers (final ObserverOps.Session op)
|
||||
{
|
||||
final Notifier noty = new Notifier(code, cause);
|
||||
Runnable unit = new Runnable() {
|
||||
public void run () {
|
||||
synchronized (_observers) {
|
||||
_observers.apply(noty);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// we need to run immediately if this is WILL_LOGON, WILL_LOGOFF or if we have no RunQueue
|
||||
// (which currently only happens in some really obscure circumstances where we're using a
|
||||
// Client instance on the server so that we can sort of pretend to be a real client)
|
||||
if (code == CLIENT_WILL_LOGON || code == CLIENT_WILL_LOGOFF || _runQueue == null) {
|
||||
unit.run();
|
||||
return noty.getRejected();
|
||||
|
||||
if (_runQueue == null) {
|
||||
_observers.apply(op);
|
||||
} else {
|
||||
// otherwise we can queue this notification up with our RunQueue and ensure that it's
|
||||
// run on the proper thread
|
||||
_runQueue.postRunnable(unit);
|
||||
return false;
|
||||
_runQueue.postRunnable(new Runnable() {
|
||||
public void run () {
|
||||
_observers.apply(op);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -809,11 +829,15 @@ public class Client
|
||||
// now that the communicator is cleaned up; this allows a logon failure listener to
|
||||
// immediately try another logon (hopefully with something changed like the server
|
||||
// or port)
|
||||
if (logonError != null) {
|
||||
notifyObservers(CLIENT_FAILED_TO_LOGON, logonError);
|
||||
} else {
|
||||
notifyObservers(CLIENT_DID_CLEAR, null);
|
||||
}
|
||||
notifyObservers(new ObserverOps.Client() {
|
||||
protected void notify (ClientObserver obs) {
|
||||
if (logonError != null) {
|
||||
obs.clientFailedToLogon(Client.this, logonError);
|
||||
} else {
|
||||
obs.clientDidClear(Client.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -844,81 +868,6 @@ public class Client
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to notify client observers of events.
|
||||
*/
|
||||
protected class Notifier
|
||||
implements ObserverList.ObserverOp<SessionObserver>
|
||||
{
|
||||
public Notifier (int code, Exception cause)
|
||||
{
|
||||
_code = code;
|
||||
_cause = cause;
|
||||
_rejected = false;
|
||||
}
|
||||
|
||||
public boolean getRejected ()
|
||||
{
|
||||
return _rejected;
|
||||
}
|
||||
|
||||
public boolean apply (SessionObserver obs)
|
||||
{
|
||||
switch (_code) {
|
||||
case CLIENT_WILL_LOGON:
|
||||
obs.clientWillLogon(Client.this);
|
||||
break;
|
||||
|
||||
case CLIENT_DID_LOGON:
|
||||
obs.clientDidLogon(Client.this);
|
||||
break;
|
||||
|
||||
case CLIENT_FAILED_TO_LOGON:
|
||||
if (obs instanceof ClientObserver) {
|
||||
((ClientObserver)obs).clientFailedToLogon(Client.this, _cause);
|
||||
}
|
||||
break;
|
||||
|
||||
case CLIENT_OBJECT_CHANGED:
|
||||
obs.clientObjectDidChange(Client.this);
|
||||
break;
|
||||
|
||||
case CLIENT_CONNECTION_FAILED:
|
||||
if (obs instanceof ClientObserver) {
|
||||
((ClientObserver)obs).clientConnectionFailed(Client.this, _cause);
|
||||
}
|
||||
break;
|
||||
|
||||
case CLIENT_WILL_LOGOFF:
|
||||
if (obs instanceof ClientObserver) {
|
||||
if (!((ClientObserver)obs).clientWillLogoff(Client.this)) {
|
||||
_rejected = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case CLIENT_DID_LOGOFF:
|
||||
obs.clientDidLogoff(Client.this);
|
||||
break;
|
||||
|
||||
case CLIENT_DID_CLEAR:
|
||||
if (obs instanceof ClientObserver) {
|
||||
((ClientObserver)obs).clientDidClear(Client.this);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Invalid code supplied to notifyObservers: " + _code);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected int _code;
|
||||
protected Exception _cause;
|
||||
protected boolean _rejected;
|
||||
}
|
||||
|
||||
/** Handles the process of switching between servers.
|
||||
* See {@link Client#moveToServer(String,int[],int[],ConfirmListener)}. */
|
||||
protected class ServerSwitcher extends ClientAdapter
|
||||
@@ -1037,8 +986,7 @@ public class Client
|
||||
protected int[] _datagramPorts;
|
||||
|
||||
/** Our list of client observers. */
|
||||
protected ObserverList<SessionObserver> _observers =
|
||||
new ObserverList<SessionObserver>(ObserverList.SAFE_IN_ORDER_NOTIFY);
|
||||
protected ObserverList<SessionObserver> _observers = ObserverList.newSafeInOrder();
|
||||
|
||||
/** The entity that manages our network communications. */
|
||||
protected Communicator _comm;
|
||||
@@ -1075,14 +1023,4 @@ public class Client
|
||||
|
||||
/** How often we recompute our time offset from the server. */
|
||||
protected static final long CLOCK_SYNC_INTERVAL = 600 * 1000L;
|
||||
|
||||
// client observer codes
|
||||
static final int CLIENT_WILL_LOGON = 0;
|
||||
static final int CLIENT_DID_LOGON = 1;
|
||||
static final int CLIENT_FAILED_TO_LOGON = 2;
|
||||
static final int CLIENT_OBJECT_CHANGED = 3;
|
||||
static final int CLIENT_CONNECTION_FAILED = 4;
|
||||
static final int CLIENT_WILL_LOGOFF = 5;
|
||||
static final int CLIENT_DID_LOGOFF = 6;
|
||||
static final int CLIENT_DID_CLEAR = 7;
|
||||
}
|
||||
|
||||
@@ -44,10 +44,10 @@ import com.threerings.presents.dobj.ObjectDestroyedEvent;
|
||||
import com.threerings.presents.dobj.Subscriber;
|
||||
import com.threerings.presents.net.BootstrapData;
|
||||
import com.threerings.presents.net.BootstrapNotification;
|
||||
import com.threerings.presents.net.DownstreamMessage;
|
||||
import com.threerings.presents.net.EventNotification;
|
||||
import com.threerings.presents.net.FailureResponse;
|
||||
import com.threerings.presents.net.ForwardEventRequest;
|
||||
import com.threerings.presents.net.Message;
|
||||
import com.threerings.presents.net.ObjectResponse;
|
||||
import com.threerings.presents.net.PongResponse;
|
||||
import com.threerings.presents.net.SubscribeRequest;
|
||||
@@ -157,10 +157,10 @@ public class ClientDObjectMgr
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the communicator when a downstream message arrives from the network layer. We
|
||||
* queue it up for processing and request some processing time on the main thread.
|
||||
* Called by the communicator when a message arrives from the network layer. We queue it up for
|
||||
* processing and request some processing time on the main thread.
|
||||
*/
|
||||
public void processMessage (DownstreamMessage msg)
|
||||
public void processMessage (Message msg)
|
||||
{
|
||||
// append it to our queue
|
||||
_actions.append(msg);
|
||||
@@ -211,6 +211,9 @@ public class ClientDObjectMgr
|
||||
} else {
|
||||
doUnsubscribe(act.oid, act.target);
|
||||
}
|
||||
|
||||
} else {
|
||||
log.warning("Unknown action", "action", obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
|
||||
package com.threerings.presents.client;
|
||||
|
||||
import com.threerings.presents.net.AuthResponse;
|
||||
import com.threerings.presents.net.AuthResponseData;
|
||||
import com.threerings.presents.net.Message;
|
||||
import com.threerings.presents.net.UpstreamMessage;
|
||||
|
||||
/**
|
||||
@@ -66,7 +69,70 @@ public abstract class Communicator
|
||||
/**
|
||||
* Returns the time at which we last sent a packet to the server.
|
||||
*/
|
||||
public abstract long getLastWrite ();
|
||||
public long getLastWrite ()
|
||||
{
|
||||
return _lastWrite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a note of the time at which we last communicated with the server.
|
||||
*/
|
||||
protected synchronized void updateWriteStamp ()
|
||||
{
|
||||
_lastWrite = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses must call this method when they receive the authentication response.
|
||||
*/
|
||||
protected void gotAuthResponse (AuthResponse rsp)
|
||||
throws LogonException
|
||||
{
|
||||
AuthResponseData data = rsp.getData();
|
||||
if (!data.code.equals(AuthResponseData.SUCCESS)) {
|
||||
throw new LogonException(data.code);
|
||||
}
|
||||
logonSucceeded(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the authentication process completes successfully. Derived classes can override
|
||||
* this method and complete any initialization that need wait for authentication success.
|
||||
*/
|
||||
protected synchronized void logonSucceeded (AuthResponseData data)
|
||||
{
|
||||
// create our distributed object manager
|
||||
_omgr = new ClientDObjectMgr(this, _client);
|
||||
|
||||
// fill the auth data into the client's local field so that it can be requested by external
|
||||
// entities
|
||||
_client._authData = data;
|
||||
|
||||
// wait for the bootstrap notification before we claim that we're actually logged on
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback called by the reader thread when it has parsed a new message from the socket and
|
||||
* wishes to have it processed.
|
||||
*/
|
||||
protected void processMessage (Message msg)
|
||||
{
|
||||
// post this message to the dobjmgr queue
|
||||
_omgr.processMessage(msg);
|
||||
}
|
||||
|
||||
protected void notifyClientObservers (ObserverOps.Session op)
|
||||
{
|
||||
_client.notifyObservers(op);
|
||||
}
|
||||
|
||||
protected void clientCleanup (Exception logonError)
|
||||
{
|
||||
_client.cleanup(logonError);
|
||||
_client = null; // prevent any post-cleanup tomfoolery
|
||||
}
|
||||
|
||||
protected Client _client;
|
||||
protected ClientDObjectMgr _omgr;
|
||||
protected long _lastWrite;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2008 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or modify it
|
||||
// under the terms of the GNU Lesser General Public License as published
|
||||
// by the Free Software Foundation; either version 2.1 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
package com.threerings.presents.client;
|
||||
|
||||
import com.samskivert.util.ObserverList;
|
||||
|
||||
/**
|
||||
* Used to notify session and client observers.
|
||||
*/
|
||||
public class ObserverOps
|
||||
{
|
||||
public abstract static class Session implements ObserverList.ObserverOp<SessionObserver>
|
||||
{
|
||||
public boolean apply (SessionObserver obs) {
|
||||
notify(obs);
|
||||
return true;
|
||||
}
|
||||
protected abstract void notify (SessionObserver obs);
|
||||
}
|
||||
|
||||
public abstract static class Client extends Session
|
||||
{
|
||||
@Override public void notify (SessionObserver obs) {
|
||||
if (obs instanceof ClientObserver) {
|
||||
notify((ClientObserver)obs);
|
||||
}
|
||||
}
|
||||
protected abstract void notify (ClientObserver obs);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user