/*
* Copyright 2002-2005 Uwyn bvba/sprl <info[remove] at uwyn dot com>
* Distributed under the terms of the GNU Lesser General Public
* License, v2.1 or later
*
* $Id: Server.java 2286 2005-08-15 22:01:57Z gbevin $
*/
package com.uwyn.drone.core;
import com.uwyn.drone.core.exceptions.*;
import java.io.*;
import com.uwyn.drone.core.ServerListener;
import com.uwyn.drone.protocol.ServerMessage;
import com.uwyn.drone.protocol.commands.IrcCommand;
import com.uwyn.drone.protocol.commands.Ping;
import com.uwyn.rife.tools.ExceptionUtils;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.logging.Logger;
public class Server implements Runnable, TimedOutputStreamListener
{
private String mServerName = null;
private ServerInfo mServerInfo = null;
private Thread mServerThread = null;
private Bot mBot = null;
private Socket mServerSocket = null;
private BufferedReader mInput = null;
private BufferedWriter mOutput = null;
private TimedOutputStream mTimedOutput = null;
private HashMap mChannels = null;
private boolean mConnected = false;
private String mConnectedHost = null;
private int mReadTimeOutCount = 0;
private CharsetDecoder mCharsetDecoder = null;
private CharsetEncoder mCharsetEncoder = null;
private HashSet mServerListeners = null;
private HashSet mCommandListeners = null;
private HashSet mResponseListeners = null;
private Object mServerListenersMonitor = new Object();
private Object mCommandListenersMonitor = new Object();
private Object mResponseListenersMonitor = new Object();
Server(String serverName, ServerInfo serverInfo, Bot bot)
{
assert serverName != null;
assert serverInfo != null;
assert bot != null;
mServerName = serverName.toLowerCase();
mServerInfo = serverInfo;
mBot = bot;
mChannels = new HashMap();
mServerListeners = new HashSet();
mCommandListeners = new HashSet();
mResponseListeners = new HashSet();
cleanUp();
}
private void cleanUp()
{
mServerSocket = null;
mInput = null;
mOutput = null;
mConnected = false;
mConnectedHost = null;
}
public String getServerName()
{
return mServerName;
}
public ServerInfo getServerInfo()
{
return mServerInfo;
}
public Socket getServerSocket()
{
return mServerSocket;
}
public Channel getChannel(String name)
{
if (null == name) throw new IllegalArgumentException("name can't be null.");
if (0 == name.length()) throw new IllegalArgumentException("name can't be empty.");
Channel channel = null;
name = name.toLowerCase();
synchronized (mChannels)
{
channel = (Channel)mChannels.get(name);
if (null == channel)
{
channel = new Channel(name, this);
mChannels.put(name, channel);
}
}
assert channel != null;
assert channel.getName().equals(name);
assert channel.getServer() == this;
return channel;
}
public synchronized void connect()
throws CoreException
{
if (0 == mServerInfo.getAddresses().size())
{
throw new MissingAddressException(this);
}
InputStream input_stream = null;
Iterator addresses_it = null;
InetSocketAddress address = null;
boolean is_connected = false;
while (!is_connected)
{
try
{
// try all addresses, one by one until a valid connection
// could
addresses_it = mServerInfo.getAddresses().iterator();
while (addresses_it.hasNext())
{
address = (InetSocketAddress)addresses_it.next();
try
{
mServerSocket = new Socket(address.getAddress(), address.getPort());
break;
}
catch (IOException e)
{
mServerSocket = null;
}
}
// no valid address was found, throw an error
if (null == mServerSocket)
{
throw new NoValidAddressException(this);
}
// continue with the first valid address
// and setup the socket timeout
mServerSocket.setSoTimeout(mServerInfo.getTimeout());
input_stream = mServerSocket.getInputStream();
if (mTimedOutput != null)
{
mTimedOutput.close();
}
mTimedOutput = new TimedOutputStream(mServerSocket.getOutputStream(), mServerInfo.getMax(), mServerInfo.getAmount(), mServerInfo.getInterval());
mTimedOutput.addTimedOutputStreamListener(this);
is_connected = true;
}
catch (IOException e)
{
Logger.getLogger("com.uwyn.drone.core").severe("Error while connecting from the server '"+this+"', retrying : "+ExceptionUtils.getExceptionStackTrace(e));
}
}
mCharsetDecoder = Charset.forName(mServerInfo.getCharset()).newDecoder();
mCharsetDecoder.replaceWith("?");
mCharsetDecoder.onMalformedInput(CodingErrorAction.REPLACE);
mCharsetDecoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
mCharsetEncoder = Charset.forName(mServerInfo.getCharset()).newEncoder();
mInput = new BufferedReader(new InputStreamReader(input_stream, mCharsetDecoder));
mOutput = new BufferedWriter(new OutputStreamWriter(mTimedOutput, mCharsetEncoder));
mConnected = true;
mServerThread = new Thread(this);
mServerThread.start();
fireConnected();
assert mServerSocket.isConnected();
assert mInput != null;
assert mOutput != null;
}
public synchronized void disconnect()
throws CoreException
{
mConnected = false;
mConnectedHost = null;
try
{
mInput = null;
if (mOutput != null)
{
mOutput.close();
}
if (mServerSocket != null)
{
mServerSocket.close();
}
}
catch (IOException e)
{
Logger.getLogger("com.uwyn.drone.core").severe("Error while disconnecting from the server '"+this+"', reconnecting : "+ExceptionUtils.getExceptionStackTrace(e));
cleanUp();
try
{
connect();
}
catch (CoreException e2)
{
throw new ServerDisconnectionErrorException(this, e2);
}
return;
}
this.notifyAll();
cleanUp();
fireDisconnected();
}
private void handleTerminatedStream(Throwable e)
{
if (mConnected)
{
mConnected = false;
mConnectedHost = null;
while (!mConnected)
{
if (e != null)
{
Logger.getLogger("com.uwyn.drone.core").severe("Error during the server connection, reconnecting : "+ExceptionUtils.getExceptionStackTrace(e));
}
else
{
Logger.getLogger("com.uwyn.drone.core").severe("Stream to IRC server has terminated, reconnecting.");
}
try
{
reconnect();
}
catch (CoreException e2)
{
Logger.getLogger("com.uwyn.drone.core").severe("Unable to restore connection after a terminated server stream : "+ExceptionUtils.getExceptionStackTrace(e2));
try
{
// sleep 30 seconds before trying again
Thread.sleep(30000);
}
catch (InterruptedException e3)
{
return;
}
}
}
}
return;
}
public void run()
{
while (mConnected)
{
try
{
if (mInput != null)
{
try
{
String message_string = mInput.readLine();
mReadTimeOutCount = 0;
// check if a message has been received, if this is not the case,
// the connection has been terminated and the stream also
// try to reconnect until this succeeds
if (null == message_string)
{
handleTerminatedStream(null);
return;
}
// handle the received message
else
{
ServerMessage message = ServerMessage.parse(message_string);
// detect the hostname of the irc server
if (null == mConnectedHost &&
message.getPrefix() != null)
{
mConnectedHost = message.getPrefix().getServerName();
}
fireServerMessageEvents(message);
}
}
catch (SocketTimeoutException e)
{
// if there were two read timeouts in a row, the connection is closed
// and let the exception bubble up
if (mReadTimeOutCount > 1)
{
throw e;
}
mReadTimeOutCount++;
try
{
send(new Ping(mConnectedHost));
}
catch (CoreException e2)
{
throw e;
}
}
}
}
catch (IOException e)
{
handleTerminatedStream(e);
return;
}
Thread.yield();
}
mServerThread = null;
}
private void fireServerMessageEvents(ServerMessage message)
{
if (message.isResponse())
{
fireReceivedResponse(message);
}
if (message.isCommand())
{
fireReceivedCommand(message);
}
}
public boolean isConnected()
{
return mConnected;
}
synchronized boolean send(IrcCommand command)
throws CoreException
{
if (null == mServerSocket ||
!mServerSocket.isConnected() ||
mServerSocket.isOutputShutdown() ||
!mServerSocket.isBound())
{
return false;
}
try
{
mOutput.write(command.getCommand());
mOutput.newLine();
}
catch(IOException e)
{
throw new SendErrorException(this, command, e);
}
flush();
fireServerMessageEvents(mBot.createServerMessage(command));
return true;
}
public void reconnect()
throws CoreException
{
synchronized (this)
{
disconnect();
connect();
}
}
public void exceptionThrow(IOException e)
{
try
{
reconnect();
}
catch (CoreException e2)
{
Logger.getLogger("com.uwyn.drone.core").severe("Unable to restore connection after socket error : "+ExceptionUtils.getExceptionStackTrace(e));
}
}
synchronized private boolean flush()
throws CoreException
{
if (null == mServerSocket ||
!mServerSocket.isConnected() ||
mServerSocket.isOutputShutdown())
{
return false;
}
try
{
mOutput.flush();
}
catch(IOException e)
{
throw new FlushErrorException(this, e);
}
return true;
}
private void fireConnected()
throws CoreException
{
Iterator listeners = mServerListeners.iterator();
while (listeners.hasNext())
{
((ServerListener)listeners.next()).connected(this);
}
}
private void fireDisconnected()
throws CoreException
{
Iterator listeners = mServerListeners.iterator();
while (listeners.hasNext())
{
((ServerListener)listeners.next()).disconnected(this);
}
}
private void fireReceivedCommand(ServerMessage command)
{
Iterator listeners = mCommandListeners.iterator();
while (listeners.hasNext())
{
try
{
((CommandListener)listeners.next()).receivedCommand(command);
}
catch (CoreException e)
{
Logger.getLogger("com.uwyn.drone.core").severe(ExceptionUtils.getExceptionStackTrace(e));
}
}
}
private void fireReceivedResponse(ServerMessage Response)
{
Iterator listeners = mResponseListeners.iterator();
while (listeners.hasNext())
{
try
{
((ResponseListener)listeners.next()).receivedResponse(Response);
}
catch (CoreException e)
{
Logger.getLogger("com.uwyn.drone.core").severe(ExceptionUtils.getExceptionStackTrace(e));
}
}
}
public boolean addServerListener(ServerListener listener)
{
if (null == listener) throw new IllegalArgumentException("listener can't be null.");
boolean result = false;
synchronized (mServerListenersMonitor)
{
if (!mServerListeners.contains(listener))
{
HashSet clone = (HashSet)mServerListeners.clone();
result = clone.add(listener);
mServerListeners = clone;
}
else
{
result = true;
}
}
assert true == mServerListeners.contains(listener);
return result;
}
public boolean removeServerListener(ServerListener listener)
{
if (null == listener) throw new IllegalArgumentException("listener can't be null.");
boolean result = false;
synchronized (mServerListenersMonitor)
{
HashSet clone = (HashSet)mServerListeners.clone();
result = clone.remove(listener);
mServerListeners = clone;
}
assert false == mServerListeners.contains(listener);
return result;
}
public boolean addCommandListener(CommandListener listener)
{
if (null == listener) throw new IllegalArgumentException("listener can't be null.");
boolean result = false;
synchronized (mCommandListenersMonitor)
{
if (!mCommandListeners.contains(listener))
{
HashSet clone = (HashSet)mCommandListeners.clone();
result = clone.add(listener);
mCommandListeners = clone;
}
else
{
result = true;
}
}
assert true == mCommandListeners.contains(listener);
return result;
}
public boolean removeCommandListener(CommandListener listener)
{
if (null == listener) throw new IllegalArgumentException("listener can't be null.");
boolean result = false;
synchronized (mCommandListenersMonitor)
{
HashSet clone = (HashSet)mCommandListeners.clone();
result = clone.remove(listener);
mCommandListeners = clone;
}
assert false == mCommandListeners.contains(listener);
return result;
}
public boolean addResponseListener(ResponseListener listener)
{
if (null == listener) throw new IllegalArgumentException("listener can't be null.");
boolean result = false;
synchronized (mResponseListenersMonitor)
{
if (!mResponseListeners.contains(listener))
{
HashSet clone = (HashSet)mResponseListeners.clone();
result = clone.add(listener);
mResponseListeners = clone;
}
else
{
result = true;
}
}
assert true == mResponseListeners.contains(listener);
return result;
}
public boolean removeResponseListener(ResponseListener listener)
{
if (null == listener) throw new IllegalArgumentException("listener can't be null.");
boolean result = false;
synchronized (mResponseListenersMonitor)
{
HashSet clone = (HashSet)mResponseListeners.clone();
result = clone.remove(listener);
mResponseListeners = clone;
}
assert false == mResponseListeners.contains(listener);
return result;
}
}