ClientGUI: user import wildcards, add ProgressMonitor, simplified logging, add new...
authorEddie <dev@fun2be.me>
Sat, 6 Jun 2015 10:45:41 +0000 (11:45 +0100)
committerEddie <dev@fun2be.me>
Sat, 6 Jun 2015 10:57:45 +0000 (11:57 +0100)
 * use import x.y.z.* wildcards to reduce clutter in header
 * use project-standardised simplified logp()
 * refactor some method names to be more descriptive e.g. loginDialog() -> doLogin()
 * add exitCleanly() to shutdown background threads
 * add setServiceTitles() to update network service Sender fields after user login
 * add ServiceAddressMap updates from Neigbour multicast announcements (no longer done by NetworkServer*)
 * Login intent: add self to members online
 * add Logout intent handling
 * replace main() with generic app handling used in all other project executables

src/uk/ac/ntu/n0521366/wsyd/client/ClientGUI.java

index 8fe4601..a9e3d1f 100644 (file)
@@ -25,75 +25,128 @@ package uk.ac.ntu.n0521366.wsyd.client;
 
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
+import java.awt.event.WindowEvent;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
 import java.io.IOException;
+import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.Socket;
+import java.net.UnknownHostException;
 import java.text.MessageFormat;
-import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.SortedMap;
-import javax.swing.JDialog;
-import javax.swing.JFrame;
-import javax.swing.JTextField;
-import java.util.TreeSet;
+import java.util.concurrent.ExecutionException;
 import java.util.logging.Level;
 import java.util.logging.Logger;
-import javax.swing.JProgressBar;
+import javax.swing.JOptionPane;
 import javax.swing.ProgressMonitor;
+import javax.swing.SwingWorker;
 import javax.swing.Timer;
 import uk.ac.ntu.n0521366.wsyd.libs.WSYD_Member;
-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.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.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.NetworkServerUDPMulticast;
-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.WSYD_SocketAddress;
+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 user client application. Needs ServerSocial and ServerChat for full functionality.
+ *
+ * @see uk.ac.ntu.n0521366.wsyd.server.ServerSocial
+ * @see uk.ac.ntu.n0521366.wsyd.server.ServerChat
+ *
  *
  * @author Eddie Berrisford-Lynch
  */
