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