* @author niki
*/
public class TraceHandler {
- private boolean showErrorDetails;
+ private boolean showErrors;
private boolean showTraces;
+ private boolean showErrorDetails;
/**
- * Show more details (usually equivalent to the value of DEBUG).
- *
- * @return TRUE or FALSE
+ * Create a default {@link TraceHandler} that will print errors on stderr
+ * (without details) and no traces.
*/
- public boolean isShowErrorDetails() {
- return showErrorDetails;
+ public TraceHandler() {
+ this(true, false, false);
}
/**
- * Show more details (usually equivalent to the value of DEBUG).
+ * Create a default {@link TraceHandler}.
*
+ * @param showErrors
+ * show errors on stderr
* @param showErrorDetails
- * TRUE or FALSE
- */
- public void setShowErrorDetails(boolean showErrorDetails) {
- this.showErrorDetails = showErrorDetails;
- }
-
- /**
- * Show DEBUG traces.
- *
- * @return TRUE or FALSE
- */
- public boolean isShowTraces() {
- return showTraces;
- }
-
- /**
- * Show DEBUG traces.
- *
+ * show more details when printing errors
* @param showTraces
- * TRUE or FALSE
+ * show traces on stdout
*/
- public void setShowTraces(boolean showTraces) {
+ public TraceHandler(boolean showErrors, boolean showErrorDetails,
+ boolean showTraces) {
+ this.showErrors = showErrors;
+ this.showErrorDetails = showErrorDetails;
this.showTraces = showTraces;
}
* the exception
*/
public void error(Exception e) {
- if (isShowErrorDetails()) {
- e.printStackTrace();
- } else {
- error(e.getMessage());
+ if (showErrors) {
+ if (showErrorDetails) {
+ e.printStackTrace();
+ } else {
+ error(e.getMessage());
+ }
}
}
* the error message
*/
public void error(String message) {
- System.err.println(message);
+ if (showErrors) {
+ System.err.println(message);
+ }
}
/**
* the trace message
*/
public void trace(String message) {
- if (isShowTraces()) {
+ if (showTraces) {
System.out.println(message);
}
}
}).start();
}
+ // connect, do the action (sync)
public void connect() {
try {
in = new BufferedReader(new InputStreamReader(s.getInputStream(),
NoSuchMethodException, ClassNotFoundException {
synchronized (lock) {
String rep = sendString(new Exporter().append(data).toString(true));
- return new Importer().read(rep).getValue();
+ if (rep != null) {
+ return new Importer().read(rep).getValue();
+ }
+
+ return null;
}
}
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
+import be.nikiroo.utils.TraceHandler;
import be.nikiroo.utils.Version;
+/**
+ * This class implements a simple server that can listen for connections and
+ * send/receive objects.
+ * <p>
+ * Note: this {@link Server} has to be discarded after use (cannot be started
+ * twice).
+ *
+ * @author niki
+ */
abstract public class Server implements Runnable {
static private final String[] ANON_CIPHERS = getAnonCiphers();
- private int port;
- private boolean ssl;
+ private final String name;
+ private final boolean ssl;
+ private final Object lock = new Object();
+ private final Object counterLock = new Object();
+
private ServerSocket ss;
+ private int port;
+
private boolean started;
private boolean exiting = false;
private int counter;
- private Object lock = new Object();
- private Object counterLock = new Object();
+
+ private TraceHandler tracer = new TraceHandler();
@Deprecated
public Server(@SuppressWarnings("unused") Version notUsed, int port,
this(port, ssl);
}
+ /**
+ * Create a new server that will start listening on the network when
+ * {@link Server#start()} is called.
+ *
+ * @param port
+ * the port to listen on, or 0 to assign any unallocated port
+ * found (which can later on be queried via
+ * {@link Server#getPort()}
+ * @param ssl
+ * use a SSL connection (or not)
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
public Server(int port, boolean ssl) throws IOException {
+ this((String) null, port, ssl);
+ }
+
+ /**
+ * Create a new server that will start listening on the network when
+ * {@link Server#start()} is called.
+ *
+ * @param name
+ * the server name (only used for debug info and traces)
+ * @param port
+ * the port to listen on
+ * @param ssl
+ * use a SSL connection (or not)
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
+ public Server(String name, int port, boolean ssl) throws IOException {
+ this.name = name;
this.port = port;
this.ssl = ssl;
this.ss = createSocketServer(port, ssl);
+
+ if (this.port == 0) {
+ this.port = this.ss.getLocalPort();
+ }
+ }
+
+ /**
+ * The traces handler for this {@link Server}.
+ *
+ * @return the traces handler
+ */
+ public TraceHandler getTraceHandler() {
+ return tracer;
}
+ /**
+ * The traces handler for this {@link Server}.
+ *
+ * @param tracer
+ * the new traces handler
+ */
+ public void setTraceHandler(TraceHandler tracer) {
+ if (tracer == null) {
+ tracer = new TraceHandler(false, false, false);
+ }
+
+ this.tracer = tracer;
+ }
+
+ /**
+ * Return the assigned port.
+ */
+ public int getPort() {
+ return port;
+ }
+
+ /**
+ * Start the server (listen on the network for new connections).
+ * <p>
+ * Can only be called once.
+ */
public void start() {
+ boolean ok = false;
synchronized (lock) {
- if (!started) {
+ if (!started && ss != null) {
started = true;
new Thread(this).start();
+ ok = true;
}
}
+
+ if (ok) {
+ tracer.trace(name + ": server started on port " + port);
+ } else if (ss == null) {
+ tracer.error(name + ": cannot start server on port " + port
+ + ", it has already been used");
+ } else {
+ tracer.error(name + ": cannot start server on port " + port
+ + ", it is already started");
+ }
}
+ /**
+ * Will stop the server, synchronously and without a timeout.
+ */
public void stop() {
+ tracer.trace(name + ": stopping server");
stop(0, true);
}
- // wait = wait before returning (sync VS async) timeout in ms, 0 or -1 for
- // never
+ /**
+ * Stop the server.
+ *
+ * @param timeout
+ * the maximum timeout to wait for existing actions to complete,
+ * or 0 for "no timeout"
+ * @param wait
+ * wait for the server to be stopped before returning
+ * (synchronous) or not (asynchronous)
+ */
public void stop(final long timeout, final boolean wait) {
if (wait) {
stop(timeout);
}
}
- // timeout in ms, 0 or -1 or never
+ /**
+ * Stop the server (synchronous).
+ *
+ * @param timeout
+ * the maximum timeout to wait for existing actions to complete,
+ * or 0 for "no timeout"
+ */
private void stop(long timeout) {
synchronized (lock) {
if (started && !exiting) {
@Override
public void run() {
try {
+ tracer.trace(name + ": server starting on port " + port);
while (started && !exiting) {
count(1);
Socket s = ss.accept();
}
}
+ /**
+ * This is the method that is called on each client request.
+ * <p>
+ * You are expected to react to it and return an answer (which can be NULL).
+ *
+ * @param action
+ * the client action
+ * @param clientVersion
+ * the client version
+ * @param data
+ * the data sent by the client (which can be NULL)
+ *
+ * @return the answer to return to the client (which can be NULL)
+ *
+ * @throws Exception
+ * in case of an exception, the error will only be logged
+ */
abstract protected Object onRequest(ConnectActionServer action,
Version clientVersion, Object data) throws Exception;
+ /**
+ * This method will be called on errors.
+ * <p>
+ * By default, it will only call the trace handler (so you may want to call
+ * super {@link Server#onError} if you override it).
+ *
+ * @param e
+ * the error
+ */
protected void onError(Exception e) {
- if (e == null) {
- e = new Exception("Unknown error");
- }
-
- e.printStackTrace();
+ tracer.error(e);
}
+ /**
+ * Change the number of currently serviced actions.
+ *
+ * @param change
+ * the number to increase or decrease
+ *
+ * @return the current number after this operation
+ */
private int count(int change) {
synchronized (counterLock) {
counter += change;
}
}
+ /**
+ * Create a {@link Socket}.
+ *
+ * @param host
+ * the host to connect to
+ * @param port
+ * the port to connect to
+ * @param ssl
+ * TRUE for SSL mode (or FALSE for plain text mode)
+ *
+ * @return the {@link Socket}
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
static Socket createSocket(String host, int port, boolean ssl)
throws IOException {
Socket s;
return s;
}
+ /**
+ * Create a {@link ServerSocket}.
+ *
+ * @param port
+ * the port to accept connections on
+ * @param ssl
+ * TRUE for SSL mode (or FALSE for plain text mode)
+ *
+ * @return the {@link ServerSocket}
+ *
+ * @throws IOException
+ * in case of I/O error
+ */
static ServerSocket createSocketServer(int port, boolean ssl)
throws IOException {
ServerSocket ss;
return ss;
}
+ /**
+ * Return all the supported ciphers that do not use authentication.
+ *
+ * @return the list of such supported ciphers
+ */
private static String[] getAnonCiphers() {
List<String> anonCiphers = new ArrayList<String>();
for (String cipher : ((SSLSocketFactory) SSLSocketFactory.getDefault())
package be.nikiroo.utils.test;
import be.nikiroo.utils.Version;
+import be.nikiroo.utils.serial.ConnectActionClient;
import be.nikiroo.utils.serial.ConnectActionServer;
import be.nikiroo.utils.serial.Exporter;
import be.nikiroo.utils.serial.Importer;
super("Serial test", null);
}
+ private TestCase[] createServerTestCases(final boolean ssl) {
+ final String ssls = (ssl ? "(ssl)" : "(plain text)");
+ return new TestCase[] {
+ new TestCase("Client/Server simple connection " + ssls) {
+ @Override
+ public void test() throws Exception {
+ final Object[] rec = new Object[1];
+
+ Server server = new Server("testy", 0, ssl) {
+ @Override
+ protected Object onRequest(
+ ConnectActionServer action,
+ Version clientVersion, Object data)
+ throws Exception {
+ return null;
+ }
+ };
+
+ assertEquals("A port should have been assigned", true,
+ server.getPort() > 0);
+
+ server.start();
+
+ try {
+ new ConnectActionClient(null, server.getPort(), ssl) {
+ @Override
+ public void action(Version serverVersion)
+ throws Exception {
+ rec[0] = true;
+ }
+ }.connect();
+ } finally {
+ server.stop();
+ }
+
+ assertNotNull("The client action was not run", rec[0]);
+ assertEquals(true, (boolean) ((Boolean) rec[0]));
+ }
+ }, //
+ new TestCase("Client/Server simple exchange " + ssls) {
+ final Object[] rec = new Object[3];
+
+ @Override
+ public void test() throws Exception {
+ Server server = new Server("testy", 0, ssl) {
+ @Override
+ protected Object onRequest(
+ ConnectActionServer action,
+ Version clientVersion, Object data)
+ throws Exception {
+ rec[0] = data;
+ return "pong";
+ }
+
+ @Override
+ protected void onError(Exception e) {
+ super.onError(e);
+ rec[2] = e;
+ }
+ };
+
+ assertEquals("A port should have been assigned", true,
+ server.getPort() > 0);
+
+ server.start();
+
+ try {
+ new ConnectActionClient(null, server.getPort(), ssl) {
+ @Override
+ public void action(Version serverVersion)
+ throws Exception {
+ rec[1] = send("ping");
+ }
+ }.connect();
+ } finally {
+ server.stop();
+ }
+
+ if (rec[2] != null) {
+ fail("An exception was thrown: "
+ + ((Exception) rec[2]).getMessage());
+ }
+
+ assertEquals("ping", rec[0]);
+ assertEquals("pong", rec[1]);
+ }
+ }, //
+ new TestCase("Client/Server multiple exchanges " + ssls) {
+ final Object[] sent = new Object[3];
+ final Object[] recd = new Object[3];
+ final Exception[] err = new Exception[1];
+
+ @Override
+ public void test() throws Exception {
+ Server server = new Server("testy", 0, ssl) {
+ @Override
+ protected Object onRequest(
+ ConnectActionServer action,
+ Version clientVersion, Object data)
+ throws Exception {
+ sent[0] = data;
+ action.send("pong");
+ sent[1] = action.flush();
+ return "pong2";
+ }
+
+ @Override
+ protected void onError(Exception e) {
+ super.onError(e);
+ err[0] = e;
+ }
+ };
+
+ server.start();
+
+ try {
+ new ConnectActionClient(null, server.getPort(), ssl) {
+ @Override
+ public void action(Version serverVersion)
+ throws Exception {
+ recd[0] = send("ping");
+ recd[1] = send("ping2");
+ }
+ }.connect();
+ } finally {
+ server.stop();
+ }
+
+ if (err[0] != null) {
+ fail("An exception was thrown: "
+ + err[0].getMessage());
+ }
+
+ assertEquals("ping", sent[0]);
+ assertEquals("pong", recd[0]);
+ assertEquals("ping2", sent[1]);
+ assertEquals("pong2", recd[1]);
+ }
+ }, //
+ new TestCase("Client/Server multiple call from client " + ssls) {
+ final Object[] sent = new Object[3];
+ final Object[] recd = new Object[3];
+ final Exception[] err = new Exception[1];
+
+ @Override
+ public void test() throws Exception {
+ Server server = new Server("testy", 0, ssl) {
+ @Override
+ protected Object onRequest(
+ ConnectActionServer action,
+ Version clientVersion, Object data)
+ throws Exception {
+ sent[(Integer) data] = data;
+ return ((Integer) data) * 2;
+ }
+
+ @Override
+ protected void onError(Exception e) {
+ super.onError(e);
+ err[0] = e;
+ }
+ };
+
+ server.start();
+
+ try {
+ new ConnectActionClient(null, server.getPort(), ssl) {
+ @Override
+ public void action(Version serverVersion)
+ throws Exception {
+ for (int i = 0; i < 3; i++) {
+ recd[i] = send(i);
+ }
+ }
+ }.connect();
+ } finally {
+ server.stop();
+ }
+
+ if (err[0] != null) {
+ fail("An exception was thrown: "
+ + err[0].getMessage());
+ }
+
+ assertEquals(0, sent[0]);
+ assertEquals(0, recd[0]);
+ assertEquals(1, sent[1]);
+ assertEquals(2, recd[1]);
+ assertEquals(2, sent[2]);
+ assertEquals(4, recd[2]);
+ }
+ }, //
+ };
+ }
+
public SerialTest(String[] args) {
super("Serial test", args);
+ for (TestCase test : createServerTestCases(false)) {
+ addTest(test);
+ }
+
+ for (TestCase test : createServerTestCases(true)) {
+ addTest(test);
+ }
+
addTest(new TestCase("Simple class Import/Export") {
@Override
public void test() throws Exception {