/**
* Copyright (C) 2013 Alexander Szczuczko
*
* This file may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
package ca.szc.keratin.core.net.io;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import javax.net.SocketFactory;
import org.pmw.tinylog.Logger;
import ca.szc.keratin.core.event.IrcMessageEvent;
import ca.szc.keratin.core.event.connection.IrcConnect;
import ca.szc.keratin.core.event.connection.IrcDisconnect;
import ca.szc.keratin.core.event.message.recieve.ReceiveChannelMode;
import ca.szc.keratin.core.event.message.recieve.ReceiveInvite;
import ca.szc.keratin.core.event.message.recieve.ReceiveJoin;
import ca.szc.keratin.core.event.message.recieve.ReceiveKick;
import ca.szc.keratin.core.event.message.recieve.ReceiveMode;
import ca.szc.keratin.core.event.message.recieve.ReceiveNick;
import ca.szc.keratin.core.event.message.recieve.ReceiveNotice;
import ca.szc.keratin.core.event.message.recieve.ReceivePart;
import ca.szc.keratin.core.event.message.recieve.ReceivePing;
import ca.szc.keratin.core.event.message.recieve.ReceivePong;
import ca.szc.keratin.core.event.message.recieve.ReceivePrivmsg;
import ca.szc.keratin.core.event.message.recieve.ReceiveQuit;
import ca.szc.keratin.core.event.message.recieve.ReceiveReply;
import ca.szc.keratin.core.event.message.recieve.ReceiveTopic;
import ca.szc.keratin.core.event.message.recieve.ReceiveUserMode;
import ca.szc.keratin.core.net.mbassador.MBassadorWrapper;
import ca.szc.keratin.core.net.message.InvalidMessageCommandException;
import ca.szc.keratin.core.net.message.InvalidMessageException;
import ca.szc.keratin.core.net.message.InvalidMessageParamException;
import ca.szc.keratin.core.net.message.InvalidMessagePrefixException;
import ca.szc.keratin.core.net.message.IrcMessage;
/**
* Receives lines from the socket and sends out MessageRecieve events
*/
public class ConnectionThread
extends Thread
{
/**
* States for the run loop
*/
private enum RunState
{
CONNECT, DISCONNECT, END, READ
}
/**
* How long a read on the socket will block before throwing SocketTimeoutException
*/
private static final int SOCKET_TIMEOUT = 20 * 1000;
private static boolean isDigits( String str )
{
try
{
Integer.parseInt( str );
return true;
}
catch ( NumberFormatException e )
{
return false;
}
}
private final MBassadorWrapper bus;
private final InetSocketAddress endpoint;
private final OutputQueue wrappedOutputQueue;
private final BlockingQueue<IrcMessage> outputQueue;
private BufferedWriter outputWriter;
private final Object outputWriterMutex = new Object();
private final SocketFactory socketFactory;
public ConnectionThread( MBassadorWrapper bus, InetSocketAddress endpoint, SocketFactory socketFactory )
{
this.bus = bus;
this.endpoint = endpoint;
this.socketFactory = socketFactory;
outputQueue = new LinkedBlockingQueue<IrcMessage>();
wrappedOutputQueue = new OutputQueue( outputQueue );
}
private void createOutput( Socket socket )
throws IOException
{
OutputStream outputStream = socket.getOutputStream();
synchronized ( outputWriterMutex )
{
outputWriter = new BufferedWriter( new OutputStreamWriter( outputStream, IoConfig.CHARSET ) );
// Don't want to carry forward the old stuff from before reconnecting
outputQueue.clear();
}
Logger.trace( "Output created" );
}
/**
* Get the output queue for the socket. May be called immediately.
*/
public OutputQueue getOutputQueue()
{
return wrappedOutputQueue;
}
@Override
public void run()
{
Thread.currentThread().setName( "InputThread" );
Logger.trace( "Input thread running" );
// Converts submitted IrcMessage objects to a raw message and sends it.
new Thread()
{
@Override
public void run()
{
Thread.currentThread().setName( "OutputThread" );
Logger.trace( "Output thread running" );
while ( !Thread.interrupted() )
{
IrcMessage msg;
try
{
msg = outputQueue.take();
}
catch ( InterruptedException e1 )
{
Logger.trace( "Interrupted" );
break;
}
String rawCommand = msg.toString();
if ( outputWriter != null )
{
try
{
writeLine( rawCommand );
}
catch ( IOException e )
{
Logger.trace( "Error writing IrcMessage: " + rawCommand );
}
}
else
{
Logger.error( "First connection has not been established, can't send IRC message '"
+ rawCommand + "'" );
}
try
{
// Sending messages too close to each other can cause problems on some servers, if the order of
// the messages matters (ex: NICK must proceed USER at the start of a connection).
Thread.sleep( 50 );
}
catch ( InterruptedException e )
{
Logger.trace( "Interrupted" );
break;
}
}
Logger.trace( "Run loop ends, thread exiting" );
}
}.start();
RunState state = RunState.CONNECT;
Socket socket = null;
BufferedReader input = null;
while ( state != RunState.END )
{
if ( Thread.interrupted() )
{
Logger.trace( "Interrupted, exiting" );
state = RunState.END;
}
if ( state == RunState.CONNECT )
{
try
{
Logger.trace( "Creating/connecting socket" );
socket = socketFactory.createSocket( endpoint.getAddress(), endpoint.getPort() );
Logger.info( "Successfully connected socket" );
socket.setSoTimeout( SOCKET_TIMEOUT );
createOutput( socket );
InputStream inputStream = socket.getInputStream();
input = new BufferedReader( new InputStreamReader( inputStream, IoConfig.CHARSET ) );
Logger.trace( "Input created" );
bus.publishAsync( new IrcConnect( wrappedOutputQueue, socket ) );
state = RunState.READ;
}
catch ( IOException e )
{
Logger.error( e, "Failed to connect socket, sleeping before retrying." );
}
try
{
Thread.sleep( IoConfig.WAIT_TIME );
}
catch ( InterruptedException e )
{
state = RunState.END;
}
}
else if ( state == RunState.READ )
{
try
{
String line = input.readLine();
if ( line != null )
{
// Logger.trace( "Got line " + line );
try
{
IrcMessage message = IrcMessage.parseMessage( line );
// String prefix = message.getPrefix();
String command = message.getCommand();
// String[] params = message.getParams();
IrcMessageEvent messageEvent = null;
try
{
// INVITE
if ( ReceiveInvite.COMMAND.equals( command ) )
messageEvent = new ReceiveInvite( wrappedOutputQueue, message );
// JOIN
else if ( ReceiveJoin.COMMAND.equals( command ) )
messageEvent = new ReceiveJoin( wrappedOutputQueue, message );
// KICK
else if ( ReceiveKick.COMMAND.equals( command ) )
messageEvent = new ReceiveKick( wrappedOutputQueue, message );
// MODE
else if ( ReceiveMode.COMMAND.equals( command ) )
{
if ( message.getParams().length == 2 )
messageEvent = new ReceiveUserMode( wrappedOutputQueue, message );
else
messageEvent = new ReceiveChannelMode( wrappedOutputQueue, message );
}
// NICK
else if ( ReceiveNick.COMMAND.equals( command ) )
messageEvent = new ReceiveNick( wrappedOutputQueue, message );
// NOTICE
else if ( ReceiveNotice.COMMAND.equals( command ) )
messageEvent = new ReceiveNotice( wrappedOutputQueue, message );
// PART
else if ( ReceivePart.COMMAND.equals( command ) )
messageEvent = new ReceivePart( wrappedOutputQueue, message );
// PING
else if ( ReceivePing.COMMAND.equals( command ) )
messageEvent = new ReceivePing( wrappedOutputQueue, message );
// PONG
else if ( ReceivePong.COMMAND.equals( command ) )
messageEvent = new ReceivePong( wrappedOutputQueue, message );
// PRIVMSG
else if ( ReceivePrivmsg.COMMAND.equals( command ) )
messageEvent = new ReceivePrivmsg( wrappedOutputQueue, message );
// QUIT
else if ( ReceiveQuit.COMMAND.equals( command ) )
messageEvent = new ReceiveQuit( wrappedOutputQueue, message );
// replies
else if ( isDigits( command ) )
messageEvent = new ReceiveReply( wrappedOutputQueue, message );
// TOPIC
else if ( ReceiveTopic.COMMAND.equals( command ) )
messageEvent = new ReceiveTopic( wrappedOutputQueue, message );
// others
else
Logger.error( "Unknown message '" + message.toString().replace( "\n", "\\n" ) + "'" );
}
catch ( Exception e )
{
String type = "null";
if ( messageEvent != null )
type = messageEvent.getClass().getSimpleName();
Logger.error( "Error when creating message event, type: " + type + ", content: "
+ messageEvent );
}
if ( messageEvent != null )
{
Logger.trace( "Publishing message event type: "
+ messageEvent.getClass().getSimpleName() + ", content: " + messageEvent );
bus.publishAsync( messageEvent );
}
}
catch ( IndexOutOfBoundsException | InvalidMessagePrefixException
| InvalidMessageCommandException | InvalidMessageParamException e )
{
Logger.error( e, "Couldn't create IrcMessage instance out of parsed data from line " + line );
}
catch ( InvalidMessageException e )
{
Logger.error( e, "Couldn't parse line " + line );
}
}
else
{
// Definite connection loss
Logger.error( "Input end of stream" );
state = RunState.DISCONNECT;
}
}
catch ( SocketTimeoutException e )
{
Logger.trace( "Read line timed out, checking if connection is active" );
// Sending some data is pretty much the only way to tell if the TCP connection is still alive. We
// don't really care about the reply, just that sending it doesn't cause a connection error.
try
{
writeLine( "PING client" );
}
catch ( IOException e1 )
{
state = RunState.DISCONNECT;
}
}
catch ( IOException e )
{
Logger.error( e, "Could not read line" );
state = RunState.DISCONNECT;
}
}
else if ( state == RunState.DISCONNECT )
{
if ( socket != null && !socket.isClosed() )
{
try
{
socket.close();
}
catch ( IOException e )
{
Logger.trace( e, "Error when closing open socket" );
}
}
bus.publishAsync( new IrcDisconnect( wrappedOutputQueue, socket ) );
Logger.info( "Disconnected, attempting reconnect." );
state = RunState.CONNECT;
}
}
Logger.trace( "Run loop ends, thread exiting" );
}
private void writeLine( String line )
throws IOException
{
synchronized ( outputWriterMutex )
{
Logger.trace( "Writing line: " + line );
outputWriter.write( line + "\n" );
try
{
outputWriter.flush();
}
catch ( IOException e )
{
Logger.error( e, "Could not flush output stream" );
}
}
}
}