From ecd9549e77bc61e3e96f28228195e316e699fb92 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Mon, 9 May 2022 08:34:57 +0200 Subject: [PATCH] Added more component stuff in preparation for integrations. --- pom.xml | 4 + .../dao/ComponentAccessTokenRepository.java | 12 +++ .../model/ComponentAccessToken.java | 62 +++++++++++ .../nl/andrewl/railsignalapi/model/Label.java | 18 ++-- .../model/component/Component.java | 13 +-- .../model/component/ComponentType.java | 3 +- .../model/component/Position.java | 4 + .../page/IndexPageController.java | 4 + .../rest/ComponentsApiController.java | 9 +- .../dto/component/in/ComponentPayload.java | 18 ++++ .../rest/dto/component/in/LabelPayload.java | 9 ++ .../component/in/SegmentBoundaryPayload.java | 13 +++ .../rest/dto/component/in/SignalPayload.java | 8 +- .../rest/dto/component/in/SwitchPayload.java | 18 ++++ .../dto/component/out/ComponentResponse.java | 14 ++- .../rest/dto/component/out/LabelResponse.java | 11 ++ .../out/SimpleComponentResponse.java | 4 +- .../service/ComponentCreationService.java | 101 ++++++++++++++++++ .../service/ComponentService.java | 90 ++-------------- .../websocket/BranchUpdateMessage.java | 4 - ...fig.java => ComponentWebSocketConfig.java} | 6 +- ...omponentWebSocketHandshakeInterceptor.java | 32 ++++++ .../websocket/SignalUpdateMessage.java | 8 -- .../websocket/SignalUpdateType.java | 15 --- .../websocket/SignalWebSocketHandler.java | 2 +- 25 files changed, 344 insertions(+), 138 deletions(-) create mode 100644 src/main/java/nl/andrewl/railsignalapi/dao/ComponentAccessTokenRepository.java create mode 100644 src/main/java/nl/andrewl/railsignalapi/model/ComponentAccessToken.java create mode 100644 src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/LabelPayload.java create mode 100644 src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/SegmentBoundaryPayload.java create mode 100644 src/main/java/nl/andrewl/railsignalapi/rest/dto/component/in/SwitchPayload.java create mode 100644 src/main/java/nl/andrewl/railsignalapi/rest/dto/component/out/LabelResponse.java create mode 100644 src/main/java/nl/andrewl/railsignalapi/service/ComponentCreationService.java delete mode 100644 src/main/java/nl/andrewl/railsignalapi/websocket/BranchUpdateMessage.java rename src/main/java/nl/andrewl/railsignalapi/websocket/{WebSocketConfig.java => ComponentWebSocketConfig.java} (69%) create mode 100644 src/main/java/nl/andrewl/railsignalapi/websocket/ComponentWebSocketHandshakeInterceptor.java delete mode 100644 src/main/java/nl/andrewl/railsignalapi/websocket/SignalUpdateMessage.java delete mode 100644 src/main/java/nl/andrewl/railsignalapi/websocket/SignalUpdateType.java 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); }