diff --git a/pom.xml b/pom.xml
index 93c7516..af347ba 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,6 +25,10 @@
org.springframework.boot
spring-boot-starter-web
+
+ org.springframework.boot
+ spring-boot-starter-security
+
org.springframework.boot
spring-boot-starter-thymeleaf
diff --git a/railsignal-app/.env.development b/railsignal-app/.env.development
index 430da52..a90eb52 100644
--- a/railsignal-app/.env.development
+++ b/railsignal-app/.env.development
@@ -1 +1,2 @@
-VITE_API_URL=http://localhost:8080/api
\ No newline at end of file
+VITE_API_URL=http://localhost:8080/api
+VITE_WS_URL=ws://localhost:8080/api/ws/app
\ No newline at end of file
diff --git a/railsignal-app/.env.production b/railsignal-app/.env.production
index 430da52..a90eb52 100644
--- a/railsignal-app/.env.production
+++ b/railsignal-app/.env.production
@@ -1 +1,2 @@
-VITE_API_URL=http://localhost:8080/api
\ No newline at end of file
+VITE_API_URL=http://localhost:8080/api
+VITE_WS_URL=ws://localhost:8080/api/ws/app
\ No newline at end of file
diff --git a/railsignal-app/src/components/AppNavbar.vue b/railsignal-app/src/components/AppNavbar.vue
index 62bcef5..cf86a87 100644
--- a/railsignal-app/src/components/AppNavbar.vue
+++ b/railsignal-app/src/components/AppNavbar.vue
@@ -63,12 +63,6 @@ export default {
components: {AddRailSystem: AddRailSystemModal, ConfirmModal},
setup() {
const rsStore = useRailSystemsStore();
- rsStore.$subscribe(mutation => {
- const evt = mutation.events;
- if (evt.key === "selectedRailSystem" && evt.newValue !== null) {
- rsStore.fetchSelectedRailSystemData();
- }
- });
return {
rsStore
};
diff --git a/railsignal-app/src/main.js b/railsignal-app/src/main.js
index 62748cd..c0af016 100644
--- a/railsignal-app/src/main.js
+++ b/railsignal-app/src/main.js
@@ -3,10 +3,20 @@ import { createPinia } from 'pinia';
import App from './App.vue'
import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap";
+import {useRailSystemsStore} from "./stores/railSystemsStore";
const pinia = createPinia();
const app = createApp(App);
app.use(pinia);
+// Configure rail system updates.
+const rsStore = useRailSystemsStore();
+rsStore.$subscribe(mutation => {
+ const evt = mutation.events;
+ if (evt.key === "selectedRailSystem" && evt.newValue !== null) {
+ rsStore.onSelectedRailSystemChanged();
+ }
+});
+
app.mount('#app')
diff --git a/railsignal-app/src/stores/railSystemsStore.js b/railsignal-app/src/stores/railSystemsStore.js
index b2d506f..5d893c0 100644
--- a/railsignal-app/src/stores/railSystemsStore.js
+++ b/railsignal-app/src/stores/railSystemsStore.js
@@ -5,10 +5,17 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
state: () => ({
railSystems: [],
/**
- * @type {{segments: [Object], components: [Object], selectedComponent: Object} | null}
+ * @type {{
+ * segments: [Object],
+ * components: [Object],
+ * selectedComponent: Object | null,
+ * websocket: WebSocket | null
+ * } | null}
*/
selectedRailSystem: null,
- apiUrl: import.meta.env.VITE_API_URL
+ websocket: null,
+ apiUrl: import.meta.env.VITE_API_URL,
+ wsUrl: import.meta.env.VITE_WS_URL
}),
actions: {
refreshRailSystems() {
@@ -65,10 +72,24 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
});
});
},
- fetchSelectedRailSystemData() {
+ onSelectedRailSystemChanged() {
if (!this.selectedRailSystem) return;
this.refreshSegments(this.selectedRailSystem);
this.refreshAllComponents(this.selectedRailSystem);
+ if (this.websocket !== null) {
+ this.websocket.close();
+ }
+ console.log(this.wsUrl);
+ this.websocket = new WebSocket(this.wsUrl);
+ this.websocket.onopen = event => {
+ console.log("Opened websocket connection.");
+ };
+ this.websocket.onclose = event => {
+ console.log("Closed websocket connection.");
+ };
+ this.websocket.onmessage = () => {
+
+ };
},
addSegment(name) {
const rs = this.selectedRailSystem;
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlink.java b/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlink.java
new file mode 100644
index 0000000..67bb723
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlink.java
@@ -0,0 +1,24 @@
+package nl.andrewl.railsignalapi.live;
+
+import lombok.Getter;
+
+public abstract class ComponentDownlink {
+ @Getter
+ private final long id;
+
+ public ComponentDownlink(long id) {
+ this.id = id;
+ }
+
+ public abstract void send(Object msg) throws Exception;
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof ComponentDownlink cd && cd.id == this.id;
+ }
+
+ @Override
+ public int hashCode() {
+ return Long.hashCode(id);
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlinkService.java b/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlinkService.java
new file mode 100644
index 0000000..e2f3147
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlinkService.java
@@ -0,0 +1,34 @@
+package nl.andrewl.railsignalapi.live;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A service that manages all the active component downlink connections.
+ */
+@Service
+@RequiredArgsConstructor
+public class ComponentDownlinkService {
+ private final Map> componentDownlinks = new HashMap<>();
+
+ public synchronized void registerDownlink(ComponentDownlink downlink, Set componentIds) {
+ componentDownlinks.put(downlink, componentIds);
+ }
+
+ public synchronized void deregisterDownlink(ComponentDownlink downlink) {
+ componentDownlinks.remove(downlink);
+ }
+
+ public synchronized void deregisterDownlink(long tokenId) {
+ List removeSet = componentDownlinks.keySet().stream()
+ .filter(downlink -> downlink.getId() == tokenId).toList();
+ for (var downlink : removeSet) {
+ componentDownlinks.remove(downlink);
+ }
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpDownlink.java b/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpDownlink.java
new file mode 100644
index 0000000..007c76e
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpDownlink.java
@@ -0,0 +1,24 @@
+package nl.andrewl.railsignalapi.live.tcp_socket;
+
+import nl.andrewl.railsignalapi.live.ComponentDownlink;
+import nl.andrewl.railsignalapi.util.JsonUtils;
+
+import java.io.DataOutputStream;
+import java.nio.charset.StandardCharsets;
+
+public class TcpDownlink extends ComponentDownlink {
+ private final DataOutputStream out;
+
+ public TcpDownlink(long id, DataOutputStream out) {
+ super(id);
+ this.out = out;
+ }
+
+ @Override
+ public void send(Object msg) throws Exception {
+ byte[] jsonBytes = JsonUtils.toJson(msg).getBytes(StandardCharsets.UTF_8);
+ out.writeInt(jsonBytes.length);
+ out.write(jsonBytes);
+ out.flush();
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpSocketServer.java b/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpSocketServer.java
new file mode 100644
index 0000000..69b298e
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpSocketServer.java
@@ -0,0 +1,79 @@
+package nl.andrewl.railsignalapi.live.tcp_socket;
+
+import lombok.extern.slf4j.Slf4j;
+import nl.andrewl.railsignalapi.dao.ComponentAccessTokenRepository;
+import nl.andrewl.railsignalapi.model.ComponentAccessToken;
+import nl.andrewl.railsignalapi.util.JsonUtils;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.event.ContextClosedEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Component;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+/**
+ * A plain TCP server socket which can be used to connect to components that
+ * don't have access to a full websocket client implementation.
+ */
+@Component
+@Slf4j
+public class TcpSocketServer {
+ private final ServerSocket serverSocket;
+ private final ComponentAccessTokenRepository tokenRepository;
+ private final PasswordEncoder passwordEncoder;
+
+ public TcpSocketServer(ComponentAccessTokenRepository tokenRepository, PasswordEncoder passwordEncoder) throws IOException {
+ this.tokenRepository = tokenRepository;
+ this.passwordEncoder = passwordEncoder;
+ this.serverSocket = new ServerSocket();
+ serverSocket.setReuseAddress(true);
+ serverSocket.bind(new InetSocketAddress("localhost", 8081));
+ }
+
+ @EventListener(ApplicationReadyEvent.class)
+ public void runServer() {
+ new Thread(() -> {
+ log.info("Starting TCP Socket for Component links at " + serverSocket.getInetAddress());
+ while (!serverSocket.isClosed()) {
+ try {
+ Socket socket = serverSocket.accept();
+ initializeConnection(socket);
+ } catch (IOException e) {
+ log.warn("An IOException occurred while waiting to accept a TCP socket connection.", e);
+ }
+ }
+ });
+ }
+
+ @EventListener(ContextClosedEvent.class)
+ public void closeServer() throws IOException {
+ serverSocket.close();
+ }
+
+ private void initializeConnection(Socket socket) throws IOException {
+ DataOutputStream out = new DataOutputStream(socket.getOutputStream());
+ DataInputStream in = new DataInputStream(socket.getInputStream());
+ int tokenLength = in.readInt();
+ String rawToken = new String(in.readNBytes(tokenLength));
+ if (rawToken.length() < ComponentAccessToken.PREFIX_SIZE) {
+ byte[] respBytes = JsonUtils.toJson(Map.of("message", "Invalid token")).getBytes(StandardCharsets.UTF_8);
+ out.writeInt(respBytes.length);
+ out.write(respBytes);
+ socket.close();
+ }
+ Iterable tokens = tokenRepository.findAllByTokenPrefix(rawToken.substring(0, ComponentAccessToken.PREFIX_SIZE));
+ for (var token : tokens) {
+ if (passwordEncoder.matches(rawToken, token.getTokenHash())) {
+
+ }
+ }
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandler.java b/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandler.java
new file mode 100644
index 0000000..5599f1f
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandler.java
@@ -0,0 +1,35 @@
+package nl.andrewl.railsignalapi.live.websocket;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.TextWebSocketHandler;
+
+/**
+ * A websocket handler for all websocket connections to the Rail Signal web
+ * app frontend.
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class AppWebsocketHandler extends TextWebSocketHandler {
+ @Override
+ public void afterConnectionEstablished(WebSocketSession session) throws Exception {
+ super.afterConnectionEstablished(session);
+ log.info("App websocket session established.");
+ }
+
+ @Override
+ protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
+ super.handleTextMessage(session, message);
+ }
+
+ @Override
+ public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
+ super.afterConnectionClosed(session, status);
+ log.info("App websocket session closed.");
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketHandshakeInterceptor.java b/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandshakeInterceptor.java
similarity index 53%
rename from src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketHandshakeInterceptor.java
rename to src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandshakeInterceptor.java
index e25c88b..7c75921 100644
--- a/src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketHandshakeInterceptor.java
+++ b/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandshakeInterceptor.java
@@ -1,32 +1,39 @@
-package nl.andrewl.railsignalapi.websocket;
+package nl.andrewl.railsignalapi.live.websocket;
import lombok.RequiredArgsConstructor;
-import nl.andrewl.railsignalapi.dao.ComponentRepository;
import nl.andrewl.railsignalapi.dao.RailSystemRepository;
+import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
-import java.util.Arrays;
import java.util.Map;
+/**
+ * This interceptor is used to check incoming websocket connections from the
+ * web app, to ensure that they're directed towards a valid rail system.
+ */
@Component
@RequiredArgsConstructor
-public class ComponentWebSocketHandshakeInterceptor implements HandshakeInterceptor {
+public class AppWebsocketHandshakeInterceptor implements HandshakeInterceptor {
private final RailSystemRepository railSystemRepository;
- private final ComponentRepository componentRepository;
@Override
- public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception {
- String[] queryParams = request.getURI().getQuery().split("&");
- System.out.println(Arrays.toString(queryParams));
- return false;
+ public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) {
+ String path = request.getURI().getPath();
+ Long railSystemId = Long.parseLong(path.substring(path.lastIndexOf('/')));
+ if (!railSystemRepository.existsById(railSystemId)) {
+ response.setStatusCode(HttpStatus.NOT_FOUND);
+ return false;
+ }
+ attributes.put("railSystemId", railSystemId);
+ return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
-
+ // Nothing to do after the handshake.
}
}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandler.java b/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandler.java
new file mode 100644
index 0000000..f39e940
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandler.java
@@ -0,0 +1,49 @@
+package nl.andrewl.railsignalapi.live.websocket;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import nl.andrewl.railsignalapi.dao.ComponentAccessTokenRepository;
+import nl.andrewl.railsignalapi.live.ComponentDownlinkService;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.TextWebSocketHandler;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Handler for websocket connections that components open to send and receive
+ * real-time updates from the server.
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class ComponentWebsocketHandler extends TextWebSocketHandler {
+ private final ComponentAccessTokenRepository tokenRepository;
+ private final ComponentDownlinkService componentDownlinkService;
+
+ @Override
+ @Transactional(readOnly = true)
+ public void afterConnectionEstablished(WebSocketSession session) throws Exception {
+ long tokenId = (long) session.getAttributes().get("tokenId");
+ var token = tokenRepository.findById(tokenId).orElseThrow();
+ Set componentIds = token.getComponents().stream()
+ .map(nl.andrewl.railsignalapi.model.component.Component::getId)
+ .collect(Collectors.toSet());
+ componentDownlinkService.registerDownlink(new WebsocketDownlink(tokenId, session), componentIds);
+ }
+
+ @Override
+ protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
+// var msg = mapper.readValue(message.getPayload(), SignalUpdateMessage.class);
+ //signalService.handleSignalUpdate(msg);
+ }
+
+ @Override
+ public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
+ componentDownlinkService.deregisterDownlink((long) session.getAttributes().get("tokenId"));
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandshakeInterceptor.java b/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandshakeInterceptor.java
new file mode 100644
index 0000000..189eeba
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandshakeInterceptor.java
@@ -0,0 +1,56 @@
+package nl.andrewl.railsignalapi.live.websocket;
+
+import lombok.RequiredArgsConstructor;
+import nl.andrewl.railsignalapi.dao.ComponentAccessTokenRepository;
+import nl.andrewl.railsignalapi.model.ComponentAccessToken;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+import java.util.Map;
+
+/**
+ * An interceptor that checks incoming component websocket connections to
+ * ensure that they have a required "token" query parameter that refers to one
+ * or more components in a rail system. If the token is valid, we pass its id
+ * on as an attribute for the handler that will register the connection.
+ */
+@Component
+@RequiredArgsConstructor
+public class ComponentWebsocketHandshakeInterceptor implements HandshakeInterceptor {
+ private final ComponentAccessTokenRepository tokenRepository;
+ private final PasswordEncoder passwordEncoder;
+
+ @Override
+ public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception {
+ String query = request.getURI().getQuery();
+ int tokenIdx = query.lastIndexOf("token=");
+ if (tokenIdx == -1) {
+ response.setStatusCode(HttpStatus.BAD_REQUEST);
+ return false;
+ }
+ String rawToken = query.substring(tokenIdx);
+ if (rawToken.length() < ComponentAccessToken.PREFIX_SIZE) {
+ response.setStatusCode(HttpStatus.BAD_REQUEST);
+ return false;
+ }
+ Iterable tokens = tokenRepository.findAllByTokenPrefix(rawToken.substring(0, ComponentAccessToken.PREFIX_SIZE));
+ for (var token : tokens) {
+ if (passwordEncoder.matches(rawToken, token.getTokenHash())) {
+ attributes.put("tokenId", token.getId());
+ return true;
+ }
+ }
+ response.setStatusCode(HttpStatus.UNAUTHORIZED);
+ return false;
+ }
+
+ @Override
+ public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
+ // Don't need to do anything after the handshake.
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/websocket/WebsocketConfig.java b/src/main/java/nl/andrewl/railsignalapi/live/websocket/WebsocketConfig.java
new file mode 100644
index 0000000..00bd502
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/live/websocket/WebsocketConfig.java
@@ -0,0 +1,40 @@
+package nl.andrewl.railsignalapi.live.websocket;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistration;
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
+
+import java.util.Set;
+
+/**
+ * Configuration for Rail Signal's websockets. This includes both app and
+ * component connections.
+ */
+@Configuration
+@EnableWebSocket
+@RequiredArgsConstructor
+public class WebsocketConfig implements WebSocketConfigurer {
+ private final ComponentWebsocketHandler componentHandler;
+ private final ComponentWebsocketHandshakeInterceptor componentInterceptor;
+ private final AppWebsocketHandler appHandler;
+ private final AppWebsocketHandshakeInterceptor appInterceptor;
+ private final Environment env;
+
+ @Override
+ public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
+ registry.addHandler(componentHandler, "/api/ws/component")
+ .setAllowedOrigins("*")
+ .addInterceptors(componentInterceptor);
+ WebSocketHandlerRegistration appHandlerReg = registry.addHandler(appHandler, "/api/ws/app/*")
+ .addInterceptors(appInterceptor);
+ // If we're in a development profile, allow any origin to access the app websocket.
+ // This is so that we can use a standalone JS dev server.
+ if (Set.of(env.getActiveProfiles()).contains("development")) {
+ appHandlerReg.setAllowedOrigins("*");
+ }
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/websocket/WebsocketDownlink.java b/src/main/java/nl/andrewl/railsignalapi/live/websocket/WebsocketDownlink.java
new file mode 100644
index 0000000..a76e635
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/live/websocket/WebsocketDownlink.java
@@ -0,0 +1,20 @@
+package nl.andrewl.railsignalapi.live.websocket;
+
+import nl.andrewl.railsignalapi.live.ComponentDownlink;
+import nl.andrewl.railsignalapi.util.JsonUtils;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+public class WebsocketDownlink extends ComponentDownlink {
+ private final WebSocketSession webSocketSession;
+
+ public WebsocketDownlink(long id, WebSocketSession webSocketSession) {
+ super(id);
+ this.webSocketSession = webSocketSession;
+ }
+
+ @Override
+ public void send(Object msg) throws Exception {
+ webSocketSession.sendMessage(new TextMessage(JsonUtils.toJson(msg)));
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/model/ComponentAccessToken.java b/src/main/java/nl/andrewl/railsignalapi/model/ComponentAccessToken.java
index 61cd0d6..72ee36e 100644
--- a/src/main/java/nl/andrewl/railsignalapi/model/ComponentAccessToken.java
+++ b/src/main/java/nl/andrewl/railsignalapi/model/ComponentAccessToken.java
@@ -18,6 +18,8 @@ import java.util.Set;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class ComponentAccessToken {
+ public static final byte PREFIX_SIZE = 7;
+
@Id
@GeneratedValue
private Long id;
@@ -37,7 +39,7 @@ public class ComponentAccessToken {
/**
* A short prefix of the token, which is useful for speeding up lookup.
*/
- @Column(nullable = false, length = 7)
+ @Column(nullable = false, length = PREFIX_SIZE)
private String tokenPrefix;
/**
diff --git a/src/main/java/nl/andrewl/railsignalapi/util/JsonUtils.java b/src/main/java/nl/andrewl/railsignalapi/util/JsonUtils.java
new file mode 100644
index 0000000..1c13973
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/util/JsonUtils.java
@@ -0,0 +1,12 @@
+package nl.andrewl.railsignalapi.util;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class JsonUtils {
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ public static String toJson(Object o) throws JsonProcessingException {
+ return mapper.writeValueAsString(o);
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketConfig.java b/src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketConfig.java
deleted file mode 100644
index 2881be3..0000000
--- a/src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketConfig.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package nl.andrewl.railsignalapi.websocket;
-
-import lombok.RequiredArgsConstructor;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.web.socket.config.annotation.EnableWebSocket;
-import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
-import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
-
-@Configuration
-@EnableWebSocket
-@RequiredArgsConstructor
-public class ComponentWebSocketConfig implements WebSocketConfigurer {
- private final SignalWebSocketHandler webSocketHandler;
- private final ComponentWebSocketHandshakeInterceptor handshakeInterceptor;
-
- @Override
- public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
- registry.addHandler(webSocketHandler, "/api/ws/component")
- .addInterceptors(handshakeInterceptor);
- }
-}
diff --git a/src/main/java/nl/andrewl/railsignalapi/websocket/SignalWebSocketHandler.java b/src/main/java/nl/andrewl/railsignalapi/websocket/SignalWebSocketHandler.java
deleted file mode 100644
index 20e9b9d..0000000
--- a/src/main/java/nl/andrewl/railsignalapi/websocket/SignalWebSocketHandler.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package nl.andrewl.railsignalapi.websocket;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-import org.springframework.web.socket.CloseStatus;
-import org.springframework.web.socket.TextMessage;
-import org.springframework.web.socket.WebSocketSession;
-import org.springframework.web.socket.handler.TextWebSocketHandler;
-
-import java.util.HashSet;
-import java.util.Set;
-
-@Component
-@RequiredArgsConstructor
-@Slf4j
-public class SignalWebSocketHandler extends TextWebSocketHandler {
- private final ObjectMapper mapper = new ObjectMapper();
-
- @Override
- public void afterConnectionEstablished(WebSocketSession session) throws Exception {
- String signalIdHeader = session.getHandshakeHeaders().getFirst("X-RailSignal-SignalId");
- if (signalIdHeader == null || signalIdHeader.isBlank()) {
- session.close(CloseStatus.PROTOCOL_ERROR);
- return;
- }
- Set ids = new HashSet<>();
- for (var idStr : signalIdHeader.split(",")) {
- ids.add(Long.parseLong(idStr.trim()));
- }
- //signalService.registerSignalWebSocketSession(ids, session);
- log.info("Connection established with signals {}.", ids);
- }
-
- @Override
- protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
-// var msg = mapper.readValue(message.getPayload(), SignalUpdateMessage.class);
- //signalService.handleSignalUpdate(msg);
- }
-
- @Override
- public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
- //signalService.deregisterSignalWebSocketSession(session);
- log.info("Closed connection {}. Status: {}", session.getId(), status.toString());
- }
-}