diff --git a/build.gradle b/build.gradle index 1d4df44..6800130 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,7 @@ sourceCompatibility = 17 // Set our project variables project.ext { dropwizardVersion = '4.0.5' + jettyVersion = '11.0.19' } repositories { @@ -62,6 +63,8 @@ dependencies { 'io.dropwizard:dropwizard-hibernate:' + dropwizardVersion, 'io.dropwizard:dropwizard-auth:' + dropwizardVersion, 'io.dropwizard:dropwizard-jdbi3:' + dropwizardVersion, + 'org.eclipse.jetty.websocket:websocket-jetty-api:' + jettyVersion, + 'org.eclipse.jetty.websocket:websocket-jetty-server:' + jettyVersion, ) // Database implementation('com.h2database:h2:2.2.220') diff --git a/src/main/java/telraam/App.java b/src/main/java/telraam/App.java index 8729ea2..d7bfdba 100644 --- a/src/main/java/telraam/App.java +++ b/src/main/java/telraam/App.java @@ -11,6 +11,7 @@ import jakarta.servlet.DispatcherType; import jakarta.servlet.FilterRegistration; import org.eclipse.jetty.servlets.CrossOriginFilter; +import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; import org.jdbi.v3.core.Jdbi; import telraam.api.*; import telraam.database.daos.*; @@ -21,6 +22,7 @@ import telraam.logic.robust.RobustLapper; import telraam.station.Fetcher; import telraam.util.AcceptedLapsUtil; +import telraam.websocket.WebSocketConnection; import java.io.IOException; import java.util.EnumSet; @@ -78,6 +80,15 @@ public void run(AppConfiguration configuration, Environment environment) throws // Initialize AcceptedLapUtil AcceptedLapsUtil.createInstance(this.database); + // Register websocket endpoint + JettyWebSocketServletContainerInitializer.configure( + environment.getApplicationContext(), + (servletContext, wsContainer) -> { + wsContainer.setMaxTextMessageSize(65535); + wsContainer.addMapping("/ws", (req, res) -> new WebSocketConnection()); + } + ); + // Add api resources JerseyEnvironment jersey = environment.jersey(); jersey.register(new BatonResource(database.onDemand(BatonDAO.class))); @@ -94,7 +105,6 @@ public void run(AppConfiguration configuration, Environment environment) throws jersey.register(new MonitoringResource(database)); environment.healthChecks().register("template", new TemplateHealthCheck(configuration.getTemplate())); - // Enable CORS final FilterRegistration.Dynamic cors = environment.servlets().addFilter("CORS", CrossOriginFilter.class); diff --git a/src/main/java/telraam/api/TeamResource.java b/src/main/java/telraam/api/TeamResource.java index 64201dd..56975c9 100644 --- a/src/main/java/telraam/api/TeamResource.java +++ b/src/main/java/telraam/api/TeamResource.java @@ -2,14 +2,14 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import telraam.database.daos.BatonSwitchoverDAO; import telraam.database.daos.TeamDAO; import telraam.database.models.BatonSwitchover; import telraam.database.models.Team; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.core.MediaType; import java.sql.Timestamp; import java.time.Instant; import java.util.List; diff --git a/src/main/java/telraam/websocket/WebSocketConnection.java b/src/main/java/telraam/websocket/WebSocketConnection.java new file mode 100644 index 0000000..191fea0 --- /dev/null +++ b/src/main/java/telraam/websocket/WebSocketConnection.java @@ -0,0 +1,41 @@ +package telraam.websocket; + +import java.io.IOException; +import java.util.logging.Logger; + +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.WebSocketAdapter; + +public class WebSocketConnection extends WebSocketAdapter { + private static final Logger logger = Logger.getLogger(WebSocketConnection.class.getName()); + + @Override + public void onWebSocketConnect(Session session) { + super.onWebSocketConnect(session); + WebSocketMessageSingleton.getInstance().registerConnection(this); + logger.info("Instance with remote \"%s\" connected".formatted(getRemote().getRemoteAddress())); + } + + @Override + public void onWebSocketClose(int statusCode, String reason) { + super.onWebSocketClose(statusCode, reason); + WebSocketMessageSingleton.getInstance().unregisterConnection(this); + logger.info("Instance with remote \"%s\" closed: [%s] %s".formatted(getRemote().getRemoteAddress(), statusCode, reason)); + } + + @Override + public void onWebSocketError(Throwable cause) { + super.onWebSocketError(cause); + logger.severe("WebSocket error in instance with remote \"%s\": %s".formatted(getRemote().getRemoteAddress(), cause)); + } + + public void send(String s) { + try { + getRemote().sendString(s); + } catch (IOException e) { + logger.severe("Sending \"%s\" through instance with remote \"%s\" failed with %s".formatted(s, getRemote().getRemoteAddress(), e)); + return; + } + logger.finest("Sent \"%s\" through instance with remote \"%s\"".formatted(s, getRemote().getRemoteAddress())); + } +} diff --git a/src/main/java/telraam/websocket/WebSocketMessageSingleton.java b/src/main/java/telraam/websocket/WebSocketMessageSingleton.java new file mode 100644 index 0000000..de12115 --- /dev/null +++ b/src/main/java/telraam/websocket/WebSocketMessageSingleton.java @@ -0,0 +1,37 @@ +package telraam.websocket; + +import lombok.Getter; + +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Logger; + +public class WebSocketMessageSingleton { + private static final Logger logger = Logger.getLogger(WebSocketMessageSingleton.class.getName()); + + @Getter + private static final WebSocketMessageSingleton instance = new WebSocketMessageSingleton(); + private static final Set registeredConnections = new HashSet<>(); + + private WebSocketMessageSingleton() { + } + + public void registerConnection(WebSocketConnection conn) { + boolean modified = registeredConnections.add(conn); + if (modified) { + logger.info("Registered WebSocketConnection %s".formatted(conn)); + } + } + + public void unregisterConnection(WebSocketConnection conn) { + boolean modified = registeredConnections.remove(conn); + if (modified) { + logger.info("Unregistered WebSocketConnection %s".formatted(conn)); + } + } + + public void sendToAll(String s) { + logger.finest("Sending \"%s\" to all registered WebSocketConnection instances".formatted(s)); + registeredConnections.forEach(conn -> conn.send(s)); + } +}