WebSocket API
This commit is contained in:
parent
b6dba62fa4
commit
fd828d600e
@ -96,9 +96,14 @@ public class ThrowableUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the provided Throwable unckecked if necessary.
|
||||||
private static RuntimeException uncheck(Throwable t, boolean convertReflectionExceptionToError) {
|
* @param t the throwable to eventually wrap into a {@link RuntimeException}.
|
||||||
|
* @param convertReflectionExceptionToError true to convert reflection related exception to their error counterpart.
|
||||||
|
* @return a {@link RuntimeException}
|
||||||
|
* @throws Error if one is passed as the parameter.
|
||||||
|
*/
|
||||||
|
public static RuntimeException uncheck(Throwable t, boolean convertReflectionExceptionToError) {
|
||||||
if (t instanceof Error er) {
|
if (t instanceof Error er) {
|
||||||
throw er;
|
throw er;
|
||||||
}
|
}
|
||||||
|
24
pandalib-ws-client/pom.xml
Normal file
24
pandalib-ws-client/pom.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>pandalib-parent</artifactId>
|
||||||
|
<groupId>fr.pandacube.lib</groupId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<relativePath>../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>pandalib-ws-client</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>fr.pandacube.lib</groupId>
|
||||||
|
<artifactId>pandalib-ws</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
@ -0,0 +1,117 @@
|
|||||||
|
package fr.pandacube.lib.ws.client;
|
||||||
|
|
||||||
|
import fr.pandacube.lib.util.Log;
|
||||||
|
import fr.pandacube.lib.util.ThrowableUtil;
|
||||||
|
import fr.pandacube.lib.ws.AbstractWS;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.WebSocket;
|
||||||
|
import java.net.http.WebSocket.Listener;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.CompletionStage;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
// TODO implement auto-reconnect
|
||||||
|
/**
|
||||||
|
* Minimal implementation of a Websocket client endpoint using the java.net.http Websocket API.
|
||||||
|
*/
|
||||||
|
public abstract class AbstractClientWS implements AbstractWS {
|
||||||
|
|
||||||
|
private final URI uri;
|
||||||
|
private boolean autoReconnect;
|
||||||
|
private final AtomicReference<WebSocket> socket = new AtomicReference<>();
|
||||||
|
|
||||||
|
|
||||||
|
private final Listener receiveListener = new Listener() {
|
||||||
|
@Override
|
||||||
|
public void onOpen(WebSocket webSocket) {
|
||||||
|
AbstractClientWS.this.onConnect();
|
||||||
|
Listener.super.onOpen(webSocket);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
|
||||||
|
AbstractClientWS.this.handleReceivedMessage(data.toString());
|
||||||
|
return Listener.super.onText(webSocket, data, last);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletionStage<?> onBinary(WebSocket webSocket, ByteBuffer data, boolean last) {
|
||||||
|
AbstractClientWS.this.handleReceivedBinary();
|
||||||
|
return Listener.super.onBinary(webSocket, data, last);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
|
||||||
|
AbstractClientWS.this.onClose(statusCode, reason);
|
||||||
|
return Listener.super.onClose(webSocket, statusCode, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(WebSocket webSocket, Throwable error) {
|
||||||
|
AbstractClientWS.this.onError(error);
|
||||||
|
Listener.super.onError(webSocket, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Websocket client that connect to the provided url.
|
||||||
|
* @param uri the destination endpoint.
|
||||||
|
* @param autoReconnect if this websocket should automatically try to reconnect when disconnected.
|
||||||
|
* @throws URISyntaxException if the provided URI is invalid.
|
||||||
|
*/
|
||||||
|
public AbstractClientWS(String uri, boolean autoReconnect) throws URISyntaxException {
|
||||||
|
this.uri = new URI(uri);
|
||||||
|
this.autoReconnect = autoReconnect;
|
||||||
|
if (autoReconnect) {
|
||||||
|
Log.warning("Websocket client auto-reconnect is not yet implemented.");
|
||||||
|
}
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void connect() {
|
||||||
|
synchronized (socket) {
|
||||||
|
socket.set(HttpClient.newHttpClient()
|
||||||
|
.newWebSocketBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(2))
|
||||||
|
.buildAsync(uri, receiveListener)
|
||||||
|
.join()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void sendString(String message) throws IOException {
|
||||||
|
try {
|
||||||
|
synchronized (socket) {
|
||||||
|
socket.get().sendText(message, true).join();
|
||||||
|
}
|
||||||
|
} catch (CompletionException ce) {
|
||||||
|
if (ce.getCause() instanceof IOException ioe)
|
||||||
|
throw ioe;
|
||||||
|
throw ThrowableUtil.uncheck(ce.getCause(), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRemoteIdentifier() {
|
||||||
|
return uri.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void sendClose(int code, String reason) throws IOException {
|
||||||
|
synchronized (socket) {
|
||||||
|
autoReconnect = false; // if we ask for closing connection, dont reconnect automatically
|
||||||
|
socket.get().sendClose(code, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
package fr.pandacube.lib.ws.client;
|
||||||
|
|
||||||
|
import fr.pandacube.lib.ws.payloads.ErrorPayload;
|
||||||
|
import fr.pandacube.lib.ws.payloads.LoginPayload;
|
||||||
|
import fr.pandacube.lib.ws.payloads.LoginSucceedPayload;
|
||||||
|
import fr.pandacube.lib.ws.payloads.Payload;
|
||||||
|
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Websocket client that implements the login logic with a key protected server endpoint.
|
||||||
|
*/
|
||||||
|
public abstract class KeyProtectedClientWS extends AbstractClientWS {
|
||||||
|
|
||||||
|
private final String key;
|
||||||
|
|
||||||
|
private boolean loginSucceed = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Websocket client that connect to the provided url and uses the provided key.
|
||||||
|
* @param uri the destination endpoint.
|
||||||
|
* @param autoReconnect if this websocket should automatically try to reconnect when disconnected.
|
||||||
|
* @param key the login key.
|
||||||
|
* @throws URISyntaxException if the provided URI is invalid.
|
||||||
|
*/
|
||||||
|
public KeyProtectedClientWS(String uri, boolean autoReconnect, String key) throws URISyntaxException {
|
||||||
|
super(uri, autoReconnect);
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onConnect() {
|
||||||
|
trySendAsJson(new LoginPayload(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onReceivePayload(Payload payload) {
|
||||||
|
if (loginSucceed) {
|
||||||
|
onReceivePayloadLoggedIn(payload);
|
||||||
|
}
|
||||||
|
else if (payload instanceof LoginSucceedPayload) {
|
||||||
|
loginSucceed = true;
|
||||||
|
onLoginSucceed();
|
||||||
|
}
|
||||||
|
else if (payload instanceof ErrorPayload err){
|
||||||
|
logError("Received ErrorPayload instead of LoginSuccessPayload: " + err.message, err.throwable);
|
||||||
|
trySendClose();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logError("Received unexpected Payload instead of LoginSuccessPayload: " + payload.getClass().getSimpleName());
|
||||||
|
trySendClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onClose(int code, String reason) {
|
||||||
|
if (loginSucceed) {
|
||||||
|
loginSucceed = false;
|
||||||
|
onCloseLoggedIn(code, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onError(Throwable cause) {
|
||||||
|
if (loginSucceed) {
|
||||||
|
loginSucceed = false;
|
||||||
|
onErrorLoggedIn(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when this Websocket is succesfully logged in to the server.
|
||||||
|
*/
|
||||||
|
public abstract void onLoginSucceed();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on reception of a valid payload when already logged in.
|
||||||
|
* @param payload the received payload.
|
||||||
|
*/
|
||||||
|
public abstract void onReceivePayloadLoggedIn(Payload payload);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on reception of a websocket Close packet, only if this client is already logged in.
|
||||||
|
* The connection is closed after this method call.
|
||||||
|
* @param code the close code. 1000 for a normal closure.
|
||||||
|
* @param reason the close reason.
|
||||||
|
*/
|
||||||
|
public abstract void onCloseLoggedIn(int code, String reason);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an error occurs with the websocket API, only if this client is already logged in.
|
||||||
|
* The connection is already closed when this method is called.
|
||||||
|
* @param cause the error cause.
|
||||||
|
*/
|
||||||
|
public abstract void onErrorLoggedIn(Throwable cause);
|
||||||
|
}
|
29
pandalib-ws-server/pom.xml
Normal file
29
pandalib-ws-server/pom.xml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>pandalib-parent</artifactId>
|
||||||
|
<groupId>fr.pandacube.lib</groupId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<relativePath>../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>pandalib-ws-server</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>fr.pandacube.lib</groupId>
|
||||||
|
<artifactId>pandalib-ws</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty.websocket</groupId>
|
||||||
|
<artifactId>websocket-jetty-api</artifactId>
|
||||||
|
<version>10.0.5</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
@ -0,0 +1,60 @@
|
|||||||
|
package fr.pandacube.lib.ws.server;
|
||||||
|
|
||||||
|
import fr.pandacube.lib.ws.AbstractWS;
|
||||||
|
import org.eclipse.jetty.websocket.api.Session;
|
||||||
|
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal implementation of a Websocket server endpoint using the Jetty Websocket API.
|
||||||
|
*/
|
||||||
|
public abstract class AbstractServerWS extends WebSocketAdapter implements AbstractWS {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onWebSocketConnect(Session sess)
|
||||||
|
{
|
||||||
|
super.onWebSocketConnect(sess);
|
||||||
|
onConnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onWebSocketBinary(byte[] payload, int offset, int len) {
|
||||||
|
handleReceivedBinary();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onWebSocketText(String message) {
|
||||||
|
handleReceivedMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onWebSocketClose(int statusCode, String reason) {
|
||||||
|
onClose(statusCode, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onWebSocketError(Throwable cause) {
|
||||||
|
onError(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public final void sendString(String message) throws IOException {
|
||||||
|
getSession().getRemote().sendString(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void sendClose(int code, String reason) throws IOException {
|
||||||
|
getSession().close(code, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getRemoteIdentifier() {
|
||||||
|
return getSession().getRemoteAddress().toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
package fr.pandacube.lib.ws.server;
|
||||||
|
|
||||||
|
import fr.pandacube.lib.ws.payloads.ErrorPayload;
|
||||||
|
import fr.pandacube.lib.ws.payloads.LoginPayload;
|
||||||
|
import fr.pandacube.lib.ws.payloads.LoginSucceedPayload;
|
||||||
|
import fr.pandacube.lib.ws.payloads.Payload;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Websocket server endpoint that is protected with a key.
|
||||||
|
*/
|
||||||
|
public abstract class KeyProtectedServerWS extends AbstractServerWS {
|
||||||
|
|
||||||
|
private boolean loginSucceed = false;
|
||||||
|
|
||||||
|
private final Supplier<String> keySupplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a websocket server endpoint protected by the key given by the provided {@link Supplier}.
|
||||||
|
* @param keySupplier a {@link Supplier} for the key.
|
||||||
|
*/
|
||||||
|
public KeyProtectedServerWS(Supplier<String> keySupplier) {
|
||||||
|
this.keySupplier = keySupplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onConnect() {
|
||||||
|
// nothing, just wait for the client to login
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onReceivePayload(Payload payload) {
|
||||||
|
if (loginSucceed) {
|
||||||
|
onReceivePayloadLoggedIn(payload);
|
||||||
|
}
|
||||||
|
else if (payload instanceof LoginPayload login) {
|
||||||
|
if (keySupplier.get().equals(login.key)) {
|
||||||
|
loginSucceed = true;
|
||||||
|
trySendAsJson(new LoginSucceedPayload());
|
||||||
|
onLoginSucceed();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logAndTrySendError(new ErrorPayload("Bad key"));
|
||||||
|
trySendClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logAndTrySendError(new ErrorPayload("Please use the login packet first. Received " + payload.getClass().getSimpleName() + " instead."));
|
||||||
|
trySendClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onClose(int code, String reason) {
|
||||||
|
if (loginSucceed) {
|
||||||
|
onCloseLoggedIn(code, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void onError(Throwable cause) {
|
||||||
|
if (loginSucceed) {
|
||||||
|
onErrorLoggedIn(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the client endpoint is succesfully logged in.
|
||||||
|
*/
|
||||||
|
public abstract void onLoginSucceed();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on reception of a valid payload from the already logged in client.
|
||||||
|
* @param payload the received payload.
|
||||||
|
*/
|
||||||
|
public abstract void onReceivePayloadLoggedIn(Payload payload);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on reception of a websocket Close packet, only if the client endpoint is already logged in.
|
||||||
|
* The connection is closed after this method call.
|
||||||
|
* @param code the close code. 1000 for a normal closure.
|
||||||
|
* @param reason the close reason.
|
||||||
|
*/
|
||||||
|
public abstract void onCloseLoggedIn(int code, String reason);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an error occurs with the websocket API, only if the client endpoint is already logged in.
|
||||||
|
* The connection is already closed when this method is called.
|
||||||
|
* @param cause the error cause.
|
||||||
|
*/
|
||||||
|
public abstract void onErrorLoggedIn(Throwable cause);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
24
pandalib-ws/pom.xml
Normal file
24
pandalib-ws/pom.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>pandalib-parent</artifactId>
|
||||||
|
<groupId>fr.pandacube.lib</groupId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<relativePath>../pom.xml</relativePath>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<artifactId>pandalib-ws</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>fr.pandacube.lib</groupId>
|
||||||
|
<artifactId>pandalib-core</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
257
pandalib-ws/src/main/java/fr/pandacube/lib/ws/AbstractWS.java
Normal file
257
pandalib-ws/src/main/java/fr/pandacube/lib/ws/AbstractWS.java
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
package fr.pandacube.lib.ws;
|
||||||
|
|
||||||
|
import fr.pandacube.lib.util.Log;
|
||||||
|
import fr.pandacube.lib.util.ThrowableUtil.RunnableException;
|
||||||
|
import fr.pandacube.lib.ws.payloads.ErrorPayload;
|
||||||
|
import fr.pandacube.lib.ws.payloads.Payload;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Super interface for all websocket endpoint, either client or server, that provides minimal implementation of our
|
||||||
|
* custom sub-protocol.
|
||||||
|
*/
|
||||||
|
public interface AbstractWS {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Receiving
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the reception of a message, by deserializing the payloads and handling eventual deserialization errors.
|
||||||
|
* @param message the raw message received.
|
||||||
|
*/
|
||||||
|
default void handleReceivedMessage(String message) {
|
||||||
|
Payload payload;
|
||||||
|
try {
|
||||||
|
payload = PayloadRegistry.fromString(message);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
logAndTrySendError(new ErrorPayload(e.getMessage())); // no need to log or send full exception stack trace
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
onReceivePayload(payload);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
trySendAsJson(new ErrorPayload("Error handling payload: " + t));
|
||||||
|
if (t instanceof Exception)
|
||||||
|
logError("Error handling payload", t);
|
||||||
|
else
|
||||||
|
throw t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the reception of binary data. The default implementation reject any binary data by sending an
|
||||||
|
* {@link ErrorPayload} to the remote endpoint.
|
||||||
|
*/
|
||||||
|
default void handleReceivedBinary() {
|
||||||
|
trySendAsJson(new ErrorPayload("Cannot accept binary payload."));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the websocket connection is established.
|
||||||
|
*/
|
||||||
|
void onConnect();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on reception of a valid {@link Payload}.
|
||||||
|
* @param payload the received {@link Payload}.
|
||||||
|
*/
|
||||||
|
void onReceivePayload(Payload payload);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on reception of a websocket Close packet.
|
||||||
|
* The connection is closed after this method call.
|
||||||
|
* @param code the close code. 1000 for a normal closure.
|
||||||
|
* @param reason the close reason.
|
||||||
|
*/
|
||||||
|
void onClose(int code, String reason);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an error occurs with the websocket API.
|
||||||
|
* The connection is already closed when this method is called.
|
||||||
|
* @param cause the error cause.
|
||||||
|
*/
|
||||||
|
void onError(Throwable cause);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sending
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the provided raw string to the remote endpoint.
|
||||||
|
* <p>
|
||||||
|
* <b>It is not advised for subclasses to call directly this method.
|
||||||
|
* Please use {@link #sendAsJson(Payload)} or {@link #sendAsJson(String, Object, boolean)} instead.</b>
|
||||||
|
* @param message the raw message to send.
|
||||||
|
* @throws IOException if an IO error occurs when sending the message.
|
||||||
|
*/
|
||||||
|
void sendString(String message) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the provided type and object that will be serialized using
|
||||||
|
* {@link PayloadRegistry#arbitraryToString(String, Object, boolean)}.
|
||||||
|
* @param type the type.
|
||||||
|
* @param obj the object to Jsonify.
|
||||||
|
* @param serializeNulls if null propreties must be included in the json object.
|
||||||
|
* @throws IOException if an IO error occurs when sending the data.
|
||||||
|
* @see PayloadRegistry#arbitraryToString(String, Object, boolean)
|
||||||
|
*/
|
||||||
|
default void sendAsJson(String type, Object obj, boolean serializeNulls) throws IOException {
|
||||||
|
sendString(PayloadRegistry.arbitraryToString(type, obj, serializeNulls));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the provided type and object that will be serialized using
|
||||||
|
* {@link PayloadRegistry#arbitraryToString(String, Object, boolean)}.
|
||||||
|
* @param type the type.
|
||||||
|
* @param obj the object to Jsonify.
|
||||||
|
* @param serializeNulls if null propreties must be included in the json object.
|
||||||
|
* @return true if the data is sent successfully, false if an IO error occurs.
|
||||||
|
* @see PayloadRegistry#arbitraryToString(String, Object, boolean)
|
||||||
|
*/
|
||||||
|
default boolean trySendAsJson(String type, Object obj, boolean serializeNulls) {
|
||||||
|
return trySend(() -> sendAsJson(type, obj, serializeNulls), "Error sending object as json");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the provided {@link Payload} to the remote endpoint.
|
||||||
|
* @param payload the {@link Payload} to send.
|
||||||
|
* @throws IOException if an IO error occurs when sending the data.
|
||||||
|
* @see PayloadRegistry#toString(Payload)
|
||||||
|
*/
|
||||||
|
default void sendAsJson(Payload payload) throws IOException {
|
||||||
|
sendString(PayloadRegistry.toString(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the provided {@link Payload} to the remote endpoint.
|
||||||
|
* @param payload the {@link Payload} to send.
|
||||||
|
* @return true if the data is sent successfully, false if an IO error occurs.
|
||||||
|
* @see PayloadRegistry#toString(Payload)
|
||||||
|
*/
|
||||||
|
default boolean trySendAsJson(Payload payload) {
|
||||||
|
return trySend(() -> sendAsJson(payload), "Error sending payload as json");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gracefully closes the connection by sending the close packet with the provided data.
|
||||||
|
* @param code the status code.
|
||||||
|
* @param reason the reason.
|
||||||
|
* @throws IOException if an IO error occurs when sending the close packet.
|
||||||
|
*/
|
||||||
|
void sendClose(int code, String reason) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gracefully closes the connection by sending the close packet with the provided data.
|
||||||
|
* @param code the status code.
|
||||||
|
* @param reason the reason.
|
||||||
|
* @return true if the data is sent successfully, false if an IO error occurs.
|
||||||
|
*/
|
||||||
|
default boolean trySendClose(int code, String reason) {
|
||||||
|
return trySend(() -> sendClose(code, reason), "Error sending close");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gracefully closes the connection by sending the close packet with the default status code (1000) and an empty
|
||||||
|
* reason.
|
||||||
|
* @throws IOException if an IO error occurs when sending the close packet.
|
||||||
|
*/
|
||||||
|
default void sendClose() throws IOException {
|
||||||
|
sendClose(1000, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gracefully closes the connection by sending the close packet with the default status code (1000) and an empty
|
||||||
|
* reason.
|
||||||
|
* @return true if the data is sent successfully, false if an IO error occurs.
|
||||||
|
*/
|
||||||
|
default boolean trySendClose() {
|
||||||
|
return trySend(this::sendClose, "Error sending close");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the error from the provided {@link ErrorPayload} and sends it to the remote endpoint.
|
||||||
|
* @param p the {@link ErrorPayload}.
|
||||||
|
* @throws IOException if an IO error occurs when sending the data.
|
||||||
|
*/
|
||||||
|
default void logAndSendError(ErrorPayload p) throws IOException {
|
||||||
|
logError(p.message, p.throwable);
|
||||||
|
sendAsJson(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the error from the provided {@link ErrorPayload} and sends it to the remote endpoint.
|
||||||
|
* @param p the {@link ErrorPayload}.
|
||||||
|
* @return true if the data is sent successfully, false if an IO error occurs.
|
||||||
|
*/
|
||||||
|
default boolean logAndTrySendError(ErrorPayload p) {
|
||||||
|
return trySend(() -> logAndSendError(p), "Error sending error payload as json");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to wrap sending operation into a try-catch.
|
||||||
|
* @param run the sending operation that may throw an {@link IOException}.
|
||||||
|
* @param errorMessage the error message to log if the runnable throws an {@link IOException}.
|
||||||
|
* @return true if the data if the runnable is executed successfully, false if an IO error occurs.
|
||||||
|
*/
|
||||||
|
default boolean trySend(RunnableException<IOException> run, String errorMessage) {
|
||||||
|
try {
|
||||||
|
run.run();
|
||||||
|
return true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
logError(errorMessage, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Log
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the provided message with logger level {@link Level#INFO}, prefixed with infos avout this web-socket.
|
||||||
|
* @param message the message to log.
|
||||||
|
*/
|
||||||
|
default void log(String message) {
|
||||||
|
Log.info(formatLogMessage(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the provided message with logger level {@link Level#SEVERE}, prefixed with infos avout this web-socket.
|
||||||
|
* @param message the message to log.
|
||||||
|
*/
|
||||||
|
default void logError(String message) {
|
||||||
|
logError(message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the provided message and {@link Throwable} with logger level {@link Level#SEVERE}, prefixed with infos avout this web-socket.
|
||||||
|
* @param message the message to log.
|
||||||
|
* @param t the throwable to log.
|
||||||
|
*/
|
||||||
|
default void logError(String message, Throwable t) {
|
||||||
|
Log.severe(formatLogMessage(message), t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an identifier for this web-socket, used for logging. May be the remote IP:port or URI.
|
||||||
|
* @return an identifier for this web-socket.
|
||||||
|
*/
|
||||||
|
String getRemoteIdentifier();
|
||||||
|
|
||||||
|
private String formatLogMessage(String message) {
|
||||||
|
return "[WS/" + getClass().getSimpleName() + "] [" + getRemoteIdentifier() + "] " + message;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
package fr.pandacube.lib.ws;
|
||||||
|
|
||||||
|
import com.google.gson.JsonParseException;
|
||||||
|
import fr.pandacube.lib.core.json.Json;
|
||||||
|
import fr.pandacube.lib.util.BiMap;
|
||||||
|
import fr.pandacube.lib.ws.payloads.ErrorPayload;
|
||||||
|
import fr.pandacube.lib.ws.payloads.LoginPayload;
|
||||||
|
import fr.pandacube.lib.ws.payloads.LoginSucceedPayload;
|
||||||
|
import fr.pandacube.lib.ws.payloads.MessagePayload;
|
||||||
|
import fr.pandacube.lib.ws.payloads.Payload;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the registration of all the {@link Payload} types, the serialization and deserialization of the payload data.
|
||||||
|
*/
|
||||||
|
public class PayloadRegistry {
|
||||||
|
|
||||||
|
private static final String PAYLOAD_TYPE_SEPARATOR = ":";
|
||||||
|
|
||||||
|
private static final BiMap<String, Class<? extends Payload>> payloadClasses = new BiMap<>();
|
||||||
|
|
||||||
|
|
||||||
|
private static boolean isTypeValid(String type) {
|
||||||
|
return !type.contains(PAYLOAD_TYPE_SEPARATOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String validateType(String type) {
|
||||||
|
if (isTypeValid(type))
|
||||||
|
return type;
|
||||||
|
throw new IllegalArgumentException("Invalid characters in type identifier '" + type + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new {@link Payload} type.
|
||||||
|
* @param type the id of the payload type.
|
||||||
|
* @param clazz the payload class to register.
|
||||||
|
*/
|
||||||
|
public static void registerPayloadType(String type, Class<? extends Payload> clazz) {
|
||||||
|
payloadClasses.put(validateType(type), clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialize the provided String into a valid Payload.
|
||||||
|
* @param message the serialized data.
|
||||||
|
* @return the {@link Payload}.
|
||||||
|
* @throws IllegalArgumentException if the serialized data does not have the proper format.
|
||||||
|
*/
|
||||||
|
public static Payload fromString(String message) {
|
||||||
|
String[] split = message.split(Pattern.quote(PAYLOAD_TYPE_SEPARATOR), 2);
|
||||||
|
if (split.length != 2) {
|
||||||
|
throw new IllegalArgumentException("Malformed message: does not respect format '<type>" + PAYLOAD_TYPE_SEPARATOR + "<jsonObject>'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Class<? extends Payload> detectedClass = payloadClasses.get(split[0]);
|
||||||
|
if (detectedClass == null) {
|
||||||
|
throw new IllegalArgumentException("Unrecognized data type '" + split[0] + "'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Json.gson.fromJson(split[1], detectedClass);
|
||||||
|
} catch (JsonParseException e) {
|
||||||
|
throw new IllegalArgumentException(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize the provided {@link Payload}.
|
||||||
|
* @param p the {@link Payload} to serialize. Must be of a registered type.
|
||||||
|
* @return the serialized data.
|
||||||
|
*/
|
||||||
|
public static String toString(Payload p) {
|
||||||
|
String type = payloadClasses.getKey(p.getClass());
|
||||||
|
if (type == null)
|
||||||
|
throw new IllegalArgumentException(p.getClass() + " is not a registered payload type.");
|
||||||
|
return arbitraryToString(type, p, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize the provided arbitrary data, that consist of a type and an object that will be converted
|
||||||
|
* to a Json string.
|
||||||
|
* @param type the type
|
||||||
|
* @param obj the object to Jsonify
|
||||||
|
* @param serializeNulls if null propreties must be included in the json object.
|
||||||
|
* @return the String to send through the websocket
|
||||||
|
*/
|
||||||
|
public static String arbitraryToString(String type, Object obj, boolean serializeNulls) {
|
||||||
|
return validateType(type) + PAYLOAD_TYPE_SEPARATOR + (serializeNulls ? Json.gsonSerializeNulls : Json.gson).toJson(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
static {
|
||||||
|
registerPayloadType("error", ErrorPayload.class);
|
||||||
|
registerPayloadType("message", MessagePayload.class);
|
||||||
|
registerPayloadType("login", LoginPayload.class);
|
||||||
|
registerPayloadType("login-succeed", LoginSucceedPayload.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package fr.pandacube.lib.ws.payloads;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error message payload.
|
||||||
|
*/
|
||||||
|
public class ErrorPayload extends Payload {
|
||||||
|
/**
|
||||||
|
* The error message.
|
||||||
|
*/
|
||||||
|
public String message;
|
||||||
|
/**
|
||||||
|
* The error Throwable, may be null.
|
||||||
|
*/
|
||||||
|
public Throwable throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize an error payload with a message but not throwable.
|
||||||
|
* @param message the error message.
|
||||||
|
*/
|
||||||
|
public ErrorPayload(String message) {
|
||||||
|
this(message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize an error payload with a message and a throwable.
|
||||||
|
* @param message the error message.
|
||||||
|
* @param throwable the error Throwable, may be null.
|
||||||
|
*/
|
||||||
|
public ErrorPayload(String message, Throwable throwable) {
|
||||||
|
this.message = message;
|
||||||
|
this.throwable = throwable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private ErrorPayload() { } // for Json deserialization
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package fr.pandacube.lib.ws.payloads;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload used by the client to login to key-protected websocket endpoint.
|
||||||
|
*/
|
||||||
|
public class LoginPayload extends Payload {
|
||||||
|
/**
|
||||||
|
* The key to use for login.
|
||||||
|
*/
|
||||||
|
public String key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new LoginPayload with the provided key.
|
||||||
|
* @param key the key to use for login.
|
||||||
|
*/
|
||||||
|
public LoginPayload(String key) {
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private LoginPayload() { } // for Json deserialization
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
package fr.pandacube.lib.ws.payloads;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload used by the server in inform the client the login was successfull.
|
||||||
|
*/
|
||||||
|
public class LoginSucceedPayload extends Payload {
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package fr.pandacube.lib.ws.payloads;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A general use message Payload.
|
||||||
|
*/
|
||||||
|
public class MessagePayload extends Payload {
|
||||||
|
/**
|
||||||
|
* The message.
|
||||||
|
*/
|
||||||
|
public String message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialite a new MessagePayload with the provided message.
|
||||||
|
* @param message the message.
|
||||||
|
*/
|
||||||
|
public MessagePayload(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private MessagePayload() { } // for Json deserialization
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package fr.pandacube.lib.ws.payloads;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Superclass of all payload sent through our websockets.
|
||||||
|
*/
|
||||||
|
public abstract class Payload {
|
||||||
|
|
||||||
|
}
|
3
pom.xml
3
pom.xml
@ -77,6 +77,9 @@
|
|||||||
<module>pandalib-players-permissible</module>
|
<module>pandalib-players-permissible</module>
|
||||||
<module>pandalib-reflect</module>
|
<module>pandalib-reflect</module>
|
||||||
<module>pandalib-util</module>
|
<module>pandalib-util</module>
|
||||||
|
<module>pandalib-ws</module>
|
||||||
|
<module>pandalib-ws-client</module>
|
||||||
|
<module>pandalib-ws-server</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
Loading…
Reference in New Issue
Block a user