package com.esri.geoevent.solutions.transport.irc.jerklib;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import com.esri.geoevent.solutions.transport.irc.jerklib.events.IRCEvent.Type;
import com.esri.geoevent.solutions.transport.irc.jerklib.events.impl.ConnectionLostEventImpl;
import com.esri.geoevent.solutions.transport.irc.jerklib.events.modes.ModeAdjustment;
import com.esri.geoevent.solutions.transport.irc.jerklib.events.modes.ModeAdjustment.Action;
import com.esri.geoevent.solutions.transport.irc.jerklib.listeners.IRCEventListener;
import com.esri.geoevent.solutions.transport.irc.jerklib.parsers.CommandParser;
import com.esri.geoevent.solutions.transport.irc.jerklib.parsers.DefaultInternalEventParser;
import com.esri.geoevent.solutions.transport.irc.jerklib.parsers.InternalEventParser;
import com.esri.geoevent.solutions.transport.irc.jerklib.tasks.Task;
import com.esri.geoevent.solutions.transport.irc.jerklib.tasks.TaskImpl;
/**
*
* Session contains methods to manage an IRC connection.
* Like {@link Session#changeNick(String)} , {@link Session#setRejoinOnKick(boolean)} , {@link Session#getUserModes()} etc.
*<p>
* Session is where Tasks and Listeners should be added
* to be notified of IRCEvents coming from the connected server.
*<p>
* You can override the default parsing and internal event handling
* of a Session with {@link Session#setInternalEventHandler(IRCEventListener)} and
* {@link Session#setInternalParser(InternalEventParser)}.
*<p>
* New Session instances are obtained by requesting a connection with the
* ConnectionManager
*
* @see ConnectionManager#requestConnection(String)
* @see ConnectionManager#requestConnection(String, int)
* @see ConnectionManager#requestConnection(String, int, Profile)
*
* @author mohadib
*/
public class Session extends RequestGenerator
{
private final List<IRCEventListener> listenerList = new ArrayList<IRCEventListener>();
private final Map<Type, List<Task>> taskMap = new HashMap<Type, List<Task>>();
private final RequestedConnection rCon;
private Connection con;
private final ConnectionManager conman;
private boolean rejoinOnKick = true, profileUpdating, isAway , isLoggedIn , useAltNicks = true;
private Profile tmpProfile;
private long lastRetry = -1, lastResponse = System.currentTimeMillis();
private ServerInformation serverInfo = new ServerInformation();
private State state = State.DISCONNECTED;
private InternalEventParser parser;
private IRCEventListener internalEventHandler;
private List<ModeAdjustment> userModes = new ArrayList<ModeAdjustment>();
private final Map<String, Channel> channelMap = new HashMap<String, Channel>();
public static enum State
{
CONNECTED,
CONNECTING,
HALF_CONNECTED,
DISCONNECTED,
MARKED_FOR_REMOVAL,
NEED_TO_PING,
PING_SENT,
NEED_TO_RECONNECT
}
/**
* @param rCon
* @param conman
*/
Session(RequestedConnection rCon, ConnectionManager conman)
{
this.rCon = rCon;
this.conman = conman;
setSession(this);
}
/**
* Gets the InternalEventParser this Session uses for event parsing
*
* @see InternalEventParser
* @see DefaultInternalEventParser
* @see CommandParser
* @return InternalEventParser
*/
public InternalEventParser getInternalEventParser()
{
return parser;
}
/**
* Sets the InternalEventParser this Session should use for
* event parsing
*
*
* @see InternalEventParser
* @see DefaultInternalEventParser
* @see CommandParser
* @param parser
*/
public void setInternalParser(InternalEventParser parser)
{
this.parser = parser;
}
/**
* Sets the internal event handler this Session should use
*
* @see IRCEventListener
* @see DefaultInternalEventHandler
* @param handler
*/
public void setInternalEventHandler(IRCEventListener handler)
{
internalEventHandler = handler;
}
/**
* Returns the internal event handler this Session is using
*
* @see IRCEventListener
* @see DefaultInternalEventHandler
* @return event handler
*
*/
public IRCEventListener getInternalEventHandler()
{
return internalEventHandler;
}
/**
* Called when UserMode events are received for this Session.
*
* @param modes
*/
void updateUserModes(List<ModeAdjustment> modes)
{
for (ModeAdjustment ma : modes)
{
updateUserMode(ma);
}
}
/**
* If Action is MINUS and the same mode exists with a PLUS Action then just
* remove the PLUS mode ModeAdjustment from the collection.
*
* If Action is MINUS and the same mode with PLUS does not exist then add the
* MINUS mode to the ModeAdjustment collection
*
* if Action is PLUS and the same mode exists with a MINUS Action then remove
* MINUS mode and add PLUS mode
*
* If Action is PLUS and the same mode with MINUS does not exist then just add
* PLUS mode to collection
*
* @param mode
*/
private void updateUserMode(ModeAdjustment mode)
{
int index = indexOfMode(mode.getMode(), userModes);
if (mode.getAction() == Action.MINUS)
{
if (index != -1)
{
ModeAdjustment ma = userModes.remove(index);
if (ma.getAction() == Action.MINUS) userModes.add(ma);
}
else
{
userModes.add(mode);
}
}
else
{
if (index != -1) userModes.remove(index);
userModes.add(mode);
}
}
/**
* Finds the index of a mode in a list modes
* @param mode
* @param modes
* @return index of mode or -1 if mode if not found
*/
private int indexOfMode(char mode, List<ModeAdjustment> modes)
{
for (int i = 0; i < modes.size(); i++)
{
ModeAdjustment ma = modes.get(i);
if (ma.getMode() == mode) return i;
}
return -1;
}
/**
* returns a List of UserModes for this Session
*
* @return UserModes
*/
public List<ModeAdjustment> getUserModes()
{
return new ArrayList<ModeAdjustment>(userModes);
}
/**
* Speak in a Channel
*
* @see Channel#say(String)
* @param channel
* @param msg
*/
public void sayChannel(Channel channel, String msg)
{
super.sayChannel(msg, channel);
}
/* general methods */
/**
* Is this Session currently connected to an IRC server?
*
* @return true if connected else false
*/
public boolean isConnected()
{
return state == State.CONNECTED;
}
/**
* Should this Session rejoin channels it is Kicked from?
* Default is true.
*
* @return true if channels should be rejoined else false
*/
public boolean isRejoinOnKick()
{
return rejoinOnKick;
}
/**
* Sets that this Sessions should or should not rejoin Channels
* kiced from
*
* @param rejoin
*/
public void setRejoinOnKick(boolean rejoin)
{
rejoinOnKick = rejoin;
}
/**
* Called to alert the Session that login was a success
*/
void loginSuccess()
{
isLoggedIn = true;
}
/**
* Returns true if the Session has an active Connection and
* has successfully logged on to the Connection.
* @return if logged in
*/
public boolean isLoggedIn()
{
return isLoggedIn;
}
/**
* Set Session yo try alternate nicks
* on connection if a nick in use event is received , or not.
* True by default.
*
* @param use
*/
public void setShouldUseAltNicks(boolean use)
{
useAltNicks = use;
}
/**
* Returns if Session should try alternate nicks
* on connection if a nick in use event is received.
* True by default.
*
* @return should use alt nicks
*/
public boolean getShouldUseAltNicks()
{
return useAltNicks;
}
/**
* Disconnect from server and destroy Session
*
* @param quitMessage
*/
public void close(String quitMessage)
{
if (con != null)
{
con.quit(quitMessage);
}
conman.removeSession(this);
isLoggedIn = false;
}
/**
* Nick used for Session
*
* @return nick
*/
public String getNick()
{
return getRequestedConnection().getProfile().getActualNick();
}
/* (non-Javadoc)
* @see com.esri.ges.transport.Irc.jerklib.RequestGenerator#changeNick(java.lang.String)
*/
public void changeNick(String newNick)
{
tmpProfile = rCon.getProfile().clone();
tmpProfile.setActualNick(newNick);
tmpProfile.setFirstNick(newNick);
profileUpdating = true;
super.changeNick(newNick);
}
/**
* Profile is updating when a new nick is requested but has not been approved from server yet.
* @return true if profile is updating
*/
public boolean isProfileUpdating()
{
return profileUpdating;
}
/**
* Is this Session marked away?
*
* @return true if away else false
*/
public boolean isAway()
{
return isAway;
}
/* (non-Javadoc)
* @see com.esri.ges.transport.Irc.jerklib.RequestGenerator#setAway(java.lang.String)
*/
public void setAway(String message)
{
isAway = true;
super.setAway(message);
}
/**
* Unset away
*/
public void unsetAway()
{
/* if we're not away let's not bother even delegating */
if (isAway)
{
super.unSetAway();
isAway = false;
}
}
/* methods to get information about connection and server */
/**
* Get ServerInformation for Session
* @see ServerInformation
* @return ServerInformation for Session
*/
public ServerInformation getServerInformation()
{
return serverInfo;
}
/**
* Get RequestedConnection for Session
* @see RequestedConnection
* @return RequestedConnection for Session
*/
public RequestedConnection getRequestedConnection()
{
return rCon;
}
/**
* Returns host name this Session is connected to.
* If the session is disconnectd an empty string will be returned.
*
* @return hostname or an empty string if not connected
* @see Session#getRequestedConnection()
* @see RequestedConnection#getHostName()
*/
public String getConnectedHostName()
{
return con == null?"":con.getHostName();
}
/**
* Adds an IRCEventListener to the Session. This listener will be
* notified of all IRCEvents coming from the connected sever.
*
* @param listener
*/
public void addIRCEventListener(IRCEventListener listener)
{
listenerList.add(listener);
}
/**
* Remove IRCEventListner from Session
*
* @param listener
* @return true if listener was removed else false
*/
public boolean removeIRCEventListener(IRCEventListener listener)
{
return listenerList.remove(listener);
}
/**
* Get a collection of all IRCEventListeners attached to Session
*
* @return listeners
*/
public Collection<IRCEventListener> getIRCEventListeners()
{
return Collections.unmodifiableCollection(listenerList);
}
/**
* Add a task to be ran when any IRCEvent is received
* @see Task
* @see TaskImpl
* @param task
*/
public void onEvent(Task task)
{
// null means task should be notified of all Events
onEvent(task, (Type) null);
}
/**
* Add a task to be ran when any of the given Types
* of IRCEvents are received
*
* @see Task
* @see TaskImpl
* @param task - task to run
* @param types - types of events task should run on
*/
public void onEvent(Task task, Type... types)
{
synchronized (taskMap)
{
for (Type type : types)
{
if (!taskMap.containsKey(type))
{
List<Task> tasks = new ArrayList<Task>();
tasks.add(task);
taskMap.put(type, tasks);
}
else
{
taskMap.get(type).add(task);
}
}
}
}
/**
* Gets All Tasks attacthed to Session
* Indexed by the Type the task is receving events for.
* Task type of null are default tasks that receive all events.
* Some Tasks can possibly be the value for many Types.
*
* @return tasks
*/
Map<Type, List<Task>> getTasks()
{
return new HashMap<Type, List<Task>>(taskMap);
}
/**
* Removes a Task from the Session.
* Some Tasks can possibly be the value for many Types.
*
* @param t
*/
public void removeTask(Task t)
{
synchronized (taskMap)
{
for (Iterator<Type> it = taskMap.keySet().iterator(); it.hasNext();)
{
List<Task> tasks = taskMap.get(it.next());
if (tasks != null)
{
tasks.remove(t);
}
}
}
}
/**
* Called to alert Session if the profile was updated
*
* @param success
*/
void updateProfileSuccessfully(boolean success)
{
if (success)
{
rCon.setProfile(tmpProfile);
}
tmpProfile = null;
profileUpdating = false;
}
/**
* Get a List of Channels Session is currently in
*
* @see Channel
* @return channels
*/
public List<Channel> getChannels()
{
return Collections.unmodifiableList(new ArrayList<Channel>(channelMap.values()));
}
/**
* Gets a Channel by name
*
* @param channelName
* @return Channel or null if no such Channel is joined.
*/
public Channel getChannel(String channelName)
{
Channel chan = channelMap.get(channelName);
return chan == null ? channelMap.get(channelName.toLowerCase()) : chan;
}
/**
* Add a Channel to the session
* @see Channel
* @param channel
*/
void addChannel(Channel channel)
{
channelMap.put(channel.getName(), channel);
}
/**
* Remove a channel from the Session
* @param channel
* @return true if channel was removed else false
*/
boolean removeChannel(Channel channel)
{
return channelMap.remove(channel.getName()) == null;
}
/**
* Updates a nick in all channels currently joined
*
* @param oldNick
* @param newNick
*/
void nickChanged(String oldNick, String newNick)
{
synchronized (channelMap)
{
for (Channel chan : channelMap.values())
{
if (chan.getNicks().contains(oldNick))
{
chan.nickChanged(oldNick, newNick);
}
}
}
}
/**
* Removes a nick from all channels
* @param nick
* @return list of Channels nick was found in
*/
public List<Channel> removeNickFromAllChannels(String nick)
{
List<Channel> returnList = new ArrayList<Channel>();
for (Channel chan : channelMap.values())
{
if (chan.removeNick(nick))
{
returnList.add(chan);
}
}
return returnList;
}
/* methods to track connection attempts */
/**
* return time of last reconnect attempt
* @return
*/
long getLastRetry()
{
return lastRetry;
}
/**
* sets time of last reconnect event
*/
void retried()
{
lastRetry = System.currentTimeMillis();
}
/**
* Sets the connection for this Session
* @param con
*/
void setConnection(Connection con)
{
this.con = con;
}
/**
* Gets Connection used for this Session. Can return null if
* Session is disconnected.
*
* @return Connection
*/
Connection getConnection()
{
return con;
}
/**
* Got ping response
*/
void gotResponse()
{
lastResponse = System.currentTimeMillis();
state = State.CONNECTED;
}
/**
* Ping has been sent but no response yet
*/
void pingSent()
{
state = State.PING_SENT;
}
/**
*Session has been disconnected
*/
void disconnected()
{
if (state == State.DISCONNECTED) return;
state = State.DISCONNECTED;
if (con != null)
{
con.quit("");
con = null;
}
isLoggedIn = false;
conman.addToRelayList(new ConnectionLostEventImpl(this));
}
/**
* Session is now connected
*/
void connected()
{
gotResponse();
}
/**
* Session is connecting
*/
void connecting()
{
state = State.CONNECTING;
}
/**
* Session is half connected
*/
void halfConnected()
{
state = State.HALF_CONNECTED;
}
/**
* Session has been marked for removal
*/
void markForRemoval()
{
state = State.MARKED_FOR_REMOVAL;
}
/**
* Get the State of the Session
* @return Session state
* @see State
*/
State getState()
{
long current = System.currentTimeMillis();
if (state == State.DISCONNECTED) return state;
if (current - lastResponse > 300000 && state == State.NEED_TO_PING)
{
state = State.NEED_TO_RECONNECT;
}
else if (current - lastResponse > 200000 && state != State.PING_SENT)
{
state = State.NEED_TO_PING;
}
return state;
}
/**
* Test if a String starts with a known channel prefix
* @param token
* @return true if starts with a channel prefix else false
*/
public boolean isChannelToken(String token)
{
ServerInformation serverInfo = getServerInformation();
String[] chanPrefixes = serverInfo.getChannelPrefixes();
for (String prefix : chanPrefixes)
{
if (token.startsWith(prefix)) { return true; }
}
return false;
}
/**
* Send login messages to server
*/
void login()
{
// test :irc.inter.net.il CAP * LS :multi-prefix
// writeRequests.add(new WriteRequest("CAP LS", this));
sayRaw("NICK " + getNick());
sayRaw("USER " + rCon.getProfile().getName() + " 0 0 :" + rCon.getProfile().getName());
}
/* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
public int hashCode()
{
return rCon.getHostName().hashCode();
}
/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals(Object o)
{
if (o instanceof Session && o.hashCode() == hashCode())
{
return ((Session) o).getRequestedConnection().getHostName().matches(getRequestedConnection().getHostName())
&& ((Session) o).getNick().matches(getNick());
}
return false;
}
}