-public class ClientGUI extends javax.swing.JFrame implements NetworkMessageEventListener {
-    
-    private boolean loginSuccessful = false;
-    
-    private boolean registrationSuccessful = false;
-    
-    private final Login loginDialog = new Login(this, true);
-    private final EditProfile registrationDialog = new EditProfile(this, true);
-    
+public class ClientGUI extends javax.swing.JFrame implements NetworkMessageEventListener, PropertyChangeListener {
+
+    /**
+     * Track status of login.
+     */
+    private boolean _loggedIn = false;
+
+    /**
+     * Background task controlling a ProgressMonitor.
+     *
+     * Log-in dialog is not shown until the Social Server is available.
+     */
+    class WaitForServerSocial extends javax.swing.SwingWorker<Boolean, Void> {
+
+        /**
+         * Waits up to 30 seconds for the Social Server to appear.
+         *
+         * @return true if the server was found before the time expires
+         * @throws Exception
+         */
+        @Override
+        @SuppressWarnings("SleepWhileInLoop")
+        protected Boolean doInBackground() throws Exception {
+            boolean result = false;
+
+            int duration = _progressMonitor.getMaximum(); // milliseconds
+            int countdown = duration;
+            int pause = 500;
+            while (countdown > 0 && !result && !isCancelled()) {
+                result = _serviceToAddressMap.containsService("ServerSocialMC");
+                this.setProgress((duration - countdown) / 1000);
+                Thread.sleep(pause);
+                countdown -= pause;
+            }
+            return result;
+        }
+    }
+
+    private Login loginDialog;
+    private EditProfile registrationDialog;
+    private ProgressMonitor _progressMonitor;
+    WaitForServerSocial _waitForServerSocial;
+
     /**
      * Member record for this client's user.
      */
     private WSYD_Member myMember;
-    
-    Socket socialSocket;
-    
+
+    /**
+     * The address of the social server.
+     */
+    Socket socketSocial;
+
+    /**
+     * The address of the chat server.
+     */
+    Socket socketChat;
+
+    /**
+     * Title used before member has authenticated to the server.
+     */
+    static final String DEFAULT_TITLE = "ClientGUI";
+
     /**
      * Readable/displayable name of this application
-     * 
-     * TODO: title should be assigned userID when SocialServer connection established
      */
-    private  String _title = null;
-    
+    private String _title;
+
+    /**
+     * Name of the network log server to look for in multicast neighbour
+     * discovery
+     */
+    private final String logServiceName = "LogService";
+
+    /**
+     * Address of the Log Server.
+     */
+    InetSocketAddress _serverLog;
+
     /**
      * Network services to address map.
      */
     ServiceAddressMap _serviceToAddressMap;
-    
+
     /**
      * Handles display and sending of log messages.
      */
     @SuppressWarnings("NonConstantLogger")
-    private static Logger LOGGER;
-    
+    private Logger LOGGER;
+
     /**
      * SortedMap wraps a TreeMap that has been made thread-safe by
      * Collections.synchronizedSortedMap() in readMembers().
@@ -102,35 +155,87 @@ public class ClientGUI extends javax.swing.JFrame implements NetworkMessageEvent
      * WSYD_Member member record
      */
     SortedMap<Long, WSYD_Member> _members;
-    
+
     /**
      * userIDs of members currently logged in
      */
     ArrayList<Long> _membersOnline;
-    
+
+    /**
+     * Regular presence announcements
+     */
     Timer _regularTasks;
-    
-    WSYD_SocketAddress _multicastAdvertiserSA;
-    
-    NetworkServerUDPMulticast _multicastService;
-    
-    WSYD_SocketAddress _udpNotificationServiceSA;
-    
-    NetworkServerUDP _udpNotificationService;
-    
-    WSYD_SocketAddress _tcpChatServiceSA;
-    
-    NetworkServerTCP _tcpChatService;
-    
-    NetworkStreamManager _tcpStreamManager;
-    
+
+     /**
+     * Whether process should announce itself using multicast.
+     *
+     * Provides neighbour discovery without DNS or manual IP address configuration.
+     */
+    boolean _multicastAnnouncements = false;
+
     /**
-     * Creates new form ClientGUI
+     * Multi-cast neighbour advertise and discover service in a SwingWorker thread.
+     */
+    NetworkServerUDPMulticast _multicastService = null;
+
+    /**
+     * Chat request service listener.
+     */
+    NetworkServerTCP _tcpChatService = null;
+
+    /**
+     * Manager of ServerSocial and ServerChat TCP streams.
+     */
+    NetworkStreamManager _tcpStreamManager = null;
+
+    /**
+     * Default constructor. Sets Logger level and initialises GUI and TCP stream manager.
      */
     public ClientGUI() {
+        _title = DEFAULT_TITLE;
+        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();
+        }
+
         initComponents();
         _serviceToAddressMap = new ServiceAddressMap(_title, LOGGER);
         _tcpStreamManager = new NetworkStreamManager();
+        _membersOnline = new ArrayList<>();
+   }
+
+    /**
+     * Creates new form ClientGUI
+     * @param multicastAnnouncements true if application should advertise itself using multicast discovery
+     * @param serverSocial optional manually set IP address for Server Social (if multicast discovery isn't working)
+     */
+    public ClientGUI(boolean multicastAnnouncements, InetAddress serverSocial) {
+        this();
+        this._multicastAnnouncements = multicastAnnouncements;
+        if (serverSocial != null)
+            _serviceToAddressMap.put("ServerSocial", new ServiceAddressMap.LastSeenHost(new InetSocketAddress(serverSocial, Network.PORTS_SERVER_SOCIAL), ServiceAddressMap.LastSeenHost.STATE.STATIC));
+    }
+
+    /**
+     * 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
+     */
+    protected void logp(Level level, String format, Object... message) {
+        if (LOGGER != null)
+            LOGGER.logp(level, _title, null, MessageFormat.format(format, (Object[]) message));
     }
 
     /**
@@ -395,35 +500,40 @@ public class ClientGUI extends javax.swing.JFrame implements NetworkMessageEvent
         // TODO add your handling code here:
     }//GEN-LAST:event_formWindowActivated
 
-    private ClientGUI initNeighbourListener() {
-        _multicastAdvertiserSA = new WSYD_SocketAddress(Network.MULTICAST_IP, Network.PORTS_MULTICAST_DISCOVERY, WSYD_SocketAddress.Protocol.UDP);
+    /**
+     * Start the multicast neighbour discovery service in order to discover Social Server as soon as possible.
+     *
+     * @return this object
+     * @throws UnknownHostException
+     */
+    private ClientGUI initNeighbourListener() throws UnknownHostException {
+        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 ServiceAddressMap.LastSeenHost(new InetSocketAddress(Network.MULTICAST_IP, Network.PORTS_MULTICAST_DISCOVERY), ServiceAddressMap.LastSeenHost.STATE.STATIC));
-               
+        _serviceToAddressMap.put("all", new ServiceAddressMap.LastSeenHost(new InetSocketAddress(Network.MULTICAST_IPv4, Network.PORTS_MULTICAST_DISCOVERY), ServiceAddressMap.LastSeenHost.STATE.STATIC));
+
         return this;
     }
