1164 lines
36 KiB
Java
1164 lines
36 KiB
Java
/*
|
|
* IRCConnection.java
|
|
*
|
|
* Copyright (C) 2000, 2001, 2002, 2003, 2004 Ben Damm
|
|
*
|
|
* 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.
|
|
*
|
|
* See: http://www.fsf.org/copyleft/lesser.txt
|
|
*/
|
|
|
|
package f00f.net.irc.martyr;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.BufferedWriter;
|
|
import java.io.IOException;
|
|
import java.io.InputStreamReader;
|
|
import java.io.OutputStreamWriter;
|
|
import java.net.InetAddress;
|
|
import java.net.Socket;
|
|
import java.util.LinkedList;
|
|
import java.util.Observer;
|
|
import java.util.StringTokenizer;
|
|
|
|
import f00f.net.irc.martyr.clientstate.ClientState;
|
|
import f00f.net.irc.martyr.commands.UnknownCommand;
|
|
import f00f.net.irc.martyr.errors.UnknownError;
|
|
import f00f.net.irc.martyr.replies.UnknownReply;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
|
|
// TODO:
|
|
//
|
|
// Add synchronous disconnect.
|
|
//
|
|
/**
|
|
* <p><code>IRCConnection</code> is the core class for Martyr.
|
|
* <code>IRCConnection</code> manages the socket, giving commands to the server
|
|
* and passing results to the parse engine. It manages passing information out
|
|
* to the application via the command listeners and state listeners.
|
|
* <code>IRCConnection</code> has no IRC intelligence of its own, that is left
|
|
* up to the classes on the command and state listener lists. A number of
|
|
* listeners that do various tasks are provided as part of the framework.</p>
|
|
*
|
|
* <p><b>Please read this entirely before using the framework.</b> Or
|
|
* what the heck, try out the example below and see if it works for ya.</p>
|
|
*
|
|
* <h2>States and State Listeners</h2>
|
|
* <p><code>IRCConnection</code> is always in one of three states.
|
|
* <code>UNCONNECTED</code>, <code>UNREGISTERED</code> or
|
|
* <code>REGISTERED</code>. It keeps a list of listeners that
|
|
* would like to know when a state change occurs. When a state change
|
|
* occurs each listener in the list, in the order they were added, is
|
|
* notified. If a listener early up on the list causes something to happen
|
|
* that changes the state before your listener gets notified, you will be
|
|
* notified of the state change even though the state has changed. You
|
|
* will be notified again of the new state. That is, state change
|
|
* notifications will always be in order, but they may not always reflect
|
|
* the "current" state.</p>
|
|
*
|
|
* <h2>Commands and Command Listeners</h2>
|
|
* <p><code>IRCConnection</code> also keeps a list of listeners for
|
|
* when a command arrives from the server. When a command arrives, it
|
|
* is first parsed into an object. That object is then passed around
|
|
* to all the listeners, again, in order. Commands can be received
|
|
* and the socket closed before the commands are actually send to the
|
|
* listeners, so beware that even though you receive a command, you
|
|
* may not always be guaranteed to have an open socket to send a
|
|
* response back on. A consumer of the command should never modify
|
|
* the command object. If you try to send a command to a closed
|
|
* socket, <code>IRCConnection</code> will silently ignore your
|
|
* command. Commands should always be received in order by all
|
|
* listeners, even if a listener higher up in the list sends a
|
|
* response to the server which elicits a response back from the
|
|
* server before you've been told of the first command.</p>
|
|
*
|
|
* <h2>Connecting and staying connected</h2>
|
|
* <p>The AutoReconnect class can connect you and will try to stay
|
|
* connected. Using AutoReconnect to connect the
|
|
* first time is recommended, use the <code>go(server,port)</code> method once
|
|
* you are ready to start.</p>
|
|
*
|
|
* <h2>Registration On The Network</h2>
|
|
* <p>The AutoRegister class can register you automatically on the
|
|
* network. Otherwise, registration is left up to the consumer.
|
|
* Registration should occur any time the state changes to
|
|
* <code>UNREGISTERED</code>. The consumer will know this because it
|
|
* has registered some class as a state observer.
|
|
* </p>
|
|
*
|
|
* <h2>Auto Response</h2>
|
|
* <p>Some commands, such as Ping require an automatic response.
|
|
* Commands that fall into this category can be handled by the
|
|
* <code>AutoResponder</code> class. For a list of what commands
|
|
* <code>AutoResponder</code> auto responds to, see the source.</p>
|
|
*
|
|
* <h2>Joining and Staying Joined</h2>
|
|
* <p>You can use the <code>AutoJoin</code> class to join a channel
|
|
* and stay there. <code>AutoJoin</code> will try to re-join if
|
|
* kicked or if the connection is lost and the server re-connects.
|
|
* <code>AutoJoin</code> can be used any time a join is desired. If
|
|
* the server is not connected, it will wait until the server
|
|
* connects. If the server is connected, it will try to join right
|
|
* away.</p>
|
|
*
|
|
* <h2>Example Usage</h2>
|
|
* <p>You will probably want to at least use the
|
|
* <code>AutoRegister</code> and <code>AutoResponder</code> classes.
|
|
* Example:</p>
|
|
*
|
|
* <p>Note that in the example, the first line is optional.
|
|
* <code>IRCConnection</code> can be called with its default
|
|
* constructor. See note below about why this is done.
|
|
* <code>IRCConnection</code> will instantiate its own
|
|
* <code>ClientState</code> object if you do not provide one.</p>
|
|
*
|
|
* <pre>
|
|
* ClientState clientState = new MyAppClientState();
|
|
* IRCConnection connection = new IRCConnection( clientState );
|
|
*
|
|
* // AutoRegister and AutoResponder both add themselves to the
|
|
* // appropriate observerables. Both will remove themselves with the
|
|
* // disable() method.
|
|
*
|
|
* AutoRegister autoReg
|
|
* = new AutoRegister( "repp", "bdamm", "Ben Damm", connection );
|
|
* AutoReconnect autoRecon = new AutoReconnect( connection );
|
|
* AutoResponder autoRes = new AutoResponder( connection );
|
|
*
|
|
* // Very important that the objects above get added before the connect.
|
|
* // If done otherwise, AutoRegister will throw an
|
|
* // IllegalStateException, as AutoRegister can't catch the
|
|
* // state switch to UNREGISTERED from UNCONNECTED.
|
|
*
|
|
* autoRecon.go( server, port );
|
|
* </pre>
|
|
*
|
|
* <h2>Client State</h2>
|
|
* <p>The <code>ClientStateMonitor</code> class tells commands to
|
|
* change the client state when they are received.
|
|
* <code>ClientStateMonitor</code> is automatically added to the
|
|
* command queue before any other command, so that you can be
|
|
* guaranteed that the <code>ClientState</code> is updated before any
|
|
* observer sees a command.</p>
|
|
*
|
|
* <p>So, how does an application know when a channel has been joined,
|
|
* a user has joined a channel we are already on, etc? How does the
|
|
* application get fine-grained access to client state change info?
|
|
* This is a tricky question, and the current solution is to sublcass
|
|
* the <code>clientstate.ClientState</code> and
|
|
* <code>clientstate.Channel</code> classes with your own, overriding
|
|
* the <code>setXxxxXxxx</code> methods. Each method would call
|
|
* <code>super.setXxxXxxx</code> and then proceed to change the
|
|
* application as required.</p>
|
|
*
|
|
* <h2>Startup</h2>
|
|
* <p>IRCConnection starts in the <code>UNCONNECTED</code> state and
|
|
* makes no attempt to connect until the <code>connect(...)</code>
|
|
* method is called.</p>
|
|
*
|
|
* <p><code>IRCConnection</code> starts a single thread at
|
|
* construction time. This thread simply waits for events. An event
|
|
* is a disconnection request or an incoming message. Events are
|
|
* dealt with by this thread. If connect is called, a second thread
|
|
* is created to listen for input from the server (InputHandler).
|
|
*
|
|
* @see f00f.net.irc.martyr.A_FAQ
|
|
* @see f00f.net.irc.martyr.clientstate.ClientState
|
|
* @see f00f.net.irc.martyr.services.AutoRegister
|
|
* @see f00f.net.irc.martyr.services.AutoResponder
|
|
* @see f00f.net.irc.martyr.State
|
|
*
|
|
*/
|
|
/*
|
|
* Event handling re-org
|
|
*
|
|
* - A message is an event
|
|
* - A disconnect request is an event, placed on the queue?
|
|
* -- Off I go to do other stuff.
|
|
*/
|
|
public class IRCConnection {
|
|
static Logger log = Logger.getLogger(IRCConnection.class.getName());
|
|
|
|
public IRCConnection()
|
|
{
|
|
this( new ClientState() );
|
|
}
|
|
|
|
public IRCConnection( ClientState clientState )
|
|
{
|
|
// State observers are notified of state changes.
|
|
// Command observers are sent a copy of each message that arrives.
|
|
stateObservers = new StateObserver();
|
|
commandObservers = new CommandObserver();
|
|
this.clientState = clientState;
|
|
stateQueue = new LinkedList<State>();
|
|
|
|
commandRegister = new CommandRegister();
|
|
commandSender = new DefaultCommandSender();
|
|
|
|
setState( State.UNCONNECTED );
|
|
|
|
new ClientStateMonitor( this );
|
|
|
|
localEventQueue = new LinkedList<String>();
|
|
|
|
eventThread = new EventThread();
|
|
eventThread.setDaemon( true );
|
|
startEventThread();
|
|
}
|
|
|
|
/**
|
|
* This method exists so that subclasses may perform operations before
|
|
* the event thread starts, but overriding this method.
|
|
* */
|
|
protected void startEventThread()
|
|
{
|
|
eventThread.start();
|
|
}
|
|
|
|
/**
|
|
* In the event you want to stop martyr, call this. This asks the
|
|
* event thread to finish the current event, then die.
|
|
* */
|
|
public void stop()
|
|
{
|
|
eventThread.doStop();
|
|
}
|
|
|
|
/**
|
|
* Performs a standard connection to the server and port. If we are already
|
|
* connected, this just returns.
|
|
*
|
|
* @param server Server to connect to
|
|
* @param port Port to connect to
|
|
* @throws IOException if we could not connect
|
|
*/
|
|
public void connect( String server, int port )
|
|
throws IOException
|
|
{
|
|
synchronized( connectMonitor )
|
|
{
|
|
//log.debug("IRCConnection: Connecting to " + server + ":" + port);
|
|
if( connected )
|
|
{
|
|
log.severe("IRCConnection: Connect requested, but we are already connected!");
|
|
return;
|
|
}
|
|
|
|
connectUnsafe( new Socket( server, port ), server );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This allows the developer to provide a pre-connected socket, ready for use.
|
|
* This is so that any options that the developer wants to set on the socket
|
|
* can be set. The server parameter is passed in, rather than using the
|
|
* customSocket.getInetAddr() because a DNS lookup may be undesirable. Thus,
|
|
* the canonical server name, whatever that is, should be provided. This is
|
|
* then passed on to the client state.
|
|
*
|
|
* @param customSocket Custom socket that we will connect over
|
|
* @param server Server to connect to
|
|
* @throws IOException if we could not connect
|
|
* @throws IllegalStateException if we are already connected.
|
|
*/
|
|
public void connect( Socket customSocket, String server )
|
|
throws IOException, IllegalStateException
|
|
{
|
|
synchronized( connectMonitor )
|
|
{
|
|
if( connected )
|
|
{
|
|
throw new IllegalStateException( "Connect requested, but we are already connected!" );
|
|
}
|
|
|
|
connectUnsafe( customSocket, server );
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* <p>Orders the socket to disconnect. This doesn't actually disconnect, it
|
|
* merely schedules an event to disconnect. This way, pending incoming
|
|
* messages may be processed before a disconnect actually occurs.</p>
|
|
* <p>No errors are possible from the disconnect. If you try to disconnect an
|
|
* unconnected socket, your disconnect request will be silently ignored.</p>
|
|
*/
|
|
public void disconnect()
|
|
{
|
|
synchronized( eventMonitor )
|
|
{
|
|
disconnectPending = true;
|
|
eventMonitor.notifyAll();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the daemon status on the threads that <code>IRCConnection</code>
|
|
* creates. Default is true, that is, new InputHandler threads are
|
|
* daemon threads, although the event thread is always a daemon. The
|
|
* result is that as long as there is an active connection, the
|
|
* program will keep running.
|
|
*
|
|
* @param daemon Set if we are to be treated like a daemon
|
|
*/
|
|
public void setDaemon( boolean daemon )
|
|
{
|
|
this.daemon = daemon;
|
|
}
|
|
|
|
/**
|
|
* Signal threads to stop, and wait for them to do so.
|
|
* @param timeout *2 msec to wait at most for stop.
|
|
*
|
|
* */
|
|
public void shutdown(long timeout)
|
|
{
|
|
// Note: UNTESTED!
|
|
try
|
|
{
|
|
// 1) shut down the input thread.
|
|
synchronized( inputHandlerMonitor )
|
|
{
|
|
if( inputHandler != null )
|
|
{
|
|
inputHandler.signalShutdown();
|
|
}
|
|
synchronized( socketMonitor )
|
|
{
|
|
if( socket != null )
|
|
{
|
|
try
|
|
{
|
|
socket.close();
|
|
}
|
|
catch (IOException e)
|
|
{
|
|
// surprising?
|
|
}
|
|
}
|
|
}
|
|
if( inputHandler != null )
|
|
{
|
|
inputHandler.join(timeout);
|
|
}
|
|
}
|
|
|
|
// 2) shut down the event thread.
|
|
eventThread.shutdown();
|
|
eventThread.join(timeout);
|
|
}
|
|
catch( InterruptedException ie )
|
|
{
|
|
// We got interrupted - while waiting for death.
|
|
// Shame that.
|
|
}
|
|
}
|
|
|
|
public String toString()
|
|
{
|
|
return "IRCConnection";
|
|
}
|
|
|
|
public void addStateObserver( Observer observer )
|
|
{
|
|
//log.debug("IRCConnection: Added state observer " + observer);
|
|
stateObservers.addObserver( observer );
|
|
}
|
|
|
|
public void removeStateObserver( Observer observer )
|
|
{
|
|
//log.debug("IRCConnection: Removed state observer " + observer);
|
|
stateObservers.deleteObserver( observer );
|
|
}
|
|
|
|
public void addCommandObserver( Observer observer )
|
|
{
|
|
//log.debug("IRCConnection: Added command observer " + observer);
|
|
commandObservers.addObserver( observer );
|
|
}
|
|
|
|
public void removeCommandObserver( Observer observer )
|
|
{
|
|
//log.debug("IRCConnection: Removed command observer " + observer);
|
|
commandObservers.deleteObserver( observer );
|
|
}
|
|
|
|
|
|
public State getState()
|
|
{
|
|
return state;
|
|
}
|
|
|
|
public ClientState getClientState()
|
|
{
|
|
return clientState;
|
|
}
|
|
|
|
/**
|
|
* Accepts a command to be sent. Sends the command to the
|
|
* CommandSender.
|
|
*
|
|
* @param command Command we will send
|
|
* */
|
|
public void sendCommand( OutCommand command )
|
|
{
|
|
commandSender.sendCommand( command );
|
|
}
|
|
|
|
/**
|
|
* @return the first class in a chain of OutCommandProcessors.
|
|
* */
|
|
public CommandSender getCommandSender()
|
|
{
|
|
return commandSender;
|
|
}
|
|
|
|
/**
|
|
* @param sender sets the class that is responsible for sending commands.
|
|
* */
|
|
public void setCommandSender( CommandSender sender )
|
|
{
|
|
this.commandSender = sender;
|
|
}
|
|
|
|
/**
|
|
* @return the local address to which the socket is bound.
|
|
* */
|
|
public InetAddress getLocalAddress()
|
|
{
|
|
return socket.getLocalAddress();
|
|
}
|
|
|
|
public String getRemotehost()
|
|
{
|
|
return clientState.getServer();
|
|
}
|
|
|
|
/**
|
|
* Sets the time in milliseconds we wait after each command is sent.
|
|
*
|
|
* @param sleepTime Length of time to sleep between commands
|
|
* */
|
|
public void setSendDelay( int sleepTime )
|
|
{
|
|
this.sendDelay = sleepTime;
|
|
}
|
|
|
|
/**
|
|
* @since 0.3.2
|
|
* @return a class that can schedule timer tasks.
|
|
* */
|
|
public CronManager getCronManager()
|
|
{
|
|
if( cronManager == null )
|
|
cronManager = new CronManager();
|
|
return cronManager;
|
|
}
|
|
|
|
/**
|
|
* Inserts into the event queue a command that was not directly
|
|
* received from the server.
|
|
*
|
|
* @param fakeCommand Fake command to inject into incoming queue
|
|
* */
|
|
public void injectCommand( String fakeCommand )
|
|
{
|
|
synchronized( eventMonitor )
|
|
{
|
|
localEventQueue.add( fakeCommand );
|
|
eventMonitor.notifyAll();
|
|
}
|
|
}
|
|
|
|
// ===== package methods =============================================
|
|
|
|
void socketError( IOException ioe )
|
|
{
|
|
//log.debug("Socket error called.");
|
|
//log.debug("IRCConnection: The stack of the exception:", ioe);
|
|
|
|
//log.log(Level.SEVERE, "Socket error", ioe);
|
|
disconnect();
|
|
}
|
|
|
|
/**
|
|
* Splits a raw IRC command into three parts, the prefix, identifier,
|
|
* and parameters.
|
|
* @param wholeString String to be parsed
|
|
* @return a String array with 3 components, {prefix,ident,params}.
|
|
* */
|
|
public static String[] parseRawString( String wholeString )
|
|
{
|
|
String prefix = "";
|
|
String identifier;
|
|
String params = "";
|
|
|
|
StringTokenizer tokens = new StringTokenizer( wholeString, " " );
|
|
|
|
if( wholeString.charAt(0) == ':' )
|
|
{
|
|
prefix = tokens.nextToken();
|
|
prefix = prefix.substring( 1, prefix.length() );
|
|
}
|
|
|
|
identifier = tokens.nextToken();
|
|
|
|
if( tokens.hasMoreTokens() )
|
|
{
|
|
// The rest of the string
|
|
params = tokens.nextToken("");
|
|
}
|
|
|
|
String[] result = new String[3];
|
|
result[0] = prefix;
|
|
result[1] = identifier;
|
|
result[2] = params;
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Given the three parts of an IRC command, generates an object to
|
|
* represent that command.
|
|
*
|
|
* @param prefix Prefix of command object
|
|
* @param identifier ID of command
|
|
* @param params Params of command
|
|
* @return An InCommand object for the given command object
|
|
* */
|
|
protected InCommand getCommandObject( String prefix, String identifier, String params )
|
|
{
|
|
InCommand command;
|
|
|
|
// Remember that commands are also factories.
|
|
InCommand commandFactory = commandRegister.getCommand( identifier );
|
|
if( commandFactory == null )
|
|
{
|
|
if( UnknownError.isError( identifier ) )
|
|
{
|
|
command = new UnknownError( identifier );
|
|
log.warning("IRCConnection: Using " + command);
|
|
}
|
|
else if( UnknownReply.isReply( identifier ) )
|
|
{
|
|
command = new UnknownReply( identifier );
|
|
//log.warning("IRCConnection: Using " + command);
|
|
}
|
|
else
|
|
{
|
|
// The identifier doesn't map to a command.
|
|
log.warning("IRCConnection: Unknown command");
|
|
command = new UnknownCommand();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
command = commandFactory.parse( prefix, identifier, params);
|
|
|
|
if( command == null )
|
|
{
|
|
log.severe("IRCConnection: CommandFactory[" + commandFactory + "] returned NULL");
|
|
return null;
|
|
}
|
|
//log.debug("IRCConnection: Using " + command);
|
|
}
|
|
|
|
return command;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Executed by the event thread.
|
|
*
|
|
* @param wholeString String to be parsed and handled
|
|
* */
|
|
void incomingCommand( String wholeString )
|
|
{
|
|
//log.info("IRCConnection: RCV = " + wholeString);
|
|
|
|
// 1) Parse out the command
|
|
String cmdBits[];
|
|
|
|
try
|
|
{
|
|
cmdBits = parseRawString( wholeString );
|
|
}
|
|
catch( Exception e )
|
|
{
|
|
// So.. we can't process the command.
|
|
// So we call the error handler.
|
|
handleUnparsableCommand( wholeString, e );
|
|
return;
|
|
}
|
|
|
|
String prefix = cmdBits[0];
|
|
String identifier = cmdBits[1];
|
|
String params = cmdBits[2];
|
|
|
|
// 2) Fetch command from factory
|
|
InCommand command = getCommandObject( prefix, identifier, params );
|
|
command.setSourceString( wholeString );
|
|
|
|
// Update the state and send out to commandObservers
|
|
localCommandUpdate( command );
|
|
}
|
|
|
|
protected void handleUnparsableCommand( String wholeString, Exception e )
|
|
{
|
|
log.log(Level.SEVERE, "Unable to parse server message.", e );
|
|
}
|
|
|
|
/**
|
|
* Called only in the event thread.
|
|
*
|
|
* @param command Command to update
|
|
* */
|
|
private void localCommandUpdate( InCommand command )
|
|
{
|
|
// 3) Change the connection state if required
|
|
// This allows us to change from UNREGISTERED to REGISTERED and
|
|
// possibly back.
|
|
State cmdState = command.getState();
|
|
if( cmdState != State.UNKNOWN && cmdState != getState() )
|
|
setState( cmdState );
|
|
|
|
// TODO: Bug here?
|
|
|
|
// 4) Notify command observers
|
|
try
|
|
{
|
|
commandObservers.setChanged();
|
|
commandObservers.notifyObservers( command );
|
|
}
|
|
catch( Throwable e )
|
|
{
|
|
log.log(Level.SEVERE, "IRCConnection: Command notify failed.", e);
|
|
}
|
|
|
|
}
|
|
|
|
// ===== private variables ===========================================
|
|
|
|
/** Object used to send commands. */
|
|
private CommandSender commandSender;
|
|
|
|
private CronManager cronManager;
|
|
|
|
/** State of the session. */
|
|
private State state;
|
|
|
|
/**
|
|
* Client state (our name, nick, groups, etc). Stored here mainly
|
|
* because there isn't anywhere else to stick it.
|
|
*/
|
|
private ClientState clientState;
|
|
|
|
/**
|
|
* Maintains a list of classes observing the state and notifies them
|
|
* when it changes.
|
|
*/
|
|
private StateObserver stateObservers;
|
|
|
|
/**
|
|
* Maintains a list of classes observing commands when they come in.
|
|
*/
|
|
private CommandObserver commandObservers;
|
|
|
|
/**
|
|
* The actual socket used for communication.
|
|
*/
|
|
private Socket socket;
|
|
|
|
/**
|
|
* Monitor access to socket.
|
|
* */
|
|
private final Object socketMonitor = new Object();
|
|
|
|
/**
|
|
* We want to prevent connecting and disconnecting at the same time.
|
|
*/
|
|
private final Object connectMonitor = new Object();
|
|
|
|
/**
|
|
* This object should be notified if we want the main thread to check for
|
|
* events. An event is either an incoming message or a disconnect request.
|
|
* Sending commands to the server is synchronized by the eventMonitor.
|
|
*/
|
|
private final Object eventMonitor = new Object();
|
|
|
|
/**
|
|
* This tells the processEvents() method to check if we should disconnect
|
|
* after processing all incoming messages.
|
|
*/
|
|
// Protected by:
|
|
// inputHandlerMonitor
|
|
// eventMonitor
|
|
// connectMonitor
|
|
private boolean disconnectPending = false;
|
|
|
|
/**
|
|
* The writer to use for output.
|
|
*/
|
|
private BufferedWriter socketWriter;
|
|
|
|
/**
|
|
* Command register, contains a list of commands that can be received
|
|
* by the server and have matching Command objects.
|
|
*/
|
|
private CommandRegister commandRegister;
|
|
|
|
/**
|
|
* Maintains a handle on the input handler.
|
|
*/
|
|
private InputHandler inputHandler;
|
|
|
|
/**
|
|
* Access control for the input handler.
|
|
*/
|
|
private final Object inputHandlerMonitor = new Object();
|
|
|
|
/**
|
|
* State queue keeps a queue of requests to switch state.
|
|
*/
|
|
private LinkedList<State> stateQueue;
|
|
|
|
/**
|
|
* localEventQueue allows events not received from the server to be
|
|
* processed.
|
|
* */
|
|
private LinkedList<String> localEventQueue;
|
|
|
|
/**
|
|
* Setting state prevents setState from recursing in an uncontrolled
|
|
* manner.
|
|
*/
|
|
private boolean settingState = false;
|
|
|
|
/**
|
|
* Event thread waits for events and executes them.
|
|
*/
|
|
private EventThread eventThread;
|
|
|
|
/**
|
|
* Determines the time to sleep every time we send a message. We do this
|
|
* so that the server doesn't boot us for flooding.
|
|
*/
|
|
private int sendDelay = 300;
|
|
|
|
/**
|
|
* connected just keeps track of whether we are connected or not.
|
|
*/
|
|
private boolean connected = false;
|
|
|
|
/**
|
|
* Are we set to be a daemon thread?
|
|
*/
|
|
private boolean daemon = false;
|
|
|
|
// ===== private methods =============================================
|
|
|
|
/**
|
|
* Unsafe, because this method can only be called by a method that has a lock
|
|
* on connectMonitor.
|
|
*
|
|
* @param socket Socket to connect over
|
|
* @param server Server to connect to
|
|
* @throws IOException if connection fails
|
|
*/
|
|
private void connectUnsafe( Socket socket, String server )
|
|
throws IOException
|
|
{
|
|
synchronized(socketMonitor)
|
|
{
|
|
this.socket = socket;
|
|
}
|
|
|
|
socketWriter =
|
|
new BufferedWriter( new OutputStreamWriter( socket.getOutputStream() ) );
|
|
|
|
/**
|
|
* The reader to use for input. Managed by the InputHandler.
|
|
*/
|
|
BufferedReader socketReader =
|
|
new BufferedReader( new InputStreamReader( socket.getInputStream() ) );
|
|
|
|
// A simple thread that waits for input from the server and passes
|
|
// it off to the IRCConnection class.
|
|
//if( inputHandler != null )
|
|
//{
|
|
// log.fatal("IRCConnection: Non-null input handler on connect!!");
|
|
// return;
|
|
//}
|
|
|
|
synchronized( inputHandlerMonitor )
|
|
{
|
|
// Pending events get processed after a disconnect call, and there
|
|
// shouldn't be any events generated while disconnected, so it makes
|
|
// sense to test for this condition.
|
|
if( inputHandler != null && inputHandler.pendingMessages() )
|
|
{
|
|
log.severe("IRCConnection: Tried to connect, but there are pending messages!");
|
|
return;
|
|
}
|
|
|
|
if( inputHandler != null && inputHandler.isAlive() )
|
|
{
|
|
log.severe("IRCConnection: Tried to connect, but the input handler is still alive!");
|
|
return;
|
|
}
|
|
|
|
clientState.setServer( server );
|
|
clientState.setPort( socket.getPort() );
|
|
|
|
connected = true;
|
|
|
|
inputHandler = new InputHandler( socketReader, this, eventMonitor );
|
|
inputHandler.setDaemon( daemon );
|
|
inputHandler.start();
|
|
}
|
|
setState( State.UNREGISTERED );
|
|
}
|
|
|
|
private class EventThread extends Thread
|
|
{
|
|
private boolean doShutdown = false;
|
|
|
|
public EventThread()
|
|
{
|
|
super("EventThread");
|
|
}
|
|
|
|
public void run()
|
|
{
|
|
handleEvents();
|
|
}
|
|
|
|
public void shutdown()
|
|
{
|
|
synchronized(eventMonitor)
|
|
{
|
|
this.doShutdown = true;
|
|
eventMonitor.notifyAll();
|
|
}
|
|
}
|
|
|
|
private void handleEvents()
|
|
{
|
|
try
|
|
{
|
|
while( true )
|
|
{
|
|
// Process all events in the event queue.
|
|
//log.debug("IRCConnection: Processing events");
|
|
while( processEvents() ) { }
|
|
|
|
// We can't process events while synchronized on the
|
|
// eventMonitor because we may end up in deadlock.
|
|
synchronized( eventMonitor )
|
|
{
|
|
if( !doShutdown && !pendingEvents() )
|
|
{
|
|
eventMonitor.wait();
|
|
}
|
|
|
|
if( doShutdown )
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch( InterruptedException ie )
|
|
{
|
|
log.log(Level.WARNING, "Interrupted while handling events.", ie );
|
|
// And we do what?
|
|
// We die, that's what we do.
|
|
}
|
|
}
|
|
|
|
public void doStop()
|
|
{
|
|
shutdown();
|
|
}
|
|
|
|
public String toString()
|
|
{
|
|
return "EventThread";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method synchronizes on the inputHandlerMonitor. Note that if
|
|
* additional event types are processed, they also need to be added to
|
|
* pendingEvents().
|
|
* @return true if events were processed, false if there were no events to
|
|
* process.
|
|
*/
|
|
private boolean processEvents()
|
|
{
|
|
boolean events = false;
|
|
|
|
// the inputHandlerMonitor here serves two purposes: To protect
|
|
// from inputHandler changes and to ensure only one thread is
|
|
// operating in processEvents.
|
|
//
|
|
// Perhaps a different monitor should be used?
|
|
synchronized( inputHandlerMonitor )
|
|
{
|
|
while( inputHandler != null && inputHandler.pendingMessages() )
|
|
{
|
|
String msg = inputHandler.getMessage();
|
|
incomingCommand( msg );
|
|
events = true;
|
|
}
|
|
|
|
while( localEventQueue != null && !localEventQueue.isEmpty() )
|
|
{
|
|
String msg = localEventQueue.removeFirst();
|
|
incomingCommand( msg );
|
|
events = true;
|
|
}
|
|
|
|
if( disconnectPending )
|
|
{
|
|
//log.debug("IRCConnection: Process events: Disconnect pending.");
|
|
doDisconnect();
|
|
events = true;
|
|
}
|
|
}
|
|
|
|
return events;
|
|
}
|
|
|
|
/**
|
|
* Does no synchronization on its own. This does not synchronize on
|
|
* any of the IRCConnection monitors or objects and returns after making a
|
|
* minimum of method calls.
|
|
* @return true if there are pending events that need processing.
|
|
*/
|
|
private boolean pendingEvents()
|
|
{
|
|
if( inputHandler != null && inputHandler.pendingMessages() )
|
|
return true;
|
|
if( disconnectPending )
|
|
return true;
|
|
if( localEventQueue != null && !localEventQueue.isEmpty() )
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Synchronized by inputHandlerMonitor, called only from processEvents.
|
|
private void doDisconnect()
|
|
{
|
|
synchronized( connectMonitor )
|
|
{
|
|
disconnectPending = false;
|
|
|
|
if( !connected )
|
|
{
|
|
return;
|
|
}
|
|
connected = false;
|
|
|
|
try
|
|
{
|
|
final long startTime = System.currentTimeMillis();
|
|
final long sleepTime = 1000;
|
|
final long stopTime = startTime + sleepTime;
|
|
//log.debug("IRCConnection: Sleeping for a bit ("
|
|
// + sleepTime + ")..");
|
|
// Slow things down a bit so the server doesn't kill us
|
|
// Also, we want to give a second to let any pending messages
|
|
// get processed and any pending disconnect() calls to be made.
|
|
// It is important that we use wait instead of sleep!
|
|
while( stopTime - System.currentTimeMillis() > 0 )
|
|
{
|
|
connectMonitor.wait( stopTime - System.currentTimeMillis() );
|
|
}
|
|
}
|
|
catch( InterruptedException ie )
|
|
{
|
|
// Ignore
|
|
}
|
|
|
|
//log.debug("IRCConnection: Stopping input handler.");
|
|
// Deprecated?
|
|
// inputHandler.stop();
|
|
// inputHandler = null;
|
|
|
|
//log.debug("IRCConnection: Closing socket.");
|
|
try
|
|
{
|
|
socket.close();
|
|
}
|
|
catch( IOException ioe )
|
|
{
|
|
// And we are supposed to do what?
|
|
// This probably means we've called disconnect on a closed
|
|
// socket.
|
|
handleSocketCloseException( ioe );
|
|
return;
|
|
}
|
|
finally
|
|
{
|
|
connected = false;
|
|
}
|
|
}
|
|
|
|
// The input handler should die, because we closed the socket.
|
|
// We'll wait for it to die.
|
|
synchronized( inputHandlerMonitor )
|
|
{
|
|
//log.debug("IRCConnection: Waiting for the input handler to die..");
|
|
try
|
|
{
|
|
// log.debug("IRCConnection: Stack:");
|
|
|
|
if( inputHandler.isAlive() )
|
|
inputHandler.join();
|
|
else
|
|
{
|
|
//log.debug("IRCConnection: No waiting required, input hander is already dead.");
|
|
}
|
|
}
|
|
catch( InterruptedException ie )
|
|
{
|
|
//log.debug("IRCConnection: Error in join(): " + ie);
|
|
}
|
|
//log.debug("IRCConnection: Done waiting for the input handler to die.");
|
|
}
|
|
|
|
|
|
// There may be pending messages that we should process before we
|
|
// actually notify all the state listeners.
|
|
processEvents();
|
|
|
|
// It is important that the state be switched last. One of the
|
|
// state listeners may try to re-connect us.
|
|
setState( State.UNCONNECTED );
|
|
|
|
}
|
|
|
|
protected void handleSocketCloseException( IOException ioe )
|
|
{
|
|
log.log(Level.WARNING, "Error closing socket.", ioe );
|
|
}
|
|
|
|
/**
|
|
* Signals to trigger a state change. Won't actually send a state change
|
|
* until a previous attempt at changing the state finishes. This is
|
|
* important if one of the state listeners affects the state (ie tries to
|
|
* reconnect if we disconnect, etc).
|
|
*
|
|
* @param newState New state to set connection to.
|
|
*/
|
|
private void setState( State newState )
|
|
{
|
|
if( settingState )
|
|
{
|
|
// We are already setting the state. We want to complete changing
|
|
// to one state before changing to another, so that we don't have
|
|
// out-of-order state change signals.
|
|
stateQueue.addLast( newState );
|
|
return;
|
|
}
|
|
|
|
settingState = true;
|
|
|
|
if( state == newState )
|
|
return;
|
|
|
|
while( true )
|
|
{
|
|
state = newState;
|
|
|
|
//log.debug("IRCConnection: State switch: " + state);
|
|
|
|
try
|
|
{
|
|
stateObservers.setChanged();
|
|
stateObservers.notifyObservers( newState );
|
|
}
|
|
catch( Throwable e )
|
|
{
|
|
log.log(Level.SEVERE, "IRCConnection: State update failed.", e);
|
|
}
|
|
|
|
if( stateQueue.isEmpty() )
|
|
break;
|
|
newState = stateQueue.removeFirst();
|
|
}
|
|
|
|
settingState = false;
|
|
}
|
|
|
|
private class DefaultCommandSender implements CommandSender
|
|
{
|
|
public CommandSender getNextCommandSender()
|
|
{
|
|
return null;
|
|
}
|
|
|
|
public void sendCommand( OutCommand oc )
|
|
{
|
|
finalSendCommand( oc.render() );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends the command down the socket, with the required 'CRLF' on the
|
|
* end. Waits for a little bit after sending the command so that we don't
|
|
* accidentally flood the server.
|
|
*
|
|
* @param str String to send
|
|
*/
|
|
private void finalSendCommand( String str )
|
|
{
|
|
try
|
|
{
|
|
synchronized( eventMonitor )
|
|
{
|
|
//log.info("IRCConnection: SEND= " + str);
|
|
|
|
if( disconnectPending )
|
|
{
|
|
//log.debug("IRCConnection: Send cancelled, disconnect pending.");
|
|
return;
|
|
}
|
|
|
|
socketWriter.write( str + "\r\n" );
|
|
socketWriter.flush();
|
|
|
|
try
|
|
{
|
|
// Slow things down a bit so the server doesn't kill us
|
|
// We do this after the send so that if the send fails the
|
|
// exception is handled right away.
|
|
Thread.sleep( sendDelay );
|
|
}
|
|
catch( InterruptedException ie )
|
|
{
|
|
// Ignore
|
|
}
|
|
}
|
|
}
|
|
catch( IOException ioe )
|
|
{
|
|
socketError( ioe );
|
|
}
|
|
}
|
|
// ----- END IRCConnection -------------------------------------------
|
|
}
|