4 * Copyright 2015 Eddie Berrisford-Lynch <dev@fun2be.me>.
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.InetSocketAddress;
51 import java.net.SocketException;
52 import java.util.Arrays;
53 import java.util.Iterator;
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;
81 * The main Social Network server.
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.
87 * @author Eddie Berrisford-Lynch <dev@fun2be.me>
89 public final class ServerSocial implements NetworkMessageEventListener, ConnectionEstablishedEventListener {
91 * Persistent storage in file-system when server exits.
93 static final String _membersFile = "WSYD_Members.serialized";
98 * If it exists in the file system, only used if there is no _membersFile
100 static final String _testData = "WSYD_TestData.csv";
103 * Readable/displayable name of this application
105 final String _title = "ServerSocial";
108 * Network services to address map.
110 ServiceAddressMap _serviceToAddressMap;
113 * Indicates to start() loop and main() methods to exit completely.
115 public static boolean exitRequested = false;
118 * Indicates to start() loop to exit, and to main() to restart the server.
120 public static boolean restartRequested = true;
123 * Handles display and sending of log messages.
125 @SuppressWarnings("NonConstantLogger")
126 private static Logger LOGGER;
129 * SortedMap wraps a TreeMap that has been made thread-safe by
130 * Collections.synchronizedSortedMap() in readMembers().
132 * Long key, the userID
133 * WSYD_Member member record
135 SortedMap<Long, WSYD_Member> _members;
138 * userIDs of members currently logged in
140 ArrayList<Long> _membersOnline;
145 WSYD_SocketAddress _multicastAdvertiserSA;
150 NetworkServerUDPMulticast _multicastService;
152 Timer _servicesAnnounce;
154 WSYD_SocketAddress _udpControlServiceSA;
156 NetworkServerUDP _udpControlService;
158 WSYD_SocketAddress _tcpListeningServiceSA;
160 NetworkServerTCP _tcpListeningService;
162 NetworkStreamManager _tcpStreamManager;
167 * Default constructor.
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());
177 _serviceToAddressMap = new ServiceAddressMap(_title, LOGGER);
178 readMembers(_membersFile);
179 _membersOnline = new ArrayList<>();
180 _tcpStreamManager = new NetworkStreamManager();
186 ServerSocial InitListeners()
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));
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();
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();
204 ActionListener servicesAnnounceActionListener = new ActionListener() {
206 * Activated by timer events to send multi-cast neighbour announcements and other regular notifications.
210 public void actionPerformed(ActionEvent e) {
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);
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");
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
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
245 _servicesAnnounce = new Timer(1000, servicesAnnounceActionListener);
246 _servicesAnnounce.setInitialDelay(100);
247 _servicesAnnounce.start();
253 * Main execution loop of the server
255 * @return true if no errors encountered
256 * @throws java.lang.InterruptedException
258 @SuppressWarnings("SleepWhileInLoop")
259 public boolean start() throws InterruptedException {
262 // TODO: start() create TCP listener
263 // TODO: start() create UDP Multicast group listener and broadcast adverts
264 // wait for connections
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;
273 _servicesAnnounce.stop();
274 _multicastService.cancel(true);
275 _udpControlService.cancel(true);
277 result = writeMembers(_membersFile);
283 * Deserialize the collection of WSYD_Members from file
285 * @param fileName serialized members data file
286 * @return true if successfully deserialized
288 @SuppressWarnings("CallToPrintStackTrace")
289 public boolean readMembers(String fileName) {
290 boolean result = false;
293 FileInputStream f = new FileInputStream(fileName);
294 ObjectInputStream in = new ObjectInputStream(f);
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.
302 _members = Collections.synchronizedSortedMap( new TreeMap<Long, WSYD_Member>() );
303 if (!_members.isEmpty())
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.
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());
318 LOGGER.log(Level.INFO, "Members database read from {0}", fileName);
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);
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);
334 catch(IOException e) {
335 LOGGER.log(Level.SEVERE, "Unable to read database file {0}", fileName);
338 catch(ClassNotFoundException e) {
339 LOGGER.log(Level.SEVERE, "Unable to deserialize database file {0}", fileName);
347 * Serialize the WSYD_Members collection to a file
349 * @param fileName database file
350 * @return true if collection was successfully serialized
352 @SuppressWarnings("CallToPrintStackTrace")
353 public boolean writeMembers(String fileName) {
354 boolean result = false;
356 if (!_members.isEmpty()) { // don't write an empty database
358 FileOutputStream f = new FileOutputStream(fileName);
359 ObjectOutputStream out = new ObjectOutputStream(f);
362 out.writeObject(_members);
364 LOGGER.log(Level.INFO, "Members database written to {0}", fileName);
367 catch(IOException e) {
368 LOGGER.log(Level.SEVERE, "Unable to write database file {0}", fileName);
379 * Read a CSV file containing WSYD_Member records and add it to the in-memory
382 * @param fileName name of CSV file
383 * @return true if successfully imported
385 @SuppressWarnings("CallToPrintStackTrace")
386 public boolean importCSV(String fileName) {
387 boolean result = false;
390 FileInputStream fis = new FileInputStream(fileName);
391 InputStreamReader isr = new InputStreamReader(fis);
392 BufferedReader br = new BufferedReader(isr);
396 while ((line = br.readLine()) != null) {
397 LOGGER.log(Level.FINEST, line);
399 WSYD_Member temp = WSYD_Member.createWSYD_Member(line);
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");
407 catch(IOException e) {
408 LOGGER.log(Level.SEVERE, "Unable to import CSV file {0}", fileName);
416 * Export WSYD_Members collection to a CSV file.
418 * @param fileName name of the CSV file to write
419 * @return true if successful
421 @SuppressWarnings("CallToPrintStackTrace")
422 public boolean exportCSV(String fileName) {
423 boolean result = false;
426 FileOutputStream fos = new FileOutputStream(fileName);
427 OutputStreamWriter osw = new OutputStreamWriter(fos, "utf-8");
428 BufferedWriter bw = new BufferedWriter(osw);
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());
437 catch(IOException e) {
438 LOGGER.log(Level.SEVERE, "Unable to export to CSV file {0}", fileName);
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);
452 private void memberOnline (long userID) {
453 if (!_membersOnline.contains(userID)) {
454 notifyMemberPrescence(userID, true);
455 _membersOnline.add(userID);
459 private void memberOffline (long userID) {
460 if (_membersOnline.contains(userID)) {
461 _membersOnline.remove(userID);
462 notifyMemberPrescence(userID, false);
467 public void connectionEstablished(ConnectionEstablishedEvent event) {
468 System.err.println("connectionEstablished()");
469 event.getStream().getEventManager().addNetworkMessageEventListener(this);
472 * Process received network messages.
474 * @param event the network message event
477 public void NetworkMessageReceived(NetworkMessageEvent event)
479 NetworkMessage nm = event.getNetworkMessage();
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;
495 if (type.equals(MessageLogin.getType())) {
496 NetworkStream ns = _tcpStreamManager._tcpStreams.get(nm.getKey());
497 MessageLogin ml = (MessageLogin)nm.getMessage();
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();
506 _tcpStreamManager.updateKey(nm.getKey(), element.getKey()); // replace temp key in stream manager
507 _membersOnline.add(element.getKey()); // make the member online
514 System.err.println("Login: cannot find stream for ID:" + nm.getKey());
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);
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);
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);
546 System.err.println("Login: cannot find stream for ID:" + nm.getKey());
551 System.err.println("Unhandled NetworkMessage received for intent " + nm.getIntent());
558 * Entry point which starts, restarts, and exits the application.
560 * @param args the command line arguments
561 * @throws java.lang.InterruptedException
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;
568 System.err.println("Encountered error running Social Server");
569 break; // leave the while loop