package com.esri.geoevent.solutions.transport.irc.jerklib; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.CancelledKeyException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.nio.channels.UnresolvedAddressException; import java.util.List; import java.util.ArrayList; import java.util.HashMap; import java.util.Collections; import java.util.Map; import java.util.Timer; import java.util.Iterator; import java.util.TimerTask; import java.util.Collection; import com.esri.geoevent.solutions.transport.irc.jerklib.Session.State; import com.esri.geoevent.solutions.transport.irc.jerklib.events.ErrorEvent; import com.esri.geoevent.solutions.transport.irc.jerklib.events.IRCEvent; import com.esri.geoevent.solutions.transport.irc.jerklib.events.IRCEvent.Type; import com.esri.geoevent.solutions.transport.irc.jerklib.events.impl.UnresolvedHostnameErrorEventImpl; import com.esri.geoevent.solutions.transport.irc.jerklib.listeners.IRCEventListener; import com.esri.geoevent.solutions.transport.irc.jerklib.listeners.WriteRequestListener; 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.util.IdentServer; /** * This class is used to control/store Sessions/Connections. * Request new connections with this class. * * @author mohadib * */ public class ConnectionManager { /* maps to index sessions by name and socketchannel */ final Map<String, Session> sessionMap = Collections.synchronizedMap(new HashMap<String, Session>()); final Map<SocketChannel, Session> socChanMap = Collections.synchronizedMap(new HashMap<SocketChannel, Session>()); /* event listener lists */ private final List<WriteRequestListener> writeListeners = Collections.synchronizedList(new ArrayList<WriteRequestListener>(1)); /* event queues */ private final List<IRCEvent> eventQueue = new ArrayList<IRCEvent>(); private final List<IRCEvent> relayQueue = new ArrayList<IRCEvent>(); private final List<WriteRequest> requestForWriteListenerEventQueue = new ArrayList<WriteRequest>(); /* internal event parser */ // private InternalEventParser parser = new InternalEventParserImpl(this); private IRCEventListener internalEventHandler = new DefaultInternalEventHandler(this); private InternalEventParser internalEventParser = new DefaultInternalEventParser(); /* main loop timer */ private Timer loopTimer; /* event dispatch timer */ private Timer dispatchTimer; /* default user profile to use for new connections */ private Profile defaultProfile; /* NIO Selector */ private Selector selector; /** * Takes a profile to use as default profile for new * Connections * * @param defaultProfile default user profile * @see Profile */ public ConnectionManager(Profile defaultProfile) { this.defaultProfile = defaultProfile; try { selector = Selector.open(); } catch (IOException e) { e.printStackTrace(); } startMainLoop(); } /** * This is for testing purposes only. * Do not use unless testing. * */ ConnectionManager() { } /** * get a list of Sessions * * @return Session list */ public List<Session> getSessions() { return Collections.unmodifiableList(new ArrayList<Session>(sessionMap.values())); } /** * gets a session by name * * @param name session name - the hostname of the server this session is for * @return Session or null if no Session with name exists */ public Session getSession(String name) { return sessionMap.get(name); } /** * Adds a listener to be notified of all writes * * @param listener to be notified */ public void addWriteRequestListener(WriteRequestListener listener) { writeListeners.add(listener); } /** * gets an unmodifiable list of WriteListeners * * @return listeners */ public List<WriteRequestListener> getWriteListeners() { return Collections.unmodifiableList(writeListeners); } /** * request a new connection to a host with the default port of 6667 * * @param hostName DNS name or IP of host to connect to * @return the {@link Session} for this connection */ public Session requestConnection(String hostName) { return requestConnection(hostName, 6667); } /** * request a new connection to a host * * @param hostName DNS name or IP of host to connect to * @param port port to use for connection * @return the {@link Session} for this connection */ public Session requestConnection(String hostName, int port) { return requestConnection(hostName, port, defaultProfile.clone()); } /** * request a new connection to a host * * @param hostName DNS name or IP of host to connect to * @param port port to use for connection * @param profile profile to use for this connection * @return the {@link Session} for this connection */ public Session requestConnection(String hostName, int port, Profile profile) { RequestedConnection rCon = new RequestedConnection(hostName, port, profile); Session session = new Session(rCon , this); session.setInternalParser(internalEventParser); sessionMap.put(hostName, session); new IdentServer(defaultProfile.getName()); return session; } /** * Closes all connections and shuts down manager * * @param quitMsg quit message */ public synchronized void quit(String quitMsg) { loopTimer.cancel(); dispatchTimer.cancel(); for (Session session : new ArrayList<Session>(sessionMap.values())) { session.close(quitMsg); } sessionMap.clear(); socChanMap.clear(); try { selector.close(); } catch (IOException e) { e.printStackTrace(); } } /** * Closes all Sessions and exits library */ public synchronized void quit() { quit(""); } /** * gets the default profile used for new connections * * @return default profile */ public Profile getDefaultProfile() { return defaultProfile; } /** * sets the default profile to use for new connections * * @param profile * default profile to use for connections */ public void setDefaultProfile(Profile profile) { this.defaultProfile = profile; } /** * Sets the InternalEventHandler to use for this Session. * * @param handler */ public void setDefaultInternalEventHandler(IRCEventListener handler) { internalEventHandler = handler; } /** * Gets the InternalEventHandler to use for this Session. * @return default Event Handler */ public IRCEventListener getDefaultEventHandler() { return internalEventHandler; } /** * Set the InternalEventParser used for this Session. * * @param parser */ public void setDefaultInternalEventParser(InternalEventParser parser) { internalEventParser = parser; } /** * Get the InternalEventParser used for this Session. * @return InternalEventParser for Session */ public InternalEventParser getDefaultInternalEventParser() { return internalEventParser; } /** * Remove a session * * @param session */ void removeSession(Session session) { sessionMap.remove(session.getRequestedConnection().getHostName()); for (Iterator<Session> it = socChanMap.values().iterator(); it.hasNext();) { if (it.next().equals(session)) { it.remove(); return; } } } /** * Add an event to the EventQueue to be parsed and dispatched to Listeners * * @param event */ void addToEventQueue(IRCEvent event) { eventQueue.add(event); } /** * Add an event to be dispatched to Listeners(will not be parsed) * * @param event */ void addToRelayList(IRCEvent event) { if (event == null) { new Exception().printStackTrace(); quit("Null Pointers ?? In my Code??! :("); return; } synchronized (relayQueue) { relayQueue.add(event); } } /** * Starts a Thread for IO/Parsing/Checking-Making Connections * Start another Thread for relaying events */ void startMainLoop() { dispatchTimer = new Timer(); loopTimer = new Timer(); TimerTask dispatchTask = new TimerTask() { public void run() { relayEvents(); notifyWriteListeners(); } }; TimerTask loopTask = new TimerTask() { public void run() { makeConnections(); doNetworkIO(); parseEvents(); checkServerConnections(); } }; loopTimer.schedule(loopTask, 0, 200); dispatchTimer.schedule(dispatchTask, 0, 200); } /** * Makes read and write request via Connections when * they can be done without blocking. * */ void doNetworkIO() { try { if (selector.selectNow() > 0) { Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); Session session = socChanMap.get(key.channel()); it.remove(); try { if (!key.isValid()) { continue; } if (key.isReadable()) { socChanMap.get(key.channel()).getConnection().read(); } if (key.isWritable()) { socChanMap.get(key.channel()).getConnection().doWrites(); } if (key.isConnectable()) { finishConnection(key); } } catch (CancelledKeyException ke) { session.disconnected(); } } } } catch (IOException e) { e.printStackTrace(); } } /** * Attempts to finish a connection * @param key */ void finishConnection(SelectionKey key) { SocketChannel chan = (SocketChannel) key.channel(); Session session = socChanMap.get(chan); if (chan.isConnectionPending()) { try { if (session.getConnection().finishConnect()) { session.halfConnected(); session.login(); } else { session.connecting(); } } catch (IOException e) { session.markForRemoval(); key.cancel(); e.printStackTrace(); } } } /** * Check livelyness of server connections */ void checkServerConnections() { synchronized (sessionMap) { for (Iterator<Session> it = sessionMap.values().iterator(); it.hasNext();) { Session session = it.next(); State state = session.getState(); if (state == State.MARKED_FOR_REMOVAL) { it.remove(); } else if (state == State.NEED_TO_PING) { session.getConnection().ping(); } } } } /** * Parse Events */ void parseEvents() { synchronized (eventQueue) { if (eventQueue.isEmpty()) { return; } for (IRCEvent event : eventQueue) { IRCEvent newEvent = event.getSession().getInternalEventParser().receiveEvent(event); internalEventHandler.receiveEvent(newEvent); } eventQueue.clear(); } } /** * Remove Cancelled Tasks for a Session * @param session * @return remanding valid tasks */ Map<Type, List<Task>> removeCanceled(Session session) { Map<Type, List<Task>> tasks = session.getTasks(); synchronized (tasks) { for (Iterator<List<Task>> it = tasks.values().iterator(); it.hasNext();) { List<Task> thisTasks = it.next(); for (Iterator<Task> x = thisTasks.iterator(); x.hasNext();) { Task rmTask = x.next(); if (rmTask.isCanceled()) { x.remove(); } } } } return tasks; } /** * Relay events to Listeners/Tasks */ void relayEvents() { List<IRCEvent> events = new ArrayList<IRCEvent>(); List<IRCEventListener> templisteners = new ArrayList<IRCEventListener>(); Map<Type, List<Task>> tempTasks = new HashMap<Type, List<Task>>(); synchronized (relayQueue) { events.addAll(relayQueue); relayQueue.clear(); } for (IRCEvent event : events) { Session s = event.getSession(); // if session is null , this means the session has been removed or // quit() in Session has been called , but not before a few // events could queue up for that session. So we should continue // to the next event if (s == null) { continue; } Collection<IRCEventListener> listeners = s.getIRCEventListeners(); synchronized (listeners) { templisteners.addAll(listeners); } tempTasks.putAll(removeCanceled(s)); List<Task> typeTasks = tempTasks.get(event.getType()); if (typeTasks != null) { templisteners.addAll(typeTasks); } List<Task> nullTasks = tempTasks.get(null); if (nullTasks != null) { templisteners.addAll(nullTasks); } for (IRCEventListener listener : templisteners) { try { listener.receiveEvent(event); } catch (Exception e) { System.err.println("com.esri.ges.transport.Irc.jerklib:Cought Client Exception"); e.printStackTrace(); } } templisteners.clear(); tempTasks.clear(); } } /** * Relay write requests to listeners */ void notifyWriteListeners() { List<WriteRequestListener> list = new ArrayList<WriteRequestListener>(); List<WriteRequest> wRequests = new ArrayList<WriteRequest>(); synchronized (requestForWriteListenerEventQueue) { if (requestForWriteListenerEventQueue.isEmpty()) { return; } wRequests.addAll(requestForWriteListenerEventQueue); requestForWriteListenerEventQueue.clear(); } synchronized (writeListeners) { list.addAll(writeListeners); } for (WriteRequestListener listener : list) { for (WriteRequest request : wRequests) { listener.receiveEvent(request); } } } /** * Make COnnections */ void makeConnections() { synchronized (sessionMap) { for (Iterator<Session> it = sessionMap.values().iterator(); it.hasNext();) { Session session = it.next(); State state = session.getState(); if (state == State.NEED_TO_RECONNECT) { session.disconnected(); } if (state == State.DISCONNECTED) { long last = session.getLastRetry(); long current = System.currentTimeMillis(); if (last > 0 && current - last < 10000) { continue; } try { session.retried(); connect(session); } catch (UnresolvedAddressException e) { ErrorEvent error = new UnresolvedHostnameErrorEventImpl(session, e.getMessage(), session.getRequestedConnection().getHostName(), e); addToRelayList(error); session.markForRemoval(); } catch (IOException e) { e.printStackTrace(); session.disconnected(); } } } } } /** * Connect a Session to a server * * @param session * @throws IOException */ void connect(Session session) throws IOException { SocketChannel sChannel = SocketChannel.open(); sChannel.configureBlocking(false); sChannel.connect(new InetSocketAddress(session.getRequestedConnection().getHostName(), session.getRequestedConnection().getPort())); sChannel.register(selector, sChannel.validOps()); Connection con = new Connection(this, sChannel, session); session.setConnection(con); socChanMap.put(sChannel, session); } }