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