4 * Copyright 2015 TJ <hacker@iam.tj>.
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:
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
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
24 package uk.ac.ntu.n0521366.wsyd.server;
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;
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;
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;
67 * The main Social Network server.
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.
73 * @author TJ <hacker@iam.tj>
75 public final class ServerSocial implements NetworkMessageEventListener {
77 * Persistent storage in file-system when server exits.
79 static final String _membersFile = "WSYD_Members.serialized";
84 * If it exists in the file system, only used if there is no _membersFile
86 static final String _testData = "WSYD_TestData.csv";
89 * Indicates to start() loop and main() methods to exit completely.
91 public static boolean exitRequested = false;
94 * Indicates to start() loop to exit, and to main() to restart the server.
96 public static boolean restartRequested = true;
99 * Handles display and sending of log messages.
101 @SuppressWarnings("NonConstantLogger")
102 private static Logger LOGGER;
105 * SortedMap wraps a TreeMap that has been made thread-safe by
106 * Collections.synchronizedSortedMap() in readMembers().
108 * Long key, the userID
109 * WSYD_Member member record
111 SortedMap<Long, WSYD_Member> _members;
114 * userIDs of members currently logged in
116 ArrayList<Long> _membersOnline;
121 WSYD_SocketAddress _multicastAdvertiserSA;
126 NetworkServerUDPMulticast _multicastServer;
128 Timer _multicastAnnounce;
130 WSYD_SocketAddress _udpServerSA;
132 NetworkServerUDP _udpServer;
136 * Default constructor.
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<>();
149 ServerSocial InitListeners()
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();
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();
161 ActionListener multicastAnnounceActionListener = new ActionListener() {
163 * Activated by timer events to send multi-cast neighbour announcements for the Log Service.
167 public void actionPerformed(ActionEvent e) {
168 LOGGER.log(Level.INFO, "Neighbour advert");
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);
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) {
187 _multicastAnnounce = new Timer(1000, multicastAnnounceActionListener);
188 _multicastAnnounce.setInitialDelay(100);
189 _multicastAnnounce.start();
195 * Main execution loop of the server
197 * @return true if no errors encountered
198 * @throws java.lang.InterruptedException
200 @SuppressWarnings("SleepWhileInLoop")
201 public boolean start() throws InterruptedException {
204 // TODO: start() create TCP listener
205 // TODO: start() create UDP Multicast group listener and broadcast adverts
206 // wait for connections
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;
216 _multicastServer.serverClose();
218 catch (SocketException e)
222 _multicastAnnounce.stop();
223 _multicastServer.cancel(true);
224 _udpServer.cancel(true);
226 result = writeMembers(_membersFile);
232 * Deserialize the collection of WSYD_Members from file
234 * @param fileName serialized members data file
235 * @return true if successfully deserialized
237 @SuppressWarnings("CallToPrintStackTrace")
238 public boolean readMembers(String fileName) {
239 boolean result = false;
242 FileInputStream f = new FileInputStream(fileName);
243 ObjectInputStream in = new ObjectInputStream(f);
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.
251 _members = Collections.synchronizedSortedMap( new TreeMap<Long, WSYD_Member>() );
252 if (!_members.isEmpty())
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.
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());
267 LOGGER.log(Level.INFO, "Members database read from {0}", fileName);
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);
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);
283 catch(IOException e) {
284 LOGGER.log(Level.SEVERE, "Unable to read database file {0}", fileName);
287 catch(ClassNotFoundException e) {
288 LOGGER.log(Level.SEVERE, "Unable to deserialize database file {0}", fileName);
296 * Serialize the WSYD_Members collection to a file
298 * @param fileName database file
299 * @return true if collection was successfully serialized
301 @SuppressWarnings("CallToPrintStackTrace")
302 public boolean writeMembers(String fileName) {
303 boolean result = false;
305 if (!_members.isEmpty()) { // don't write an empty database
307 FileOutputStream f = new FileOutputStream(fileName);
308 ObjectOutputStream out = new ObjectOutputStream(f);
311 out.writeObject(_members);
313 LOGGER.log(Level.INFO, "Members database written to {0}", fileName);
316 catch(IOException e) {
317 LOGGER.log(Level.SEVERE, "Unable to write database file {0}", fileName);
328 * Read a CSV file containing WSYD_Member records and add it to the in-memory
331 * @param fileName name of CSV file
332 * @return true if successfully imported
334 @SuppressWarnings("CallToPrintStackTrace")
335 public boolean importCSV(String fileName) {
336 boolean result = false;
339 FileInputStream fis = new FileInputStream(fileName);
340 InputStreamReader isr = new InputStreamReader(fis);
341 BufferedReader br = new BufferedReader(isr);
345 while ((line = br.readLine()) != null) {
346 LOGGER.log(Level.FINEST, line);
348 WSYD_Member temp = WSYD_Member.createWSYD_Member(line);
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");
356 catch(IOException e) {
357 LOGGER.log(Level.SEVERE, "Unable to import CSV file {0}", fileName);
365 * Export WSYD_Members collection to a CSV file.
367 * @param fileName name of the CSV file to write
368 * @return true if successful
370 @SuppressWarnings("CallToPrintStackTrace")
371 public boolean exportCSV(String fileName) {
372 boolean result = false;
375 FileOutputStream fos = new FileOutputStream(fileName);
376 OutputStreamWriter osw = new OutputStreamWriter(fos, "utf-8");
377 BufferedWriter bw = new BufferedWriter(osw);
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());
386 catch(IOException e) {
387 LOGGER.log(Level.SEVERE, "Unable to export to CSV file {0}", fileName);
395 public void NetworkMessageReceived(NetworkMessageEvent event)
397 System.err.println("Packet received.");
398 //TODO: NetworkMessageReceived: Handle Messages
399 NetworkMessage nm = event.getNetworkMessage();
403 System.err.println(nm.getIntent());
404 if ("Control".equals(nm.getIntent()))
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;
416 * Entry point which starts, restarts, and exits the application.
418 * @param args the command line arguments
419 * @throws java.lang.InterruptedException
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;
426 System.err.println("Encountered error running Social Server");
427 break; // leave the while loop