NetworkServerAbstract: make LastSeenHost immutable and create method getTargeAddress...
[WeStealzYourDataz.git] / src / uk / ac / ntu / n0521366 / wsyd / libs / net / NetworkServerAbstract.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.libs.net;
25
26 import java.text.MessageFormat;
27 import java.net.InetSocketAddress;
28 import java.net.SocketException;
29 import java.util.concurrent.ConcurrentLinkedQueue;
30 import java.util.concurrent.ConcurrentHashMap;
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.logging.Logger;
34 import java.util.logging.Level;
35 import javax.swing.SwingWorker;
36
37 /**
38  * Abstract dual-use multithreading network server that can be used stand-alone
39  * or in a Swing GUI application as a background worker thread.
40  * 
41  * Concrete classes are required to implement the Socket-specific functionality.
42  * 
43  * The arguments to the Generics superclass SwingWorker<T, V> are:
44  * 
45  *  < return-TYPE-of doInBackground(), publish(parameter-TYPE) >
46  * 
47  * Here doInBackground() returns an Integer connection counter and publish() takes
48  * a NetworkMessage type.
49  *
50  * Server sockets block in the operating system kernel waiting
51  * for connections or incoming packets.
52  *
53  * SwingWorker objects avoid using the GUI event dispatcher thread. Without that the
54  * user interface could be unresponsive for considerable periods whilst server
55  * sockets wait for incoming connections via the blocking in
56  * ServerSocket.accept() (TCP) or DatagramSocket.receive() (UDP) method.
57  *
58  * This design combines the multithreading support of the java.lang.Runnable
59  * interface with the javax.swing.SwingWorker inheritance so that this single class
60  * can be used in non-GUI daemon services and GUI applications, avoiding the need
61  * to write the same server code in more than one class.
62  * 
63  * The server registers NetworkMessageEventListener objects and notifies them
64  * when a new NetworkMessage has been received.
65  * 
66  * @see javax.swing.SwingWorker
67  * 
68  * @author TJ <hacker@iam.tj>
69  */
70 public abstract class NetworkServerAbstract extends SwingWorker<Integer, NetworkMessage> implements NetworkMessageEventGenerator {
71
72     /**
73      * Single Logger for the class used by all object instances.
74      * 
75      * Can be instantiated once by objects of any sub-class.
76      */
77     @SuppressWarnings("NonConstantLogger")
78     protected static Logger LOGGER = null;
79
80     /**
81      * Inject simulated received NetworkMessages.
82      * 
83      * A helpful tool for debugging.
84      */
85     protected boolean _simulate = false;
86
87     /**
88      * Count of packets or connections received.
89      */
90     int _connectionCount;
91
92     /**
93      * Service name for this server instance.
94      * 
95      * E.g. "ServerSocial", "ServerChat", "ServerControl", "ClientControl", "ClientChat", "ServerLog"
96      */
97     String _title;
98
99     /**
100      * Socket parameters for this server.
101      */
102     WSYD_SocketAddress _socketAddress;
103
104     /**
105      * Thread safe First In, First Out Queue of NetworkMessage objects waiting to be sent.
106      * 
107      * Allows the Owner Thread to submit new messages for sending that the Worker Thread
108      * can safely access.
109      */
110     protected ConcurrentLinkedQueue<NetworkMessage> _sendMessageQueue = new ConcurrentLinkedQueue<>();
111
112     /**
113      * Encapsulates a unique network host and the last time it was seen.
114      */
115     protected class LastSeenHost {
116         final long timeInMillis;
117         final InetSocketAddress address;
118         
119         LastSeenHost(InetSocketAddress address, long timeInMillis) {
120             this.address = address;
121             this.timeInMillis = timeInMillis;
122         }
123         LastSeenHost(InetSocketAddress host) {
124             this(host, System.currentTimeMillis());
125         }
126         
127         /**
128          * Formatted string representation of IneAddress and timestamp.
129          * @return the representation
130          */
131         @Override
132         public String toString() {
133             return MessageFormat.format("{0}:{1,number,integer}@{2}", this.address.getHostString(), this.address.getPort(), this.timeInMillis);
134         }
135     };
136     /**
137      * Maps service _title to its parent network host.
138      * <p>
139      * Used by methods on the Owner Thread to determine the list of valid service
140      * names it can submit messages to (by iterating the keys using keySet()).</p>
141      * <p>
142      * New service names can be added in two ways:<br/>
143      * <ol>
144      *  <li>by the Worker Thread from received messages</li>
145      *  <li>by the Owner or (other thread) from a service discovery helper (such as multicast discovery)</li>
146      * </ol>
147      */
148     protected ConcurrentHashMap<String, LastSeenHost> _serviceToHostMap = new ConcurrentHashMap<>();;
149
150     /**
151      * Wrapper for filtering NetworkMessageEvents based on the message intent
152      */
153     public class NetworkMessageEventListenerWithIntent {
154         String _intent;
155         NetworkMessageEventListener _listener;
156         
157         public NetworkMessageEventListenerWithIntent(NetworkMessageEventListener listener, String intent) {
158             _intent = intent;
159             _listener = listener;
160         }
161     }
162     protected ArrayList<NetworkMessageEventListenerWithIntent> _NetworkMessageEventListeners = new ArrayList<>();
163
164     /**
165      * 
166      * @param level message importance
167      * @param title source identifier
168      * @param formatter parameter Formatter for log message
169      * @param parameters variable length list of replaceable parameters for formatter
170      */
171     protected static void log(Level level, String title, String formatter, ArrayList<String> parameters) {
172         if (LOGGER == null)
173             return;
174         // formatter = "{" + Integer.toString(parameters.size()) + "}: " + formatter;
175         // parameters.add(title);
176         LOGGER.logp(level, title, null, MessageFormat.format(formatter, parameters.toArray()));
177     }
178     /**
179      * 
180      * @param level message importance
181      * @param title source identifier
182      * @param message the log entry
183      */
184     protected static void log(Level level, String title, String message) {
185         if (LOGGER == null)
186             return;
187         LOGGER.logp(level, title, null, message);
188     }
189
190     /**
191      * Set the log level for the server
192      * @param level a new log level
193      * @return the old log level
194      */
195     public Level setLogLevel(Level level) {
196         Level result = Level.OFF;
197         if (LOGGER != null) {
198             Level temp = LOGGER.getLevel();
199             LOGGER.setLevel(level);
200             result = temp;
201         }
202         return result;
203     }
204
205     /**
206      * Default constructor.
207      */
208     NetworkServerAbstract() {
209         this._connectionCount = 0;
210         this._title = null;
211         this._socketAddress = null;
212     }
213     
214     /**
215      * Construct the server with a Logger.
216      * 
217      * No socket is opened.
218      * 
219      * @param socketAddress The socket to listen on
220      * @param title source identifier for use in log messages and sent NetworkMessage objects
221      * @param logger An instance of Logger to be used by all objects of this class
222      */
223     public NetworkServerAbstract(WSYD_SocketAddress socketAddress, String title, Logger logger) {
224         this._connectionCount = 0;
225         this._title = title;
226         this._socketAddress = socketAddress;
227         if (LOGGER == null) // do not replace existing logger reference
228             LOGGER = logger;
229     }
230
231     /**
232      * Construct the server without a Logger.
233      * 
234      * No socket is opened.
235      * 
236      * @param socketAddress The socket to listen on
237      * @param title source identifier for use in log messages and sent NetworkMessage objects
238      */
239     public NetworkServerAbstract(WSYD_SocketAddress socketAddress, String title) {
240         this(socketAddress, title, null);
241     }
242
243     /**
244      * Enable or disable simulated received packet injection.
245      * 
246      * @param simulate true to simulate received messages
247      */
248     public void setSimulate(boolean simulate) {
249         this._simulate = simulate;
250     }
251
252     /**
253      * Get the simulation state.
254      * 
255      * @return true if simulation is enabled.
256      */
257     public boolean getSimulate() {
258         return this._simulate;
259     }
260
261
262     /* XXX: The following Methods execute on the background Worker Thread */
263     
264     /**
265      * The primary SwingWorker method, started on the Worker Thread when the Owner
266      * Thread calls execute().
267      * 
268      * Loops until isCancelled() == true. Within the loop calls serverListen() to
269      * allow reception of one packet or connection and if so counts it.
270      * Then  it checks if there are any messages to be sent out and if so calls
271      * serverSend().
272      * 
273      * @return the number of connections accepted
274      */
275     @Override
276     public Integer doInBackground() {
277         ArrayList<String> logMessages = new ArrayList<>();
278         try {
279             logMessages.add(_socketAddress.toString());
280             log(Level.INFO, _title, "Opening socket {0}", logMessages);
281             this.serverOpen();
282         }
283         catch(SocketException e) {
284             logMessages.clear();
285             logMessages.add(_socketAddress.getAddress().toString());
286             logMessages.add(Integer.toString(_socketAddress.getPort()));
287             logMessages.add(_socketAddress.getProtocol().toString());
288             log(Level.SEVERE, _title, "{0}: Unable to open socket on {1}:{2} {3}", logMessages);
289         }
290         
291         // unless cancelled keep waiting for new packets or connections
292         while (!this.isCancelled()) {
293             if (this.serverListen())
294                 this._connectionCount++;
295
296             // send a queued message
297             NetworkMessage temp =  this.dequeueMessage();
298             if (temp != null) {
299                 if (!this.serverSend(temp)) {
300                     logMessages.clear();
301                     logMessages.add(temp.getSender());
302                     logMessages.add(temp.getTarget());
303                     log(Level.WARNING, _title, "Unable to send message from {0} to {1}", logMessages);
304                 }
305             }
306         }
307      
308         try {
309             logMessages.clear();
310             logMessages.add(_socketAddress.toString());
311             log(Level.INFO, _title, "Closing socket {0}", logMessages);
312             this.serverClose();
313         }
314         catch(SocketException e) {
315             logMessages.clear();
316             logMessages.add(_socketAddress.getAddress().toString());
317             logMessages.add(Integer.toString(_socketAddress.getPort()));
318             logMessages.add(_socketAddress.getProtocol().toString());
319             log(Level.SEVERE, _title, "{0}: Unable to close socket on {1}:{2} {3}", logMessages);
320         }
321         
322         return this._connectionCount;
323     }
324
325
326     /**
327      * Open the socket ready for accepting data or connections.
328      * 
329      * It should also set a reasonable socket timeout with a call to setSoTimeout()
330      * 
331      * @see java.net.ServerSocket#setSoTimeout
332      * @see java.net.DatagramSocket#setSoTimeout
333      * @throws SocketException 
334      */
335     public abstract void serverOpen() throws SocketException;
336     
337     /**
338      * Close the socket.
339      * 
340      * @throws SocketException
341      */
342     public abstract void serverClose() throws SocketException;
343     
344     /**
345      * Send an unsolicited message to a remote service.
346      * 
347      * This method is called by the main worker loop if there is a message to
348      * be sent.
349      * 
350      * @param message must have its _serviceTarget parameter set
351      * @return true if the message was sent
352      */
353     protected abstract boolean serverSend(NetworkMessage message);
354
355     /**
356      * Accept packet or connection from remote hosts.
357      * 
358      * This method must wait for a single incoming connection or packet, process it,
359      * and then publish() it for consumption by process().
360      * 
361      * It must add newly seen remote service names to _serviceToHostMap so that
362      * methods on the Owner Thread can discover the destination service titles
363      * they can use in new NetworkMessage submissions.
364      * 
365      * @return true if the server should continue listening
366      */
367     public abstract boolean serverListen();
368
369     /**
370      * Removes a message from the queue of pending messages.
371      *
372      * This method is called on the Worker Thread by the doInBackground() main loop.
373      *
374      * @return a message to be sent
375      */
376     protected NetworkMessage dequeueMessage() {
377         return this._sendMessageQueue.poll();
378     }
379     
380     /* XXX: Methods below here all execute on the GUI Event Dispatch Thread */
381
382
383     /**
384      * Fetch messages received by the server.
385      * 
386      * For delivery to event listeners; usually Swing GUI components. This method will run on the
387      * Owner Thread so must complete quickly it that is the GUI Event Dispatch Thread.
388      * 
389      * @param list messages received and queued
390      */
391     @Override
392     protected void process(List<NetworkMessage> list) {
393         for (NetworkMessage message: list) {
394             fireNetworkMessageEvent(message);
395         }
396     }
397
398     /**
399      * Clean up after doInBackground() has returned.
400      * 
401      * This method will run on the GUI Event Dispatch Thread so must complete quickly.
402      */
403     @Override
404     protected abstract void done();
405
406
407     /**
408      * Ensure service is in the map of known hosts.
409      * @param service the service name to check
410      * @return true is the target service is known
411      */
412     protected boolean isServiceValid(String service) {
413         return this._serviceToHostMap.containsKey(service);
414     }
415
416     /**
417      * Adds a message to the queue of pending messages.
418      * 
419      * This method will usually be called from the Owner Thread.
420      * 
421      * @param message to be sent
422      * @return true if the message was added to the queue
423      * @throws IllegalArgumentException if the target does not exist in the serviceToHost mapping
424      */
425     public boolean queueMessage(NetworkMessage message) throws IllegalArgumentException {
426         boolean result = false;
427         if (message != null) {
428             // ensure the target is set and is a valid service
429             String target = message.getTarget();
430             if (target == null)
431                 throw new IllegalArgumentException("target cannot be null");
432             if(!isServiceValid(target))
433                 throw new IllegalArgumentException("target service does not exist: " + target);
434             
435             NetworkMessage temp;
436             try { // make a deep clone of the message
437                 temp = NetworkMessage.clone(message);
438                 result = this._sendMessageQueue.add(temp);
439             } catch (CloneNotSupportedException e) {
440                 // TODO: queueMessage() log CloneNotSupportedException
441                 e.printStackTrace();
442             }
443         }
444         return result;
445     }
446
447     /**
448      * Get the current InetAddress of a target from the services map.
449      * 
450      * @param target name of the service
451      * @return IP address and port of the service
452      */
453     public InetSocketAddress getTargetAddress(String target) {
454         InetSocketAddress result = null;
455         
456         if (target != null && target.length() > 0) {
457             LastSeenHost host = this._serviceToHostMap.get(target);
458             if (host != null)
459                 result = host.address;
460         }
461         
462         return result;
463     }
464
465     /**
466      * Add a NetworkMessageEvent listener.
467      * 
468      * Listens to all intents.
469      * 
470      * @param listener 
471      */
472     @Override
473     public synchronized void addNetworkMessageEventListener(NetworkMessageEventListener listener) {
474         _NetworkMessageEventListeners.add(new NetworkMessageEventListenerWithIntent(listener, null));
475     }
476
477     /**
478      * Add a filtered NetworkMessageEvent listener.
479      * 
480      * Filters on the intent of the NetworkMessage.
481      * @param listener
482      * @param intent null to listen to all intents, otherwise the intent to listen for
483      */
484     @Override
485     public synchronized void addNetworkMessageEventListener(NetworkMessageEventListener listener, String intent) {
486         _NetworkMessageEventListeners.add(new NetworkMessageEventListenerWithIntent(listener, intent));        
487     }
488
489     /**
490      * Remove a NetworkMessageEvent listener.
491      * 
492      * @param listener 
493      */
494     @Override
495     public synchronized void removeNetworkMessageEventListener(NetworkMessageEventListener listener) {
496         for (NetworkMessageEventListenerWithIntent intentListener : _NetworkMessageEventListeners)
497             if (intentListener._listener == listener)
498                 _NetworkMessageEventListeners.remove(intentListener);
499     }
500     
501     /**
502      * Send a NetworkMessageEvent to all listeners.
503      * 
504      * Only sends the message to listeners registered for the same intent, or for all messages.
505      * 
506      * @param message the NetworkMessage to send
507      */
508     private synchronized void fireNetworkMessageEvent(NetworkMessage message) {
509         NetworkMessageEvent event = new NetworkMessageEvent(this, message);
510         for (NetworkMessageEventListenerWithIntent intentListener : _NetworkMessageEventListeners) {
511             if (intentListener._intent.equals(message._intent) || intentListener._intent == null)
512                 intentListener._listener.NetworkMessageReceived(event);
513         }
514     }
515 }