ServerSocial: complete network message handling
[WeStealzYourDataz.git] / src / uk / ac / ntu / n0521366 / wsyd / server / ServerSocial.java
1 /*
2  * The MIT License
3  *
4  * Copyright 2015 TJ <hacker@iam.tj>.
5  *
6  * Permission is hereby granted, free of charge, to any person obtaining a copy
7  * of this software and associated documentation files (the "Software"), to deal
8  * in the Software without restriction, including without limitation the rights
9  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10  * copies of the Software, and to permit persons to whom the Software is
11  * furnished to do so, subject to the following conditions:
12  *
13  * The above copyright notice and this permission notice shall be included in
14  * all copies or substantial portions of the Software.
15  *
16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22  * THE SOFTWARE.
23  */
24 package uk.ac.ntu.n0521366.wsyd.server;
25
26 import java.awt.event.ActionEvent;
27 import java.awt.event.ActionListener;
28 import java.util.Date;
29 import java.util.ArrayList;
30 import java.text.SimpleDateFormat;
31 import java.text.ParseException;
32 import java.util.TreeSet;
33 import java.util.SortedMap;
34 import java.util.TreeMap;
35 import java.util.Map;
36 import java.util.Collections;
37 import java.util.logging.Logger;
38 import java.util.logging.Level;
39 import java.io.BufferedReader;
40 import java.io.BufferedWriter;
41 import java.io.File;
42 import java.io.FileInputStream;
43 import java.io.FileOutputStream;
44 import java.io.InputStreamReader;
45 import java.io.OutputStreamWriter;
46 import java.io.ObjectInputStream;
47 import java.io.ObjectOutputStream;
48 import java.io.IOException;
49 import java.io.FileNotFoundException;
50 import java.net.InetSocketAddress;
51 import java.net.SocketException;
52 import java.util.Arrays;
53 import java.util.Iterator;
54 import java.util.Set;
55 import java.util.logging.LogRecord;
56 import javax.swing.Timer;
57 import uk.ac.ntu.n0521366.wsyd.libs.WSYD_Member;
58 import uk.ac.ntu.n0521366.wsyd.libs.WSYD_Member_Comparator_UserID;
59 import uk.ac.ntu.n0521366.wsyd.libs.message.MessageLogin;
60 import uk.ac.ntu.n0521366.wsyd.libs.message.MessageMemberState;
61 import uk.ac.ntu.n0521366.wsyd.libs.message.MessagePresence;
62 import uk.ac.ntu.n0521366.wsyd.libs.message.MessageServerControl;
63 import uk.ac.ntu.n0521366.wsyd.libs.net.ConnectionEstablishedEvent;
64 import uk.ac.ntu.n0521366.wsyd.libs.net.ConnectionEstablishedEventListener;
65 import uk.ac.ntu.n0521366.wsyd.libs.net.Network;
66 import uk.ac.ntu.n0521366.wsyd.libs.net.NetworkMessage;
67 import uk.ac.ntu.n0521366.wsyd.libs.net.NetworkMessageEvent;
68 import uk.ac.ntu.n0521366.wsyd.libs.net.NetworkServerUDPMulticast;
69 import uk.ac.ntu.n0521366.wsyd.libs.net.WSYD_SocketAddress;
70 import uk.ac.ntu.n0521366.wsyd.libs.net.NetworkMessageEventListener;
71 import uk.ac.ntu.n0521366.wsyd.libs.net.NetworkServerUDP;
72 import uk.ac.ntu.n0521366.wsyd.libs.net.ServiceAddressMap;
73 import uk.ac.ntu.n0521366.wsyd.libs.net.ServiceAddressMap.LastSeenHost;
74
75 /**
76  * The main Social Network server.
77  *
78  * Can be restarted or stopped using the class static attributes
79  * exitRequested and restartRequested. This can be done by an optional
80  * Management GUI application.
81  *
82  * @author TJ <hacker@iam.tj>
83  */
84 public final class ServerSocial implements NetworkMessageEventListener, ConnectionEstablishedEventListener {
85     /**
86      * Persistent storage in file-system when server exits.
87      */
88     static final String _membersFile = "WSYD_Members.serialized";
89
90     /**
91      * CSV test data file.
92      * 
93      * If it exists in the file system, only used if there is no _membersFile
94      */
95     static final String _testData = "WSYD_TestData.csv";
96     
97     /**
98      * Readable/displayable name of this application
99      */
100     final String _title = "ServerSocial";
101
102     /**
103      * Network services to address map.
104      */
105     ServiceAddressMap _serviceToAddressMap;
106
107     /**
108      * Indicates to start() loop and main() methods to exit completely.
109      */
110     public static boolean exitRequested = false;
111
112     /**
113      * Indicates to start() loop to exit, and to main() to restart the server.
114      */
115     public static boolean restartRequested = true;
116
117     /**
118      * Handles display and sending of log messages.
119      */
120     @SuppressWarnings("NonConstantLogger")
121     private static Logger LOGGER;
122
123     /**
124      * SortedMap wraps a TreeMap that has been made thread-safe by
125      * Collections.synchronizedSortedMap() in readMembers().
126      *
127      * Long key, the userID
128      * WSYD_Member member record
129      */
130     SortedMap<Long, WSYD_Member> _members;
131
132     /**
133      * userIDs of members currently logged in
134      */
135     ArrayList<Long> _membersOnline;
136
137     /**
138      * 
139      */
140     WSYD_SocketAddress _multicastAdvertiserSA;
141     
142     /**
143      * 
144      */
145     NetworkServerUDPMulticast _multicastService;
146     
147     Timer _servicesAnnounce;
148     
149     WSYD_SocketAddress _udpControlServiceSA;
150     
151     NetworkServerUDP _udpControlService;
152    
153
154     /**
155      * Default constructor.
156      */
157     public ServerSocial() {
158         String[] className = this.getClass().getName().split("\\.");
159         LOGGER = Logger.getLogger(className[className.length - 1]);
160         LOGGER.setLevel(Level.ALL);
161         _serviceToAddressMap = new ServiceAddressMap(_title, LOGGER);
162         readMembers(_membersFile);
163         _membersOnline = new ArrayList<>();
164     }
165     
166     /**
167      * Init listener
168      */
169     ServerSocial InitListeners()
170     {
171         _multicastAdvertiserSA = new WSYD_SocketAddress(Network.MULTICAST_IP, Network.PORTS_MULTICAST_DISCOVERY, WSYD_SocketAddress.Protocol.UDP);
172         _multicastService = new NetworkServerUDPMulticast(_multicastAdvertiserSA, _title + "MC", _serviceToAddressMap, LOGGER);
173         _multicastService.getEventManager().addNetworkMessageEventListener(this, "Neighbour");
174         _multicastService.execute();
175         _serviceToAddressMap.put("all", new LastSeenHost(new InetSocketAddress(Network.MULTICAST_IP, Network.PORTS_MULTICAST_DISCOVERY), LastSeenHost.STATE.STATIC));
176         
177         _udpControlServiceSA = new WSYD_SocketAddress(Network.IPv4_WILDCARD, Network.PORTS_EPHEMERAL, WSYD_SocketAddress.Protocol.UDP);
178         _udpControlService = new NetworkServerUDP(_udpControlServiceSA, _title + "Control", _serviceToAddressMap, LOGGER);
179         _udpControlService.getEventManager().addNetworkMessageEventListener(this, "Control");
180         _udpControlService.execute();
181         
182         _tcpListeningServiceSA = new WSYD_SocketAddress(Network.IPv4_WILDCARD, Network.PORTS_SERVER_SOCIAL, WSYD_SocketAddress.Protocol.TCP);
183         _tcpListeningService = new NetworkServerTCP(_tcpListeningServiceSA, _title + "Listener", _serviceToAddressMap, _tcpStreamManager, LOGGER);
184         _tcpListeningService.addConnectionEstablisedEventListener(this);
185         _tcpListeningService.execute();
186         
187         ActionListener servicesAnnounceActionListener = new ActionListener() {
188             /**
189              * Activated by timer events to send multi-cast neighbour announcements and other regular notifications.
190              * @param e 
191              */
192             @Override
193             public void actionPerformed(ActionEvent e) {
194
195                 // Announce the Social Server Neighbour service
196                 MessagePresence mp = new MessagePresence(_title, _multicastService.getSocketAddress());
197                 NetworkMessage nm = NetworkMessage.createNetworkMessage("Neighbour", "all", mp);
198                 nm.setSender(_title + "MC");
199                 _multicastService.queueMessage(nm);
200
201                 // Notify ServerManagement of the Social Server Control service
202                 String target = "ServerManagementControl";
203                 LastSeenHost targetHost = _serviceToAddressMap.get(target);
204                 if (targetHost != null) {
205                     mp = new MessagePresence(_title + "Control", _udpControlService.getSocketAddress());
206                     nm = NetworkMessage.createNetworkMessage("Control", target, mp);
207                     nm.setSender(_title + "Control");
208                     try {
209                         _udpControlService.queueMessage(nm);
210                         LOGGER.log(Level.INFO, "Control notification sent to ServerManagement");
211                     } catch (IllegalArgumentException ex) {
212                         // Not fatal - ServerManagement may not be currently known
213                     }
214                 }
215                 
216                 // clean up the known hosts map
217                 ArrayList<String> servicesRemoved = _serviceToAddressMap.cleanServiceAddressMap(5000);
218                 for (String service: servicesRemoved) {
219                     // FIXME: does the process care if hosts have been removed? if not, remove this array iteration
220                     // TODO: If user client gone, remove from _membersOnline
221                     switch (service) {
222                     }
223                 }
224
225             }
226         };
227         
228         _servicesAnnounce = new Timer(1000, servicesAnnounceActionListener);
229         _servicesAnnounce.setInitialDelay(100);
230         _servicesAnnounce.start();
231         
232         return this;
233     }
234     
235     /**
236      * Main execution loop of the server
237      *
238      * @return true if no errors encountered
239      * @throws java.lang.InterruptedException
240      */
241     @SuppressWarnings("SleepWhileInLoop")
242     public boolean start() throws InterruptedException {
243         boolean result;
244
245         // TODO: start() create TCP listener
246         // TODO: start() create UDP Multicast group listener and broadcast adverts
247         // wait for connections
248         int loopCount = 200;
249         while (!ServerSocial.exitRequested && ! ServerSocial.restartRequested) {
250             Thread.sleep(1000); // wait a second
251             System.out.println("start() loop " + loopCount);
252             if (loopCount-- == 0)
253                 ServerSocial.exitRequested = true;
254         }
255         /*try
256         {
257             _multicastServer.serverClose();
258         }
259         catch (SocketException e)
260         {
261             //TODO: Do something
262         }*/
263         _servicesAnnounce.stop();
264         _multicastService.cancel(true);
265         _udpControlService.cancel(true);
266         
267         result = writeMembers(_membersFile);
268
269         return result;
270     }
271
272     /**
273      * Deserialize the collection of WSYD_Members from file
274      *
275      * @param fileName serialized members data file
276      * @return true if successfully deserialized
277      */
278     @SuppressWarnings("CallToPrintStackTrace")
279     public boolean readMembers(String fileName) {
280         boolean result = false;
281
282         try (
283             FileInputStream f = new FileInputStream(fileName);
284             ObjectInputStream in  = new ObjectInputStream(f);
285             )
286         {
287             if (_members == null)
288                 /* XXX: do not pass a Comparator to the constructor if collection is being deserialized as one was already saved during serialization.
289                  *      If the Comparator is passed to the constructor the serialized object will 'grow' by ~17 bytes each time as multi Comparator
290                  *      objects are written each time the collection is serialized.
291                 */
292                 _members = Collections.synchronizedSortedMap( new TreeMap<Long, WSYD_Member>() );
293             if (!_members.isEmpty())
294                 _members.clear();
295             /* Need explicit cast to SortedMap for Object type returned by readObject()
296              * but this can cause an "unchecked cast" compiler warning since the compiler
297              * cannot be sure the Object returned from readObject() is really a
298              * SortedMap<Long, WSYD_Member> so need to tell the compiler that in this case
299              * we are sure it is. The following for() iteration will cause a
300              * ClassCastException if the type is not as expected.
301              */
302             @SuppressWarnings("unchecked")
303             SortedMap<Long, WSYD_Member> temp = (SortedMap<Long, WSYD_Member>) in.readObject();
304             _members = Collections.synchronizedSortedMap( temp );
305             for (Map.Entry<Long, WSYD_Member> e : _members.entrySet()) {
306                 System.out.println(e.getKey() + ": " + e.getValue().toString());
307             }
308             LOGGER.log(Level.INFO, "Members database read from {0}", fileName);
309             result = true;
310         }
311         catch(FileNotFoundException e) {
312             _members = Collections.synchronizedSortedMap( new TreeMap<Long, WSYD_Member>( new WSYD_Member_Comparator_UserID() ) );
313             LOGGER.log(Level.INFO, "Starting new members database: no database file found ({0})", fileName);
314             result = true;
315
316             // if test data CSV exists import it
317             File csv = new File(_testData);
318             if (csv.exists() && csv.isFile()) {
319                 LOGGER.log(Level.INFO, "Importing test data from {0}", _testData);
320                 importCSV(_testData);
321             }
322
323         }
324         catch(IOException e) {
325             LOGGER.log(Level.SEVERE, "Unable to read database file {0}", fileName);
326             e.printStackTrace();
327         }
328         catch(ClassNotFoundException e) {
329             LOGGER.log(Level.SEVERE, "Unable to deserialize database file {0}", fileName);
330             e.printStackTrace();
331         }
332
333         return result;
334     }
335
336     /**
337      * Serialize the WSYD_Members collection to a file
338      *
339      * @param fileName database file
340      * @return true if collection was successfully serialized
341      */
342     @SuppressWarnings("CallToPrintStackTrace")
343     public boolean writeMembers(String fileName) {
344         boolean result = false;
345
346         if (!_members.isEmpty()) { // don't write an empty database
347             try (
348                 FileOutputStream f = new FileOutputStream(fileName);
349                 ObjectOutputStream out = new ObjectOutputStream(f);
350             )
351             {
352                 out.writeObject(_members);
353
354                 LOGGER.log(Level.INFO, "Members database written to {0}", fileName);
355                 result = true;
356             }
357             catch(IOException e) {
358                 LOGGER.log(Level.SEVERE, "Unable to write database file {0}", fileName);
359                 e.printStackTrace();
360             }
361         }
362         else
363             result = true;
364
365         return result;
366     }
367
368     /**
369      * Read a CSV file containing WSYD_Member records and add it to the in-memory
370      * collection.
371      * 
372      * @param fileName name of CSV file
373      * @return true if successfully imported
374      */
375     @SuppressWarnings("CallToPrintStackTrace")
376     public boolean importCSV(String fileName) {
377         boolean result = false;
378
379         try (
380             FileInputStream fis = new FileInputStream(fileName);
381             InputStreamReader isr = new InputStreamReader(fis);
382             BufferedReader br = new BufferedReader(isr);
383         )
384         {
385             String line;
386             while ((line = br.readLine()) != null) {
387                 LOGGER.log(Level.FINEST, line);
388                 try {
389                     WSYD_Member temp = WSYD_Member.createWSYD_Member(line);
390                     if (temp != null)
391                         _members.put(temp._userID, temp); // add new member to collection
392                 } catch (IllegalArgumentException e) {     
393                     LOGGER.log(Level.WARNING, "Ignoring bad CSV import line");
394                 }
395             }
396         }
397         catch(IOException e) {
398                 LOGGER.log(Level.SEVERE, "Unable to import CSV file {0}", fileName);
399                 e.printStackTrace();
400         }
401
402         return result;
403     }
404
405     /**
406      * Export WSYD_Members collection to a CSV file.
407      * 
408      * @param fileName name of the CSV file to write
409      * @return true if successful
410      */
411     @SuppressWarnings("CallToPrintStackTrace")
412     public boolean exportCSV(String fileName) {
413         boolean result = false;
414
415         try (
416             FileOutputStream fos = new FileOutputStream(fileName);
417             OutputStreamWriter osw = new OutputStreamWriter(fos, "utf-8");
418             BufferedWriter bw = new BufferedWriter(osw);
419         )
420         {
421             bw.write("# 0     , 1       , 2       , 3              , 4  , 5        , 6        , 7      , 8                  , 9");
422             bw.write("# userID, userName, password, currentLocation, bio, birthDate, interests, friends, friendsRequestsSent, friendsRequestsReceived");
423             for (Map.Entry<Long, WSYD_Member> e: _members.entrySet()) {
424                 bw.write(e.getKey() + ": " + e.getValue().toString());
425             }
426         }
427         catch(IOException e) {
428             LOGGER.log(Level.SEVERE, "Unable to export to CSV file {0}", fileName);
429             e.printStackTrace();
430         }
431
432         return result;
433     }
434     
435     @Override
436     public void NetworkMessageReceived(NetworkMessageEvent event)
437     {
438         System.err.println("NetworkMessage received");
439         //TODO: NetworkMessageReceived: Handle Messages
440         NetworkMessage nm = event.getNetworkMessage();
441         if (nm == null)
442             return;
443         //Exit or Restart?
444         System.err.println("Packet Received for intent " + nm.getIntent());
445         String type = nm.getMessage().getMessageType();
446         switch (nm.getIntent()) {
447             case "Control":
448                 if (type.equals(MessageServerControl.getType())) { // ServerControl
449                     if ("ServerManagement".equals(nm.getSender())) {
450                         MessageServerControl mp = (MessageServerControl)nm.getMessage();
451                         if (mp.exitReq == MessageServerControl.EXIT.YES) ServerSocial.exitRequested = true;
452                         if (mp.restartReq == MessageServerControl.RESTART.YES) ServerSocial.restartRequested = true;
453                     }
454                 }
455                 break;
456             case "Login":
457                 if (type.equals(MessageLogin.getType())) {
458                     MessageLogin ml = (MessageLogin)nm.getMessage();
459                     
460                     Set<Map.Entry<Long, WSYD_Member>> tempSet = _members.entrySet();
461                     Iterator<Map.Entry<Long, WSYD_Member>> tempIter = tempSet.iterator();
462                     while (tempIter.hasNext()) {
463                         Map.Entry<Long, WSYD_Member> element = tempIter.next();
464                         if (element.getValue()._userName.equals(ml._uName) && element.getValue()._password.equals(ml._uPass)) {
465                             ml._userID = element.getKey();
466                             ml._loggedIn = true;
467                             _tcpStreamManager.updateKey(nm.getKey(), element.getKey()); // replace temp key in stream manager
468                             _membersOnline.add(element.getKey()); // make the member online
469                         }
470                     }
471                     _tcpStreamManager._tcpStreams.get(nm.getKey()).write(nm);
472                 }
473         }
474         
475     }
476     
477     private void notifyMemberPrescence(long userID, boolean state) {
478         NetworkMessage message = new NetworkMessage("MemberNotification", null, new MessageMemberState(userID, state));
479         for (long ID : _membersOnline) {
480             _tcpStreamManager._tcpStreams.get(ID).write(message);
481         }
482     }
483     
484     private void memberOnline (long userID) {
485         if (!_membersOnline.contains(userID)) {
486             notifyMemberPrescence(userID, true);
487             _membersOnline.add(userID);
488         }
489     }
490     
491     private void memberOffline (long userID) {
492         if (_membersOnline.contains(userID)) {
493             _membersOnline.remove(userID);
494             notifyMemberPrescence(userID, false);
495         }
496     }
497
498     /**
499      * Entry point which starts, restarts, and exits the application.
500      * 
501      * @param args the command line arguments
502      * @throws java.lang.InterruptedException
503      */
504     public static void main(String[] args) throws InterruptedException {
505         while (!ServerSocial.exitRequested && ServerSocial.restartRequested) {
506             ServerSocial app = new ServerSocial().InitListeners();
507             ServerSocial.restartRequested = false;
508             if (!app.start()) {
509                 System.err.println("Encountered error running Social Server");
510                 break; // leave the while loop
511             }
512         }
513     }
514
515     @Override
516     public void connectionEstablished(ConnectionEstablishedEvent event) {
517         System.err.println("connectionEstablished()");
518         event.getStream().getEventManager().addNetworkMessageEventListener(this);
519     }
520 }
521     
522