-    
+
+    /**
+     * Start the remaining services and listeners.
+     * @throws UnknownHostException
+     */
     private void initListeners() {
-        
-        _udpNotificationServiceSA = new WSYD_SocketAddress(Network.IPv4_WILDCARD, Network.PORTS_EPHEMERAL, WSYD_SocketAddress.Protocol.UDP);
-        _udpNotificationService = new NetworkServerUDP(_udpNotificationServiceSA, _title + "_Notifier", _serviceToAddressMap, LOGGER);
-        _udpNotificationService.getEventManager().addNetworkMessageEventListener(this, "Notification");
-        _udpNotificationService.execute();
-        
-        _tcpChatServiceSA = new WSYD_SocketAddress(Network.IPv4_WILDCARD, Network.PORTS_EPHEMERAL, WSYD_SocketAddress.Protocol.TCP);
+
+        WSYD_SocketAddress _tcpChatServiceSA = new WSYD_SocketAddress(Network.IPv4_WILDCARD, Network.PORTS_EPHEMERAL, WSYD_SocketAddress.Protocol.TCP);
         _tcpChatService = new NetworkServerTCP(_tcpChatServiceSA, _title + "_Chat", _serviceToAddressMap, _tcpStreamManager, LOGGER);
         _tcpChatService.getEventManager().addNetworkMessageEventListener(this, "ChatRequest");
         _tcpChatService.execute();
-        
+
         ActionListener servicesAnnounceActionListener = new ActionListener() {
             /**
              * Perform regular house-keeping tasks.
-             * @param e 
+             * @param e
              */
             @Override
-            public void actionPerformed(ActionEvent e) {   
+            public void actionPerformed(ActionEvent e) {
                 // clean up the known hosts map
                 ArrayList<String> servicesRemoved = _serviceToAddressMap.cleanServiceAddressMap(5000);
                 for (String service: servicesRemoved) {
@@ -431,90 +541,146 @@ public class ClientGUI extends javax.swing.JFrame implements NetworkMessageEvent
                     switch (service) {
                     }
                 }
-                
-                // TODO: PING server
+
+                NetworkStream serverSocial = _tcpStreamManager._tcpStreams.get(NetworkStreamManager.SERVERSOCIAL);
+                if (serverSocial != null) {
+                    // TODO: PING server
+                }
 
             }
         };
-        
+
         _regularTasks = new Timer(1000, servicesAnnounceActionListener);
         _regularTasks.setInitialDelay(100);
         _regularTasks.start();
-        
     }
-    
-    private void connectionDialog() {
-        /* FIXME: progress monitor
-        ProgressMonitor pm = new ProgressMonitor(this, "Waiting for the Server...", "", 0, 10);
-        pm.setMillisToPopup(100);
-        pm.setProgress(1);
-        */
-        while (!_serviceToAddressMap.containsService("ServerSocialMC"));
-
-        // pm.setProgress(10);
-        
+
+    /**
+     * Application is closing so shut down listening services and sockets.
+     */
+    private void exitCleanly() {
+        if (_regularTasks != null)
+            _regularTasks.stop();
+        if (_multicastService != null)
+            _multicastService.cancel(true);
+        if(_tcpChatService != null)
+            _tcpChatService.cancel(true);
+        if (_tcpStreamManager != null)
+            _tcpStreamManager.closeAll();
+
+        this.dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING));
+    }
+
+    /**
+     * At startup, must wait for Social Server to be available in order to perform Login or Registration.
+     *
+     * Displays a ProgressMonitor for up to 30 seconds before giving up.
+     *
+     * @return this object
+     */
+    private ClientGUI waitForServer() {
+        _progressMonitor = new ProgressMonitor(this, "Waiting 30 seconds for Social Server...", "", 0, 30000);
+        _progressMonitor.setMillisToPopup(100);
+        _progressMonitor.setProgress(0);
+
+        _waitForServerSocial = new WaitForServerSocial();
+        _waitForServerSocial.addPropertyChangeListener(this);
+        _waitForServerSocial.execute();
+
+        return this;
+    }
+
+    /**
+     * Receive change events from the background thread that waits for Server Social to be available.
+     * @param evt
+     */
+    @Override
+    public void propertyChange(PropertyChangeEvent evt) {
+        switch (evt.getPropertyName()) {
+            case "progress":
+                int progress = 1000 * (Integer) evt.getNewValue();
+                _progressMonitor.setProgress(progress);
+                break;
+            case "state":
+                SwingWorker.StateValue state = (SwingWorker.StateValue) evt.getNewValue();
+                if (state.equals(SwingWorker.StateValue.DONE)) {
+                    _progressMonitor.close();
+                    try {
+                        if (_waitForServerSocial.get()) {
+                            initListeners();
+                            connectToServer();
+                            doLogin();
+                        } else {
+                            JOptionPane.showMessageDialog(rootPane, "Unable to locate Social Server", "Connecting", JOptionPane.ERROR_MESSAGE);
+                            exitCleanly();
+                        }
+                    } catch (InterruptedException | ExecutionException ex) {
+                        Logger.getLogger(ClientGUI.class.getName()).log(Level.SEVERE, null, ex);
+                    }
+                }
+        }
+    }
+
+    private void connectToServer()
+    {
         try {
-            socialSocket = new Socket(_serviceToAddressMap.getServiceAddress("ServerSocialMC").getAddress(), Network.PORTS_SERVER_SOCIAL);
-            NetworkStream newStream = new NetworkStream(socialSocket, _tcpStreamManager);
+            socketSocial = new Socket(_serviceToAddressMap.getServiceAddress("ServerSocialMC").getAddress(), Network.PORTS_SERVER_SOCIAL);
+            NetworkStream newStream = new NetworkStream(socketSocial, _tcpStreamManager);
             _tcpStreamManager.addStream(NetworkStreamManager.SERVERSOCIAL, newStream);
             newStream.getEventManager().addNetworkMessageEventListener(this);
         } catch (IOException ex) {
-            //Logger.getLogger(ClientGUI.class.getName()).log(Level.SEVERE, null, ex);
-            System.err.println("IOException in connectionDialog()");
+            LOGGER.logp(Level.SEVERE, _title, null, "connectToServer()", ex);
         }
-        loginDialog();
     }
