package pl.radical.open.gg;
import pl.radical.open.gg.event.ConnectionListener;
import pl.radical.open.gg.event.GGPacketListener;
import pl.radical.open.gg.event.PingListener;
import pl.radical.open.gg.packet.GGHeader;
import pl.radical.open.gg.packet.GGIncomingPackage;
import pl.radical.open.gg.packet.GGOutgoingPackage;
import pl.radical.open.gg.packet.dicts.SessionState;
import pl.radical.open.gg.packet.handlers.PacketChain;
import pl.radical.open.gg.packet.handlers.PacketContext;
import pl.radical.open.gg.packet.out.GGPing;
import pl.radical.open.gg.utils.GGUtils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.URL;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.event.EventListenerList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The default implementation of <code>IConnectionService</code>.
* <p>
* Created on 2004-11-27
*
* @author <a href="mailto:mati@sz.home.pl">Mateusz Szczap</a>
*/
public class DefaultConnectionService implements IConnectionService {
private static final Logger LOG = LoggerFactory.getLogger(DefaultConnectionService.class);
private static final String WINDOWS_ENCODING = "windows-1250";
private final EventListenerList eventListenerList = new EventListenerList();
/** reference to session object */
private Session session = null;
private final ConcurrentLinkedQueue<GGOutgoingPackage> senderQueue = new ConcurrentLinkedQueue<GGOutgoingPackage>();
/** chain that handles packages */
private PacketChain packetChain = null;
/** thread that monitors connection */
private ConnectionThread connectionThread = null;
/** the thread that pings the connection to keep it alive */
private PingerThread connectionPinger = null;
private IServer server = null;
// friendly
DefaultConnectionService(final Session session) throws GGException {
if (session == null) {
throw new IllegalArgumentException("session cannot be null");
}
this.session = session;
packetChain = new PacketChain();
}
/**
* @see pl.radical.open.gg.IConnectionService#getServer(int) Example return:
*
* <pre>
* 0 0 91.197.13.78:8074 91.197.13.78
* </pre>
*/
public IServer[] lookupServer(final int uin) throws GGException {
if (LOG.isTraceEnabled()) {
LOG.trace("lookupServer() executed for user [" + uin + "]");
}
try {
final IGGConfiguration configuration = session.getGGConfiguration();
final URL url = new URL(configuration.getServerLookupURL() + "?fmnumber=" + Integer.valueOf(uin) + "&version=8.0.0.7669");
if (LOG.isDebugEnabled()) {
LOG.debug("GG HUB URL address: {}", url);
}
final HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setConnectTimeout(configuration.getSocketTimeoutInMiliseconds());
con.setReadTimeout(configuration.getSocketTimeoutInMiliseconds());
con.setDoInput(true);
con.connect();
final BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream(), WINDOWS_ENCODING));
final String line = reader.readLine();
reader.close();
if (LOG.isDebugEnabled()) {
LOG.debug("Dane zwrócone przez serwer: {}", line);
}
if (line != null && line.length() > 22) {
return parseAddress(line);
} else {
throw new GGException("GG HUB didn't provided a valid IP address of GG server, aborting");
}
} catch (final IOException ex) {
throw new GGException("Unable to get default server for uin: " + uin, ex);
}
}
/**
* @see pl.radical.open.gg.IConnectionService#connect()
*/
// TODO Add HTTPS support as a fallback
public void connect(final IServer[] server) throws GGException { // NOPMD by LRzanek on 04.05.10 02:28
if (server == null) {
throw new GGException("Server cannot be null");
}
this.server = server[0];
checkConnectionState();
session.getSessionAccessor().setSessionState(SessionState.CONNECTING);
try {
connectionThread = new ConnectionThread();
connectionPinger = new PingerThread();
connectionThread.openConnection(server[0].getAddress(), server[0].getPort());
connectionPinger.startPinging();
} catch (final IOException ex) {
session.getSessionAccessor().setSessionState(SessionState.CONNECTION_ERROR);
throw new GGException("Unable to connect to Gadu-Gadu server: " + server[0], ex);
}
}
/**
* @see pl.radical.open.gg.IConnectionService#disconnect()
*/
public void disconnect() throws GGException {
checkDisconnectionState();
session.getSessionAccessor().setSessionState(SessionState.DISCONNECTING);
try {
if (connectionPinger != null) {
connectionPinger.stopPinging();
connectionPinger = null;
}
if (connectionThread != null) {
connectionThread.closeConnection();
connectionThread = null;
}
server = null;
session.getSessionAccessor().setSessionState(SessionState.DISCONNECTED);
notifyConnectionClosed();
} catch (final IOException ex) {
LOG.error("IOException occured while trying to disconnect", ex);
session.getSessionAccessor().setSessionState(SessionState.CONNECTION_ERROR);
throw new GGException("Unable to close connection to server", ex);
}
}
private void checkDisconnectionState() throws GGSessionException {
if (session.getSessionState() == SessionState.DISCONNECTED) {
throw new GGSessionException(SessionState.DISCONNECTED);
}
}
/**
* @see pl.radical.open.gg.IConnectionService#isConnected()
*/
public boolean isConnected() {
final boolean authenticated = session.getSessionState() == SessionState.LOGGED_IN;
final boolean authenticationAwaiting = session.getSessionState() == SessionState.AUTHENTICATION_AWAITING;
final boolean connected = session.getSessionState() == SessionState.CONNECTED;
return authenticated || authenticationAwaiting || connected;
}
/**
* @see pl.radical.open.gg.IConnectionService#getServer()
*/
public IServer getServer() {
return server;
}
/**
* @see pl.radical.open.gg.IConnectionService#addConnectionListener(pl.radical.open.gg.event.ConnectionListener)
*/
public void addConnectionListener(final ConnectionListener connListener) {
if (connListener == null) {
throw new IllegalArgumentException("connectionListener cannot be null");
}
eventListenerList.add(ConnectionListener.class, connListener);
}
/**
* @see pl.radical.open.gg.IConnectionService#removeConnectionListener(pl.radical.open.gg.event.ConnectionListener)
*/
public void removeConnectionListener(final ConnectionListener connListener) {
if (connListener == null) {
throw new IllegalArgumentException("connectionListener cannot be null");
}
eventListenerList.remove(ConnectionListener.class, connListener);
}
/**
* @see pl.radical.open.gg.IConnectionService#addPacketListener(pl.radical.open.gg.event.GGPacketListener)
*/
public void addPacketListener(final GGPacketListener packetListener) {
if (packetListener == null) {
throw new IllegalArgumentException("packetListener cannot be null");
}
eventListenerList.add(GGPacketListener.class, packetListener);
}
/**
* @see pl.radical.open.gg.IConnectionService#removePacketListener(pl.radical.open.gg.event.GGPacketListener)
*/
public void removePacketListener(final GGPacketListener packetListener) {
if (packetListener == null) {
throw new IllegalArgumentException("packetListener cannot be null");
}
eventListenerList.remove(GGPacketListener.class, packetListener);
}
/**
* @see pl.radical.open.gg.IConnectionService#addPingListener(pl.radical.open.gg.event.PingListener)
*/
public void addPingListener(final PingListener pingListener) {
if (pingListener == null) {
throw new IllegalArgumentException("pingListener cannot be null");
}
eventListenerList.add(PingListener.class, pingListener);
}
/**
* @see pl.radical.open.gg.IConnectionService#removePingListener(pl.radical.open.gg.event.PingListener)
*/
public void removePingListener(final PingListener pingListener) {
if (pingListener == null) {
throw new IllegalArgumentException("pingListener cannot be null");
}
eventListenerList.remove(PingListener.class, pingListener);
}
protected void notifyConnectionEstablished() throws GGException {
session.getSessionAccessor().setSessionState(SessionState.AUTHENTICATION_AWAITING);
final ConnectionListener[] connListeners = eventListenerList.getListeners(ConnectionListener.class);
for (final ConnectionListener connectionListener : connListeners) {
connectionListener.connectionEstablished();
}
// this could be also realized as a ConnectionHandler in session class
}
protected void notifyConnectionClosed() throws GGException {
session.getSessionAccessor().setSessionState(SessionState.DISCONNECTED);
final ConnectionListener[] connListeners = eventListenerList.getListeners(ConnectionListener.class);
for (final ConnectionListener connectionListener : connListeners) {
connectionListener.connectionClosed();
}
}
protected void notifyConnectionError(final Exception ex) throws GGException {
final ConnectionListener[] connListeners = eventListenerList.getListeners(ConnectionListener.class);
for (final ConnectionListener connectionListener : connListeners) {
connectionListener.connectionError(ex);
}
session.getSessionAccessor().setSessionState(SessionState.CONNECTION_ERROR);
}
protected void notifyPingSent() {
final PingListener[] pingListeners = eventListenerList.getListeners(PingListener.class);
for (final PingListener pingListener : pingListeners) {
pingListener.pingSent(server);
}
}
protected void notifyPongReceived() {
final PingListener[] pingListeners = eventListenerList.getListeners(PingListener.class);
for (final PingListener pingListener : pingListeners) {
pingListener.pongReceived(server);
}
}
protected void notifyPacketReceived(final GGIncomingPackage incomingPackage) {
final GGPacketListener[] packetListeners = eventListenerList.getListeners(GGPacketListener.class);
for (final GGPacketListener packetListener : packetListeners) {
packetListener.receivedPacket(incomingPackage);
}
}
protected void notifyPacketSent(final GGOutgoingPackage outgoingPackage) {
final GGPacketListener[] packetListeners = eventListenerList.getListeners(GGPacketListener.class);
for (final GGPacketListener packetListener : packetListeners) {
packetListener.sentPacket(outgoingPackage);
}
}
protected void sendPackage(final GGOutgoingPackage outgoingPackage) throws IOException {
senderQueue.add(outgoingPackage);
}
private void checkConnectionState() throws GGSessionException {
switch (session.getSessionState()) {
case CONNECTION_AWAITING:
case DISCONNECTED:
case CONNECTION_ERROR:
break;
default:
throw new GGSessionException(session.getSessionState());
}
}
/**
* Parses the server's address.
*
* @param line
* line to be parsed.
* @return <code>Server</code> the server object.
*/
private static Server[] parseAddress(final String line) {
if (LOG.isTraceEnabled()) {
LOG.trace("Parsing token information from hub: [" + line + "]");
}
final Pattern p = Pattern.compile("\\d\\s\\d\\s((?:\\d{1,3}\\.?+){4})\\:(\\d{2,4})\\s((?:\\d{1,3}\\.?+){4})");
final Matcher m = p.matcher(line);
if (!m.matches()) {
throw new IllegalArgumentException("String returned by GG HUB is not what was expected");
} else {
final Server[] servers = new Server[2];
if (LOG.isTraceEnabled()) {
LOG.trace("Znaleziono prawidłowy string w danych przesłanych przez GG HUB:");
for (int i = 1; i <= m.groupCount(); i++) {
LOG.trace("---> znaleziona grupa w adresie [{}]: {}", i, m.group(i));
}
}
servers[0] = new Server(m.group(1), Integer.parseInt(m.group(2)));
servers[1] = new Server(m.group(3), 443);
return servers;
}
}
private class ConnectionThread extends Thread {
private static final int HEADER_LENGTH = 8;
private Socket socket = null;
private BufferedInputStream dataInput = null;
private BufferedOutputStream dataOutput = null;
private boolean active = true;
@Override
public void run() {
try {
while (active) {
handleInput();
if (!senderQueue.isEmpty()) {
handleOutput();
}
final int sleepTime = session.getGGConfiguration().getConnectionThreadSleepTimeInMiliseconds();
Thread.sleep(sleepTime);
}
dataInput = null;
dataOutput = null;
socket.close();
} catch (final Exception ex) { // FIXME Czy ten catch jest potrzebny??
try {
active = false;
notifyConnectionError(ex);
} catch (final GGException ex2) {
LOG.warn("Unable to notify listeners", ex);
}
}
}
private void handleInput() throws IOException, GGException {
final byte[] headerData = new byte[HEADER_LENGTH];
if (dataInput.available() > 0) {
dataInput.read(headerData);
decodePacket(new GGHeader(headerData));
}
}
private void handleOutput() throws IOException {
while (!senderQueue.isEmpty() && !socket.isClosed() && dataOutput != null) {
final GGOutgoingPackage outgoingPackage = senderQueue.poll();
sendPackage(outgoingPackage);
notifyPacketSent(outgoingPackage);
}
}
private boolean isActive() {
return active;
}
private void openConnection(final String host, final int port) throws IOException {
// add runtime checking for port
socket = new Socket();
final SocketAddress socketAddress = new InetSocketAddress(InetAddress.getByName(host), port);
final int socketTimeoutInMiliseconds = session.getGGConfiguration().getSocketTimeoutInMiliseconds();
socket.connect(socketAddress, socketTimeoutInMiliseconds);
socket.setKeepAlive(true);
socket.setSoTimeout(socketTimeoutInMiliseconds);
dataInput = new BufferedInputStream(socket.getInputStream());
dataOutput = new BufferedOutputStream(socket.getOutputStream());
start();
}
private void closeConnection() throws IOException {
if (LOG.isDebugEnabled()) {
LOG.debug("Closing connection...");
}
active = false;
}
private synchronized void sendPackage(final GGOutgoingPackage op) throws IOException {
if (LOG.isDebugEnabled()) {
LOG.debug("Sending packet: {}, packetPayLoad: {}", op.getPacketType(), GGUtils.prettyBytesToString(op.getContents()));
}
dataOutput.write(GGUtils.intToByte(op.getPacketType()));
dataOutput.write(GGUtils.intToByte(op.getContents().length));
if (op.getContents().length > 0) {
dataOutput.write(op.getContents());
}
dataOutput.flush();
}
private void decodePacket(final GGHeader header) throws IOException, GGException {
final byte[] keyBytes = new byte[header.getLength()];
dataInput.read(keyBytes);
final PacketContext context = new PacketContext(session, header, keyBytes);
packetChain.sendToChain(context);
}
}
private class PingerThread extends Thread {
private boolean active = false;
/**
* @see java.lang.Thread#run()
*/
@Override
public void run() {
while (active && connectionThread.isActive()) {
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Pinging...");
}
sendPackage(GGPing.getPing());
notifyPingSent();
final int pingInterval = session.getGGConfiguration().getPingIntervalInMiliseconds();
Thread.sleep(pingInterval);
} catch (final IOException ex) {
active = false;
// LOG.error("PingerThreadError: ", ex);
try {
notifyConnectionError(ex);
} catch (final GGException e) {
LOG.warn("Unable to notify connection error listeners", ex);
}
} catch (final InterruptedException ex) {
active = false;
if (LOG.isDebugEnabled()) {
LOG.debug("PingerThread was interruped", ex);
}
}
}
}
private void startPinging() {
if (LOG.isDebugEnabled()) {
LOG.debug("Starting pinging...");
}
active = true;
start();
}
private void stopPinging() {
if (LOG.isDebugEnabled()) {
LOG.debug("Stopping pinging...");
}
active = false;
}
}
}