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<>();
113 * Encapsulates a unique network host and the last time it was seen.
115 protected class LastSeenHost {
116 final long timeInMillis;
117 final InetSocketAddress address;
119 LastSeenHost(InetSocketAddress address, long timeInMillis) {
120 this.address = address;
121 this.timeInMillis = timeInMillis;
123 LastSeenHost(InetSocketAddress host) {
124 this(host, System.currentTimeMillis());
128 * Formatted string representation of IneAddress and timestamp.
129 * @return the representation
132 public String toString() {
133 return MessageFormat.format("{0}:{1,number,integer}@{2}", this.address.getHostString(), this.address.getPort(), this.timeInMillis);
137 * Maps service _title to its parent network host.
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>
142 * New service names can be added in two ways:<br/>
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>
148 protected ConcurrentHashMap<String, LastSeenHost> _serviceToHostMap = new ConcurrentHashMap<>();;
151 * Wrapper for filtering NetworkMessageEvents based on the message intent
153 public class NetworkMessageEventListenerWithIntent {
155 NetworkMessageEventListener _listener;
157 public NetworkMessageEventListenerWithIntent(NetworkMessageEventListener listener, String intent) {
159 _listener = listener;
162 protected ArrayList<NetworkMessageEventListenerWithIntent> _NetworkMessageEventListeners = new ArrayList<>();
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
171 protected static void log(Level level, String title, String formatter, ArrayList<String> parameters) {
174 // formatter = "{" + Integer.toString(parameters.size()) + "}: " + formatter;
175 // parameters.add(title);
176 LOGGER.logp(level, title, null, MessageFormat.format(formatter, parameters.toArray()));
180 * @param level message importance
181 * @param title source identifier
182 * @param message the log entry
184 protected static void log(Level level, String title, String message) {
187 LOGGER.logp(level, title, null, message);
191 * Set the log level for the server
192 * @param level a new log level
193 * @return the old log level
195 public Level setLogLevel(Level level) {
196 Level result = Level.OFF;
197 if (LOGGER != null) {
198 Level temp = LOGGER.getLevel();
199 LOGGER.setLevel(level);
206 * Default constructor.
208 NetworkServerAbstract() {
209 this._connectionCount = 0;
211 this._socketAddress = null;
215 * Construct the server with a Logger.
217 * No socket is opened.
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
223 public NetworkServerAbstract(WSYD_SocketAddress socketAddress, String title, Logger logger) {
224 this._connectionCount = 0;
226 this._socketAddress = socketAddress;
227 if (LOGGER == null) // do not replace existing logger reference
232 * Construct the server without a Logger.
234 * No socket is opened.
236 * @param socketAddress The socket to listen on
237 * @param title source identifier for use in log messages and sent NetworkMessage objects
239 public NetworkServerAbstract(WSYD_SocketAddress socketAddress, String title) {
240 this(socketAddress, title, null);
244 * Enable or disable simulated received packet injection.
246 * @param simulate true to simulate received messages
248 public void setSimulate(boolean simulate) {
249 this._simulate = simulate;
253 * Get the simulation state.
255 * @return true if simulation is enabled.
257 public boolean getSimulate() {
258 return this._simulate;
262 /* XXX: The following Methods execute on the background Worker Thread */
265 * The primary SwingWorker method, started on the Worker Thread when the Owner
266 * Thread calls execute().
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
273 * @return the number of connections accepted
276 public Integer doInBackground() {
277 ArrayList<String> logMessages = new ArrayList<>();
279 logMessages.add(_socketAddress.toString());
280 log(Level.INFO, _title, "Opening socket {0}", logMessages);
283 catch(SocketException e) {
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);
291 // unless cancelled keep waiting for new packets or connections
292 while (!this.isCancelled()) {
293 if (this.serverListen())
294 this._connectionCount++;
296 // send a queued message
297 NetworkMessage temp = this.dequeueMessage();
299 if (!this.serverSend(temp)) {
301 logMessages.add(temp.getSender());
302 logMessages.add(temp.getTarget());
303 log(Level.WARNING, _title, "Unable to send message from {0} to {1}", logMessages);
310 logMessages.add(_socketAddress.toString());
311 log(Level.INFO, _title, "Closing socket {0}", logMessages);
314 catch(SocketException e) {
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);
322 return this._connectionCount;
327 * Open the socket ready for accepting data or connections.
329 * It should also set a reasonable socket timeout with a call to setSoTimeout()
331 * @see java.net.ServerSocket#setSoTimeout
332 * @see java.net.DatagramSocket#setSoTimeout
333 * @throws SocketException
335 public abstract void serverOpen() throws SocketException;
340 * @throws SocketException
342 public abstract void serverClose() throws SocketException;
345 * Send an unsolicited message to a remote service.
347 * This method is called by the main worker loop if there is a message to
350 * @param message must have its _serviceTarget parameter set
351 * @return true if the message was sent
353 protected abstract boolean serverSend(NetworkMessage message);
356 * Accept packet or connection from remote hosts.
358 * This method must wait for a single incoming connection or packet, process it,
359 * and then publish() it for consumption by process().
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.
365 * @return true if the server should continue listening
367 public abstract boolean serverListen();
370 * Removes a message from the queue of pending messages.
372 * This method is called on the Worker Thread by the doInBackground() main loop.
374 * @return a message to be sent
376 protected NetworkMessage dequeueMessage() {
377 return this._sendMessageQueue.poll();
380 /* XXX: Methods below here all execute on the GUI Event Dispatch Thread */
384 * Fetch messages received by the server.
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.
389 * @param list messages received and queued
392 protected void process(List<NetworkMessage> list) {
393 for (NetworkMessage message: list) {
394 fireNetworkMessageEvent(message);
399 * Clean up after doInBackground() has returned.
401 * This method will run on the GUI Event Dispatch Thread so must complete quickly.
404 protected abstract void done();
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
412 protected boolean isServiceValid(String service) {
413 return this._serviceToHostMap.containsKey(service);
417 * Adds a message to the queue of pending messages.
419 * This method will usually be called from the Owner Thread.
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
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();
431 throw new IllegalArgumentException("target cannot be null");
432 if(!isServiceValid(target))
433 throw new IllegalArgumentException("target service does not exist: " + target);
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
448 * Get the current InetAddress of a target from the services map.
450 * @param target name of the service
451 * @return IP address and port of the service
453 public InetSocketAddress getTargetAddress(String target) {
454 InetSocketAddress result = null;
456 if (target != null && target.length() > 0) {
457 LastSeenHost host = this._serviceToHostMap.get(target);
459 result = host.address;
466 * Add a NetworkMessageEvent listener.
468 * Listens to all intents.
473 public synchronized void addNetworkMessageEventListener(NetworkMessageEventListener listener) {
474 _NetworkMessageEventListeners.add(new NetworkMessageEventListenerWithIntent(listener, null));
478 * Add a filtered NetworkMessageEvent listener.
480 * Filters on the intent of the NetworkMessage.
482 * @param intent null to listen to all intents, otherwise the intent to listen for
485 public synchronized void addNetworkMessageEventListener(NetworkMessageEventListener listener, String intent) {
486 _NetworkMessageEventListeners.add(new NetworkMessageEventListenerWithIntent(listener, intent));
490 * Remove a NetworkMessageEvent listener.
495 public synchronized void removeNetworkMessageEventListener(NetworkMessageEventListener listener) {
496 for (NetworkMessageEventListenerWithIntent intentListener : _NetworkMessageEventListeners)
497 if (intentListener._listener == listener)
498 _NetworkMessageEventListeners.remove(intentListener);
502 * Send a NetworkMessageEvent to all listeners.
504 * Only sends the message to listeners registered for the same intent, or for all messages.
506 * @param message the NetworkMessage to send
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);