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