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.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;
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.
39 * Concrete classes are required to implement the Socket-specific functionality.
41 * The arguments to the Generics superclass SwingWorker<T, V> are:
43 * < return-TYPE-of doInBackground(), publish(parameter-TYPE) >
45 * Here doInBackground() returns an Integer connection counter and publish() takes
46 * a NetworkMessage type.
48 * Server sockets block in the operating system kernel waiting
49 * for connections or incoming packets.
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.
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.
61 * The server registers NetworkMessageEventListener objects and notifies them
62 * when a new NetworkMessage has been received.
64 * @see javax.swing.SwingWorker
66 * @author TJ <hacker@iam.tj>
68 public abstract class NetworkServerAbstract extends SwingWorker<Integer, NetworkMessage> implements NetworkMessageEventGenerator {
71 * Single Logger for the class used by all object instances.
73 * Can be instantiated once by objects of any sub-class.
75 @SuppressWarnings("NonConstantLogger")
76 protected static Logger LOGGER = null;
79 * Inject simulated received NetworkMessages.
81 * A helpful tool for debugging.
83 protected boolean _simulate = false;
86 * Count of packets or connections received.
91 * Service name for this server instance.
93 * E.g. "ServerSocial", "ServerChat", "ServerControl", "ClientControl", "ClientChat", "ServerLog"
98 * Socket parameters for this server.
100 WSYD_SocketAddress _socketAddress;
102 protected ServiceAddressMap _serviceToHostMap;
104 * Thread safe First In, First Out Queue of NetworkMessage objects waiting to be sent.
106 * Allows the Owner Thread to submit new messages for sending that the Worker Thread
109 protected ConcurrentLinkedQueue<NetworkMessage> _sendMessageQueue = new ConcurrentLinkedQueue<>();
112 * Wrapper for filtering NetworkMessageEvents based on the message intent
114 public class NetworkMessageEventListenerWithIntent {
116 NetworkMessageEventListener _listener;
118 public NetworkMessageEventListenerWithIntent(NetworkMessageEventListener listener, String intent) {
120 _listener = listener;
123 protected ArrayList<NetworkMessageEventListenerWithIntent> _NetworkMessageEventListeners = new ArrayList<>();
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
132 protected static void log(Level level, String title, String formatter, ArrayList<String> parameters) {
135 // formatter = "{" + Integer.toString(parameters.size()) + "}: " + formatter;
136 // parameters.add(title);
137 LOGGER.logp(level, title, null, MessageFormat.format(formatter, parameters.toArray()));
141 * @param level message importance
142 * @param title source identifier
143 * @param message the log entry
145 protected static void log(Level level, String title, String message) {
148 LOGGER.logp(level, title, null, message);
152 * Set the log level for the server
153 * @param level a new log level
154 * @return the old log level
156 public Level setLogLevel(Level level) {
157 Level result = Level.OFF;
158 if (LOGGER != null) {
159 Level temp = LOGGER.getLevel();
160 LOGGER.setLevel(level);
167 * Default constructor.
169 NetworkServerAbstract() {
170 this._connectionCount = 0;
172 this._socketAddress = null;
173 this._serviceToHostMap = null;
177 * Construct the server with a Logger.
179 * No socket is opened.
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
186 public NetworkServerAbstract(WSYD_SocketAddress socketAddress, String title, ServiceAddressMap serviceToHostMap, Logger logger) {
187 this._connectionCount = 0;
189 this._socketAddress = socketAddress;
190 this._serviceToHostMap = serviceToHostMap;
191 if (LOGGER == null) // do not replace existing logger reference
196 * Construct the server without a Logger.
198 * No socket is opened.
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
204 public NetworkServerAbstract(WSYD_SocketAddress socketAddress, String title, ServiceAddressMap serviceToHostMap) {
205 this(socketAddress, title, serviceToHostMap, null);
209 * Get the socket in use - not that (possibly wildcard/ephermeral) requested.
211 * @return the port being used
213 public WSYD_SocketAddress getSocketAddress() {
214 return _socketAddress;
218 * Enable or disable simulated received packet injection.
220 * @param simulate true to simulate received messages
222 public void setSimulate(boolean simulate) {
223 this._simulate = simulate;
227 * Get the simulation state.
229 * @return true if simulation is enabled.
231 public boolean getSimulate() {
232 return this._simulate;
236 /* XXX: The following Methods execute on the background Worker Thread */
239 * The primary SwingWorker method, started on the Worker Thread when the Owner
240 * Thread calls execute().
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
247 * @return the number of connections accepted
250 public Integer doInBackground() {
251 ArrayList<String> logMessages = new ArrayList<>();
253 logMessages.add(_socketAddress.toString());
254 log(Level.INFO, _title, "Opening socket {0}", logMessages);
257 catch(SocketException e) {
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);
265 // unless cancelled keep waiting for new packets or connections
266 while (!this.isCancelled()) {
267 if (this.serverListen())
268 this._connectionCount++;
270 // send a queued message
271 NetworkMessage temp = this.dequeueMessage();
273 if (!this.serverSend(temp)) {
275 logMessages.add(temp.getSender());
276 logMessages.add(temp.getTarget());
277 log(Level.WARNING, _title, "Unable to send message from {0} to {1}", logMessages);
284 logMessages.add(_socketAddress.toString());
285 log(Level.INFO, _title, "Closing socket {0}", logMessages);
288 catch(SocketException e) {
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);
296 return this._connectionCount;
301 * Open the socket ready for accepting data or connections.
303 * It should also set a reasonable socket timeout with a call to setSoTimeout()
305 * @see java.net.ServerSocket#setSoTimeout
306 * @see java.net.DatagramSocket#setSoTimeout
307 * @throws SocketException
309 public abstract void serverOpen() throws SocketException;
314 * @throws SocketException
316 public abstract void serverClose() throws SocketException;
319 * Send an unsolicited message to a remote service.
321 * This method is called by the main worker loop if there is a message to
324 * @param message must have its _serviceTarget parameter set
325 * @return true if the message was sent
327 protected abstract boolean serverSend(NetworkMessage message);
330 * Accept packet or connection from remote hosts.
332 * This method must wait for a single incoming connection or packet, process it,
333 * and then publish() it for consumption by process().
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.
339 * @return true if the server should continue listening
341 public abstract boolean serverListen();
344 * Removes a message from the queue of pending messages.
346 * This method is called on the Worker Thread by the doInBackground() main loop.
348 * @return a message to be sent
350 protected NetworkMessage dequeueMessage() {
351 return this._sendMessageQueue.poll();
354 /* XXX: Methods below here all execute on the GUI Event Dispatch Thread */
358 * Fetch messages received by the server.
360 * For delivery to event listeners; usually Swing GUI components. This method will run on the
361 * Owner Thread so must complete quickly as that is the GUI Event Dispatch Thread.
363 * @param list messages received and queued
366 protected void process(List<NetworkMessage> list) {
367 for (NetworkMessage message: list) {
368 fireNetworkMessageEvent(message);
373 * Clean up after doInBackground() has returned.
375 * This method will run on the GUI Event Dispatch Thread so must complete quickly.
378 protected abstract void done();
381 * Adds a message to the queue of pending messages.
383 * This method will usually be called from the Owner Thread.
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
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();
395 throw new IllegalArgumentException("target cannot be null");
396 if(!_serviceToHostMap.isServiceValid(target))
397 throw new IllegalArgumentException("target service does not exist: " + target);
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
412 * Add a NetworkMessageEvent listener.
414 * Listens to all intents.
419 public synchronized void addNetworkMessageEventListener(NetworkMessageEventListener listener) {
420 _NetworkMessageEventListeners.add(new NetworkMessageEventListenerWithIntent(listener, null));
424 * Add a filtered NetworkMessageEvent listener.
426 * Filters on the intent of the NetworkMessage.
428 * @param intent null to listen to all intents, otherwise the intent to listen for
431 public synchronized void addNetworkMessageEventListener(NetworkMessageEventListener listener, String intent) {
432 _NetworkMessageEventListeners.add(new NetworkMessageEventListenerWithIntent(listener, intent));
436 * Remove a NetworkMessageEvent listener.
441 public synchronized void removeNetworkMessageEventListener(NetworkMessageEventListener listener) {
442 for (NetworkMessageEventListenerWithIntent intentListener : _NetworkMessageEventListeners)
443 if (intentListener._listener == listener)
444 _NetworkMessageEventListeners.remove(intentListener);
448 * Send a NetworkMessageEvent to all listeners.
450 * Only sends the message to listeners registered for the same intent, or for all messages.
452 * @param message the NetworkMessage to send
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);