ServerSocial: add simplified logging, refactor variables, add new functionality...
authorEddie <dev@fun2be.me>
Sat, 6 Jun 2015 10:20:00 +0000 (11:20 +0100)
committerEddie <dev@fun2be.me>
Sat, 6 Jun 2015 10:37:40 +0000 (11:37 +0100)
 * simplified logging
 * replace console messages with logging
 * refactor variables
 * introduce setMemberOnlineState() to encapsulate all state-change functionality
 * introduce unicastMemberPresence() to notify over TCP streams
 * remove notifyMemberPresence(), memberOnline(), memberOffline()
 * set member offline when client disappears
 * at login use setMemberOnlineState() to do all required actions
 * add ServiceAddressMap updates from Neigbour multicast announcements (no longer done by NetworkServer*)
 * switch to logging via PacketHandler when LogServer is discovered
 * replace main() with generic app handling used in all other project executables

src/uk/ac/ntu/n0521366/wsyd/server/ServerSocial.java

index 639c482..bbdcdeb 100644 (file)
@@ -25,68 +25,39 @@ package uk.ac.ntu.n0521366.wsyd.server;
 
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
-import java.util.Date;
-import java.util.ArrayList;
-import java.text.SimpleDateFormat;
-import java.text.ParseException;
-import java.util.TreeSet;
-import java.util.SortedMap;
-import java.util.TreeMap;
-import java.util.Map;
-import java.util.Collections;
-import java.util.logging.Logger;
-import java.util.logging.Level;
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.io.IOException;
-import java.io.FileNotFoundException;
+import java.io.*;
 import java.net.InetSocketAddress;
-import java.net.SocketException;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.Set;
-import java.util.logging.LogRecord;
+import java.text.MessageFormat;
+import java.util.*;
+import java.util.logging.*;
 import javax.swing.Timer;
 import uk.ac.ntu.n0521366.wsyd.libs.WSYD_Member;
 import uk.ac.ntu.n0521366.wsyd.libs.WSYD_Member_Comparator_UserID;
