Added websocket connectivity and improved some things.
This commit is contained in:
parent
35c13d83bd
commit
08fb892cc5
|
@ -29,7 +29,7 @@
|
|||
</ul>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="addSwitchConfigs" class="form-label">Segment</label>
|
||||
<label for="addSwitchConfigs" class="form-label">Select two nodes this switch can connect.</label>
|
||||
<select id="addSwitchConfigs" class="form-select" multiple v-model="formData.possibleConfigQueue">
|
||||
<option v-for="node in getEligibleNodes()" :key="node.id" :value="node">
|
||||
{{node.name}}
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
</table>
|
||||
<SignalComponentView v-if="component.type === 'SIGNAL'" :signal="component" />
|
||||
<SegmentBoundaryNodeComponentView v-if="component.type === 'SEGMENT_BOUNDARY'" :node="component" />
|
||||
<SwitchComponentView v-if="component.type === 'SWITCH'" :sw="component"/>
|
||||
<PathNodeComponentView v-if="component.connectedNodes" :pathNode="component" :railSystem="railSystem" />
|
||||
<button @click="removeComponent()" class="btn btn-sm btn-danger">Remove</button>
|
||||
</div>
|
||||
|
@ -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,
|
||||
|
|
|
@ -1,25 +1,17 @@
|
|||
<template>
|
||||
<h5>Connected Nodes</h5>
|
||||
<table class="table" v-if="pathNode.connectedNodes.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="node in pathNode.connectedNodes" :key="node.id">
|
||||
<td>{{node.name}}</td>
|
||||
<td>
|
||||
<button
|
||||
@click="rsStore.removeConnection(pathNode, node)"
|
||||
class="btn btn-sm btn-danger"
|
||||
<ul class="list-group list-group-flush mb-2 border" v-if="pathNode.connectedNodes.length > 0" style="overflow: auto; max-height: 150px;">
|
||||
<li
|
||||
v-for="node in pathNode.connectedNodes"
|
||||
:key="node.id"
|
||||
class="list-group-item"
|
||||
>
|
||||
{{node.name}}
|
||||
<button @click="rsStore.removeConnection(pathNode, node)" class="btn btn-sm btn-danger float-end">
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="pathNode.connectedNodes.length === 0">
|
||||
There are no connected nodes.
|
||||
</p>
|
||||
|
|
|
@ -4,11 +4,13 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Occupied</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="segment in node.segments" :key="segment.id">
|
||||
<td>{{segment.name}}</td>
|
||||
<td>{{segment.occupied}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<tbody>
|
||||
<tr>
|
||||
<th>Connected to</th>
|
||||
<td>{{signal.segment.name}}</td>
|
||||
<td>{{signal.segment.name}}, Occupied: {{signal.segment.occupied}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<h5>Switch Configurations</h5>
|
||||
<ul class="list-group list-group-flush border" v-if="sw.possibleConfigurations.length > 0" style="overflow: auto; max-height: 150px;">
|
||||
<li
|
||||
v-for="config in sw.possibleConfigurations"
|
||||
:key="config.id"
|
||||
class="list-group-item"
|
||||
>
|
||||
<span
|
||||
v-for="node in config.nodes"
|
||||
:key="node.id"
|
||||
class="badge bg-secondary me-1"
|
||||
>
|
||||
{{node.name}}
|
||||
</span>
|
||||
<span
|
||||
v-if="sw.activeConfiguration !== null && sw.activeConfiguration.id === config.id"
|
||||
class="badge bg-success"
|
||||
>
|
||||
Active
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SwitchComponentView",
|
||||
props: {
|
||||
sw: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<ComponentAccessToken, Long> {
|
||||
Iterable<ComponentAccessToken> findAllByTokenPrefix(String prefix);
|
||||
boolean existsByLabel(String label);
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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<LinkToken, Long> {
|
||||
Iterable<LinkToken> findAllByTokenPrefix(String prefix);
|
||||
boolean existsByLabel(String label);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ComponentDownlink, Set<Long>> componentDownlinks = new HashMap<>();
|
||||
private final Map<Long, Set<ComponentDownlink>> downlinksByCId = new HashMap<>();
|
||||
|
||||
public synchronized void registerDownlink(ComponentDownlink downlink, Set<Long> componentIds) {
|
||||
componentDownlinks.put(downlink, componentIds);
|
||||
private final LinkTokenRepository tokenRepository;
|
||||
private final ComponentRepository<Component> 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<Component> 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<ComponentDownlink> 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<Long> componentIds = componentDownlinks.remove(downlink);
|
||||
if (componentIds != null) {
|
||||
for (var cId : componentIds) {
|
||||
componentRepository.findById(cId).ifPresent(component -> {
|
||||
component.setOnline(false);
|
||||
componentRepository.save(component);
|
||||
});
|
||||
Set<ComponentDownlink> 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<ComponentDownlink> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package nl.andrewl.railsignalapi.live.dto;
|
||||
|
||||
public record SegmentStatusMessage (
|
||||
long cId,
|
||||
boolean occupied
|
||||
) {}
|
|
@ -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<Long> configuration;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package nl.andrewl.railsignalapi.live.tcp_socket;
|
||||
|
||||
public record ConnectMessage(
|
||||
boolean valid,
|
||||
String message
|
||||
) {}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
* <p>
|
||||
* In response to the connection packet, the server will send a
|
||||
* {@link ConnectMessage} response.
|
||||
* </p>
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class TcpSocketServer {
|
||||
private final ServerSocket serverSocket;
|
||||
private final ComponentAccessTokenRepository tokenRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final Set<TcpLinkManager> 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) {
|
||||
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<ComponentAccessToken> tokens = tokenRepository.findAllByTokenPrefix(rawToken.substring(0, ComponentAccessToken.PREFIX_SIZE));
|
||||
} else {
|
||||
Iterable<LinkToken> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Long, Set<WebSocketSession>> sessions = new HashMap<>();
|
||||
|
||||
private final ComponentRepository<Component> componentRepository;
|
||||
|
||||
public synchronized void registerSession(long rsId, WebSocketSession session) {
|
||||
Set<WebSocketSession> sessionsForRs = sessions.computeIfAbsent(rsId, x -> new HashSet<>());
|
||||
sessionsForRs.add(session);
|
||||
}
|
||||
|
||||
public synchronized void deregisterSession(WebSocketSession session) {
|
||||
Set<Long> 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<WebSocketSession> 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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ public class AppWebsocketHandshakeInterceptor implements HandshakeInterceptor {
|
|||
@Override
|
||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> 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;
|
||||
|
|
|
@ -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<Long> 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
|
||||
|
|
|
@ -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<ComponentAccessToken> tokens = tokenRepository.findAllByTokenPrefix(rawToken.substring(0, ComponentAccessToken.PREFIX_SIZE));
|
||||
Iterable<LinkToken> tokens = tokenRepository.findAllByTokenPrefix(rawToken.substring(0, LinkToken.PREFIX_SIZE));
|
||||
for (var token : tokens) {
|
||||
if (passwordEncoder.matches(rawToken, token.getTokenHash())) {
|
||||
attributes.put("tokenId", token.getId());
|
||||
|
|
|
@ -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<Component> components;
|
||||
|
||||
public ComponentAccessToken(RailSystem railSystem, String label, String tokenPrefix, String tokenHash, Set<Component> components) {
|
||||
public LinkToken(RailSystem railSystem, String label, String tokenPrefix, String tokenHash, Set<Component> components) {
|
||||
this.railSystem = railSystem;
|
||||
this.label = label;
|
||||
this.tokenPrefix = tokenPrefix;
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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;
|
|
@ -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<Segment> segments;
|
||||
|
|
|
@ -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<SwitchConfiguration> findConfiguration(Set<Long> pathNodeIds) {
|
||||
for (var config : possibleConfigurations) {
|
||||
Set<Long> configNodeIds = config.getNodes().stream().map(Component::getId).collect(Collectors.toSet());
|
||||
if (pathNodeIds.equals(configNodeIds)) return Optional.of(config);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -22,6 +22,4 @@ public class WebConfig implements WebMvcConfigurer {
|
|||
.allowedOrigins("*")
|
||||
.allowedMethods("*");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.*;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<PathNode> 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.");
|
||||
}
|
||||
|
|
|
@ -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<Component> componentRepository;
|
||||
private final ComponentRepository<SegmentBoundaryNode> segmentBoundaryRepository;
|
||||
|
||||
private final ComponentDownlinkService downlinkService;
|
||||
private final AppUpdateService appUpdateService;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<SegmentResponse> 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<Segment> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Switch> 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());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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> T readMessage(DataInputStream in, Class<T> type) throws IOException {
|
||||
short len = in.readShort();
|
||||
byte[] data = in.readNBytes(len);
|
||||
return mapper.readValue(data, type);
|
||||
}
|
||||
|
||||
public static <T> T readMessage(String in, Class<T> type) throws IOException {
|
||||
return mapper.readValue(in, type);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue