4 * Copyright 2015 Eddie Berrisford-Lynch <dev@fun2be.me>.
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 Eddie Berrisford-Lynch <dev@fun2be.me>
68 public abstract class NetworkServerAbstract extends SwingWorker<Integer, NetworkMessage> {
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 private final NetworkMessageEventListenerManager _eventManager;
107 * @param level message importance
108 * @param title source identifier
109 * @param formatter parameter Formatter for log message
110 * @param parameters variable length list of replaceable parameters for formatter
112 protected static void log(Level level, String title, String formatter, ArrayList<String> parameters) {
115 // formatter = "{" + Integer.toString(parameters.size()) + "}: " + formatter;
116 // parameters.add(title);
117 LOGGER.logp(level, title, null, MessageFormat.format(formatter, parameters.toArray()));
121 * @param level message importance
122 * @param title source identifier
123 * @param message the log entry
125 protected static void log(Level level, String title, String message) {
128 LOGGER.logp(level, title, null, message);
132 * Set the log level for the server
133 * @param level a new log level
134 * @return the old log level
136 public Level setLogLevel(Level level) {
137 Level result = Level.OFF;
138 if (LOGGER != null) {
139 Level temp = LOGGER.getLevel();
140 LOGGER.setLevel(level);
147 * Default constructor.
149 NetworkServerAbstract() {
150 this._connectionCount = 0;
152 this._socketAddress = null;
153 this._serviceToHostMap = null;
154 this._eventManager = null;
158 * Construct the server with a Logger.
160 * No socket is opened.
162 * @param socketAddress The socket to listen on
163 * @param title source identifier for use in log messages and sent NetworkMessage objects
164 * @param serviceToHostMap the map object used for host <> InetSocketAddress lookups
165 * @param logger An instance of Logger to be used by all objects of this class
167 public NetworkServerAbstract(WSYD_SocketAddress socketAddress, String title, ServiceAddressMap serviceToHostMap, Logger logger) {
168 this._connectionCount = 0;
170 this._socketAddress = socketAddress;
171 this._serviceToHostMap = serviceToHostMap;
172 this._eventManager = new NetworkMessageEventListenerManager();
173 if (LOGGER == null) // do not replace existing logger reference
178 * Construct the server without a Logger.
180 * No socket is opened.
182 * @param socketAddress The socket to listen on
183 * @param title source identifier for use in log messages and sent NetworkMessage objects
184 * @param serviceToHostMap the map object used for host <> InetSocketAddress lookups
186 public NetworkServerAbstract(WSYD_SocketAddress socketAddress, String title, ServiceAddressMap serviceToHostMap) {
187 this(socketAddress, title, serviceToHostMap, null);
191 * Get the socket in use - not that (possibly wildcard/ephermeral) requested.
193 * @return the port being used
195 public WSYD_SocketAddress getSocketAddress() {
196 return _socketAddress;
200 * Enable or disable simulated received packet injection.
202 * @param simulate true to simulate received messages
204 public void setSimulate(boolean simulate) {
205 this._simulate = simulate;
209 * Get the simulation state.
211 * @return true if simulation is enabled.
213 public boolean getSimulate() {
214 return this._simulate;
218 /* XXX: The following Methods execute on the background Worker Thread */
221 * The primary SwingWorker method, started on the Worker Thread when the Owner
222 * Thread calls execute().
224 * Loops until isCancelled() == true. Within the loop calls serverListen() to
225 * allow reception of one packet or connection and if so counts it.
226 * Then it checks if there are any messages to be sent out and if so calls
229 * @return the number of connections accepted
232 public Integer doInBackground() {
233 ArrayList<String> logMessages = new ArrayList<>();
235 logMessages.add(_socketAddress.toString());
236 log(Level.INFO, _title, "Opening socket {0}", logMessages);
239 catch(SocketException e) {
241 logMessages.add(_socketAddress.getAddress().toString());
242 logMessages.add(Integer.toString(_socketAddress.getPort()));
243 logMessages.add(_socketAddress.getProtocol().toString());
244 log(Level.SEVERE, _title, "{0}: Unable to open socket on {1}:{2} {3}", logMessages);
247 // unless cancelled keep waiting for new packets or connections
248 while (!this.isCancelled()) {
249 if (this.serverListen())
250 this._connectionCount++;
252 // send a queued message
253 NetworkMessage temp = this.sendMessage();
255 if (!this.serverSend(temp)) {
257 logMessages.add(temp.getSender());
258 logMessages.add(temp.getTarget());
259 log(Level.WARNING, _title, "Unable to send message from {0} to {1}", logMessages);
266 logMessages.add(_socketAddress.toString());
267 log(Level.INFO, _title, "Closing socket {0}", logMessages);
270 catch(SocketException e) {
272 logMessages.add(_socketAddress.getAddress().toString());
273 logMessages.add(Integer.toString(_socketAddress.getPort()));
274 logMessages.add(_socketAddress.getProtocol().toString());
275 log(Level.SEVERE, _title, "{0}: Unable to close socket on {1}:{2} {3}", logMessages);
278 return this._connectionCount;
282 * Removes a message from a queue of pending messages.
284 * This method is called on the Worker Thread by the doInBackground() main loop.
286 * Sub-classes that have the ability to transmit messages should implement this fully.
288 * @return a message to be sent
290 protected abstract NetworkMessage sendMessage();
292 * Open the socket ready for accepting data or connections.
294 * It should also set a reasonable socket timeout with a call to setSoTimeout()
296 * @see java.net.ServerSocket#setSoTimeout
297 * @see java.net.DatagramSocket#setSoTimeout
298 * @throws SocketException
300 public abstract void serverOpen() throws SocketException;
305 * @throws SocketException
307 public abstract void serverClose() throws SocketException;
310 * Send an unsolicited message to a remote service.
312 * This method is called by the main worker loop if there is a message to
315 * @param message must have its _serviceTarget parameter set
316 * @return true if the message was sent
318 protected abstract boolean serverSend(NetworkMessage message);
321 * Accept packet or connection from remote hosts.
323 * This method must wait for a single incoming connection or packet, process it,
324 * and then publish() it for consumption by process().
326 * It must add newly seen remote service names to _serviceToHostMap so that
327 * methods on the Owner Thread can discover the destination service titles
328 * they can use in new NetworkMessage submissions.
330 * @return true if the server should continue listening
332 public abstract boolean serverListen();
334 /* XXX: Methods below here all execute on the GUI Event Dispatch Thread */
337 public NetworkMessageEventListenerManager getEventManager() {
338 return this._eventManager;
342 * Fetch messages received by the server.
344 * For delivery to event listeners; usually Swing GUI components. This method will run on the
345 * Owner Thread so must complete quickly as that is the GUI Event Dispatch Thread.
347 * @param list messages received and queued
350 protected void process(List<NetworkMessage> list) {
351 for (NetworkMessage message: list) {
352 this._eventManager.fireNetworkMessageEvent(message);
357 * Clean up after doInBackground() has returned.
359 * This method will run on the GUI Event Dispatch Thread so must complete quickly.
362 protected abstract void done();