Added websocket connectivity and improved some things.
This commit is contained in:
parent
35c13d83bd
commit
08fb892cc5
|
@ -29,7 +29,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<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">
|
<select id="addSwitchConfigs" class="form-select" multiple v-model="formData.possibleConfigQueue">
|
||||||
<option v-for="node in getEligibleNodes()" :key="node.id" :value="node">
|
<option v-for="node in getEligibleNodes()" :key="node.id" :value="node">
|
||||||
{{node.name}}
|
{{node.name}}
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
</table>
|
</table>
|
||||||
<SignalComponentView v-if="component.type === 'SIGNAL'" :signal="component" />
|
<SignalComponentView v-if="component.type === 'SIGNAL'" :signal="component" />
|
||||||
<SegmentBoundaryNodeComponentView v-if="component.type === 'SEGMENT_BOUNDARY'" :node="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" />
|
<PathNodeComponentView v-if="component.connectedNodes" :pathNode="component" :railSystem="railSystem" />
|
||||||
<button @click="removeComponent()" class="btn btn-sm btn-danger">Remove</button>
|
<button @click="removeComponent()" class="btn btn-sm btn-danger">Remove</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,9 +46,11 @@ import PathNodeComponentView from "./PathNodeComponentView.vue";
|
||||||
import SegmentBoundaryNodeComponentView from "./SegmentBoundaryNodeComponentView.vue";
|
import SegmentBoundaryNodeComponentView from "./SegmentBoundaryNodeComponentView.vue";
|
||||||
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
|
import {useRailSystemsStore} from "../../../stores/railSystemsStore";
|
||||||
import ConfirmModal from "../../ConfirmModal.vue";
|
import ConfirmModal from "../../ConfirmModal.vue";
|
||||||
|
import SwitchComponentView from "./SwitchComponentView.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
SwitchComponentView,
|
||||||
ConfirmModal,
|
ConfirmModal,
|
||||||
SegmentBoundaryNodeComponentView,
|
SegmentBoundaryNodeComponentView,
|
||||||
SignalComponentView,
|
SignalComponentView,
|
||||||
|
|
|
@ -1,40 +1,32 @@
|
||||||
<template>
|
<template>
|
||||||
<h5>Connected Nodes</h5>
|
<h5>Connected Nodes</h5>
|
||||||
<table class="table" v-if="pathNode.connectedNodes.length > 0">
|
<ul class="list-group list-group-flush mb-2 border" v-if="pathNode.connectedNodes.length > 0" style="overflow: auto; max-height: 150px;">
|
||||||
<thead>
|
<li
|
||||||
<tr>
|
v-for="node in pathNode.connectedNodes"
|
||||||
<th>Name</th>
|
:key="node.id"
|
||||||
</tr>
|
class="list-group-item"
|
||||||
</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"
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p v-if="pathNode.connectedNodes.length === 0">
|
|
||||||
There are no connected nodes.
|
|
||||||
</p>
|
|
||||||
<form
|
|
||||||
@submit.prevent="rsStore.addConnection(pathNode, formData.nodeToAdd)"
|
|
||||||
v-if="getEligibleConnections().length > 0"
|
|
||||||
class="input-group mb-3"
|
|
||||||
>
|
|
||||||
<select v-model="formData.nodeToAdd" class="form-select form-select-sm">
|
|
||||||
<option v-for="node in this.getEligibleConnections()" :key="node.id" :value="node">
|
|
||||||
{{node.name}}
|
{{node.name}}
|
||||||
</option>
|
<button @click="rsStore.removeConnection(pathNode, node)" class="btn btn-sm btn-danger float-end">
|
||||||
</select>
|
Remove
|
||||||
<button type="submit" class="btn btn-sm btn-success">Add Connection</button>
|
</button>
|
||||||
</form>
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p v-if="pathNode.connectedNodes.length === 0">
|
||||||
|
There are no connected nodes.
|
||||||
|
</p>
|
||||||
|
<form
|
||||||
|
@submit.prevent="rsStore.addConnection(pathNode, formData.nodeToAdd)"
|
||||||
|
v-if="getEligibleConnections().length > 0"
|
||||||
|
class="input-group mb-3"
|
||||||
|
>
|
||||||
|
<select v-model="formData.nodeToAdd" class="form-select form-select-sm">
|
||||||
|
<option v-for="node in this.getEligibleConnections()" :key="node.id" :value="node">
|
||||||
|
{{node.name}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="btn btn-sm btn-success">Add Connection</button>
|
||||||
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -4,11 +4,13 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
<th>Occupied</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="segment in node.segments" :key="segment.id">
|
<tr v-for="segment in node.segments" :key="segment.id">
|
||||||
<td>{{segment.name}}</td>
|
<td>{{segment.name}}</td>
|
||||||
|
<td>{{segment.occupied}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Connected to</th>
|
<th>Connected to</th>
|
||||||
<td>{{signal.segment.name}}</td>
|
<td>{{signal.segment.name}}, Occupied: {{signal.segment.occupied}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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();
|
this.websocket.close();
|
||||||
}
|
}
|
||||||
console.log(this.wsUrl);
|
console.log(this.wsUrl);
|
||||||
this.websocket = new WebSocket(this.wsUrl);
|
this.websocket = new WebSocket(this.wsUrl + "/" + this.selectedRailSystem.id);
|
||||||
this.websocket.onopen = event => {
|
this.websocket.onopen = event => {
|
||||||
console.log("Opened websocket connection.");
|
console.log("Opened websocket connection.");
|
||||||
};
|
};
|
||||||
this.websocket.onclose = event => {
|
this.websocket.onclose = event => {
|
||||||
console.log("Closed websocket connection.");
|
console.log("Closed websocket connection.");
|
||||||
};
|
};
|
||||||
this.websocket.onmessage = () => {
|
this.websocket.onmessage = (msg) => {
|
||||||
|
console.log(msg);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
addSegment(name) {
|
addSegment(name) {
|
||||||
|
|
|
@ -2,8 +2,11 @@ package nl.andrewl.railsignalapi;
|
||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication(exclude = {
|
||||||
|
UserDetailsServiceAutoConfiguration.class
|
||||||
|
})
|
||||||
public class RailSignalApiApplication {
|
public class RailSignalApiApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
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;
|
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 nl.andrewl.railsignalapi.model.RailSystem;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
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;
|
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 {
|
public abstract class ComponentDownlink {
|
||||||
@Getter
|
@Getter
|
||||||
private final long id;
|
private final long tokenId;
|
||||||
|
|
||||||
public ComponentDownlink(long id) {
|
public ComponentDownlink(long tokenId) {
|
||||||
this.id = id;
|
this.tokenId = tokenId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void send(Object msg) throws Exception;
|
public abstract void send(Object msg) throws Exception;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
return o instanceof ComponentDownlink cd && cd.id == this.id;
|
return o instanceof ComponentDownlink cd && cd.tokenId == this.tokenId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Long.hashCode(id);
|
return Long.hashCode(tokenId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,91 @@
|
||||||
package nl.andrewl.railsignalapi.live;
|
package nl.andrewl.railsignalapi.live;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
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.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.*;
|
||||||
import java.util.List;
|
import java.util.stream.Collectors;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service that manages all the active component downlink connections.
|
* A service that manages all the active component downlink connections.
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
public class ComponentDownlinkService {
|
public class ComponentDownlinkService {
|
||||||
private final Map<ComponentDownlink, Set<Long>> componentDownlinks = new HashMap<>();
|
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) {
|
private final LinkTokenRepository tokenRepository;
|
||||||
componentDownlinks.put(downlink, componentIds);
|
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) {
|
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) {
|
public synchronized void deregisterDownlink(long tokenId) {
|
||||||
List<ComponentDownlink> removeSet = componentDownlinks.keySet().stream()
|
List<ComponentDownlink> removeSet = componentDownlinks.keySet().stream()
|
||||||
.filter(downlink -> downlink.getId() == tokenId).toList();
|
.filter(downlink -> downlink.getTokenId() == tokenId).toList();
|
||||||
for (var downlink : removeSet) {
|
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;
|
package nl.andrewl.railsignalapi.live.tcp_socket;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import nl.andrewl.railsignalapi.dao.ComponentAccessTokenRepository;
|
import nl.andrewl.railsignalapi.dao.LinkTokenRepository;
|
||||||
import nl.andrewl.railsignalapi.model.ComponentAccessToken;
|
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 nl.andrewl.railsignalapi.util.JsonUtils;
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
import org.springframework.context.event.ContextClosedEvent;
|
import org.springframework.context.event.ContextClosedEvent;
|
||||||
|
@ -16,23 +18,48 @@ import java.io.IOException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A plain TCP server socket which can be used to connect to components that
|
* 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
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class TcpSocketServer {
|
public class TcpSocketServer {
|
||||||
private final ServerSocket serverSocket;
|
private final ServerSocket serverSocket;
|
||||||
private final ComponentAccessTokenRepository tokenRepository;
|
private final Set<TcpLinkManager> linkManagers;
|
||||||
private final PasswordEncoder passwordEncoder;
|
|
||||||
|
|
||||||
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.tokenRepository = tokenRepository;
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
this.componentDownlinkService = componentDownlinkService;
|
||||||
|
this.uplinkMessageHandler = uplinkMessageHandler;
|
||||||
|
|
||||||
|
this.linkManagers = new HashSet<>();
|
||||||
this.serverSocket = new ServerSocket();
|
this.serverSocket = new ServerSocket();
|
||||||
serverSocket.setReuseAddress(true);
|
serverSocket.setReuseAddress(true);
|
||||||
serverSocket.bind(new InetSocketAddress("localhost", 8081));
|
serverSocket.bind(new InetSocketAddress("localhost", 8081));
|
||||||
|
@ -47,33 +74,42 @@ public class TcpSocketServer {
|
||||||
Socket socket = serverSocket.accept();
|
Socket socket = serverSocket.accept();
|
||||||
initializeConnection(socket);
|
initializeConnection(socket);
|
||||||
} catch (IOException e) {
|
} 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)
|
@EventListener(ContextClosedEvent.class)
|
||||||
public void closeServer() throws IOException {
|
public void closeServer() throws IOException {
|
||||||
serverSocket.close();
|
serverSocket.close();
|
||||||
|
for (var linkManager : linkManagers) linkManager.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeConnection(Socket socket) throws IOException {
|
private void initializeConnection(Socket socket) throws IOException {
|
||||||
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
|
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
|
||||||
DataInputStream in = new DataInputStream(socket.getInputStream());
|
DataInputStream in = new DataInputStream(socket.getInputStream());
|
||||||
int tokenLength = in.readInt();
|
short tokenLength = in.readShort();
|
||||||
String rawToken = new String(in.readNBytes(tokenLength));
|
String rawToken = new String(in.readNBytes(tokenLength));
|
||||||
if (rawToken.length() < ComponentAccessToken.PREFIX_SIZE) {
|
if (rawToken.length() < LinkToken.PREFIX_SIZE) {
|
||||||
byte[] respBytes = JsonUtils.toJson(Map.of("message", "Invalid token")).getBytes(StandardCharsets.UTF_8);
|
JsonUtils.writeJsonString(out, new ConnectMessage(false, "Invalid or missing token."));
|
||||||
out.writeInt(respBytes.length);
|
|
||||||
out.write(respBytes);
|
|
||||||
socket.close();
|
socket.close();
|
||||||
}
|
} else {
|
||||||
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) {
|
for (var token : tokens) {
|
||||||
if (passwordEncoder.matches(rawToken, token.getTokenHash())) {
|
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
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class AppWebsocketHandler extends TextWebSocketHandler {
|
public class AppWebsocketHandler extends TextWebSocketHandler {
|
||||||
|
private final AppUpdateService appUpdateService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
public void afterConnectionEstablished(WebSocketSession session) {
|
||||||
super.afterConnectionEstablished(session);
|
long railSystemId = (long) session.getAttributes().get("railSystemId");
|
||||||
log.info("App websocket session established.");
|
appUpdateService.registerSession(railSystemId, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
|
||||||
super.handleTextMessage(session, message);
|
// Don't do anything with messages from the web app. At least not yet.
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
|
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
|
||||||
super.afterConnectionClosed(session, status);
|
appUpdateService.deregisterSession(session);
|
||||||
log.info("App websocket session closed.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ public class AppWebsocketHandshakeInterceptor implements HandshakeInterceptor {
|
||||||
@Override
|
@Override
|
||||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) {
|
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) {
|
||||||
String path = request.getURI().getPath();
|
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)) {
|
if (!railSystemRepository.existsById(railSystemId)) {
|
||||||
response.setStatusCode(HttpStatus.NOT_FOUND);
|
response.setStatusCode(HttpStatus.NOT_FOUND);
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -2,8 +2,10 @@ package nl.andrewl.railsignalapi.live.websocket;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import nl.andrewl.railsignalapi.dao.ComponentAccessTokenRepository;
|
|
||||||
import nl.andrewl.railsignalapi.live.ComponentDownlinkService;
|
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.stereotype.Component;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.socket.CloseStatus;
|
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.WebSocketSession;
|
||||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
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
|
* Handler for websocket connections that components open to send and receive
|
||||||
* real-time updates from the server.
|
* real-time updates from the server.
|
||||||
|
@ -22,24 +21,20 @@ import java.util.stream.Collectors;
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ComponentWebsocketHandler extends TextWebSocketHandler {
|
public class ComponentWebsocketHandler extends TextWebSocketHandler {
|
||||||
private final ComponentAccessTokenRepository tokenRepository;
|
|
||||||
private final ComponentDownlinkService componentDownlinkService;
|
private final ComponentDownlinkService componentDownlinkService;
|
||||||
|
private final ComponentUplinkMessageHandler uplinkMessageHandler;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
public void afterConnectionEstablished(WebSocketSession session) {
|
||||||
long tokenId = (long) session.getAttributes().get("tokenId");
|
long tokenId = (long) session.getAttributes().get("tokenId");
|
||||||
var token = tokenRepository.findById(tokenId).orElseThrow();
|
componentDownlinkService.registerDownlink(new WebsocketDownlink(tokenId, session));
|
||||||
Set<Long> componentIds = token.getComponents().stream()
|
|
||||||
.map(nl.andrewl.railsignalapi.model.component.Component::getId)
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
componentDownlinkService.registerDownlink(new WebsocketDownlink(tokenId, session), componentIds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||||
// var msg = mapper.readValue(message.getPayload(), SignalUpdateMessage.class);
|
var msg = JsonUtils.readMessage(message.getPayload(), ComponentUplinkMessage.class);
|
||||||
//signalService.handleSignalUpdate(msg);
|
uplinkMessageHandler.messageReceived(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package nl.andrewl.railsignalapi.live.websocket;
|
package nl.andrewl.railsignalapi.live.websocket;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import nl.andrewl.railsignalapi.dao.ComponentAccessTokenRepository;
|
import nl.andrewl.railsignalapi.dao.LinkTokenRepository;
|
||||||
import nl.andrewl.railsignalapi.model.ComponentAccessToken;
|
import nl.andrewl.railsignalapi.model.LinkToken;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.server.ServerHttpRequest;
|
import org.springframework.http.server.ServerHttpRequest;
|
||||||
import org.springframework.http.server.ServerHttpResponse;
|
import org.springframework.http.server.ServerHttpResponse;
|
||||||
|
@ -22,7 +22,7 @@ import java.util.Map;
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ComponentWebsocketHandshakeInterceptor implements HandshakeInterceptor {
|
public class ComponentWebsocketHandshakeInterceptor implements HandshakeInterceptor {
|
||||||
private final ComponentAccessTokenRepository tokenRepository;
|
private final LinkTokenRepository tokenRepository;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -34,11 +34,11 @@ public class ComponentWebsocketHandshakeInterceptor implements HandshakeIntercep
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
String rawToken = query.substring(tokenIdx);
|
String rawToken = query.substring(tokenIdx);
|
||||||
if (rawToken.length() < ComponentAccessToken.PREFIX_SIZE) {
|
if (rawToken.length() < LinkToken.PREFIX_SIZE) {
|
||||||
response.setStatusCode(HttpStatus.BAD_REQUEST);
|
response.setStatusCode(HttpStatus.BAD_REQUEST);
|
||||||
return false;
|
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) {
|
for (var token : tokens) {
|
||||||
if (passwordEncoder.matches(rawToken, token.getTokenHash())) {
|
if (passwordEncoder.matches(rawToken, token.getTokenHash())) {
|
||||||
attributes.put("tokenId", token.getId());
|
attributes.put("tokenId", token.getId());
|
||||||
|
|
|
@ -17,7 +17,7 @@ import java.util.Set;
|
||||||
@Entity
|
@Entity
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
@Getter
|
@Getter
|
||||||
public class ComponentAccessToken {
|
public class LinkToken {
|
||||||
public static final byte PREFIX_SIZE = 7;
|
public static final byte PREFIX_SIZE = 7;
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
|
@ -54,7 +54,7 @@ public class ComponentAccessToken {
|
||||||
@ManyToMany
|
@ManyToMany
|
||||||
private Set<Component> components;
|
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.railSystem = railSystem;
|
||||||
this.label = label;
|
this.label = label;
|
||||||
this.tokenPrefix = tokenPrefix;
|
this.tokenPrefix = tokenPrefix;
|
|
@ -3,6 +3,7 @@ package nl.andrewl.railsignalapi.model;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
import nl.andrewl.railsignalapi.model.component.SegmentBoundaryNode;
|
import nl.andrewl.railsignalapi.model.component.SegmentBoundaryNode;
|
||||||
import nl.andrewl.railsignalapi.model.component.Signal;
|
import nl.andrewl.railsignalapi.model.component.Signal;
|
||||||
|
|
||||||
|
@ -31,6 +32,13 @@ public class Segment {
|
||||||
@Column
|
@Column
|
||||||
private String name;
|
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.
|
* 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.AccessLevel;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||||
import nl.andrewl.railsignalapi.model.component.Component;
|
import nl.andrewl.railsignalapi.model.component.Component;
|
||||||
import nl.andrewl.railsignalapi.model.component.ComponentType;
|
import nl.andrewl.railsignalapi.model.component.ComponentType;
|
||||||
import nl.andrewl.railsignalapi.model.component.Position;
|
import nl.andrewl.railsignalapi.model.component.Position;
|
|
@ -20,7 +20,8 @@ import java.util.Set;
|
||||||
public class SegmentBoundaryNode extends PathNode {
|
public class SegmentBoundaryNode extends PathNode {
|
||||||
/**
|
/**
|
||||||
* The set of segments that this boundary node connects. This should
|
* 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
|
@ManyToMany
|
||||||
private Set<Segment> segments;
|
private Set<Segment> segments;
|
||||||
|
|
|
@ -7,7 +7,9 @@ import lombok.Setter;
|
||||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
import nl.andrewl.railsignalapi.model.RailSystem;
|
||||||
|
|
||||||
import javax.persistence.*;
|
import javax.persistence.*;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A switch is a component that directs traffic between several connected
|
* A switch is a component that directs traffic between several connected
|
||||||
|
@ -36,4 +38,12 @@ public class Switch extends PathNode {
|
||||||
this.possibleConfigurations = possibleConfigurations;
|
this.possibleConfigurations = possibleConfigurations;
|
||||||
this.activeConfiguration = activeConfiguration;
|
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("*")
|
.allowedOrigins("*")
|
||||||
.allowedMethods("*");
|
.allowedMethods("*");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,11 @@ import nl.andrewl.railsignalapi.model.Segment;
|
||||||
public class SegmentResponse {
|
public class SegmentResponse {
|
||||||
public long id;
|
public long id;
|
||||||
public String name;
|
public String name;
|
||||||
|
public boolean occupied;
|
||||||
public SegmentResponse(long id, String name) {
|
|
||||||
this.id = id;
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SegmentResponse(Segment s) {
|
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.JsonIgnoreProperties;
|
||||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
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 nl.andrewl.railsignalapi.model.component.Position;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.NotBlank;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package nl.andrewl.railsignalapi.rest.dto.component.out;
|
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.*;
|
import nl.andrewl.railsignalapi.model.component.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package nl.andrewl.railsignalapi.rest.dto.component.out;
|
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 class LabelResponse extends ComponentResponse {
|
||||||
public String text;
|
public String text;
|
||||||
|
|
|
@ -4,7 +4,7 @@ import lombok.RequiredArgsConstructor;
|
||||||
import nl.andrewl.railsignalapi.dao.ComponentRepository;
|
import nl.andrewl.railsignalapi.dao.ComponentRepository;
|
||||||
import nl.andrewl.railsignalapi.dao.RailSystemRepository;
|
import nl.andrewl.railsignalapi.dao.RailSystemRepository;
|
||||||
import nl.andrewl.railsignalapi.dao.SegmentRepository;
|
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.RailSystem;
|
||||||
import nl.andrewl.railsignalapi.model.Segment;
|
import nl.andrewl.railsignalapi.model.Segment;
|
||||||
import nl.andrewl.railsignalapi.model.component.*;
|
import nl.andrewl.railsignalapi.model.component.*;
|
||||||
|
@ -66,6 +66,7 @@ public class ComponentCreationService {
|
||||||
|
|
||||||
private Component createSwitch(RailSystem rs, SwitchPayload payload) {
|
private Component createSwitch(RailSystem rs, SwitchPayload payload) {
|
||||||
Switch s = new Switch(rs, payload.position, payload.name, new HashSet<>(), new HashSet<>(), null);
|
Switch s = new Switch(rs, payload.position, payload.name, new HashSet<>(), new HashSet<>(), null);
|
||||||
|
s = componentRepository.save(s);
|
||||||
for (var config : payload.possibleConfigurations) {
|
for (var config : payload.possibleConfigurations) {
|
||||||
Set<PathNode> pathNodes = new HashSet<>();
|
Set<PathNode> pathNodes = new HashSet<>();
|
||||||
for (var node : config.nodes) {
|
for (var node : config.nodes) {
|
||||||
|
@ -74,6 +75,8 @@ public class ComponentCreationService {
|
||||||
if (c instanceof PathNode pathNode) {
|
if (c instanceof PathNode pathNode) {
|
||||||
pathNodes.add(pathNode);
|
pathNodes.add(pathNode);
|
||||||
s.getConnectedNodes().add(pathNode);
|
s.getConnectedNodes().add(pathNode);
|
||||||
|
pathNode.getConnectedNodes().add(s);
|
||||||
|
componentRepository.save(pathNode);
|
||||||
} else {
|
} else {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Id " + node.id + " does not refer to a PathNode component.");
|
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.ComponentRepository;
|
||||||
import nl.andrewl.railsignalapi.dao.RailSystemRepository;
|
import nl.andrewl.railsignalapi.dao.RailSystemRepository;
|
||||||
import nl.andrewl.railsignalapi.dao.SegmentRepository;
|
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.Segment;
|
||||||
import nl.andrewl.railsignalapi.model.component.Component;
|
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.FullSegmentResponse;
|
||||||
|
import nl.andrewl.railsignalapi.rest.dto.SegmentPayload;
|
||||||
import nl.andrewl.railsignalapi.rest.dto.SegmentResponse;
|
import nl.andrewl.railsignalapi.rest.dto.SegmentResponse;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@ -22,6 +28,10 @@ public class SegmentService {
|
||||||
private final SegmentRepository segmentRepository;
|
private final SegmentRepository segmentRepository;
|
||||||
private final RailSystemRepository railSystemRepository;
|
private final RailSystemRepository railSystemRepository;
|
||||||
private final ComponentRepository<Component> componentRepository;
|
private final ComponentRepository<Component> componentRepository;
|
||||||
|
private final ComponentRepository<SegmentBoundaryNode> segmentBoundaryRepository;
|
||||||
|
|
||||||
|
private final ComponentDownlinkService downlinkService;
|
||||||
|
private final AppUpdateService appUpdateService;
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public List<SegmentResponse> getSegments(long rsId) {
|
public List<SegmentResponse> getSegments(long rsId) {
|
||||||
|
@ -55,4 +65,41 @@ public class SegmentService {
|
||||||
componentRepository.deleteAll(segment.getBoundaryNodes());
|
componentRepository.deleteAll(segment.getBoundaryNodes());
|
||||||
segmentRepository.delete(segment);
|
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;
|
package nl.andrewl.railsignalapi.util;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
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 {
|
public class JsonUtils {
|
||||||
private static final ObjectMapper mapper = new ObjectMapper();
|
private static final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
static {
|
||||||
|
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||||
|
}
|
||||||
|
|
||||||
public static String toJson(Object o) throws JsonProcessingException {
|
public static String toJson(Object o) throws JsonProcessingException {
|
||||||
return mapper.writeValueAsString(o);
|
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