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