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