/* * 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. // /** *
IRCConnection
is the core class for Martyr.
* IRCConnection
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.
* IRCConnection
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.
Please read this entirely before using the framework. Or * what the heck, try out the example below and see if it works for ya.
* *IRCConnection
is always in one of three states.
* UNCONNECTED
, UNREGISTERED
or
* REGISTERED
. 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.
IRCConnection
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, IRCConnection
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.
The AutoReconnect class can connect you and will try to stay
* connected. Using AutoReconnect to connect the
* first time is recommended, use the go(server,port)
method once
* you are ready to start.
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
* UNREGISTERED
. The consumer will know this because it
* has registered some class as a state observer.
*
Some commands, such as Ping require an automatic response.
* Commands that fall into this category can be handled by the
* AutoResponder
class. For a list of what commands
* AutoResponder
auto responds to, see the source.
You can use the AutoJoin
class to join a channel
* and stay there. AutoJoin
will try to re-join if
* kicked or if the connection is lost and the server re-connects.
* AutoJoin
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.
You will probably want to at least use the
* AutoRegister
and AutoResponder
classes.
* Example:
Note that in the example, the first line is optional.
* IRCConnection
can be called with its default
* constructor. See note below about why this is done.
* IRCConnection
will instantiate its own
* ClientState
object if you do not provide one.
* 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 ); ** *
The ClientStateMonitor
class tells commands to
* change the client state when they are received.
* ClientStateMonitor
is automatically added to the
* command queue before any other command, so that you can be
* guaranteed that the ClientState
is updated before any
* observer sees a command.
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 clientstate.ClientState
and
* clientstate.Channel
classes with your own, overriding
* the setXxxxXxxx
methods. Each method would call
* super.setXxxXxxx
and then proceed to change the
* application as required.
IRCConnection starts in the UNCONNECTED
state and
* makes no attempt to connect until the connect(...)
* method is called.
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. No errors are possible from the disconnect. If you try to disconnect an
* unconnected socket, your disconnect request will be silently ignored.IRCConnection
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 LinkedListIRCConnection
* 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