-    
-    private void loginDialog()
+
+    private void doLogin()
     {
-        //WSYD_Login loginDialog = new Login(this, true);
+        registrationDialog = new EditProfile(this, true);
+        loginDialog = new Login(this, true);
         loginDialog.setLocationRelativeTo(null);
         loginDialog.setVisible(true);
-        if (loginSuccessful)
+        if (_loggedIn)
         {
             this.setLocationRelativeTo(null);
             this.setVisible(true);
         }
     }
-    
+
     public void loginAbort()
     {
         this.dispose();
     }
-    
+
     public boolean validateLogin(String uName, String pWord)
     {
         boolean result = false;
-        System.out.println(uName + pWord + "<");
-        
+
         if (uName.equals("admin") && pWord.equals("admin"))
         {
-            System.out.println("True");
-            loginSuccessful = true;
+            _loggedIn = true;
             result = true;
         }
         else {
             NetworkMessage message = new NetworkMessage("Login", null, new MessageLogin(uName, pWord));
             NetworkStream ns = _tcpStreamManager._tcpStreams.get(NetworkStreamManager.SERVERSOCIAL);
             if (ns != null) {
-                ns.write(message);                
+                ns.write(message);
                 result = true;
             }
             else
-                System.err.println(" validateLogin(): do not know where ServerSocial is");
+                LOGGER.logp(Level.WARNING, _title, "validateLogin", "do not know where ServerSocial is");
         }
         return result;
     }