-import uk.ac.ntu.n0521366.wsyd.libs.message.MessageLogin;
-import uk.ac.ntu.n0521366.wsyd.libs.message.MessageMember;
-import uk.ac.ntu.n0521366.wsyd.libs.message.MessageMemberState;
-import uk.ac.ntu.n0521366.wsyd.libs.message.MessagePresence;
-import uk.ac.ntu.n0521366.wsyd.libs.message.MessageServerControl;
-import uk.ac.ntu.n0521366.wsyd.libs.net.ConnectionEstablishedEvent;
-import uk.ac.ntu.n0521366.wsyd.libs.net.ConnectionEstablishedEventListener;
-import uk.ac.ntu.n0521366.wsyd.libs.net.Network;
-import uk.ac.ntu.n0521366.wsyd.libs.net.NetworkMessage;
-import uk.ac.ntu.n0521366.wsyd.libs.net.NetworkMessageEvent;
-import uk.ac.ntu.n0521366.wsyd.libs.net.NetworkServerUDPMulticast;
-import uk.ac.ntu.n0521366.wsyd.libs.net.WSYD_SocketAddress;
-import uk.ac.ntu.n0521366.wsyd.libs.net.NetworkMessageEventListener;
-import uk.ac.ntu.n0521366.wsyd.libs.net.NetworkServerTCP;
-import uk.ac.ntu.n0521366.wsyd.libs.net.NetworkServerUDP;
-import uk.ac.ntu.n0521366.wsyd.libs.net.NetworkSocketClosing;
-import uk.ac.ntu.n0521366.wsyd.libs.net.NetworkStream;
-import uk.ac.ntu.n0521366.wsyd.libs.net.NetworkStreamManager;
-import uk.ac.ntu.n0521366.wsyd.libs.net.ServiceAddressMap;
-import uk.ac.ntu.n0521366.wsyd.libs.net.ServiceAddressMap.LastSeenHost;
+import uk.ac.ntu.n0521366.wsyd.libs.logging.PacketHandler;
+import uk.ac.ntu.n0521366.wsyd.libs.message.*;
+import uk.ac.ntu.n0521366.wsyd.libs.net.*;
 
 /**
  * The main Social Network server.
  *
- * Can be restarted or stopped using the class static attributes
- * exitRequested and restartRequested. This can be done by an optional
- * Management GUI application.
+ * Can be restarted or stopped using the class static attributes exitRequested
+ * and restartRequested. This can be done by an optional Management GUI
+ * application.
  *
  * @author Eddie Berrisford-Lynch <dev@fun2be.me>
  */
 public final class ServerSocial implements NetworkMessageEventListener, ConnectionEstablishedEventListener {
+
+    /**
+     * Indicates to start() loop and main() methods to exit completely.
+     */
+    public static boolean exitRequested = false;
+
+    /**
+     * Indicates to start() loop to exit, and to main() to restart the server.
+     */
+    public static boolean restartRequested = true;
+
     /**
      * Persistent storage in file-system when server exits.
      */
@@ -94,43 +65,43 @@ public final class ServerSocial implements NetworkMessageEventListener, Connecti
 
     /**
      * CSV test data file.
-     * 
+     *
      * If it exists in the file system, only used if there is no _membersFile
      */
     static final String _testData = "WSYD_TestData.csv";
-    
+
     /**
      * Readable/displayable name of this application
      */
     final String _title = "ServerSocial";
 
     /**
-     * Network services to address map.
+     * Name of the network log server to look for in multicast neighbour
+     * discovery
      */
-    ServiceAddressMap _serviceToAddressMap;
+    private final String logServiceName = "LogService";
 
     /**
-     * Indicates to start() loop and main() methods to exit completely.
+     * Network services to address map.
      */
-    public static boolean exitRequested = false;
+    ServiceAddressMap _serviceToAddressMap;
 
     /**
-     * Indicates to start() loop to exit, and to main() to restart the server.
+     * Handles display and sending of log messages.
      */
-    public static boolean restartRequested = true;
+    @SuppressWarnings("NonConstantLogger")
+    private Logger LOGGER;
 
     /**
-     * Handles display and sending of log messages.
+     * Address of the Log Server.
      */
-    @SuppressWarnings("NonConstantLogger")
-    private static Logger LOGGER;
+    InetSocketAddress _serverLog;
 
     /**
      * SortedMap wraps a TreeMap that has been made thread-safe by
      * Collections.synchronizedSortedMap() in readMembers().
      *
-     * Long key, the userID
-     * WSYD_Member member record
+     * Long key, the userID WSYD_Member member record
      */
     SortedMap<Long, WSYD_Member> _members;
 
@@ -140,115 +111,200 @@ public final class ServerSocial implements NetworkMessageEventListener, Connecti
     ArrayList<Long> _membersOnline;
 
     /**
-     * 
+     * The multicast background service thread.
      */
-    WSYD_SocketAddress _multicastAdvertiserSA;
-    
+    NetworkServerUDPMulticast _multicastService;
+
     /**
+     * The UDP Control command listening service.
      * 
+     * Receives commands from the ServerManagement client.
      */
-    NetworkServerUDPMulticast _multicastService;
-    
-    Timer _servicesAnnounce;
-    
-    WSYD_SocketAddress _udpControlServiceSA;
-    
     NetworkServerUDP _udpControlService;
-    
-    WSYD_SocketAddress _tcpListeningServiceSA;
-    
+
+    /**
+     * The TCP user client connection listening service.
+     */
     NetworkServerTCP _tcpListeningService;
-    
+
+    /**
+     * Manager of user client and ServerChat TCP streams.
+     */
     NetworkStreamManager _tcpStreamManager;
-   
 
     /**
+     * Whether process should announce itself using multicast.
      * 
+     * Provides neighbour discovery without DNS or manual IP address configuration.
+     */
+    boolean _multicastAnnouncements = false;
+
+    /**
+     * Fires ActionEvents to trigger regular tasks.
+     */
+    Timer _regularTasks;
+
+    /**
+     *
      * Default constructor.
      */
     public ServerSocial() {
-        String[] className = this.getClass().getName().split("\\.");
-        LOGGER = Logger.getLogger(className[className.length - 1]);
-        LOGGER.setLevel(Level.ALL);
-        if (LOGGER.getParent() != null) {
-            LOGGER.getParent().setLevel(Level.ALL);
-            System.out.println("Parent Logger level " + LOGGER.getParent().getLevel().toString());
+        _serverLog = null;
+        LOGGER = Logger.getLogger(_title);
+        // workaround to ensure all levels of log messages are recorded locally before the ServerLog service is available on the network
+        Logger l = LOGGER;
+        while (l != null) {
+            try {
+                l.setLevel(Level.ALL);
+                System.err.println("LOGGER: set level for logger " + l.getName());
+            } catch (SecurityException ex) {
+                System.err.println("LOGGER: cannot set level for logger " + l.getName());
+            }
+            l = l.getParent();
         }
         _serviceToAddressMap = new ServiceAddressMap(_title, LOGGER);
         readMembers(_membersFile);
         _membersOnline = new ArrayList<>();
         _tcpStreamManager = new NetworkStreamManager();
     }
-    
+
+    public ServerSocial(boolean multicastAnnouncements) {
+        this();
+        this._multicastAnnouncements = multicastAnnouncements;
+    }
     /**
-     * Init listener
+     * Custom simplified logging function.
+     * 
+     * Encapsulates the commonly repeated code of Logger method calls
+     * 
+     * @param level the Logger.Level of this message
+     * @param format MessageFormat.format() formatter
+     * @param message zero or more arguments for MessageFormat.format() to process
      */
-    ServerSocial InitListeners()
-    {
-        _multicastAdvertiserSA = new WSYD_SocketAddress(Network.MULTICAST_IP, Network.PORTS_MULTICAST_DISCOVERY, WSYD_SocketAddress.Protocol.UDP);
+    protected void logp(Level level, String format, Object... message) {
+        if (LOGGER != null)
+            LOGGER.logp(level, _title, null, MessageFormat.format(format, (Object[]) message));
+    }
+
+    /**
+     * Initialise the background service listener threads.
+     */
+    ServerSocial initListeners() {
+        WSYD_SocketAddress _multicastAdvertiserSA = new WSYD_SocketAddress(Network.MULTICAST_IPv4, Network.PORTS_MULTICAST_DISCOVERY, WSYD_SocketAddress.Protocol.UDP);
         _multicastService = new NetworkServerUDPMulticast(_multicastAdvertiserSA, _title + "MC", _serviceToAddressMap, LOGGER);
         _multicastService.getEventManager().addNetworkMessageEventListener(this, "Neighbour");
         _multicastService.execute();
-        _serviceToAddressMap.put("all", new LastSeenHost(new InetSocketAddress(Network.MULTICAST_IP, Network.PORTS_MULTICAST_DISCOVERY), LastSeenHost.STATE.STATIC));
-        
-        _udpControlServiceSA = new WSYD_SocketAddress(Network.IPv4_WILDCARD, Network.PORTS_EPHEMERAL, WSYD_SocketAddress.Protocol.UDP);
+        _serviceToAddressMap.put("all", new ServiceAddressMap.LastSeenHost(new InetSocketAddress(Network.MULTICAST_IPv4, Network.PORTS_MULTICAST_DISCOVERY), ServiceAddressMap.LastSeenHost.STATE.STATIC));
+
+        WSYD_SocketAddress _udpControlServiceSA = new WSYD_SocketAddress(Network.IPv4_WILDCARD, Network.PORTS_SERVER_CONTROL, WSYD_SocketAddress.Protocol.UDP);
         _udpControlService = new NetworkServerUDP(_udpControlServiceSA, _title + "Control", _serviceToAddressMap, LOGGER);
         _udpControlService.getEventManager().addNetworkMessageEventListener(this, "Control");
         _udpControlService.execute();
-        
-        _tcpListeningServiceSA = new WSYD_SocketAddress(Network.IPv4_WILDCARD, Network.PORTS_SERVER_SOCIAL, WSYD_SocketAddress.Protocol.TCP);
+
+        WSYD_SocketAddress _tcpListeningServiceSA = new WSYD_SocketAddress(Network.IPv4_WILDCARD, Network.PORTS_SERVER_SOCIAL, WSYD_SocketAddress.Protocol.TCP);
         _tcpListeningService = new NetworkServerTCP(_tcpListeningServiceSA, _title + "Listener", _serviceToAddressMap, _tcpStreamManager, LOGGER);
         _tcpListeningService.addConnectionEstablisedEventListener(this);
         _tcpListeningService.execute();
-        
-        ActionListener servicesAnnounceActionListener = new ActionListener() {
+
+        ActionListener regularTasksActionListener = new ActionListener() {
+            // provide a way to read the current version of the parent process object's fields
+            ServerSocial owner;
+
+            // XXX: Anonymous class initialisation - effectively used as the body of the object's constructor
+            {
+                // XXX: access to the outer class's instance object
+                owner = ServerSocial.this;
+            }
+
             /**
              * Activated by timer events to send multi-cast neighbour announcements and other regular notifications.
-             * @param e 
+             *
+             * @param e
              */
             @Override
             public void actionPerformed(ActionEvent e) {
-
-                // Announce the Social Server Neighbour service
-                MessagePresence mp = new MessagePresence(_title, _multicastService.getSocketAddress());
-                NetworkMessage nm = NetworkMessage.createNetworkMessage("Neighbour", "all", mp);
-                nm.setSender(_title + "MC");
-                _multicastService.queueMessage(nm);
-
-                // Notify ServerManagement of the Social Server Control service
-                String target = "ServerManagementControl";
-                LastSeenHost targetHost = _serviceToAddressMap.get(target);
-                if (targetHost != null) {
+                // check the current value ofthe parent process' flag
+                if (owner._multicastAnnouncements) {
+                    // Announce the Social Server Neighbour service
+                    MessagePresence mp = new MessagePresence(_title + "MC", _multicastService.getSocketAddress());
+                    NetworkMessage nm = NetworkMessage.createNetworkMessage("Neighbour", "all", mp);
+                    _multicastService.queueMessage(nm);
+                    // Announce the Social Server Control service
                     mp = new MessagePresence(_title + "Control", _udpControlService.getSocketAddress());
-                    nm = NetworkMessage.createNetworkMessage("Control", target, mp);
-                    nm.setSender(_title + "Control");
-                    try {
-                        _udpControlService.queueMessage(nm);
-                        LOGGER.log(Level.INFO, "Control notification sent to ServerManagement");
-                    } catch (IllegalArgumentException ex) {
-                        // Not fatal - ServerManagement may not be currently known
-                    }
+                    nm = NetworkMessage.createNetworkMessage("Neighbour", "all", mp);
+                    _multicastService.queueMessage(nm);
                 }
-                
+
                 // clean up the known hosts map
                 ArrayList<String> servicesRemoved = _serviceToAddressMap.cleanServiceAddressMap(5000);
-                for (String service: servicesRemoved) {
-                    // FIXME: does the process care if hosts have been removed? if not, remove this array iteration
-                    // TODO: If user client gone, remove from _membersOnline
-                    switch (service) {
+                for (String service : servicesRemoved) {
+                    // If user client gone remove from online list
+                    if (service != null && service.startsWith("UserID_")) {
+                        String[] parts = service.split("_");
+                        try {
+                            long userID = Long.parseLong(parts[1]);
+                            setMemberOnlineState(userID, WSYD_Member.STATE.OFFLINE);
+                            logp(Level.INFO, "servicesRemoved: {0} disappeared so userID {1} made offline", service, userID);
+                        } catch (NumberFormatException ex) {
+                            logp(Level.WARNING, "servicesRemoved: Failed to parse userID from {0}.spit(\"_\")[1]", service);
+                        }
                     }
                 }
-
             }
         };
-        
-        _servicesAnnounce = new Timer(1000, servicesAnnounceActionListener);
-        _servicesAnnounce.setInitialDelay(100);
-        _servicesAnnounce.start();
-        
+
+        _regularTasks = new Timer(1000, regularTasksActionListener);
+        _regularTasks.setInitialDelay(100);
+        _regularTasks.start();
+
         return this;
     }
-    
+
+    /**
+     * Update internal user online status and then notify all affected connections.
+     * 
+     * @param userID the user
+     * @param state the new state
+     */
+    void setMemberOnlineState(long userID, WSYD_Member.STATE state) {
+        boolean notify = false;
+        String userName = null;
+        if (state == WSYD_Member.STATE.ONLINE)
+            if (!_membersOnline.contains(userID)) {
+                _membersOnline.add(userID);
+                userName = this._members.get(userID)._userName;
+                notify = true;
+            }
+        else
+            if (_membersOnline.contains(userID)) {
+                _membersOnline.remove(userID);
+                userName = this._members.get(userID)._userName;
+                notify = true;
+            }
+        
+        if (notify) {
+            logp(Level.INFO, "User \"{0}\" (ID {1}) now {2}",userName, userID, state.toString());
+            unicastMemberPresence(userID, state);
+        }
+    }
+
+    /**
+     * Send a MemberState message to each (TCP) stream connection.
+     * @param userID the user
+     * @param state the new online state
+     */
+    private void unicastMemberPresence(long userID, WSYD_Member.STATE state) {
+        NetworkMessage message = new NetworkMessage(MessageMemberState.getType(), null, new MessageMemberState(userID, state));
+        for (long id : _membersOnline)
+            if (id != userID) {
+                NetworkStream ns = _tcpStreamManager._tcpStreams.get(id);
+                if (ns != null) {
+                    ns.write(message);
+                    logp(Level.FINEST, "User ID {0} state notifed to {1}", userID, ns.getRemoteAddress().toString());
+                }
+            }
+    }
+
     /**
      * Main execution loop of the server
      *
@@ -259,21 +315,21 @@ public final class ServerSocial implements NetworkMessageEventListener, Connecti
     public boolean start() throws InterruptedException {
         boolean result;
 
-        // TODO: start() create TCP listener
-        // TODO: start() create UDP Multicast group listener and broadcast adverts
         // wait for connections
         int loopCount = 200;
-        while (!ServerSocial.exitRequested && ! ServerSocial.restartRequested) {
+        while (!ServerSocial.exitRequested && !ServerSocial.restartRequested) {
             Thread.sleep(1000); // wait a second
-            System.out.println("start() loop " + loopCount);
-            if (loopCount-- == 0)
+            logp(Level.INFO, "start() countdown {0}", loopCount);
+            if (loopCount-- == 0) {
                 ServerSocial.exitRequested = true;
+            }
         }
 
-        _servicesAnnounce.stop();
+        _regularTasks.stop();
         _multicastService.cancel(true);
         _udpControlService.cancel(true);
-        
+        _tcpListeningService.cancel(true);
+
         result = writeMembers(_membersFile);
 
         return result;
@@ -290,18 +346,17 @@ public final class ServerSocial implements NetworkMessageEventListener, Connecti
         boolean result = false;
 
         try (
-            FileInputStream f = new FileInputStream(fileName);
-            ObjectInputStream in  = new ObjectInputStream(f);
-            )
-        {
-            if (_members == null)
-                /* XXX: do not pass a Comparator to the constructor if collection is being deserialized as one was already saved during serialization.
-                 *      If the Comparator is passed to the constructor the serialized object will 'grow' by ~17 bytes each time as multi Comparator
-                 *      objects are written each time the collection is serialized.
-                */
-                _members = Collections.synchronizedSortedMap( new TreeMap<Long, WSYD_Member>() );
-            if (!_members.isEmpty())
+                FileInputStream f = new FileInputStream(fileName);
+                ObjectInputStream in = new ObjectInputStream(f);) {
+            if (_members == null) /* XXX: do not pass a Comparator to the constructor if collection is being deserialized as one was already saved during serialization.
+             *      If the Comparator is passed to the constructor the serialized object will 'grow' by ~17 bytes each time as multi Comparator
+             *      objects are written each time the collection is serialized.
+             */ {
+                _members = Collections.synchronizedSortedMap(new TreeMap<Long, WSYD_Member>());
+            }
+            if (!_members.isEmpty()) {
                 _members.clear();
+            }
             /* Need explicit cast to SortedMap for Object type returned by readObject()
              * but this can cause an "unchecked cast" compiler warning since the compiler
              * cannot be sure the Object returned from readObject() is really a
@@ -311,32 +366,29 @@ public final class ServerSocial implements NetworkMessageEventListener, Connecti
              */
             @SuppressWarnings("unchecked")
             SortedMap<Long, WSYD_Member> temp = (SortedMap<Long, WSYD_Member>) in.readObject();
-            _members = Collections.synchronizedSortedMap( temp );
+            _members = Collections.synchronizedSortedMap(temp);
             for (Map.Entry<Long, WSYD_Member> e : _members.entrySet()) {
                 System.out.println(e.getKey() + ": " + e.getValue().toString());
             }
-            LOGGER.log(Level.INFO, "Members database read from {0}", fileName);
+            logp(Level.INFO, "Members database read from {0}", fileName);
             result = true;
-        }
-        catch(FileNotFoundException e) {
-            _members = Collections.synchronizedSortedMap( new TreeMap<Long, WSYD_Member>( new WSYD_Member_Comparator_UserID() ) );
-            LOGGER.log(Level.INFO, "Starting new members database: no database file found ({0})", fileName);
+        } catch (FileNotFoundException e) {
+            _members = Collections.synchronizedSortedMap(new TreeMap<Long, WSYD_Member>(new WSYD_Member_Comparator_UserID()));
+            logp(Level.INFO, "Starting new members database: no database file found ({0})", fileName);
             result = true;
 
             // if test data CSV exists import it
             File csv = new File(_testData);
             if (csv.exists() && csv.isFile()) {
-                LOGGER.log(Level.INFO, "Importing test data from {0}", _testData);
+                logp(Level.INFO, "Importing test data from {0}", _testData);
                 importCSV(_testData);
             }
 
-        }
-        catch(IOException e) {
-            LOGGER.log(Level.SEVERE, "Unable to read database file {0}", fileName);
+        } catch (IOException e) {
+            logp(Level.SEVERE, "Unable to read database file {0}", fileName);
             e.printStackTrace();
-        }
-        catch(ClassNotFoundException e) {
-            LOGGER.log(Level.SEVERE, "Unable to deserialize database file {0}", fileName);
+        } catch (ClassNotFoundException e) {
+            logp(Level.SEVERE, "Unable to deserialize database file {0}", fileName);
             e.printStackTrace();
         }
 
@@ -361,24 +413,23 @@ public final class ServerSocial implements NetworkMessageEventListener, Connecti
             {
                 out.writeObject(_members);
 
-                LOGGER.log(Level.INFO, "Members database written to {0}", fileName);
+                logp(Level.INFO, "Members database written to {0}", fileName);
                 result = true;
-            }
-            catch(IOException e) {
-                LOGGER.log(Level.SEVERE, "Unable to write database file {0}", fileName);
+            } catch (IOException e) {
+                logp(Level.SEVERE, "Unable to write database file {0}", fileName);
                 e.printStackTrace();
             }
-        }
-        else
+        } else {
             result = true;
+        }
 
         return result;
     }
 
     /**
-     * Read a CSV file containing WSYD_Member records and add it to the in-memory
-     * collection.
-     * 
+     * Read a CSV file containing WSYD_Member records and add it to the
+     * in-memory collection.
+     *
      * @param fileName name of CSV file
      * @return true if successfully imported
      */
@@ -394,19 +445,19 @@ public final class ServerSocial implements NetworkMessageEventListener, Connecti
         {
             String line;
             while ((line = br.readLine()) != null) {
-                LOGGER.log(Level.FINEST, line);
+                logp(Level.FINEST, line);
                 try {
                     WSYD_Member temp = WSYD_Member.createWSYD_Member(line);
-                    if (temp != null)
+                    if (temp != null) {
                         _members.put(temp._userID, temp); // add new member to collection
-                } catch (IllegalArgumentException e) {     
-                    LOGGER.log(Level.WARNING, "Ignoring bad CSV import line");
+                    }
+                } catch (IllegalArgumentException e) {
+                    logp(Level.WARNING, "Ignoring bad CSV import line");
                 }
             }
-        }
-        catch(IOException e) {
-                LOGGER.log(Level.SEVERE, "Unable to import CSV file {0}", fileName);
-                e.printStackTrace();
+        } catch (IOException e) {
+            logp(Level.SEVERE, "Unable to import CSV file {0}", fileName);
+            e.printStackTrace();
         }
 
         return result;
@@ -414,7 +465,7 @@ public final class ServerSocial implements NetworkMessageEventListener, Connecti
 
     /**
      * Export WSYD_Members collection to a CSV file.
-     * 
+     *
      * @param fileName name of the CSV file to write
      * @return true if successful
      */
@@ -430,144 +481,191 @@ public final class ServerSocial implements NetworkMessageEventListener, Connecti
         {
             bw.write("# 0     , 1       , 2       , 3              , 4  , 5        , 6        , 7      , 8                  , 9");
             bw.write("# userID, userName, password, currentLocation, bio, birthDate, interests, friends, friendsRequestsSent, friendsRequestsReceived");
-            for (Map.Entry<Long, WSYD_Member> e: _members.entrySet()) {
+            for (Map.Entry<Long, WSYD_Member> e : _members.entrySet()) {
                 bw.write(e.getKey() + ": " + e.getValue().toString());
             }
-        }
-        catch(IOException e) {
+        } catch (IOException e) {
             LOGGER.log(Level.SEVERE, "Unable to export to CSV file {0}", fileName);
             e.printStackTrace();
         }
 
         return result;
     }
-    
-    private void notifyMemberPrescence(long userID, boolean state) {
-        NetworkMessage message = new NetworkMessage("MemberNotification", null, new MessageMemberState(userID, state));
-        for (long ID : _membersOnline) {
-            _tcpStreamManager._tcpStreams.get(ID).write(message);
-        }
-    }
-    
-    private void memberOnline (long userID) {
-        if (!_membersOnline.contains(userID)) {
-            notifyMemberPrescence(userID, true);
-            _membersOnline.add(userID);
-        }
-    }
-    
-    private void memberOffline (long userID) {
-        if (_membersOnline.contains(userID)) {
-            _membersOnline.remove(userID);
-            notifyMemberPrescence(userID, false);
-        }
-    }
 
     @Override
     public void connectionEstablished(ConnectionEstablishedEvent event) {
-        System.err.println("connectionEstablished()");
+        logp(Level.FINER, "Received ConnectionEstablishedEvent for ", event.getStream().getRemoteAddress().toString());
         event.getStream().getEventManager().addNetworkMessageEventListener(this);
     }
+
     /**
      * Process received network messages.
-     * 
+     *
      * @param event the network message event
      */
     @Override
-    public void NetworkMessageReceived(NetworkMessageEvent event)
-    {
+    public void NetworkMessageReceived(NetworkMessageEvent event) {
         NetworkMessage nm = event.getNetworkMessage();
-        if (nm == null)
+        if (nm == null) {
             return;
-        System.err.println("NetworkMessage received for intent " + nm.getIntent());
+        }
         String type = nm.getMessage().getMessageType();
         switch (nm.getIntent()) {
+            case "Neighbour":
+                if (type.equals(MessagePresence.getType())) { // Presence
+                    MessagePresence mp = (MessagePresence) nm.getMessage();
+                    // add or update the last-seen time of the Sender host in the known services map
+                    ServiceAddressMap.LastSeenHost host = new ServiceAddressMap.LastSeenHost(new InetSocketAddress(nm.getReceivedFrom().getAddress(), mp.socketAddress.getPort()));
+                    this._serviceToAddressMap.put(mp.serviceName, host);
+                    logp(Level.FINEST, "Added \"{0}\" ({1}) to service map", mp.serviceName, host.address.toString());
+                    switch (mp.serviceName) {
+                        case "ServerChat":
+                            // TODO: NetworkMessageReceived(Neighbour): prepare for keeping ServerChat in sync
+                            break;
+                        case logServiceName:
+                            if (this._serverLog == null) {
+                                logp(Level.CONFIG, "Switching to network logger");
+                                this._serverLog = new InetSocketAddress(this._serviceToAddressMap.getServiceAddress(logServiceName).getAddress(), mp.socketAddress.getPort() );
+                                LOGGER.addHandler(new PacketHandler(this._serverLog, null, null, _title)); // send messages to the ServerManagement client
+                                LOGGER.setUseParentHandlers(false); // don't send messages locally now
+                                logp(Level.CONFIG, "Switched to network logger");
+                            }
+                            break;
+                    }
+                }
+                break;
+
             case "Control": // Exit or Restart?
                 if (type.equals(MessageServerControl.getType())) { // ServerControl
                     if ("ServerManagement".equals(nm.getSender())) {
-                        MessageServerControl mp = (MessageServerControl)nm.getMessage();
-                        if (mp.exitReq == MessageServerControl.EXIT.YES) ServerSocial.exitRequested = true;
-                        if (mp.restartReq == MessageServerControl.RESTART.YES) ServerSocial.restartRequested = true;
+                        MessageServerControl mp = (MessageServerControl) nm.getMessage();
+                        logp(Level.SEVERE, "Control request from {0} for {1}", nm.getSender(), mp.toString());
+                        if (mp.exitReq == MessageServerControl.EXIT.YES) {
+                            ServerSocial.exitRequested = true;
+                        }
+                        if (mp.restartReq == MessageServerControl.RESTART.YES) {
+                            ServerSocial.restartRequested = true;
+                        }
                     }
                 }
                 break;
             case "Login":
                 if (type.equals(MessageLogin.getType())) {
                     NetworkStream ns = _tcpStreamManager._tcpStreams.get(nm.getKey());
-                    MessageLogin ml = (MessageLogin)nm.getMessage();
-                    
+                    MessageLogin ml = (MessageLogin) nm.getMessage();
+
                     Set<Map.Entry<Long, WSYD_Member>> tempSet = _members.entrySet();
                     Iterator<Map.Entry<Long, WSYD_Member>> tempIter = tempSet.iterator();
+                    boolean userKnown = false;
                     while (tempIter.hasNext()) {
                         Map.Entry<Long, WSYD_Member> element = tempIter.next();
-                        if (element.getValue()._userName.equals(ml._uName) && element.getValue()._password.equals(ml._uPass)) {
-                            ml._userID = element.getKey();
-                            ml._loggedIn = true;
-                            _tcpStreamManager.updateKey(nm.getKey(), element.getKey()); // replace temp key in stream manager
-                            _membersOnline.add(element.getKey()); // make the member online
-                            break;
+                        if (element.getValue()._userName.equals(ml._userName)) {
+                            if (element.getValue()._password.equals(ml._userPassword)) {
+                                ml._userID = element.getKey();
+                                ml._loggedIn = true;
+                                _tcpStreamManager.updateKey(nm.getKey(), element.getKey()); // replace temporary key
+                                setMemberOnlineState(element.getKey(), WSYD_Member.STATE.ONLINE);
+                                userKnown = true;
+                                logp(Level.INFO, "Login UserID {0} ({1}) successful", ml._userID, element.getValue()._userName);
+
+                            } else {
+                                userKnown = true;
+                                logp(Level.WARNING, "Login UserID {0} ({1}) failed", ml._userID, ml._userID, element.getValue()._userName);
+                            }
+                            break;                                
                         }
                     }
-                    if (ns != null)
+                    if (!userKnown)
+                        logp(Level.WARNING, "Login attempt by unknown user {0} from {1}", ml._userName, nm.getReceivedFrom().toString());
+                    if (ns != null) {
                         ns.write(nm);
-                    else
-                        System.err.println("Login: cannot find stream for ID:" + nm.getKey());
+                    } else {
+                        logp(Level.SEVERE, "Login: cannot find stream for ID:", nm.getKey());
+                    }
                 }
+                break;
+
             case "Register":
                 if (type.equals(MessageMember.getType())) {
                     NetworkStream ns = _tcpStreamManager._tcpStreams.get(nm.getKey());
-                    MessageMember mm = (MessageMember)nm.getMessage();
+                    MessageMember mm = (MessageMember) nm.getMessage();
                     // assume username can be registered unless username found
-                    mm.setRegistered(MessageMember.STATE.REGISTERED);
-                    
+                    mm.setRegistered(MessageMember.STATE.UNREGISTERED);
+
                     Set<Map.Entry<Long, WSYD_Member>> tempSet = _members.entrySet();
                     Iterator<Map.Entry<Long, WSYD_Member>> tempIter = tempSet.iterator();
                     while (tempIter.hasNext()) {
                         Map.Entry<Long, WSYD_Member> element = tempIter.next();
                         if (element.getValue()._userName.equals(mm.getMember()._userName)) {
-                            mm.setRegistered(MessageMember.STATE.UNREGISTERED);
+                            mm.setRegistered(MessageMember.STATE.REGISTERED);
                             mm.setStatus("Member already registered: " + mm.getMember()._userName);
+                            logp(Level.WARNING, "Registration attempt for existing member {0}", element.getValue()._userName);
                             break;
                         }
                     }
                     // register the new member
-                    if (mm.getRegistered() == MessageMember.STATE.REGISTERED) {
+                    if (mm.getRegistered() == MessageMember.STATE.UNREGISTERED) {
                         // get the next unallocated user ID
-                        long newUserID = _members.lastKey().longValue() + 1;
+                        long newUserID = _members.lastKey() + 1;
                         _members.put(newUserID, mm.getMember());
                         // update the object so the ID can be returned to the client
                         mm.getMember()._userID = newUserID;
+                        logp(Level.INFO, "Added new member {0} ({1})", newUserID, mm.getMember()._userName);
                         // update the on-disk membership data
                         writeMembers(_membersFile);
                     }
-                    if (ns != null)
+                    if (ns != null) {
                         ns.write(nm);
-                    else
-                        System.err.println("Login: cannot find stream for ID:" + nm.getKey());
-                    
+                    } else {
+                        logp(Level.WARNING, "Login: cannot find stream for ID: ", nm.getKey());
+                    }
+
                 }
                 break;
-            default:
-                System.err.println("Unhandled NetworkMessage received for intent " + nm.getIntent());
+            default: // log all unhandled messages
+                logp(Level.WARNING, "Unhandled NetworkMessage received with intent: \"{0}\" sender: \"{1}\" target: \"{2}\"", nm.getIntent(), nm.getSender(), nm.getTarget());
         }
-        
+
     }
-    
 
     /**
      * Entry point which starts, restarts, and exits the application.
-     * 
+     *
      * @param args the command line arguments
-     * @throws java.lang.InterruptedException
      */
-    public static void main(String[] args) throws InterruptedException {
-        while (!ServerSocial.exitRequested && ServerSocial.restartRequested) {
-            ServerSocial app = new ServerSocial().InitListeners();
-            ServerSocial.restartRequested = false;
-            if (!app.start()) {
-                System.err.println("Encountered error running Social Server");
-                break; // leave the while loop
+    public static void main(String[] args) {
+        class App implements Runnable {
+            public boolean multicastAnnouncements = true;
+
+            @Override
+            public void run() {
+                while (!ServerSocial.exitRequested && ServerSocial.restartRequested) {
+                    ServerSocial app = new ServerSocial(multicastAnnouncements).initListeners();
+                    ServerSocial.restartRequested = false;
+                    try {
+                        if (!app.start()) {
+                            Logger.getLogger(ServerSocial.class.getName()).log(Level.SEVERE, null, "Encountered error running Social Server");
+                            break; // leave the while loop
+                        }
+                    } catch (InterruptedException ex) {
+                        Logger.getLogger(ServerSocial.class.getName()).log(Level.SEVERE, null, ex);
+                    }
+                }
+            }
+        }
+        App app = new App();
+
+        // process command line arguments
+        for (String arg : args) {
+            switch (arg) {
+                case "-announce":
+                    app.multicastAnnouncements = true;
+                    break;
+                case "-noannounce":
+                    app.multicastAnnouncements = false;
+                    break;
             }
         }
+        app.run();
     }
 }