Added more component stuff in preparation for integrations.
This commit is contained in:
parent
1906111ab8
commit
ecd9549e77
4
pom.xml
4
pom.xml
|
@ -29,6 +29,10 @@
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-devtools</artifactId>
|
<artifactId>spring-boot-devtools</artifactId>
|
||||||
|
|
|
@ -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<ComponentAccessToken, Long> {
|
||||||
|
Iterable<ComponentAccessToken> findAllByTokenPrefix(String prefix);
|
||||||
|
boolean existsByLabel(String label);
|
||||||
|
|
||||||
|
}
|
|
@ -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<Component> components;
|
||||||
|
|
||||||
|
public ComponentAccessToken(RailSystem railSystem, String label, String tokenPrefix, String tokenHash, Set<Component> components) {
|
||||||
|
this.railSystem = railSystem;
|
||||||
|
this.label = label;
|
||||||
|
this.tokenPrefix = tokenPrefix;
|
||||||
|
this.tokenHash = tokenHash;
|
||||||
|
this.components = components;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,9 @@ package nl.andrewl.railsignalapi.model;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
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.*;
|
import javax.persistence.*;
|
||||||
|
|
||||||
|
@ -13,19 +16,12 @@ import javax.persistence.*;
|
||||||
@Entity
|
@Entity
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
@Getter
|
@Getter
|
||||||
public class Label {
|
public class Label extends Component {
|
||||||
@Id
|
@Column(nullable = false)
|
||||||
@GeneratedValue
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
|
||||||
private RailSystem railSystem;
|
|
||||||
|
|
||||||
@Column(nullable = false, length = 63)
|
|
||||||
private String text;
|
private String text;
|
||||||
|
|
||||||
public Label(RailSystem rs, String text) {
|
public Label(RailSystem rs, Position position, String name, String text) {
|
||||||
this.railSystem = rs;
|
super(rs, position, name, ComponentType.LABEL);
|
||||||
this.text = text;
|
this.text = text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,9 @@ import nl.andrewl.railsignalapi.model.RailSystem;
|
||||||
import javax.persistence.*;
|
import javax.persistence.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a physical component of the rail system that the API can interact
|
* Represents component of the rail system that exists in the system's world,
|
||||||
* with, and send or receive data from. For example, a signal, switch, or
|
* at a specific location. Any component that exists in the rail system extends
|
||||||
* detector.
|
* from this parent entity.
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Inheritance(strategy = InheritanceType.JOINED)
|
@Inheritance(strategy = InheritanceType.JOINED)
|
||||||
|
@ -51,9 +51,9 @@ public abstract class Component {
|
||||||
* Whether this component is online, meaning that an in-world device is
|
* Whether this component is online, meaning that an in-world device is
|
||||||
* currently connected to relay information regarding this component.
|
* currently connected to relay information regarding this component.
|
||||||
*/
|
*/
|
||||||
@Column(nullable = false)
|
@Column
|
||||||
@Setter
|
@Setter
|
||||||
private boolean online = false;
|
private Boolean online = null;
|
||||||
|
|
||||||
public Component(RailSystem railSystem, Position position, String name, ComponentType type) {
|
public Component(RailSystem railSystem, Position position, String name, ComponentType type) {
|
||||||
this.railSystem = railSystem;
|
this.railSystem = railSystem;
|
||||||
|
@ -65,7 +65,8 @@ public abstract class Component {
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
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
|
@Override
|
||||||
|
|
|
@ -3,5 +3,6 @@ package nl.andrewl.railsignalapi.model.component;
|
||||||
public enum ComponentType {
|
public enum ComponentType {
|
||||||
SIGNAL,
|
SIGNAL,
|
||||||
SWITCH,
|
SWITCH,
|
||||||
SEGMENT_BOUNDARY
|
SEGMENT_BOUNDARY,
|
||||||
|
LABEL
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import javax.persistence.Embeddable;
|
import javax.persistence.Embeddable;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A three-dimensional position for a component within a system.
|
* A three-dimensional position for a component within a system.
|
||||||
|
@ -14,7 +15,10 @@ import javax.persistence.Embeddable;
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class Position {
|
public class Position {
|
||||||
|
@NotNull
|
||||||
private double x;
|
private double x;
|
||||||
|
@NotNull
|
||||||
private double y;
|
private double y;
|
||||||
|
@NotNull
|
||||||
private double z;
|
private double z;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,10 @@ import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper controller that redirects some common starting points to our embedded
|
||||||
|
* web app's index page.
|
||||||
|
*/
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping(path = {"/", "/app", "/home", "/index.html", "/index"})
|
@RequestMapping(path = {"/", "/app", "/home", "/index.html", "/index"})
|
||||||
public class IndexPageController {
|
public class IndexPageController {
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
package nl.andrewl.railsignalapi.rest;
|
package nl.andrewl.railsignalapi.rest;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import nl.andrewl.railsignalapi.rest.dto.PathNodeUpdatePayload;
|
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.rest.dto.component.out.ComponentResponse;
|
||||||
|
import nl.andrewl.railsignalapi.service.ComponentCreationService;
|
||||||
import nl.andrewl.railsignalapi.service.ComponentService;
|
import nl.andrewl.railsignalapi.service.ComponentService;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@ -15,6 +17,7 @@ import java.util.List;
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ComponentsApiController {
|
public class ComponentsApiController {
|
||||||
private final ComponentService componentService;
|
private final ComponentService componentService;
|
||||||
|
private final ComponentCreationService componentCreationService;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public List<ComponentResponse> getAllComponents(@PathVariable long rsId) {
|
public List<ComponentResponse> getAllComponents(@PathVariable long rsId) {
|
||||||
|
@ -27,8 +30,8 @@ public class ComponentsApiController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ComponentResponse createComponent(@PathVariable long rsId, @RequestBody ObjectNode data) {
|
public ComponentResponse createComponent(@PathVariable long rsId, @RequestBody ComponentPayload payload) {
|
||||||
return componentService.create(rsId, data);
|
return componentCreationService.create(rsId, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping(path = "/{cId}")
|
@DeleteMapping(path = "/{cId}")
|
||||||
|
|
|
@ -1,9 +1,27 @@
|
||||||
package nl.andrewl.railsignalapi.rest.dto.component.in;
|
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 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 {
|
public abstract class ComponentPayload {
|
||||||
|
@NotNull @NotBlank
|
||||||
public String name;
|
public String name;
|
||||||
|
@NotNull @NotBlank
|
||||||
public String type;
|
public String type;
|
||||||
|
@NotNull
|
||||||
public Position position;
|
public Position position;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,11 @@
|
||||||
package nl.andrewl.railsignalapi.rest.dto.component.in;
|
package nl.andrewl.railsignalapi.rest.dto.component.in;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
public class SignalPayload extends ComponentPayload {
|
public class SignalPayload extends ComponentPayload {
|
||||||
public long segmentId;
|
@NotNull
|
||||||
|
public SegmentPayload segment;
|
||||||
|
public static class SegmentPayload {
|
||||||
|
public long id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,27 +1,37 @@
|
||||||
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.*;
|
import nl.andrewl.railsignalapi.model.component.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base class for any component's API response object.
|
||||||
|
*/
|
||||||
public abstract class ComponentResponse {
|
public abstract class ComponentResponse {
|
||||||
public long id;
|
public long id;
|
||||||
public Position position;
|
public Position position;
|
||||||
public String name;
|
public String name;
|
||||||
public String type;
|
public String type;
|
||||||
public boolean online;
|
public Boolean online;
|
||||||
|
|
||||||
public ComponentResponse(Component c) {
|
public ComponentResponse(Component c) {
|
||||||
this.id = c.getId();
|
this.id = c.getId();
|
||||||
this.position = c.getPosition();
|
this.position = c.getPosition();
|
||||||
this.name = c.getName();
|
this.name = c.getName();
|
||||||
this.type = c.getType().name();
|
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) {
|
public static ComponentResponse of(Component c) {
|
||||||
return switch (c.getType()) {
|
return switch (c.getType()) {
|
||||||
case SIGNAL -> new SignalResponse((Signal) c);
|
case SIGNAL -> new SignalResponse((Signal) c);
|
||||||
case SWITCH -> new SwitchResponse((Switch) c);
|
case SWITCH -> new SwitchResponse((Switch) c);
|
||||||
case SEGMENT_BOUNDARY -> new SegmentBoundaryNodeResponse((SegmentBoundaryNode) c);
|
case SEGMENT_BOUNDARY -> new SegmentBoundaryNodeResponse((SegmentBoundaryNode) c);
|
||||||
|
case LABEL -> new LabelResponse((Label) c);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ public record SimpleComponentResponse (
|
||||||
Position position,
|
Position position,
|
||||||
String name,
|
String name,
|
||||||
String type,
|
String type,
|
||||||
boolean online
|
Boolean online
|
||||||
) {
|
) {
|
||||||
public SimpleComponentResponse(Component c) {
|
public SimpleComponentResponse(Component c) {
|
||||||
this(
|
this(
|
||||||
|
@ -16,7 +16,7 @@ public record SimpleComponentResponse (
|
||||||
c.getPosition(),
|
c.getPosition(),
|
||||||
c.getName(),
|
c.getName(),
|
||||||
c.getType().name(),
|
c.getType().name(),
|
||||||
c.isOnline()
|
c.getOnline()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<Component> 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<PathNode> 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<Segment> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,11 @@
|
||||||
package nl.andrewl.railsignalapi.service;
|
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 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.SwitchConfigurationRepository;
|
import nl.andrewl.railsignalapi.dao.SwitchConfigurationRepository;
|
||||||
import nl.andrewl.railsignalapi.model.RailSystem;
|
import nl.andrewl.railsignalapi.model.component.Component;
|
||||||
import nl.andrewl.railsignalapi.model.Segment;
|
import nl.andrewl.railsignalapi.model.component.PathNode;
|
||||||
import nl.andrewl.railsignalapi.model.component.*;
|
|
||||||
import nl.andrewl.railsignalapi.rest.dto.PathNodeUpdatePayload;
|
import nl.andrewl.railsignalapi.rest.dto.PathNodeUpdatePayload;
|
||||||
import nl.andrewl.railsignalapi.rest.dto.component.out.ComponentResponse;
|
import nl.andrewl.railsignalapi.rest.dto.component.out.ComponentResponse;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
@ -27,7 +22,6 @@ import java.util.Set;
|
||||||
public class ComponentService {
|
public class ComponentService {
|
||||||
private final ComponentRepository<Component> componentRepository;
|
private final ComponentRepository<Component> componentRepository;
|
||||||
private final RailSystemRepository railSystemRepository;
|
private final RailSystemRepository railSystemRepository;
|
||||||
private final SegmentRepository segmentRepository;
|
|
||||||
private final SwitchConfigurationRepository switchConfigurationRepository;
|
private final SwitchConfigurationRepository switchConfigurationRepository;
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
|
@ -48,86 +42,18 @@ public class ComponentService {
|
||||||
public void removeComponent(long rsId, long componentId) {
|
public void removeComponent(long rsId, long componentId) {
|
||||||
var c = componentRepository.findByIdAndRailSystemId(componentId, rsId)
|
var c = componentRepository.findByIdAndRailSystemId(componentId, rsId)
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
.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) {
|
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);
|
switchConfigurationRepository.deleteAllByNodesContaining(p);
|
||||||
}
|
}
|
||||||
componentRepository.delete(c);
|
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<PathNode> 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<Segment> 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
|
@Transactional
|
||||||
public ComponentResponse updatePath(long rsId, long cId, PathNodeUpdatePayload payload) {
|
public ComponentResponse updatePath(long rsId, long cId, PathNodeUpdatePayload payload) {
|
||||||
var c = componentRepository.findByIdAndRailSystemId(cId, rsId)
|
var c = componentRepository.findByIdAndRailSystemId(cId, rsId)
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
package nl.andrewl.railsignalapi.websocket;
|
|
||||||
|
|
||||||
public record BranchUpdateMessage(long branchId, String status) {
|
|
||||||
}
|
|
|
@ -9,11 +9,13 @@ import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSocket
|
@EnableWebSocket
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class WebSocketConfig implements WebSocketConfigurer {
|
public class ComponentWebSocketConfig implements WebSocketConfigurer {
|
||||||
private final SignalWebSocketHandler webSocketHandler;
|
private final SignalWebSocketHandler webSocketHandler;
|
||||||
|
private final ComponentWebSocketHandshakeInterceptor handshakeInterceptor;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||||
registry.addHandler(webSocketHandler, "/api/ws-signal");
|
registry.addHandler(webSocketHandler, "/api/ws/component")
|
||||||
|
.addInterceptors(handshakeInterceptor);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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<nl.andrewl.railsignalapi.model.component.Component> componentRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> 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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +0,0 @@
|
||||||
package nl.andrewl.railsignalapi.websocket;
|
|
||||||
|
|
||||||
public record SignalUpdateMessage(
|
|
||||||
long signalId,
|
|
||||||
long fromBranchId,
|
|
||||||
long toBranchId,
|
|
||||||
String type
|
|
||||||
) {}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -35,7 +35,7 @@ public class SignalWebSocketHandler extends TextWebSocketHandler {
|
||||||
|
|
||||||
@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 = mapper.readValue(message.getPayload(), SignalUpdateMessage.class);
|
||||||
//signalService.handleSignalUpdate(msg);
|
//signalService.handleSignalUpdate(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue