@@ -45,9 +46,11 @@ import PathNodeComponentView from "./PathNodeComponentView.vue";
import SegmentBoundaryNodeComponentView from "./SegmentBoundaryNodeComponentView.vue";
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
import ConfirmModal from "../../ConfirmModal.vue";
+import SwitchComponentView from "./SwitchComponentView.vue";
export default {
components: {
+ SwitchComponentView,
ConfirmModal,
SegmentBoundaryNodeComponentView,
SignalComponentView,
diff --git a/railsignal-app/src/components/railsystem/component/PathNodeComponentView.vue b/railsignal-app/src/components/railsystem/component/PathNodeComponentView.vue
index 6be36af..81ece24 100644
--- a/railsignal-app/src/components/railsystem/component/PathNodeComponentView.vue
+++ b/railsignal-app/src/components/railsystem/component/PathNodeComponentView.vue
@@ -1,40 +1,32 @@
-
Connected Nodes
-
-
-
-
Name
-
-
-
-
-
{{node.name}}
-
-
-
-
-
-
-
- There are no connected nodes.
-
-
+
+
+
+
+ There are no connected nodes.
+
+
+
+
\ No newline at end of file
diff --git a/railsignal-app/src/stores/railSystemsStore.js b/railsignal-app/src/stores/railSystemsStore.js
index 5d893c0..8516bbf 100644
--- a/railsignal-app/src/stores/railSystemsStore.js
+++ b/railsignal-app/src/stores/railSystemsStore.js
@@ -80,15 +80,15 @@ export const useRailSystemsStore = defineStore('RailSystemsStore', {
this.websocket.close();
}
console.log(this.wsUrl);
- this.websocket = new WebSocket(this.wsUrl);
+ this.websocket = new WebSocket(this.wsUrl + "/" + this.selectedRailSystem.id);
this.websocket.onopen = event => {
console.log("Opened websocket connection.");
};
this.websocket.onclose = event => {
console.log("Closed websocket connection.");
};
- this.websocket.onmessage = () => {
-
+ this.websocket.onmessage = (msg) => {
+ console.log(msg);
};
},
addSegment(name) {
diff --git a/src/main/java/nl/andrewl/railsignalapi/RailSignalApiApplication.java b/src/main/java/nl/andrewl/railsignalapi/RailSignalApiApplication.java
index f3b602b..8b26c1b 100644
--- a/src/main/java/nl/andrewl/railsignalapi/RailSignalApiApplication.java
+++ b/src/main/java/nl/andrewl/railsignalapi/RailSignalApiApplication.java
@@ -2,8 +2,11 @@ package nl.andrewl.railsignalapi;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
-@SpringBootApplication
+@SpringBootApplication(exclude = {
+ UserDetailsServiceAutoConfiguration.class
+})
public class RailSignalApiApplication {
public static void main(String[] args) {
diff --git a/src/main/java/nl/andrewl/railsignalapi/dao/ComponentAccessTokenRepository.java b/src/main/java/nl/andrewl/railsignalapi/dao/ComponentAccessTokenRepository.java
deleted file mode 100644
index fc31c0b..0000000
--- a/src/main/java/nl/andrewl/railsignalapi/dao/ComponentAccessTokenRepository.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package nl.andrewl.railsignalapi.dao;
-
-import nl.andrewl.railsignalapi.model.ComponentAccessToken;
-import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.stereotype.Repository;
-
-@Repository
-public interface ComponentAccessTokenRepository extends JpaRepository {
- Iterable findAllByTokenPrefix(String prefix);
- boolean existsByLabel(String label);
-
-}
diff --git a/src/main/java/nl/andrewl/railsignalapi/dao/LabelRepository.java b/src/main/java/nl/andrewl/railsignalapi/dao/LabelRepository.java
index 20e7488..9a163c5 100644
--- a/src/main/java/nl/andrewl/railsignalapi/dao/LabelRepository.java
+++ b/src/main/java/nl/andrewl/railsignalapi/dao/LabelRepository.java
@@ -1,6 +1,6 @@
package nl.andrewl.railsignalapi.dao;
-import nl.andrewl.railsignalapi.model.Label;
+import nl.andrewl.railsignalapi.model.component.Label;
import nl.andrewl.railsignalapi.model.RailSystem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
diff --git a/src/main/java/nl/andrewl/railsignalapi/dao/LinkTokenRepository.java b/src/main/java/nl/andrewl/railsignalapi/dao/LinkTokenRepository.java
new file mode 100644
index 0000000..28bd909
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/dao/LinkTokenRepository.java
@@ -0,0 +1,12 @@
+package nl.andrewl.railsignalapi.dao;
+
+import nl.andrewl.railsignalapi.model.LinkToken;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface LinkTokenRepository extends JpaRepository {
+ Iterable findAllByTokenPrefix(String prefix);
+ boolean existsByLabel(String label);
+
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlink.java b/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlink.java
index 67bb723..a72dde1 100644
--- a/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlink.java
+++ b/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlink.java
@@ -2,23 +2,27 @@ package nl.andrewl.railsignalapi.live;
import lombok.Getter;
+/**
+ * A downlink connection to one or more components (linked by a {@link nl.andrewl.railsignalapi.model.LinkToken}
+ * which we can send messages to.
+ */
public abstract class ComponentDownlink {
@Getter
- private final long id;
+ private final long tokenId;
- public ComponentDownlink(long id) {
- this.id = id;
+ public ComponentDownlink(long tokenId) {
+ this.tokenId = tokenId;
}
public abstract void send(Object msg) throws Exception;
@Override
public boolean equals(Object o) {
- return o instanceof ComponentDownlink cd && cd.id == this.id;
+ return o instanceof ComponentDownlink cd && cd.tokenId == this.tokenId;
}
@Override
public int hashCode() {
- return Long.hashCode(id);
+ return Long.hashCode(tokenId);
}
}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlinkService.java b/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlinkService.java
index e2f3147..aab0fe0 100644
--- a/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlinkService.java
+++ b/src/main/java/nl/andrewl/railsignalapi/live/ComponentDownlinkService.java
@@ -1,34 +1,91 @@
package nl.andrewl.railsignalapi.live;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import nl.andrewl.railsignalapi.dao.LinkTokenRepository;
+import nl.andrewl.railsignalapi.dao.ComponentRepository;
+import nl.andrewl.railsignalapi.model.component.Component;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
+import java.util.stream.Collectors;
/**
* A service that manages all the active component downlink connections.
*/
@Service
@RequiredArgsConstructor
+@Slf4j
public class ComponentDownlinkService {
private final Map> componentDownlinks = new HashMap<>();
+ private final Map> downlinksByCId = new HashMap<>();
- public synchronized void registerDownlink(ComponentDownlink downlink, Set componentIds) {
- componentDownlinks.put(downlink, componentIds);
+ private final LinkTokenRepository tokenRepository;
+ private final ComponentRepository componentRepository;
+
+ /**
+ * Registers a new active downlink to one or more components.
+ * @param downlink The downlink to register.
+ */
+ @Transactional
+ public synchronized void registerDownlink(ComponentDownlink downlink) {
+ Set components = tokenRepository.findById(downlink.getTokenId()).orElseThrow().getComponents();
+ componentDownlinks.put(downlink, components.stream().map(Component::getId).collect(Collectors.toSet()));
+ for (var c : components) {
+ c.setOnline(true);
+ Set downlinks = downlinksByCId.computeIfAbsent(c.getId(), aLong -> new HashSet<>());
+ downlinks.add(downlink);
+ }
+ componentRepository.saveAll(components);
+ log.info("Registered downlink with token id {}.", downlink.getTokenId());
}
+ /**
+ * De-registers a downlink to components. This should be called when this
+ * downlink is closed.
+ * @param downlink The downlink to de-register.
+ */
+ @Transactional
public synchronized void deregisterDownlink(ComponentDownlink downlink) {
- componentDownlinks.remove(downlink);
+ Set componentIds = componentDownlinks.remove(downlink);
+ if (componentIds != null) {
+ for (var cId : componentIds) {
+ componentRepository.findById(cId).ifPresent(component -> {
+ component.setOnline(false);
+ componentRepository.save(component);
+ });
+ Set downlinks = downlinksByCId.get(cId);
+ if (downlinks != null) {
+ downlinks.remove(downlink);
+ if (downlinks.isEmpty()) {
+ downlinksByCId.remove(cId);
+ }
+ }
+ }
+ }
+ log.info("De-registered downlink with token id {}.", downlink.getTokenId());
}
+ @Transactional
public synchronized void deregisterDownlink(long tokenId) {
List removeSet = componentDownlinks.keySet().stream()
- .filter(downlink -> downlink.getId() == tokenId).toList();
+ .filter(downlink -> downlink.getTokenId() == tokenId).toList();
for (var downlink : removeSet) {
- componentDownlinks.remove(downlink);
+ deregisterDownlink(downlink);
+ }
+ }
+
+ public void sendMessage(long componentId, Object msg) {
+ var downlinks = downlinksByCId.get(componentId);
+ if (downlinks != null) {
+ for (var downlink : downlinks) {
+ try {
+ downlink.send(msg);
+ } catch (Exception e) {
+ log.warn("An error occurred while sending a message to downlink with token id " + downlink.getTokenId(), e);
+ }
+ }
}
}
}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/ComponentUplinkMessageHandler.java b/src/main/java/nl/andrewl/railsignalapi/live/ComponentUplinkMessageHandler.java
new file mode 100644
index 0000000..c2f837b
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/live/ComponentUplinkMessageHandler.java
@@ -0,0 +1,32 @@
+package nl.andrewl.railsignalapi.live;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import nl.andrewl.railsignalapi.live.dto.ComponentUplinkMessage;
+import nl.andrewl.railsignalapi.live.dto.SegmentBoundaryUpdateMessage;
+import nl.andrewl.railsignalapi.live.dto.SwitchUpdateMessage;
+import nl.andrewl.railsignalapi.service.SegmentService;
+import nl.andrewl.railsignalapi.service.SwitchService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * A central service that manages all incoming component messages from any
+ * connected component links.
+ */
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class ComponentUplinkMessageHandler {
+ private final SwitchService switchService;
+ private final SegmentService segmentService;
+
+ @Transactional
+ public void messageReceived(ComponentUplinkMessage msg) {
+ if (msg instanceof SegmentBoundaryUpdateMessage sb) {
+ segmentService.onBoundaryUpdate(sb);
+ } else if (msg instanceof SwitchUpdateMessage sw) {
+ switchService.onSwitchUpdate(sw);
+ }
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/dto/ComponentUplinkMessage.java b/src/main/java/nl/andrewl/railsignalapi/live/dto/ComponentUplinkMessage.java
new file mode 100644
index 0000000..43f8e96
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/live/dto/ComponentUplinkMessage.java
@@ -0,0 +1,20 @@
+package nl.andrewl.railsignalapi.live.dto;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+/**
+ * The parent class for all uplink messages that can be sent by connected
+ * components.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true)
+@JsonSubTypes({
+ @JsonSubTypes.Type(value = SegmentBoundaryUpdateMessage.class, name = "sb"),
+ @JsonSubTypes.Type(value = SwitchUpdateMessage.class, name = "sw")
+})
+public abstract class ComponentUplinkMessage {
+ public long cId;
+ public String type;
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/dto/SegmentBoundaryUpdateMessage.java b/src/main/java/nl/andrewl/railsignalapi/live/dto/SegmentBoundaryUpdateMessage.java
new file mode 100644
index 0000000..4c7da55
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/live/dto/SegmentBoundaryUpdateMessage.java
@@ -0,0 +1,30 @@
+package nl.andrewl.railsignalapi.live.dto;
+
+/**
+ * Message that's sent by segment boundaries when a train crosses it.
+ */
+public class SegmentBoundaryUpdateMessage extends ComponentUplinkMessage {
+ /**
+ * The id of the segment that a train detected by the segment boundary is
+ * moving towards.
+ */
+ public long toSegmentId;
+
+ /**
+ * The type of boundary crossing event.
+ */
+ public Type eventType;
+
+ public enum Type {
+ /**
+ * Used when a train first begins to enter a segment, which means the
+ * train is now transitioning from its previous to next segment.
+ */
+ ENTERING,
+ /**
+ * Used when a train has completely entered a segment, which means it
+ * is completely out of its previous segment.
+ */
+ ENTERED
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/dto/SegmentStatusMessage.java b/src/main/java/nl/andrewl/railsignalapi/live/dto/SegmentStatusMessage.java
new file mode 100644
index 0000000..be90693
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/live/dto/SegmentStatusMessage.java
@@ -0,0 +1,6 @@
+package nl.andrewl.railsignalapi.live.dto;
+
+public record SegmentStatusMessage (
+ long cId,
+ boolean occupied
+) {}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/dto/SwitchUpdateMessage.java b/src/main/java/nl/andrewl/railsignalapi/live/dto/SwitchUpdateMessage.java
new file mode 100644
index 0000000..ea1e91b
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/live/dto/SwitchUpdateMessage.java
@@ -0,0 +1,13 @@
+package nl.andrewl.railsignalapi.live.dto;
+
+import java.util.Set;
+
+/**
+ * Message that's sent by a switch when its active configuration is updated.
+ */
+public class SwitchUpdateMessage extends ComponentUplinkMessage {
+ /**
+ * A set of path node ids that represents the active configuration.
+ */
+ public Set configuration;
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/ConnectMessage.java b/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/ConnectMessage.java
new file mode 100644
index 0000000..26c55a0
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/ConnectMessage.java
@@ -0,0 +1,6 @@
+package nl.andrewl.railsignalapi.live.tcp_socket;
+
+public record ConnectMessage(
+ boolean valid,
+ String message
+) {}
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
deleted file mode 100644
index 007c76e..0000000
--- a/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpDownlink.java
+++ /dev/null
@@ -1,24 +0,0 @@
-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/TcpLinkManager.java b/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpLinkManager.java
new file mode 100644
index 0000000..e5154bf
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpLinkManager.java
@@ -0,0 +1,70 @@
+package nl.andrewl.railsignalapi.live.tcp_socket;
+
+import lombok.extern.slf4j.Slf4j;
+import nl.andrewl.railsignalapi.live.ComponentDownlink;
+import nl.andrewl.railsignalapi.live.ComponentDownlinkService;
+import nl.andrewl.railsignalapi.live.ComponentUplinkMessageHandler;
+import nl.andrewl.railsignalapi.live.dto.ComponentUplinkMessage;
+import nl.andrewl.railsignalapi.util.JsonUtils;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.Socket;
+
+/**
+ * This link manager is started when a TCP link is established.
+ */
+@Slf4j
+public class TcpLinkManager extends ComponentDownlink implements Runnable {
+ private final Socket socket;
+ private final ComponentDownlinkService downlinkService;
+ private final ComponentUplinkMessageHandler uplinkMessageHandler;
+
+ private final DataOutputStream out;
+ private final DataInputStream in;
+
+ public TcpLinkManager(
+ long tokenId,
+ Socket socket,
+ ComponentDownlinkService downlinkService,
+ ComponentUplinkMessageHandler uplinkMessageHandler
+ ) throws IOException {
+ super(tokenId);
+ this.socket = socket;
+ this.downlinkService = downlinkService;
+ this.uplinkMessageHandler = uplinkMessageHandler;
+
+ this.out = new DataOutputStream(socket.getOutputStream());
+ this.in = new DataInputStream(socket.getInputStream());
+ }
+
+ @Override
+ public void run() {
+ downlinkService.registerDownlink(this);
+ while (!socket.isClosed()) {
+ try {
+ var msg = JsonUtils.readMessage(in, ComponentUplinkMessage.class);
+ uplinkMessageHandler.messageReceived(msg);
+ } catch (IOException e) {
+ log.warn("An error occurred while receiving an uplink message.", e);
+ }
+ }
+ downlinkService.deregisterDownlink(this);
+ }
+
+ public void shutdown() {
+ try {
+ this.socket.close();
+ } catch (IOException e) {
+ log.warn("An error occurred while closing TCP socket.", e);
+ }
+ }
+
+ @Override
+ public void send(Object msg) throws Exception {
+ synchronized (out) {
+ JsonUtils.writeJsonString(out, msg);
+ }
+ }
+}
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
index 69b298e..1715e0a 100644
--- a/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpSocketServer.java
+++ b/src/main/java/nl/andrewl/railsignalapi/live/tcp_socket/TcpSocketServer.java
@@ -1,8 +1,10 @@
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.dao.LinkTokenRepository;
+import nl.andrewl.railsignalapi.live.ComponentDownlinkService;
+import nl.andrewl.railsignalapi.live.ComponentUplinkMessageHandler;
+import nl.andrewl.railsignalapi.model.LinkToken;
import nl.andrewl.railsignalapi.util.JsonUtils;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.ContextClosedEvent;
@@ -16,23 +18,48 @@ 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;
+import java.util.HashSet;
+import java.util.Set;
/**
* A plain TCP server socket which can be used to connect to components that
- * don't have access to a full websocket client implementation.
+ * don't have access to a full websocket client implementation. Instead of the
+ * standard interceptor -> handler workflow for incoming connections, this
+ * server simply lets the component link send its token as an initial packet
+ * in the socket.
+ *
+ * All messages sent in this TCP socket are formatted as length-prefixed
+ * JSON messages, where a 2-byte length is sent, followed by exactly that
+ * many bytes, which can be parsed as a JSON object.
+ *
+ *
+ * In response to the connection packet, the server will send a
+ * {@link ConnectMessage} response.
+ *
*/
@Component
@Slf4j
public class TcpSocketServer {
private final ServerSocket serverSocket;
- private final ComponentAccessTokenRepository tokenRepository;
- private final PasswordEncoder passwordEncoder;
+ private final Set linkManagers;
- public TcpSocketServer(ComponentAccessTokenRepository tokenRepository, PasswordEncoder passwordEncoder) throws IOException {
+ private final LinkTokenRepository tokenRepository;
+ private final PasswordEncoder passwordEncoder;
+ private final ComponentDownlinkService componentDownlinkService;
+ private final ComponentUplinkMessageHandler uplinkMessageHandler;
+
+ public TcpSocketServer(
+ LinkTokenRepository tokenRepository,
+ PasswordEncoder passwordEncoder,
+ ComponentDownlinkService componentDownlinkService,
+ ComponentUplinkMessageHandler uplinkMessageHandler
+ ) throws IOException {
this.tokenRepository = tokenRepository;
this.passwordEncoder = passwordEncoder;
+ this.componentDownlinkService = componentDownlinkService;
+ this.uplinkMessageHandler = uplinkMessageHandler;
+
+ this.linkManagers = new HashSet<>();
this.serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true);
serverSocket.bind(new InetSocketAddress("localhost", 8081));
@@ -47,33 +74,42 @@ public class TcpSocketServer {
Socket socket = serverSocket.accept();
initializeConnection(socket);
} catch (IOException e) {
- log.warn("An IOException occurred while waiting to accept a TCP socket connection.", e);
+ if (!e.getMessage().contains("Socket closed")) {
+ log.warn("An IOException occurred while waiting to accept a TCP socket connection.", e);
+ }
}
}
- });
+ log.info("TCP Socket has been shut down.");
+ }, "TcpSocketThread").start();
}
@EventListener(ContextClosedEvent.class)
public void closeServer() throws IOException {
serverSocket.close();
+ for (var linkManager : linkManagers) linkManager.shutdown();
}
private void initializeConnection(Socket socket) throws IOException {
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
DataInputStream in = new DataInputStream(socket.getInputStream());
- int tokenLength = in.readInt();
+ short tokenLength = in.readShort();
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);
+ if (rawToken.length() < LinkToken.PREFIX_SIZE) {
+ JsonUtils.writeJsonString(out, new ConnectMessage(false, "Invalid or missing token."));
socket.close();
- }
- Iterable tokens = tokenRepository.findAllByTokenPrefix(rawToken.substring(0, ComponentAccessToken.PREFIX_SIZE));
- for (var token : tokens) {
- if (passwordEncoder.matches(rawToken, token.getTokenHash())) {
-
+ } else {
+ Iterable tokens = tokenRepository.findAllByTokenPrefix(rawToken.substring(0, LinkToken.PREFIX_SIZE));
+ for (var token : tokens) {
+ if (passwordEncoder.matches(rawToken, token.getTokenHash())) {
+ JsonUtils.writeJsonString(out, new ConnectMessage(true, "Connection established."));
+ var linkManager = new TcpLinkManager(token.getId(), socket, componentDownlinkService, uplinkMessageHandler);
+ new Thread(linkManager, "linkManager-" + token.getId()).start();
+ linkManagers.add(linkManager);
+ return;
+ }
}
+ JsonUtils.writeJsonString(out, new ConnectMessage(false, "Invalid token."));
+ socket.close();
}
}
}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppUpdateService.java b/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppUpdateService.java
new file mode 100644
index 0000000..2779dda
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppUpdateService.java
@@ -0,0 +1,80 @@
+package nl.andrewl.railsignalapi.live.websocket;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import nl.andrewl.railsignalapi.dao.ComponentRepository;
+import nl.andrewl.railsignalapi.model.component.Component;
+import nl.andrewl.railsignalapi.rest.dto.component.out.ComponentResponse;
+import nl.andrewl.railsignalapi.util.JsonUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A service that can be used to send live updates of a rail system's state
+ * to connected front-end web apps.
+ */
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class AppUpdateService {
+ private final Map> sessions = new HashMap<>();
+
+ private final ComponentRepository componentRepository;
+
+ public synchronized void registerSession(long rsId, WebSocketSession session) {
+ Set sessionsForRs = sessions.computeIfAbsent(rsId, x -> new HashSet<>());
+ sessionsForRs.add(session);
+ }
+
+ public synchronized void deregisterSession(WebSocketSession session) {
+ Set orphans = new HashSet<>();
+ // Remove the session from any rail systems it's subscribed to.
+ for (var entry : sessions.entrySet()) {
+ if (entry.getValue().contains(session)) {
+ entry.getValue().remove(session);
+ if (entry.getValue().isEmpty()) {
+ orphans.add(entry.getKey());
+ }
+ }
+ }
+ // Clean up the sessions map by removing any rail systems for which there are no subscriptions.
+ for (var orphanRsId : orphans) {
+ sessions.remove(orphanRsId);
+ }
+ }
+
+ public synchronized void sendUpdate(long rsId, Object msg) {
+ Set sessionsForRs = sessions.get(rsId);
+ if (sessionsForRs != null) {
+ try {
+ String json = JsonUtils.toJson(msg);
+ for (var session : sessionsForRs) {
+ try {
+ session.sendMessage(new TextMessage(json));
+ } catch (IOException e) {
+ log.warn("An error occurred when sending message to websocket session.", e);
+ }
+ }
+ } catch (IOException e) {
+ log.error("Failed to produce JSON for message update for apps.", e);
+ }
+
+ }
+ }
+
+ @Transactional(readOnly = true)
+ public void sendComponentUpdate(long rsId, long componentId) {
+ componentRepository.findByIdAndRailSystemId(componentId, rsId).ifPresent(component -> {
+ ComponentResponse msg = ComponentResponse.of(component);
+ sendUpdate(rsId, msg);
+ });
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandler.java b/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandler.java
index 5599f1f..4c0d866 100644
--- a/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandler.java
+++ b/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandler.java
@@ -16,20 +16,21 @@ import org.springframework.web.socket.handler.TextWebSocketHandler;
@RequiredArgsConstructor
@Slf4j
public class AppWebsocketHandler extends TextWebSocketHandler {
+ private final AppUpdateService appUpdateService;
+
@Override
- public void afterConnectionEstablished(WebSocketSession session) throws Exception {
- super.afterConnectionEstablished(session);
- log.info("App websocket session established.");
+ public void afterConnectionEstablished(WebSocketSession session) {
+ long railSystemId = (long) session.getAttributes().get("railSystemId");
+ appUpdateService.registerSession(railSystemId, session);
}
@Override
- protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
- super.handleTextMessage(session, message);
+ protected void handleTextMessage(WebSocketSession session, TextMessage message) {
+ // Don't do anything with messages from the web app. At least not yet.
}
@Override
- public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
- super.afterConnectionClosed(session, status);
- log.info("App websocket session closed.");
+ public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
+ appUpdateService.deregisterSession(session);
}
}
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandshakeInterceptor.java b/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandshakeInterceptor.java
index 7c75921..e8cccf3 100644
--- a/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandshakeInterceptor.java
+++ b/src/main/java/nl/andrewl/railsignalapi/live/websocket/AppWebsocketHandshakeInterceptor.java
@@ -23,7 +23,7 @@ public class AppWebsocketHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) {
String path = request.getURI().getPath();
- Long railSystemId = Long.parseLong(path.substring(path.lastIndexOf('/')));
+ Long railSystemId = Long.parseLong(path.substring(path.lastIndexOf('/') + 1));
if (!railSystemRepository.existsById(railSystemId)) {
response.setStatusCode(HttpStatus.NOT_FOUND);
return false;
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandler.java b/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandler.java
index f39e940..3ec526e 100644
--- a/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandler.java
+++ b/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandler.java
@@ -2,8 +2,10 @@ 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 nl.andrewl.railsignalapi.live.ComponentUplinkMessageHandler;
+import nl.andrewl.railsignalapi.live.dto.ComponentUplinkMessage;
+import nl.andrewl.railsignalapi.util.JsonUtils;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.socket.CloseStatus;
@@ -11,9 +13,6 @@ 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.
@@ -22,24 +21,20 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
@Slf4j
public class ComponentWebsocketHandler extends TextWebSocketHandler {
- private final ComponentAccessTokenRepository tokenRepository;
private final ComponentDownlinkService componentDownlinkService;
+ private final ComponentUplinkMessageHandler uplinkMessageHandler;
@Override
@Transactional(readOnly = true)
- public void afterConnectionEstablished(WebSocketSession session) throws Exception {
+ public void afterConnectionEstablished(WebSocketSession session) {
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);
+ componentDownlinkService.registerDownlink(new WebsocketDownlink(tokenId, session));
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
-// var msg = mapper.readValue(message.getPayload(), SignalUpdateMessage.class);
- //signalService.handleSignalUpdate(msg);
+ var msg = JsonUtils.readMessage(message.getPayload(), ComponentUplinkMessage.class);
+ uplinkMessageHandler.messageReceived(msg);
}
@Override
diff --git a/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandshakeInterceptor.java b/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandshakeInterceptor.java
index 189eeba..8ddc52e 100644
--- a/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandshakeInterceptor.java
+++ b/src/main/java/nl/andrewl/railsignalapi/live/websocket/ComponentWebsocketHandshakeInterceptor.java
@@ -1,8 +1,8 @@
package nl.andrewl.railsignalapi.live.websocket;
import lombok.RequiredArgsConstructor;
-import nl.andrewl.railsignalapi.dao.ComponentAccessTokenRepository;
-import nl.andrewl.railsignalapi.model.ComponentAccessToken;
+import nl.andrewl.railsignalapi.dao.LinkTokenRepository;
+import nl.andrewl.railsignalapi.model.LinkToken;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
@@ -22,7 +22,7 @@ import java.util.Map;
@Component
@RequiredArgsConstructor
public class ComponentWebsocketHandshakeInterceptor implements HandshakeInterceptor {
- private final ComponentAccessTokenRepository tokenRepository;
+ private final LinkTokenRepository tokenRepository;
private final PasswordEncoder passwordEncoder;
@Override
@@ -34,11 +34,11 @@ public class ComponentWebsocketHandshakeInterceptor implements HandshakeIntercep
return false;
}
String rawToken = query.substring(tokenIdx);
- if (rawToken.length() < ComponentAccessToken.PREFIX_SIZE) {
+ if (rawToken.length() < LinkToken.PREFIX_SIZE) {
response.setStatusCode(HttpStatus.BAD_REQUEST);
return false;
}
- Iterable tokens = tokenRepository.findAllByTokenPrefix(rawToken.substring(0, ComponentAccessToken.PREFIX_SIZE));
+ Iterable tokens = tokenRepository.findAllByTokenPrefix(rawToken.substring(0, LinkToken.PREFIX_SIZE));
for (var token : tokens) {
if (passwordEncoder.matches(rawToken, token.getTokenHash())) {
attributes.put("tokenId", token.getId());
diff --git a/src/main/java/nl/andrewl/railsignalapi/model/ComponentAccessToken.java b/src/main/java/nl/andrewl/railsignalapi/model/LinkToken.java
similarity index 89%
rename from src/main/java/nl/andrewl/railsignalapi/model/ComponentAccessToken.java
rename to src/main/java/nl/andrewl/railsignalapi/model/LinkToken.java
index 72ee36e..fb5674f 100644
--- a/src/main/java/nl/andrewl/railsignalapi/model/ComponentAccessToken.java
+++ b/src/main/java/nl/andrewl/railsignalapi/model/LinkToken.java
@@ -17,7 +17,7 @@ import java.util.Set;
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
-public class ComponentAccessToken {
+public class LinkToken {
public static final byte PREFIX_SIZE = 7;
@Id
@@ -54,7 +54,7 @@ public class ComponentAccessToken {
@ManyToMany
private Set components;
- public ComponentAccessToken(RailSystem railSystem, String label, String tokenPrefix, String tokenHash, Set components) {
+ public LinkToken(RailSystem railSystem, String label, String tokenPrefix, String tokenHash, Set components) {
this.railSystem = railSystem;
this.label = label;
this.tokenPrefix = tokenPrefix;
diff --git a/src/main/java/nl/andrewl/railsignalapi/model/Segment.java b/src/main/java/nl/andrewl/railsignalapi/model/Segment.java
index f05b9cf..dfae6f1 100644
--- a/src/main/java/nl/andrewl/railsignalapi/model/Segment.java
+++ b/src/main/java/nl/andrewl/railsignalapi/model/Segment.java
@@ -3,6 +3,7 @@ package nl.andrewl.railsignalapi.model;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
+import lombok.Setter;
import nl.andrewl.railsignalapi.model.component.SegmentBoundaryNode;
import nl.andrewl.railsignalapi.model.component.Signal;
@@ -31,6 +32,13 @@ public class Segment {
@Column
private String name;
+ /**
+ * Whether this segment is occupied by a train.
+ */
+ @Column(nullable = false)
+ @Setter
+ private boolean occupied;
+
/**
* The signals that are connected to this branch.
*/
diff --git a/src/main/java/nl/andrewl/railsignalapi/model/Label.java b/src/main/java/nl/andrewl/railsignalapi/model/component/Label.java
similarity index 87%
rename from src/main/java/nl/andrewl/railsignalapi/model/Label.java
rename to src/main/java/nl/andrewl/railsignalapi/model/component/Label.java
index 6e26399..0fcbac2 100644
--- a/src/main/java/nl/andrewl/railsignalapi/model/Label.java
+++ b/src/main/java/nl/andrewl/railsignalapi/model/component/Label.java
@@ -1,8 +1,9 @@
-package nl.andrewl.railsignalapi.model;
+package nl.andrewl.railsignalapi.model.component;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
+import nl.andrewl.railsignalapi.model.RailSystem;
import nl.andrewl.railsignalapi.model.component.Component;
import nl.andrewl.railsignalapi.model.component.ComponentType;
import nl.andrewl.railsignalapi.model.component.Position;
diff --git a/src/main/java/nl/andrewl/railsignalapi/model/component/SegmentBoundaryNode.java b/src/main/java/nl/andrewl/railsignalapi/model/component/SegmentBoundaryNode.java
index 969e727..d25496a 100644
--- a/src/main/java/nl/andrewl/railsignalapi/model/component/SegmentBoundaryNode.java
+++ b/src/main/java/nl/andrewl/railsignalapi/model/component/SegmentBoundaryNode.java
@@ -20,7 +20,8 @@ import java.util.Set;
public class SegmentBoundaryNode extends PathNode {
/**
* The set of segments that this boundary node connects. This should
- * generally always have exactly two segments.
+ * generally always have exactly two segments. It can never have more than
+ * two segments.
*/
@ManyToMany
private Set segments;
diff --git a/src/main/java/nl/andrewl/railsignalapi/model/component/Switch.java b/src/main/java/nl/andrewl/railsignalapi/model/component/Switch.java
index ce3b1e9..f77d192 100644
--- a/src/main/java/nl/andrewl/railsignalapi/model/component/Switch.java
+++ b/src/main/java/nl/andrewl/railsignalapi/model/component/Switch.java
@@ -7,7 +7,9 @@ import lombok.Setter;
import nl.andrewl.railsignalapi.model.RailSystem;
import javax.persistence.*;
+import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
/**
* A switch is a component that directs traffic between several connected
@@ -36,4 +38,12 @@ public class Switch extends PathNode {
this.possibleConfigurations = possibleConfigurations;
this.activeConfiguration = activeConfiguration;
}
+
+ public Optional findConfiguration(Set pathNodeIds) {
+ for (var config : possibleConfigurations) {
+ Set configNodeIds = config.getNodes().stream().map(Component::getId).collect(Collectors.toSet());
+ if (pathNodeIds.equals(configNodeIds)) return Optional.of(config);
+ }
+ return Optional.empty();
+ }
}
diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/SecurityConfig.java b/src/main/java/nl/andrewl/railsignalapi/rest/SecurityConfig.java
new file mode 100644
index 0000000..5ffbae3
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/rest/SecurityConfig.java
@@ -0,0 +1,28 @@
+package nl.andrewl.railsignalapi.rest;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@EnableWebSecurity
+public class SecurityConfig extends WebSecurityConfigurerAdapter {
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http.authorizeRequests().antMatchers("/**").permitAll();
+ http.cors().disable();
+ http.csrf().disable();
+ http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
+ http.formLogin().disable();
+ http.logout().disable();
+ http.httpBasic().disable();
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder(12);
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/WebConfig.java b/src/main/java/nl/andrewl/railsignalapi/rest/WebConfig.java
index 1725d16..ad2091f 100644
--- a/src/main/java/nl/andrewl/railsignalapi/rest/WebConfig.java
+++ b/src/main/java/nl/andrewl/railsignalapi/rest/WebConfig.java
@@ -22,6 +22,4 @@ public class WebConfig implements WebMvcConfigurer {
.allowedOrigins("*")
.allowedMethods("*");
}
-
-
}
diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/SegmentResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/SegmentResponse.java
index 617774b..bf5639e 100644
--- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/SegmentResponse.java
+++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/SegmentResponse.java
@@ -5,13 +5,11 @@ import nl.andrewl.railsignalapi.model.Segment;
public class SegmentResponse {
public long id;
public String name;
-
- public SegmentResponse(long id, String name) {
- this.id = id;
- this.name = name;
- }
+ public boolean occupied;
public SegmentResponse(Segment s) {
- this(s.getId(), s.getName());
+ this.id = s.getId();
+ this.name = s.getName();
+ this.occupied = s.isOccupied();
}
}
diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/ComponentPayload.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/ComponentPayload.java
index 46619d2..804f91e 100644
--- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/ComponentPayload.java
+++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/ComponentPayload.java
@@ -3,7 +3,7 @@ package nl.andrewl.railsignalapi.rest.dto.component.in;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import nl.andrewl.railsignalapi.model.Label;
+import nl.andrewl.railsignalapi.model.component.Label;
import nl.andrewl.railsignalapi.model.component.Position;
import javax.validation.constraints.NotBlank;
diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/ComponentResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/ComponentResponse.java
index cb43084..530660a 100644
--- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/ComponentResponse.java
+++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/ComponentResponse.java
@@ -1,6 +1,6 @@
package nl.andrewl.railsignalapi.rest.dto.component.out;
-import nl.andrewl.railsignalapi.model.Label;
+import nl.andrewl.railsignalapi.model.component.Label;
import nl.andrewl.railsignalapi.model.component.*;
/**
diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/LabelResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/LabelResponse.java
index 7497191..5325c5a 100644
--- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/LabelResponse.java
+++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/LabelResponse.java
@@ -1,6 +1,6 @@
package nl.andrewl.railsignalapi.rest.dto.component.out;
-import nl.andrewl.railsignalapi.model.Label;
+import nl.andrewl.railsignalapi.model.component.Label;
public class LabelResponse extends ComponentResponse {
public String text;
diff --git a/src/main/java/nl/andrewl/railsignalapi/service/ComponentCreationService.java b/src/main/java/nl/andrewl/railsignalapi/service/ComponentCreationService.java
index 567e264..73e0017 100644
--- a/src/main/java/nl/andrewl/railsignalapi/service/ComponentCreationService.java
+++ b/src/main/java/nl/andrewl/railsignalapi/service/ComponentCreationService.java
@@ -4,7 +4,7 @@ import lombok.RequiredArgsConstructor;
import nl.andrewl.railsignalapi.dao.ComponentRepository;
import nl.andrewl.railsignalapi.dao.RailSystemRepository;
import nl.andrewl.railsignalapi.dao.SegmentRepository;
-import nl.andrewl.railsignalapi.model.Label;
+import nl.andrewl.railsignalapi.model.component.Label;
import nl.andrewl.railsignalapi.model.RailSystem;
import nl.andrewl.railsignalapi.model.Segment;
import nl.andrewl.railsignalapi.model.component.*;
@@ -66,6 +66,7 @@ public class ComponentCreationService {
private Component createSwitch(RailSystem rs, SwitchPayload payload) {
Switch s = new Switch(rs, payload.position, payload.name, new HashSet<>(), new HashSet<>(), null);
+ s = componentRepository.save(s);
for (var config : payload.possibleConfigurations) {
Set pathNodes = new HashSet<>();
for (var node : config.nodes) {
@@ -74,6 +75,8 @@ public class ComponentCreationService {
if (c instanceof PathNode pathNode) {
pathNodes.add(pathNode);
s.getConnectedNodes().add(pathNode);
+ pathNode.getConnectedNodes().add(s);
+ componentRepository.save(pathNode);
} else {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Id " + node.id + " does not refer to a PathNode component.");
}
diff --git a/src/main/java/nl/andrewl/railsignalapi/service/SegmentService.java b/src/main/java/nl/andrewl/railsignalapi/service/SegmentService.java
index fbad3a2..ce136b1 100644
--- a/src/main/java/nl/andrewl/railsignalapi/service/SegmentService.java
+++ b/src/main/java/nl/andrewl/railsignalapi/service/SegmentService.java
@@ -4,16 +4,22 @@ import lombok.RequiredArgsConstructor;
import nl.andrewl.railsignalapi.dao.ComponentRepository;
import nl.andrewl.railsignalapi.dao.RailSystemRepository;
import nl.andrewl.railsignalapi.dao.SegmentRepository;
+import nl.andrewl.railsignalapi.live.ComponentDownlinkService;
+import nl.andrewl.railsignalapi.live.dto.SegmentBoundaryUpdateMessage;
+import nl.andrewl.railsignalapi.live.dto.SegmentStatusMessage;
+import nl.andrewl.railsignalapi.live.websocket.AppUpdateService;
import nl.andrewl.railsignalapi.model.Segment;
import nl.andrewl.railsignalapi.model.component.Component;
-import nl.andrewl.railsignalapi.rest.dto.SegmentPayload;
+import nl.andrewl.railsignalapi.model.component.SegmentBoundaryNode;
import nl.andrewl.railsignalapi.rest.dto.FullSegmentResponse;
+import nl.andrewl.railsignalapi.rest.dto.SegmentPayload;
import nl.andrewl.railsignalapi.rest.dto.SegmentResponse;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
+import java.util.ArrayList;
import java.util.List;
@Service
@@ -22,6 +28,10 @@ public class SegmentService {
private final SegmentRepository segmentRepository;
private final RailSystemRepository railSystemRepository;
private final ComponentRepository componentRepository;
+ private final ComponentRepository segmentBoundaryRepository;
+
+ private final ComponentDownlinkService downlinkService;
+ private final AppUpdateService appUpdateService;
@Transactional(readOnly = true)
public List getSegments(long rsId) {
@@ -55,4 +65,41 @@ public class SegmentService {
componentRepository.deleteAll(segment.getBoundaryNodes());
segmentRepository.delete(segment);
}
+
+ private void sendSegmentOccupiedStatus(Segment segment) {
+ for (var signal : segment.getSignals()) {
+ downlinkService.sendMessage(signal.getId(), new SegmentStatusMessage(signal.getId(), segment.isOccupied()));
+ appUpdateService.sendComponentUpdate(segment.getRailSystem().getId(), signal.getId());
+ }
+ }
+
+ @Transactional
+ public void onBoundaryUpdate(SegmentBoundaryUpdateMessage msg) {
+ var segmentBoundary = segmentBoundaryRepository.findById(msg.cId)
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
+ switch (msg.eventType) {
+ case ENTERING -> {
+ for (var segment : segmentBoundary.getSegments()) {
+ segment.setOccupied(true);
+ }
+ segmentRepository.saveAll(segmentBoundary.getSegments());
+ }
+ case ENTERED -> {
+ List otherSegments = new ArrayList<>(segmentBoundary.getSegments());
+ // Set the "to" segment as occupied.
+ segmentRepository.findById(msg.toSegmentId).ifPresent(segment -> {
+ segment.setOccupied(true);
+ segmentRepository.save(segment);
+ sendSegmentOccupiedStatus(segment);
+ otherSegments.remove(segment);
+ });
+ // And all others as no longer occupied.
+ for (var segment : otherSegments) {
+ segment.setOccupied(false);
+ segmentRepository.save(segment);
+ sendSegmentOccupiedStatus(segment);
+ }
+ }
+ }
+ }
}
diff --git a/src/main/java/nl/andrewl/railsignalapi/service/SwitchService.java b/src/main/java/nl/andrewl/railsignalapi/service/SwitchService.java
new file mode 100644
index 0000000..ffa65b1
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/service/SwitchService.java
@@ -0,0 +1,30 @@
+package nl.andrewl.railsignalapi.service;
+
+import lombok.RequiredArgsConstructor;
+import nl.andrewl.railsignalapi.dao.ComponentRepository;
+import nl.andrewl.railsignalapi.live.ComponentDownlinkService;
+import nl.andrewl.railsignalapi.live.dto.SwitchUpdateMessage;
+import nl.andrewl.railsignalapi.live.websocket.AppUpdateService;
+import nl.andrewl.railsignalapi.model.component.Switch;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class SwitchService {
+ private final ComponentRepository switchRepository;
+
+ private final ComponentDownlinkService downlinkService;
+ private final AppUpdateService appUpdateService;
+
+ @Transactional
+ public void onSwitchUpdate(SwitchUpdateMessage msg) {
+ switchRepository.findById(msg.cId).ifPresent(sw -> {
+ sw.findConfiguration(msg.configuration).ifPresent(config -> {
+ sw.setActiveConfiguration(config);
+ switchRepository.save(sw);
+ appUpdateService.sendComponentUpdate(sw.getRailSystem().getId(), sw.getId());
+ });
+ });
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/util/JsonUtils.java b/src/main/java/nl/andrewl/railsignalapi/util/JsonUtils.java
index 1c13973..edefeff 100644
--- a/src/main/java/nl/andrewl/railsignalapi/util/JsonUtils.java
+++ b/src/main/java/nl/andrewl/railsignalapi/util/JsonUtils.java
@@ -1,12 +1,38 @@
package nl.andrewl.railsignalapi.util;
import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
public class JsonUtils {
private static final ObjectMapper mapper = new ObjectMapper();
+ static {
+ mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ }
public static String toJson(Object o) throws JsonProcessingException {
return mapper.writeValueAsString(o);
}
+
+ public static void writeJsonString(DataOutputStream out, Object o) throws IOException {
+ byte[] data = toJson(o).getBytes(StandardCharsets.UTF_8);
+ if (data.length > Short.MAX_VALUE) throw new IOException("Data is too large!");
+ out.writeShort(data.length);
+ out.write(data);
+ }
+
+ public static T readMessage(DataInputStream in, Class type) throws IOException {
+ short len = in.readShort();
+ byte[] data = in.readNBytes(len);
+ return mapper.readValue(data, type);
+ }
+
+ public static T readMessage(String in, Class type) throws IOException {
+ return mapper.readValue(in, type);
+ }
}