diff --git a/pom.xml b/pom.xml
index 34f4cc6..93c7516 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,6 +29,10 @@
org.springframework.boot
spring-boot-starter-thymeleaf
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
org.springframework.boot
spring-boot-devtools
diff --git a/src/main/java/nl/andrewl/railsignalapi/dao/ComponentAccessTokenRepository.java b/src/main/java/nl/andrewl/railsignalapi/dao/ComponentAccessTokenRepository.java
new file mode 100644
index 0000000..fc31c0b
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/dao/ComponentAccessTokenRepository.java
@@ -0,0 +1,12 @@
+package nl.andrewl.railsignalapi.dao;
+
+import nl.andrewl.railsignalapi.model.ComponentAccessToken;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface ComponentAccessTokenRepository extends JpaRepository {
+ Iterable findAllByTokenPrefix(String prefix);
+ boolean existsByLabel(String label);
+
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/model/ComponentAccessToken.java b/src/main/java/nl/andrewl/railsignalapi/model/ComponentAccessToken.java
new file mode 100644
index 0000000..61cd0d6
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/model/ComponentAccessToken.java
@@ -0,0 +1,62 @@
+package nl.andrewl.railsignalapi.model;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import nl.andrewl.railsignalapi.model.component.Component;
+
+import javax.persistence.*;
+import java.util.Set;
+
+/**
+ * A secure token that allows users to connect to up- and down-link sockets
+ * of components. This token is passed as either a header or query param when
+ * establishing a websocket connection, or as part of the connection init
+ * packet for plain socket connections.
+ */
+@Entity
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Getter
+public class ComponentAccessToken {
+ @Id
+ @GeneratedValue
+ private Long id;
+
+ /**
+ * The rail system that this token belongs to.
+ */
+ @ManyToOne(optional = false, fetch = FetchType.LAZY)
+ private RailSystem railSystem;
+
+ /**
+ * A semantic label for this token.
+ */
+ @Column(length = 63, unique = true)
+ private String label;
+
+ /**
+ * A short prefix of the token, which is useful for speeding up lookup.
+ */
+ @Column(nullable = false, length = 7)
+ private String tokenPrefix;
+
+ /**
+ * A salted, hashed version of the full token string.
+ */
+ @Column(nullable = false, unique = true)
+ private String tokenHash;
+
+ /**
+ * The set of components that this access token grants access to.
+ */
+ @ManyToMany
+ private Set components;
+
+ public ComponentAccessToken(RailSystem railSystem, String label, String tokenPrefix, String tokenHash, Set components) {
+ this.railSystem = railSystem;
+ this.label = label;
+ this.tokenPrefix = tokenPrefix;
+ this.tokenHash = tokenHash;
+ this.components = components;
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/model/Label.java b/src/main/java/nl/andrewl/railsignalapi/model/Label.java
index 2008112..6e26399 100644
--- a/src/main/java/nl/andrewl/railsignalapi/model/Label.java
+++ b/src/main/java/nl/andrewl/railsignalapi/model/Label.java
@@ -3,6 +3,9 @@ package nl.andrewl.railsignalapi.model;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
+import nl.andrewl.railsignalapi.model.component.Component;
+import nl.andrewl.railsignalapi.model.component.ComponentType;
+import nl.andrewl.railsignalapi.model.component.Position;
import javax.persistence.*;
@@ -13,19 +16,12 @@ import javax.persistence.*;
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
-public class Label {
- @Id
- @GeneratedValue
- private Long id;
-
- @ManyToOne(optional = false, fetch = FetchType.LAZY)
- private RailSystem railSystem;
-
- @Column(nullable = false, length = 63)
+public class Label extends Component {
+ @Column(nullable = false)
private String text;
- public Label(RailSystem rs, String text) {
- this.railSystem = rs;
+ public Label(RailSystem rs, Position position, String name, String text) {
+ super(rs, position, name, ComponentType.LABEL);
this.text = text;
}
}
diff --git a/src/main/java/nl/andrewl/railsignalapi/model/component/Component.java b/src/main/java/nl/andrewl/railsignalapi/model/component/Component.java
index e5e177f..ac05695 100644
--- a/src/main/java/nl/andrewl/railsignalapi/model/component/Component.java
+++ b/src/main/java/nl/andrewl/railsignalapi/model/component/Component.java
@@ -9,9 +9,9 @@ import nl.andrewl.railsignalapi.model.RailSystem;
import javax.persistence.*;
/**
- * Represents a physical component of the rail system that the API can interact
- * with, and send or receive data from. For example, a signal, switch, or
- * detector.
+ * Represents component of the rail system that exists in the system's world,
+ * at a specific location. Any component that exists in the rail system extends
+ * from this parent entity.
*/
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@@ -51,9 +51,9 @@ public abstract class Component {
* Whether this component is online, meaning that an in-world device is
* currently connected to relay information regarding this component.
*/
- @Column(nullable = false)
+ @Column
@Setter
- private boolean online = false;
+ private Boolean online = null;
public Component(RailSystem railSystem, Position position, String name, ComponentType type) {
this.railSystem = railSystem;
@@ -65,7 +65,8 @@ public abstract class Component {
@Override
public boolean equals(Object o) {
if (this == o) return true;
- return o instanceof Component c && this.id != null && this.id.equals(c.id);
+ if (!(o instanceof Component c)) return false;
+ return (this.id != null && this.id.equals(c.id)) || this.name.equals(c.name);
}
@Override
diff --git a/src/main/java/nl/andrewl/railsignalapi/model/component/ComponentType.java b/src/main/java/nl/andrewl/railsignalapi/model/component/ComponentType.java
index af9baeb..9dcac80 100644
--- a/src/main/java/nl/andrewl/railsignalapi/model/component/ComponentType.java
+++ b/src/main/java/nl/andrewl/railsignalapi/model/component/ComponentType.java
@@ -3,5 +3,6 @@ package nl.andrewl.railsignalapi.model.component;
public enum ComponentType {
SIGNAL,
SWITCH,
- SEGMENT_BOUNDARY
+ SEGMENT_BOUNDARY,
+ LABEL
}
diff --git a/src/main/java/nl/andrewl/railsignalapi/model/component/Position.java b/src/main/java/nl/andrewl/railsignalapi/model/component/Position.java
index f7bb43f..e1cec0b 100644
--- a/src/main/java/nl/andrewl/railsignalapi/model/component/Position.java
+++ b/src/main/java/nl/andrewl/railsignalapi/model/component/Position.java
@@ -5,6 +5,7 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Embeddable;
+import javax.validation.constraints.NotNull;
/**
* A three-dimensional position for a component within a system.
@@ -14,7 +15,10 @@ import javax.persistence.Embeddable;
@AllArgsConstructor
@NoArgsConstructor
public class Position {
+ @NotNull
private double x;
+ @NotNull
private double y;
+ @NotNull
private double z;
}
diff --git a/src/main/java/nl/andrewl/railsignalapi/page/IndexPageController.java b/src/main/java/nl/andrewl/railsignalapi/page/IndexPageController.java
index e3cf732..b3d0a40 100644
--- a/src/main/java/nl/andrewl/railsignalapi/page/IndexPageController.java
+++ b/src/main/java/nl/andrewl/railsignalapi/page/IndexPageController.java
@@ -4,6 +4,10 @@ import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
+/**
+ * Helper controller that redirects some common starting points to our embedded
+ * web app's index page.
+ */
@Controller
@RequestMapping(path = {"/", "/app", "/home", "/index.html", "/index"})
public class IndexPageController {
diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/ComponentsApiController.java b/src/main/java/nl/andrewl/railsignalapi/rest/ComponentsApiController.java
index 67b2763..4ff3345 100644
--- a/src/main/java/nl/andrewl/railsignalapi/rest/ComponentsApiController.java
+++ b/src/main/java/nl/andrewl/railsignalapi/rest/ComponentsApiController.java
@@ -1,13 +1,15 @@
package nl.andrewl.railsignalapi.rest;
-import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
import nl.andrewl.railsignalapi.rest.dto.PathNodeUpdatePayload;
+import nl.andrewl.railsignalapi.rest.dto.component.in.ComponentPayload;
import nl.andrewl.railsignalapi.rest.dto.component.out.ComponentResponse;
+import nl.andrewl.railsignalapi.service.ComponentCreationService;
import nl.andrewl.railsignalapi.service.ComponentService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
+import javax.validation.Valid;
import java.util.List;
@RestController
@@ -15,6 +17,7 @@ import java.util.List;
@RequiredArgsConstructor
public class ComponentsApiController {
private final ComponentService componentService;
+ private final ComponentCreationService componentCreationService;
@GetMapping
public List getAllComponents(@PathVariable long rsId) {
@@ -27,8 +30,8 @@ public class ComponentsApiController {
}
@PostMapping
- public ComponentResponse createComponent(@PathVariable long rsId, @RequestBody ObjectNode data) {
- return componentService.create(rsId, data);
+ public ComponentResponse createComponent(@PathVariable long rsId, @RequestBody ComponentPayload payload) {
+ return componentCreationService.create(rsId, payload);
}
@DeleteMapping(path = "/{cId}")
diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/ComponentPayload.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/ComponentPayload.java
index a9066b7..46619d2 100644
--- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/ComponentPayload.java
+++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/ComponentPayload.java
@@ -1,9 +1,27 @@
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.Position;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true)
+@JsonSubTypes({
+ @JsonSubTypes.Type(value = SignalPayload.class, name = "SIGNAL"),
+ @JsonSubTypes.Type(value = SwitchPayload.class, name = "SWITCH"),
+ @JsonSubTypes.Type(value = SegmentBoundaryPayload.class, name = "SEGMENT_BOUNDARY"),
+ @JsonSubTypes.Type(value = Label.class, name = "LABEL")
+})
public abstract class ComponentPayload {
+ @NotNull @NotBlank
public String name;
+ @NotNull @NotBlank
public String type;
+ @NotNull
public Position position;
}
diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/LabelPayload.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/LabelPayload.java
new file mode 100644
index 0000000..30145d2
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/LabelPayload.java
@@ -0,0 +1,9 @@
+package nl.andrewl.railsignalapi.rest.dto.component.in;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+public class LabelPayload extends ComponentPayload {
+ @NotNull @NotBlank
+ public String text;
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/SegmentBoundaryPayload.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/SegmentBoundaryPayload.java
new file mode 100644
index 0000000..30f38cd
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/SegmentBoundaryPayload.java
@@ -0,0 +1,13 @@
+package nl.andrewl.railsignalapi.rest.dto.component.in;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Size;
+
+public class SegmentBoundaryPayload extends ComponentPayload {
+ @NotEmpty @Size(min = 1, max = 2)
+ public SegmentPayload[] segments;
+
+ public static class SegmentPayload {
+ public long id;
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/SignalPayload.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/SignalPayload.java
index c29cbf2..ee9bfd0 100644
--- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/SignalPayload.java
+++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/SignalPayload.java
@@ -1,5 +1,11 @@
package nl.andrewl.railsignalapi.rest.dto.component.in;
+import javax.validation.constraints.NotNull;
+
public class SignalPayload extends ComponentPayload {
- public long segmentId;
+ @NotNull
+ public SegmentPayload segment;
+ public static class SegmentPayload {
+ public long id;
+ }
}
diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/SwitchPayload.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/SwitchPayload.java
new file mode 100644
index 0000000..5e4fc43
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/SwitchPayload.java
@@ -0,0 +1,18 @@
+package nl.andrewl.railsignalapi.rest.dto.component.in;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.Size;
+
+public class SwitchPayload extends ComponentPayload {
+ @NotEmpty @Size(min = 2, max = 10)
+ public SwitchConfigurationPayload[] possibleConfigurations;
+
+ public static class SwitchConfigurationPayload {
+ @NotEmpty @Size(min = 2, max = 10)
+ public NodePayload[] nodes;
+
+ public static class NodePayload {
+ public long id;
+ }
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/ComponentResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/ComponentResponse.java
index 2d78269..cb43084 100644
--- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/ComponentResponse.java
+++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/ComponentResponse.java
@@ -1,27 +1,37 @@
package nl.andrewl.railsignalapi.rest.dto.component.out;
+import nl.andrewl.railsignalapi.model.Label;
import nl.andrewl.railsignalapi.model.component.*;
+/**
+ * The base class for any component's API response object.
+ */
public abstract class ComponentResponse {
public long id;
public Position position;
public String name;
public String type;
- public boolean online;
+ public Boolean online;
public ComponentResponse(Component c) {
this.id = c.getId();
this.position = c.getPosition();
this.name = c.getName();
this.type = c.getType().name();
- this.online = c.isOnline();
+ this.online = c.getOnline();
}
+ /**
+ * Builds a full response for a component of any type.
+ * @param c The component to build a response for.
+ * @return The response.
+ */
public static ComponentResponse of(Component c) {
return switch (c.getType()) {
case SIGNAL -> new SignalResponse((Signal) c);
case SWITCH -> new SwitchResponse((Switch) c);
case SEGMENT_BOUNDARY -> new SegmentBoundaryNodeResponse((SegmentBoundaryNode) c);
+ case LABEL -> new LabelResponse((Label) c);
};
}
}
diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/LabelResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/LabelResponse.java
new file mode 100644
index 0000000..7497191
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/LabelResponse.java
@@ -0,0 +1,11 @@
+package nl.andrewl.railsignalapi.rest.dto.component.out;
+
+import nl.andrewl.railsignalapi.model.Label;
+
+public class LabelResponse extends ComponentResponse {
+ public String text;
+ public LabelResponse(Label lbl) {
+ super(lbl);
+ this.text = lbl.getText();
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SimpleComponentResponse.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SimpleComponentResponse.java
index c26e66f..4e27368 100644
--- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SimpleComponentResponse.java
+++ b/src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/SimpleComponentResponse.java
@@ -8,7 +8,7 @@ public record SimpleComponentResponse (
Position position,
String name,
String type,
- boolean online
+ Boolean online
) {
public SimpleComponentResponse(Component c) {
this(
@@ -16,7 +16,7 @@ public record SimpleComponentResponse (
c.getPosition(),
c.getName(),
c.getType().name(),
- c.isOnline()
+ c.getOnline()
);
}
}
diff --git a/src/main/java/nl/andrewl/railsignalapi/service/ComponentCreationService.java b/src/main/java/nl/andrewl/railsignalapi/service/ComponentCreationService.java
new file mode 100644
index 0000000..567e264
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/service/ComponentCreationService.java
@@ -0,0 +1,101 @@
+package nl.andrewl.railsignalapi.service;
+
+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.RailSystem;
+import nl.andrewl.railsignalapi.model.Segment;
+import nl.andrewl.railsignalapi.model.component.*;
+import nl.andrewl.railsignalapi.rest.dto.component.in.*;
+import nl.andrewl.railsignalapi.rest.dto.component.out.ComponentResponse;
+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.HashSet;
+import java.util.Set;
+
+@Service
+@RequiredArgsConstructor
+public class ComponentCreationService {
+ private final RailSystemRepository railSystemRepository;
+ private final ComponentRepository componentRepository;
+ private final SegmentRepository segmentRepository;
+
+ @Transactional
+ public ComponentResponse create(long rsId, ComponentPayload payload) {
+ RailSystem rs = railSystemRepository.findById(rsId)
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
+ String name = payload.name;
+ if (name == null || name.isBlank()) {
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Missing required name.");
+ }
+ if (componentRepository.existsByNameAndRailSystem(name, rs)) {
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Component with that name already exists.");
+ }
+ ComponentType type;
+ try {
+ type = ComponentType.valueOf(payload.type);
+ } catch (IllegalArgumentException e) {
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid component type.", e);
+ }
+
+ Component c = switch (type) {
+ case SIGNAL -> createSignal(rs, (SignalPayload) payload);
+ case SWITCH -> createSwitch(rs, (SwitchPayload) payload);
+ case SEGMENT_BOUNDARY -> createSegmentBoundary(rs, (SegmentBoundaryPayload) payload);
+ case LABEL -> createLabel(rs, (LabelPayload) payload);
+ };
+ c = componentRepository.save(c);
+ return ComponentResponse.of(c);
+ }
+
+ private Component createLabel(RailSystem rs, LabelPayload payload) {
+ return new Label(rs, payload.position, payload.name, payload.text);
+ }
+
+ private Component createSignal(RailSystem rs, SignalPayload payload) {
+ long segmentId = payload.segment.id;
+ Segment segment = segmentRepository.findById(segmentId)
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
+ return new Signal(rs, payload.position, payload.name, segment);
+ }
+
+ private Component createSwitch(RailSystem rs, SwitchPayload payload) {
+ Switch s = new Switch(rs, payload.position, payload.name, new HashSet<>(), new HashSet<>(), null);
+ for (var config : payload.possibleConfigurations) {
+ Set pathNodes = new HashSet<>();
+ for (var node : config.nodes) {
+ Component c = componentRepository.findById(node.id)
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
+ if (c instanceof PathNode pathNode) {
+ pathNodes.add(pathNode);
+ s.getConnectedNodes().add(pathNode);
+ } else {
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Id " + node.id + " does not refer to a PathNode component.");
+ }
+ }
+ s.getPossibleConfigurations().add(new SwitchConfiguration(s, pathNodes));
+ }
+ if (s.getPossibleConfigurations().size() < 2) {
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "At least two switch configurations are needed.");
+ }
+ return s;
+ }
+
+ private Component createSegmentBoundary(RailSystem rs, SegmentBoundaryPayload payload) {
+ Set segments = new HashSet<>();
+ for (var segmentP : payload.segments) {
+ Segment segment = segmentRepository.findById(segmentP.id)
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
+ segments.add(segment);
+ }
+ if (segments.size() < 1 || segments.size() > 2) {
+ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid number of segments.");
+ }
+ return new SegmentBoundaryNode(rs, payload.position, payload.name, new HashSet<>(), segments);
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/service/ComponentService.java b/src/main/java/nl/andrewl/railsignalapi/service/ComponentService.java
index 2feff49..bfcb029 100644
--- a/src/main/java/nl/andrewl/railsignalapi/service/ComponentService.java
+++ b/src/main/java/nl/andrewl/railsignalapi/service/ComponentService.java
@@ -1,16 +1,11 @@
package nl.andrewl.railsignalapi.service;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
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.dao.SwitchConfigurationRepository;
-import nl.andrewl.railsignalapi.model.RailSystem;
-import nl.andrewl.railsignalapi.model.Segment;
-import nl.andrewl.railsignalapi.model.component.*;
+import nl.andrewl.railsignalapi.model.component.Component;
+import nl.andrewl.railsignalapi.model.component.PathNode;
import nl.andrewl.railsignalapi.rest.dto.PathNodeUpdatePayload;
import nl.andrewl.railsignalapi.rest.dto.component.out.ComponentResponse;
import org.springframework.http.HttpStatus;
@@ -27,7 +22,6 @@ import java.util.Set;
public class ComponentService {
private final ComponentRepository componentRepository;
private final RailSystemRepository railSystemRepository;
- private final SegmentRepository segmentRepository;
private final SwitchConfigurationRepository switchConfigurationRepository;
@Transactional(readOnly = true)
@@ -48,86 +42,18 @@ public class ComponentService {
public void removeComponent(long rsId, long componentId) {
var c = componentRepository.findByIdAndRailSystemId(componentId, rsId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
- // If this is a path node, check for and remove any switch configurations that use it.
if (c instanceof PathNode p) {
+ // Remove all connections to other path nodes.
+ for (var connectedNode : p.getConnectedNodes()) {
+ connectedNode.getConnectedNodes().remove(p);
+ componentRepository.save(connectedNode);
+ }
+ // Remove any switch configurations using this node.
switchConfigurationRepository.deleteAllByNodesContaining(p);
}
componentRepository.delete(c);
}
- @Transactional
- public ComponentResponse create(long rsId, ObjectNode data) {
- RailSystem rs = railSystemRepository.findById(rsId)
- .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
- String type = data.get("type").asText();
- Position pos = new Position();
- pos.setX(data.get("position").get("x").asDouble());
- pos.setY(data.get("position").get("y").asDouble());
- pos.setZ(data.get("position").get("z").asDouble());
- String name = data.get("name").asText();
- if (name == null || name.isBlank()) {
- throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Missing required name.");
- }
-
- if (componentRepository.existsByNameAndRailSystem(name, rs)) {
- throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Component with that name already exists.");
- }
-
- Component c = switch (type) {
- case "SIGNAL" -> createSignal(rs, pos, name, data);
- case "SWITCH" -> createSwitch(rs, pos, name, data);
- case "SEGMENT_BOUNDARY" -> createSegmentBoundary(rs, pos, name, data);
- default -> throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unsupported component type: " + type);
- };
- c = componentRepository.save(c);
- return ComponentResponse.of(c);
- }
-
- private Component createSignal(RailSystem rs, Position pos, String name, ObjectNode data) {
- long segmentId = data.get("segment").get("id").asLong();
- Segment segment = segmentRepository.findById(segmentId)
- .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
- return new Signal(rs, pos, name, segment);
- }
-
- private Component createSwitch(RailSystem rs, Position pos, String name, ObjectNode data) {
- Switch s = new Switch(rs, pos, name, new HashSet<>(), new HashSet<>(), null);
- for (JsonNode configJson : data.withArray("possibleConfigurations")) {
- Set pathNodes = new HashSet<>();
- for (JsonNode pathNodeJson : configJson.withArray("nodes")) {
- long pathNodeId = pathNodeJson.get("id").asLong();
- Component c = componentRepository.findById(pathNodeId)
- .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
- if (c instanceof PathNode pathNode) {
- pathNodes.add(pathNode);
- s.getConnectedNodes().add(pathNode);
- } else {
- throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Id " + pathNodeId + " does not refer to a PathNode component.");
- }
- }
- s.getPossibleConfigurations().add(new SwitchConfiguration(s, pathNodes));
- }
- if (s.getPossibleConfigurations().size() < 2) {
- throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "At least two switch configurations are needed.");
- }
- return s;
- }
-
- private Component createSegmentBoundary(RailSystem rs, Position pos, String name, ObjectNode data) {
- ArrayNode segmentsNode = data.withArray("segments");
- Set segments = new HashSet<>();
- for (JsonNode segmentNode : segmentsNode) {
- long segmentId = segmentNode.get("id").asLong();
- Segment segment = segmentRepository.findById(segmentId)
- .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
- segments.add(segment);
- }
- if (segments.size() < 1 || segments.size() > 2) {
- throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid number of segments.");
- }
- return new SegmentBoundaryNode(rs, pos, name, new HashSet<>(), segments);
- }
-
@Transactional
public ComponentResponse updatePath(long rsId, long cId, PathNodeUpdatePayload payload) {
var c = componentRepository.findByIdAndRailSystemId(cId, rsId)
diff --git a/src/main/java/nl/andrewl/railsignalapi/websocket/BranchUpdateMessage.java b/src/main/java/nl/andrewl/railsignalapi/websocket/BranchUpdateMessage.java
deleted file mode 100644
index fdea444..0000000
--- a/src/main/java/nl/andrewl/railsignalapi/websocket/BranchUpdateMessage.java
+++ /dev/null
@@ -1,4 +0,0 @@
-package nl.andrewl.railsignalapi.websocket;
-
-public record BranchUpdateMessage(long branchId, String status) {
-}
diff --git a/src/main/java/nl/andrewl/railsignalapi/websocket/WebSocketConfig.java b/src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketConfig.java
similarity index 69%
rename from src/main/java/nl/andrewl/railsignalapi/websocket/WebSocketConfig.java
rename to src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketConfig.java
index 21a186c..2881be3 100644
--- a/src/main/java/nl/andrewl/railsignalapi/websocket/WebSocketConfig.java
+++ b/src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketConfig.java
@@ -9,11 +9,13 @@ import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
@Configuration
@EnableWebSocket
@RequiredArgsConstructor
-public class WebSocketConfig implements WebSocketConfigurer {
+public class ComponentWebSocketConfig implements WebSocketConfigurer {
private final SignalWebSocketHandler webSocketHandler;
+ private final ComponentWebSocketHandshakeInterceptor handshakeInterceptor;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
- registry.addHandler(webSocketHandler, "/api/ws-signal");
+ registry.addHandler(webSocketHandler, "/api/ws/component")
+ .addInterceptors(handshakeInterceptor);
}
}
diff --git a/src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketHandshakeInterceptor.java b/src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketHandshakeInterceptor.java
new file mode 100644
index 0000000..e25c88b
--- /dev/null
+++ b/src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketHandshakeInterceptor.java
@@ -0,0 +1,32 @@
+package nl.andrewl.railsignalapi.websocket;
+
+import lombok.RequiredArgsConstructor;
+import nl.andrewl.railsignalapi.dao.ComponentRepository;
+import nl.andrewl.railsignalapi.dao.RailSystemRepository;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+import java.util.Arrays;
+import java.util.Map;
+
+@Component
+@RequiredArgsConstructor
+public class ComponentWebSocketHandshakeInterceptor implements HandshakeInterceptor {
+ private final RailSystemRepository railSystemRepository;
+ private final ComponentRepository componentRepository;
+
+ @Override
+ public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception {
+ String[] queryParams = request.getURI().getQuery().split("&");
+ System.out.println(Arrays.toString(queryParams));
+ return false;
+ }
+
+ @Override
+ public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
+
+ }
+}
diff --git a/src/main/java/nl/andrewl/railsignalapi/websocket/SignalUpdateMessage.java b/src/main/java/nl/andrewl/railsignalapi/websocket/SignalUpdateMessage.java
deleted file mode 100644
index 29af247..0000000
--- a/src/main/java/nl/andrewl/railsignalapi/websocket/SignalUpdateMessage.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package nl.andrewl.railsignalapi.websocket;
-
-public record SignalUpdateMessage(
- long signalId,
- long fromBranchId,
- long toBranchId,
- String type
-) {}
diff --git a/src/main/java/nl/andrewl/railsignalapi/websocket/SignalUpdateType.java b/src/main/java/nl/andrewl/railsignalapi/websocket/SignalUpdateType.java
deleted file mode 100644
index de89e9a..0000000
--- a/src/main/java/nl/andrewl/railsignalapi/websocket/SignalUpdateType.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package nl.andrewl.railsignalapi.websocket;
-
-public enum SignalUpdateType {
- /**
- * Indicates the beginning of a train's transition between two signalled
- * sections of rail.
- */
- BEGIN,
-
- /**
- * Indicates the end of a train's transition between two signalled sections
- * of rail.
- */
- END
-}
diff --git a/src/main/java/nl/andrewl/railsignalapi/websocket/SignalWebSocketHandler.java b/src/main/java/nl/andrewl/railsignalapi/websocket/SignalWebSocketHandler.java
index 81b2b1e..20e9b9d 100644
--- a/src/main/java/nl/andrewl/railsignalapi/websocket/SignalWebSocketHandler.java
+++ b/src/main/java/nl/andrewl/railsignalapi/websocket/SignalWebSocketHandler.java
@@ -35,7 +35,7 @@ public class SignalWebSocketHandler extends TextWebSocketHandler {
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
- var msg = mapper.readValue(message.getPayload(), SignalUpdateMessage.class);
+// var msg = mapper.readValue(message.getPayload(), SignalUpdateMessage.class);
//signalService.handleSignalUpdate(msg);
}