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.libs.net;
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;
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.
41 * Concrete classes are required to implement the Socket-specific functionality.
43 * The arguments to the Generics superclass SwingWorker<T, V> are:
45 * < return-TYPE-of doInBackground(), publish(parameter-TYPE) >
47 * Here doInBackground() returns an Integer connection counter and publish() takes
48 * a NetworkMessage type.
50 * Server sockets block in the operating system kernel waiting
51 * for connections or incoming packets.
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.
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.
63 * The server registers NetworkMessageEventListener objects and notifies them
64 * when a new NetworkMessage has been received.
66 * @see javax.swing.SwingWorker
68 * @author TJ <hacker@iam.tj>
70 public abstract class NetworkServerAbstract extends SwingWorker<Integer, NetworkMessage> implements NetworkMessageEventGenerator {
73 * Single Logger for the class used by all object instances.
75 * Can be instantiated once by objects of any sub-class.
77 @SuppressWarnings("NonConstantLogger")
78 protected static Logger LOGGER = null;
81 * Inject simulated received NetworkMessages.
83 * A helpful tool for debugging.
85 protected boolean _simulate = false;
88 * Count of packets or connections received.
93 * Service name for this server instance.
95 * E.g. "ServerSocial", "ServerChat", "ServerControl", "ClientControl", "ClientChat", "ServerLog"
100 * Socket parameters for this server.
102 WSYD_SocketAddress _socketAddress;
105 * Thread safe First In, First Out Queue of NetworkMessage objects waiting to be sent.
107 * Allows the Owner Thread to submit new messages for sending that the Worker Thread
110 protected ConcurrentLinkedQueue<NetworkMessage> _sendMessageQueue = new ConcurrentLinkedQueue<>();
112 protected class LastSeenHost {
114 InetSocketAddress address;
116 LastSeenHost(InetSocketAddress address, long timeInMillis) {
117 this.address = address;
118 this.timeInMillis = timeInMillis;
120 LastSeenHost(InetSocketAddress host) {
121 this(host, System.currentTimeMillis());
126 * Maps service _title to its parent network host.
128 * Used by methods on the Owner Thread to determine the list of valid service
129 * names it can submit messages to (by iterating the keys using keySet()).</p>
131 * New service names can be added in two ways:<br/>
133 * <li>by the Worker Thread from received messages</li>
134 * <li>by the Owner or (other thread) from a service discovery helper (such as multicast discovery)</li>
137 protected ConcurrentHashMap<String, LastSeenHost> _serviceToHostMap = new ConcurrentHashMap<>();;
140 * Wrapper for filtering NetworkMessageEvents based on the message intent
142 public class NetworkMessageEventListenerWithIntent {
144 NetworkMessageEventListener _listener;
146 public NetworkMessageEventListenerWithIntent(NetworkMessageEventListener listener, String intent) {
148 _listener = listener;
151 protected ArrayList<NetworkMessageEventListenerWithIntent> _NetworkMessageEventListeners = new ArrayList<>();
155 * @param level message importance
156 * @param title source identifier
157 * @param formatter parameter Formatter for log message
158 * @param parameters variable length list of replaceable parameters for formatter
160 protected static void log(Level level, String title, String formatter, ArrayList<String> parameters) {
163 // formatter = "{" + Integer.toString(parameters.size()) + "}: " + formatter;
164 // parameters.add(title);
165 LOGGER.logp(level, title, null, MessageFormat.format(formatter, parameters.toArray()));
169 * @param level message importance
170 * @param title source identifier
171 * @param message the log entry
173 protected static void log(Level level, String title, String message) {
176 LOGGER.logp(level, title, null, MessageFormat.format("{1}", message));
180 * Set the log level for the server
181 * @param level a new log level
182 * @return the old log level
184 public Level setLogLevel(Level level) {
185 Level result = Level.OFF;
186 if (LOGGER != null) {
187 Level temp = LOGGER.getLevel();
188 LOGGER.setLevel(level);
195 * Default constructor.
197 NetworkServerAbstract() {
198 this._connectionCount = 0;
200 this._socketAddress = null;
204 * Construct the server with a Logger.
206 * No socket is opened.
208 * @param socketAddress The socket to listen on
209 * @param title source identifier for use in log messages and sent NetworkMessage objects
210 * @param logger An instance of Logger to be used by all objects of this class
212 public NetworkServerAbstract(WSYD_SocketAddress socketAddress, String title, Logger logger) {
213 this._connectionCount = 0;
215 this._socketAddress = socketAddress;
216 if (LOGGER == null) // do not replace existing logger reference
221 * Construct the server without a Logger.
223 * No socket is opened.
225 * @param socketAddress The socket to listen on
226 * @param title source identifier for use in log messages and sent NetworkMessage objects
228 public NetworkServerAbstract(WSYD_SocketAddress socketAddress, String title) {
229 this(socketAddress, title, null);
233 * Enable or disable simulated received packet injection.
235 * @param simulate true to simulate received messages
237 public void setSimulate(boolean simulate) {
238 this._simulate = simulate;
242 * Get the simulation state.
244 * @return true if simulation is enabled.
246 public boolean getSimulate() {
247 return this._simulate;
251 /* XXX: The following Methods execute on the background Worker Thread */
254 * The primary SwingWorker method, started on the Worker Thread when the Owner
255 * Thread calls execute().
257 * Loops until isCancelled() == true. Within the loop calls serverListen() to
258 * allow reception of one packet or connection and if so counts it.
259 * Then it checks if there are any messages to be sent out and if so calls
262 * @return the number of connections accepted
265 public Integer doInBackground() {
266 ArrayList<String> logMessages = new ArrayList<>();
268 logMessages.add(_socketAddress.toString());
269 log(Level.INFO, _title, "Opening socket {0}", logMessages);
272 catch(SocketException e) {
274 logMessages.add(_socketAddress.getAddress().toString());
275 logMessages.add(Integer.toString(_socketAddress.getPort()));
276 logMessages.add(_socketAddress.getProtocol().toString());
277 log(Level.SEVERE, _title, "{0}: Unable to open socket on {1}:{2} {3}", logMessages);
280 // unless cancelled keep waiting for new packets or connections
281 while (!this.isCancelled()) {
282 if (this.serverListen())
283 this._connectionCount++;
285 // send a queued message
286 NetworkMessage temp = this.dequeueMessage();
288 if (!this.serverSend(temp)) {
290 logMessages.add(temp.getSender());
291 logMessages.add(temp.getTarget());
292 log(Level.WARNING, _title, "Unable to send message from {0} to {1}", logMessages);
299 logMessages.add(_socketAddress.toString());
300 log(Level.INFO, _title, "Closing socket {0}", logMessages);
303 catch(SocketException e) {
305 logMessages.add(_socketAddress.getAddress().toString());
306 logMessages.add(Integer.toString(_socketAddress.getPort()));
307 logMessages.add(_socketAddress.getProtocol().toString());
308 log(Level.SEVERE, _title, "{0}: Unable to close socket on {1}:{2} {3}", logMessages);
311 return this._connectionCount;
316 * Open the socket ready for accepting data or connections.
318 * It should also set a reasonable socket timeout with a call to setSoTimeout()
320 * @see java.net.ServerSocket#setSoTimeout
321 * @see java.net.DatagramSocket#setSoTimeout
322 * @throws SocketException
324 public abstract void serverOpen() throws SocketException;
329 * @throws SocketException
331 public abstract void serverClose() throws SocketException;
334 * Send an unsolicited message to a remote service.
336 * This method is called by the main worker loop if there is a message to
339 * @param message must have its _serviceTarget parameter set
340 * @return true if the message was sent
342 protected abstract boolean serverSend(NetworkMessage message);
345 * Accept packet or connection from remote hosts.
347 * This method must wait for a single incoming connection or packet, process it,
348 * and then publish() it for consumption by process().
350 * It must add newly seen remote service names to _serviceToHostMap so that
351 * methods on the Owner Thread can discover the destination service titles
352 * they can use in new NetworkMessage submissions.
354 * @return true if the server should continue listening
356 public abstract boolean serverListen();
359 * Removes a message from the queue of pending messages.
361 * This method is called on the Worker Thread by the doInBackground() main loop.
363 * @return a message to be sent
365 protected NetworkMessage dequeueMessage() {
366 return this._sendMessageQueue.poll();
369 /* XXX: Methods below here all execute on the GUI Event Dispatch Thread */
373 * Fetch messages received by the server.
375 * For delivery to event listeners; usually Swing GUI components. This method will run on the
376 * Owner Thread so must complete quickly it that is the GUI Event Dispatch Thread.
378 * @param list messages received and queued
381 protected void process(List<NetworkMessage> list) {
382 for (NetworkMessage message: list) {
383 fireNetworkMessageEvent(message);
388 * Clean up after doInBackground() has returned.
390 * This method will run on the GUI Event Dispatch Thread so must complete quickly.
393 protected abstract void done();
397 * Ensure service is in the map of known hosts.
398 * @param service the service name to check
399 * @return true is the target service is known
401 protected boolean isServiceValid(String service) {
402 return this._serviceToHostMap.containsKey(service);
406 * Adds a message to the queue of pending messages.
408 * This method will usually be called from the Owner Thread.
410 * @param message to be sent
411 * @return true if the message was added to the queue
412 * @throws IllegalArgumentException if the target does not exist in the serviceToHost mapping
414 public boolean queueMessage(NetworkMessage message) throws IllegalArgumentException {
415 boolean result = false;
416 if (message != null) {
417 // ensure the target is set and is a valid service
418 String target = message.getTarget();
420 throw new IllegalArgumentException("target cannot be null");
421 if(!isServiceValid(target))
422 throw new IllegalArgumentException("target service does not exist: " + target);
425 try { // make a deep clone of the message
426 temp = NetworkMessage.clone(message);
427 result = this._sendMessageQueue.add(temp);
428 } catch (CloneNotSupportedException e) {
429 // TODO: queueMessage() log CloneNotSupportedException
437 * Add a NetworkMessageEvent listener.
439 * Listens to all intents.
444 public synchronized void addNetworkMessageEventListener(NetworkMessageEventListener listener) {
445 _NetworkMessageEventListeners.add(new NetworkMessageEventListenerWithIntent(listener, null));
449 * Add a filtered NetworkMessageEvent listener.
451 * Filters on the intent of the NetworkMessage.
453 * @param intent null to listen to all intents, otherwise the intent to listen for
456 public synchronized void addNetworkMessageEventListener(NetworkMessageEventListener listener, String intent) {
457 _NetworkMessageEventListeners.add(new NetworkMessageEventListenerWithIntent(listener, intent));
461 * Remove a NetworkMessageEvent listener.
466 public synchronized void removeNetworkMessageEventListener(NetworkMessageEventListener listener) {
467 for (NetworkMessageEventListenerWithIntent intentListener : _NetworkMessageEventListeners)
468 if (intentListener._listener == listener)
469 _NetworkMessageEventListeners.remove(intentListener);
473 * Send a NetworkMessageEvent to all listeners.
475 * Only sends the message to listeners registered for the same intent, or for all messages.
477 * @param message the NetworkMessage to send
479 private synchronized void fireNetworkMessageEvent(NetworkMessage message) {
480 NetworkMessageEvent event = new NetworkMessageEvent(this, message);
481 for (NetworkMessageEventListenerWithIntent intentListener : _NetworkMessageEventListeners) {
482 if (intentListener._intent.equals(message._intent) || intentListener._intent == null)
483 intentListener._listener.NetworkMessageReceived(event);