-    
+
     public void registrationProfileDialog()
     {
         registrationDialog.setLocationRelativeTo(null);
         registrationDialog.setVisible(true);
     }
-    
+
     public void confirmRegistration(WSYD_Member registrationData)
     {
-        // TODO: confirmRegistration(): submit registrationDialog data to SocialServer
         if (registrationData != null) {
             NetworkMessage message = new NetworkMessage("Register", null, new MessageMember(registrationData));
             NetworkStream ns = this._tcpStreamManager._tcpStreams.get(NetworkStreamManager.SERVERSOCIAL);
@@ -522,48 +688,9 @@ public class ClientGUI extends javax.swing.JFrame implements NetworkMessageEvent
                 ns.write(message);
             }
             else
-                System.err.println("confirmRegistration(): do not know where SocialServer is");
+                logp(Level.WARNING, "confirmRegistration(): do not know where SocialServer is for userID {0} ({1})", registrationData._userID, registrationData._userName);
         }
     }
-    
-    
-    /**
-     * @param args the command line arguments
-     */
-    public static void main(String args[]) {
-        /* Set the Nimbus look and feel */
-        //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
-        /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
-         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html 
-         */
-        try {
-            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
-                if ("Nimbus".equals(info.getName())) {
-                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
-                    break;
-                }
-            }
-        } catch (ClassNotFoundException ex) {
-            java.util.logging.Logger.getLogger(ClientGUI.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
-        } catch (InstantiationException ex) {
-            java.util.logging.Logger.getLogger(ClientGUI.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
-        } catch (IllegalAccessException ex) {
-            java.util.logging.Logger.getLogger(ClientGUI.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
-        } catch (javax.swing.UnsupportedLookAndFeelException ex) {
-            java.util.logging.Logger.getLogger(ClientGUI.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
-        }
-        //</editor-fold>
-        //</editor-fold>
-        //</editor-fold>
-        //</editor-fold>
-
-        /* Create and display the form */
-        java.awt.EventQueue.invokeLater(new Runnable() {
-            public void run() {
-                new ClientGUI().initNeighbourListener().connectionDialog();
-            }
-        });
-    }
 
     // Variables declaration - do not modify//GEN-BEGIN:variables
     private javax.swing.JButton gButtonAddFriendChat;
@@ -595,42 +722,185 @@ public class ClientGUI extends javax.swing.JFrame implements NetworkMessageEvent
     private javax.swing.JScrollPane jScrollPane3;
     // End of variables declaration//GEN-END:variables
 
+    /**
+     * Update all service titles that reflect the user identity.
+     * @param userName the new userName or null if newUserID == 0
+     * @param oldUserID the current userID
+     * @param newUserID the new userID or 0 if logged out
+     */
+    private void setServiceTitles(String userName, long oldUserID, long newUserID) {
+        if (newUserID != 0) {
+            _title = "UserID_" + newUserID;
+            this.setTitle("WSYD logged in as " + userName);
+        } else {
+            _title = DEFAULT_TITLE;
+            this.setTitle("WSYD");
+        }
+        this._tcpChatService.setTitle(_title + "_Chat");
+        this._multicastService.setTitle(_title + "_MC");
+        for (java.util.logging.Handler handler : this.LOGGER.getHandlers()) {
+            if (handler instanceof PacketHandler) {
+                ((PacketHandler) handler).setSource(_title);
+            }
+        }
+        _tcpStreamManager.updateKey(oldUserID, newUserID); // replace temporary key in stream manager
+
+    }
+
+    /**
+     * Process received NetworkMessages and update internal status based on them.
+     *
+     * @param event The wrapper around the NetworkMessage
+     */
     @Override
     public void NetworkMessageReceived(NetworkMessageEvent event) {
         NetworkMessage nm = event.getNetworkMessage();
         if (nm == null)
             return;
-        System.err.println(MessageFormat.format("Network Message received with intent {0} from sender {1}", nm.getIntent(), nm.getSender()));
+        String type = nm.getMessage().getMessageType();
+        MessageLogin ml;
         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 "Login":
-                MessageLogin ml = (MessageLogin) nm.getMessage();
+                ml = (MessageLogin) nm.getMessage();
                 if (ml != null) {
-                    this.loginSuccessful = ml._loggedIn;
-                    if (ml._loggedIn)
+                    _loggedIn = ml._loggedIn;
+                    if (ml._loggedIn) { // update all objects that need to know the user ID
+                        this.setServiceTitles(ml._userName, nm.getKey(), ml._userID);
+                        _membersOnline.add(ml._userID); // make the member online
+
                         this.loginDialog.dispose();
+                    }
                     else
                         loginDialog.setUserName("Invalid Login: retry");
                 }
                 break;
+
+            case "Logout":
+                ml = (MessageLogin) nm.getMessage();
+                if(ml != null) {
+                    if (! ml._loggedIn) {
+                        this.setServiceTitles(null, ml._userID, 0);
+                        _membersOnline.remove(ml._userID);
+                        _loggedIn = false;
+                    }
+                }
+                break;
+
             case "Register":
                 MessageMember mm = (MessageMember) nm.getMessage();
                 if (mm != null) {
                     if (mm.getRegistered() == MessageMember.STATE.REGISTERED) {
                         this.myMember = mm.getMember();
-                        this.registrationSuccessful = true;
+                        this._loggedIn = true;
+                        this.setServiceTitles(this.myMember._userName, nm.getKey(), this.myMember._userID);
+
                         // dismiss the dialogs and show client's main window
                         registrationDialog.dispose();
                         loginDialog.dispose();
-                        this.setTitle("WSYD logged in as " + this.myMember._userName);
                         this.setLocationRelativeTo(null);
                         this.setVisible(true);
                     } else
                         javax.swing.JOptionPane.showMessageDialog(registrationDialog, mm.getStatus(), "Registration failed", javax.swing.JOptionPane.ERROR_MESSAGE);
                 }
                 break;
-            default:
-                System.err.println(MessageFormat.format("Unhandled NetworkMessage received with intent {0} from sender {1}", nm.getIntent(), nm.getSender()));
-                
+
+            default: // log all unhandled messages
+                logp(Level.WARNING, "Unhandled NetworkMessage received with intent: \"{0}\" sender: \"{1}\" target: \"{2}\"", nm.getIntent(), nm.getSender(), nm.getTarget());
+
+        }
+    }
+
+    /**
+     * @param args the command line arguments
+     */
+    public static void main(String args[]) {
+        /* Set the Nimbus look and feel */
+        //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
+        /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
+         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html
+         */
+        try {
+            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
+                if ("Nimbus".equals(info.getName())) {
+                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
+                    break;
+                }
+            }
+        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | javax.swing.UnsupportedLookAndFeelException ex) {
+            java.util.logging.Logger.getLogger(ClientGUI.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
+        }
+        //</editor-fold>
+        //</editor-fold>
+        //</editor-fold>
+        //</editor-fold>
+        class App implements Runnable {
+            public boolean multicastAnnouncements = false;
+            public InetAddress serverSocial = null;
+
+            /**
+             * Constructs, initialises and starts the server management GUI.
+             */
+            @Override
+            public void run() {
+                try {
+                    ClientGUI app = new ClientGUI(multicastAnnouncements, serverSocial).initNeighbourListener().waitForServer();
+                }
+                catch(UnknownHostException ex) {
+                    Logger.getLogger(ClientGUI.class.getName()).log(Level.SEVERE, null, MessageFormat.format("Error: initNeighbourListener(): {0}", ex.toString()));
+                }
+            }
+        }
+        App app = new App();
+
+        // process command line arguments. Use indexed for in order to easily read values of switch arguments
+        for (int i = 0; i < args.length; i++) {
+            switch (args[i]) {
+                case "-announce":
+                    app.multicastAnnouncements = true;
+                    break;
+                case "-noannounce":
+                    app.multicastAnnouncements = false;
+                    break;
+                case "-server":
+                    if (args.length >= i+1) {
+                        // read the next argument as an IP address
+                        InetAddress temp;
+                        try {
+                            temp = InetAddress.getByName(args[i+1]);
+                            app.serverSocial = temp;
+                        } catch (UnknownHostException e) {
+                            Logger.getLogger(ClientGUI.class.getName()).log(Level.SEVERE, null,MessageFormat.format("Error: {0} is not a valid hostname or IP address", args[i+1]));
+                        }
+                    }
+                break;
+            }
         }
+        java.awt.EventQueue.invokeLater(app);
     }
+
 }