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