Updated component creation and update workflow.

This commit is contained in:
Andrew Lalis 2022-05-23 12:45:28 +02:00
parent 6cfc630310
commit 2cc3c2259a
9 changed files with 155 additions and 52 deletions

View File

@ -17,6 +17,7 @@ public interface ComponentRepository<T extends Component> extends JpaRepository<
Optional<T> findByIdAndRailSystemId(long id, long rsId);
boolean existsByNameAndRailSystem(String name, RailSystem rs);
boolean existsByNameAndRailSystemId(String name, long rsId);
@Query("SELECT c FROM Component c " +
"WHERE c.railSystem = :rs AND " +

View File

@ -39,6 +39,7 @@ public abstract class Component {
* components in the rail system.
*/
@Column(nullable = false)
@Setter
private String name;
/**

View File

@ -3,12 +3,11 @@ package nl.andrewl.railsignalapi.model.component;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import nl.andrewl.railsignalapi.model.RailSystem;
import nl.andrewl.railsignalapi.model.component.Component;
import nl.andrewl.railsignalapi.model.component.ComponentType;
import nl.andrewl.railsignalapi.model.component.Position;
import javax.persistence.*;
import javax.persistence.Column;
import javax.persistence.Entity;
/**
* A simple label element that allows text to be placed in the rail system
@ -19,6 +18,7 @@ import javax.persistence.*;
@Getter
public class Label extends Component {
@Column(nullable = false)
@Setter
private String text;
public Label(RailSystem rs, Position position, String name, String text) {

View File

@ -3,6 +3,7 @@ package nl.andrewl.railsignalapi.model.component;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import nl.andrewl.railsignalapi.model.RailSystem;
import nl.andrewl.railsignalapi.model.Segment;
@ -23,6 +24,7 @@ public class Signal extends Component {
* The segment that this signal connects to.
*/
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@Setter
private Segment segment;
public Signal(RailSystem railSystem, Position position, String name, Segment segment) {

View File

@ -1,7 +1,6 @@
package nl.andrewl.railsignalapi.rest;
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.rest.dto.component.out.SimpleComponentResponse;
@ -13,6 +12,7 @@ import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@RestController
@ -43,7 +43,7 @@ public class ComponentsApiController {
}
@PostMapping
public ComponentResponse createComponent(@PathVariable long rsId, @RequestBody ComponentPayload payload) {
public ComponentResponse createComponent(@PathVariable long rsId, @RequestBody @Valid ComponentPayload payload) {
return componentCreationService.create(rsId, payload);
}
@ -53,8 +53,8 @@ public class ComponentsApiController {
return ResponseEntity.noContent().build();
}
@PatchMapping(path = "/{cId}/connectedNodes")
public ComponentResponse updateConnectedNodes(@PathVariable long rsId, @PathVariable long cId, @RequestBody PathNodeUpdatePayload payload) {
return componentService.updatePath(rsId, cId, payload);
@PatchMapping(path = "/{cId}")
public ComponentResponse updateComponent(@PathVariable long rsId, @PathVariable long cId, @RequestBody @Valid ComponentPayload payload) {
return componentService.updateComponent(rsId, cId, payload);
}
}

View File

@ -1,10 +0,0 @@
package nl.andrewl.railsignalapi.rest.dto;
import java.util.List;
public class PathNodeUpdatePayload {
public List<NodeIdObj> connectedNodes;
public static class NodeIdObj {
public long id;
}
}

View File

@ -1,13 +1,21 @@
package nl.andrewl.railsignalapi.rest.dto.component.in;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class SegmentBoundaryPayload extends ComponentPayload {
@NotEmpty @Size(min = 1, max = 2)
public SegmentPayload[] segments;
@NotNull @Size(max = 2)
public NodePayload[] connectedNodes;
public static class SegmentPayload {
public long id;
}
public static class NodePayload {
public long id;
}
}

View File

@ -16,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
@Service
@ -59,7 +60,7 @@ public class ComponentCreationService {
private Component createSignal(RailSystem rs, SignalPayload payload) {
long segmentId = payload.segment.id;
Segment segment = segmentRepository.findById(segmentId)
Segment segment = segmentRepository.findByIdAndRailSystemId(segmentId, rs.getId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
return new Signal(rs, payload.position, payload.name, segment);
}
@ -70,7 +71,7 @@ public class ComponentCreationService {
for (var config : payload.possibleConfigurations) {
Set<PathNode> pathNodes = new HashSet<>();
for (var node : config.nodes) {
Component c = componentRepository.findById(node.id)
Component c = componentRepository.findByIdAndRailSystemId(node.id, rs.getId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
if (c instanceof PathNode pathNode) {
pathNodes.add(pathNode);
@ -78,9 +79,12 @@ public class ComponentCreationService {
pathNode.getConnectedNodes().add(s);
componentRepository.save(pathNode);
} else {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Id " + node.id + " does not refer to a PathNode component.");
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Id " + node.id + " does not refer to a valid path node.");
}
}
if (pathNodes.size() != 2) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid switch configuration. All switch configurations must connect 2 path nodes.");
}
s.getPossibleConfigurations().add(new SwitchConfiguration(s, pathNodes));
}
if (s.getPossibleConfigurations().size() < 2) {
@ -92,13 +96,33 @@ public class ComponentCreationService {
private Component createSegmentBoundary(RailSystem rs, SegmentBoundaryPayload payload) {
Set<Segment> segments = new HashSet<>();
for (var segmentP : payload.segments) {
Segment segment = segmentRepository.findById(segmentP.id)
Segment segment = segmentRepository.findByIdAndRailSystemId(segmentP.id, rs.getId())
.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);
Set<PathNode> connectedNodes = new HashSet<>();
if (payload.connectedNodes != null) {
for (var nodeData : payload.connectedNodes) {
var component = componentRepository.findByIdAndRailSystemId(nodeData.id, rs.getId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid path node: " + nodeData.id));
if (component instanceof PathNode pathNode) {
connectedNodes.add(pathNode);
} else {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid path node: " + nodeData.id);
}
}
}
var segmentBoundary = new SegmentBoundaryNode(rs, payload.position, payload.name, connectedNodes, segments);
segmentBoundary = componentRepository.save(segmentBoundary);
for (var connectedNode : connectedNodes) {
connectedNode.getConnectedNodes().add(segmentBoundary);
componentRepository.save(connectedNode);
}
return segmentBoundary;
}
}

View File

@ -3,10 +3,11 @@ 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.dao.SwitchConfigurationRepository;
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.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 nl.andrewl.railsignalapi.rest.dto.component.out.SimpleComponentResponse;
import org.springframework.data.domain.Page;
@ -28,6 +29,7 @@ public class ComponentService {
private final ComponentRepository<Component> componentRepository;
private final RailSystemRepository railSystemRepository;
private final SwitchConfigurationRepository switchConfigurationRepository;
private final SegmentRepository segmentRepository;
@Transactional(readOnly = true)
public List<ComponentResponse> getComponents(long rsId) {
@ -72,38 +74,113 @@ public class ComponentService {
}
@Transactional
public ComponentResponse updatePath(long rsId, long cId, PathNodeUpdatePayload payload) {
var c = componentRepository.findByIdAndRailSystemId(cId, rsId)
public ComponentResponse updateComponent(long rsId, long componentId, ComponentPayload payload) {
var c = componentRepository.findByIdAndRailSystemId(componentId, rsId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
if (!(c instanceof PathNode p)) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Component is not a PathNode.");
Set<PathNode> newNodes = new HashSet<>();
for (var nodeObj : payload.connectedNodes) {
long id = nodeObj.id;
var c1 = componentRepository.findByIdAndRailSystemId(id, rsId);
if (c1.isPresent() && c1.get() instanceof PathNode pn) {
newNodes.add(pn);
} else {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Component with id " + id + " is not a PathNode in the same rail system.");
if (!c.getType().name().equalsIgnoreCase(payload.type)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot update a component's type. Remove and create a new component instead.");
}
if (!c.getName().equals(payload.name) && componentRepository.existsByNameAndRailSystemId(payload.name, rsId)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Cannot rename component because another component has that name.");
}
c.setName(payload.name);
c.getPosition().setX(payload.position.getX());
c.getPosition().setY(payload.position.getY());
c.getPosition().setZ(payload.position.getZ());
if (c instanceof Signal s && payload instanceof SignalPayload sp) {
updateSignal(s, sp, rsId);
}
if (c instanceof SegmentBoundaryNode sb && payload instanceof SegmentBoundaryPayload sbp) {
updateSegmentBoundary(sb, sbp, rsId);
}
if (c instanceof Label lbl && payload instanceof LabelPayload lp) {
lbl.setText(lp.text);
}
if (c instanceof Switch sw && payload instanceof SwitchPayload sp) {
updateSwitch(sw, sp, rsId);
}
return ComponentResponse.of(componentRepository.save(c));
}
private void updateSignal(Signal s, SignalPayload sp, long rsId) {
Segment segment = segmentRepository.findByIdAndRailSystemId(sp.segment.id, rsId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Signal's attached segment with id " + sp.segment.id + " is invalid."));
s.setSegment(segment);
}
private void updateSegmentBoundary(SegmentBoundaryNode sb, SegmentBoundaryPayload sbp, long rsId) {
Set<Segment> newSegments = new HashSet<>();
for (var segData : sbp.segments) {
newSegments.add(segmentRepository.findByIdAndRailSystemId(segData.id, rsId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Segment boundary's attached segment with id " + segData.id + " is invalid.")));
}
sb.getSegments().retainAll(newSegments);
sb.getSegments().addAll(newSegments);
if (sbp.connectedNodes != null) {
Set<PathNode> connectedNodes = new HashSet<>();
for (var nodeData : sbp.connectedNodes) {
var component = componentRepository.findByIdAndRailSystemId(nodeData.id, rsId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid path node: " + nodeData.id));
if (component instanceof PathNode pathNode && !component.equals(sb)) {
connectedNodes.add(pathNode);
} else {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid path node: " + nodeData.id);
}
}
updateConnectedNodes(sb, connectedNodes);
}
}
private void updateSwitch(Switch sw, SwitchPayload sp, long rsId) {
Set<SwitchConfiguration> newConfigs = new HashSet<>();
Set<PathNode> connectedNodes = new HashSet<>();
for (var configData : sp.possibleConfigurations) {
Set<PathNode> nodes = new HashSet<>();
for (var nodeData : configData.nodes) {
var component = componentRepository.findByIdAndRailSystemId(nodeData.id, rsId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid path node id: " + nodeData.id));
if (component instanceof PathNode pathNode && !pathNode.getId().equals(sw.getId())) {
nodes.add(pathNode);
connectedNodes.add(pathNode);
} else {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid path node: " + nodeData.id);
}
}
if (nodes.size() != 2) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Switch configuration doesn't have required 2 path nodes.");
}
newConfigs.add(new SwitchConfiguration(sw, nodes));
}
if (newConfigs.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "There must be at least one switch configuration.");
}
Set<PathNode> nodesToRemove = new HashSet<>(p.getConnectedNodes());
nodesToRemove.removeAll(newNodes);
sw.getPossibleConfigurations().retainAll(newConfigs);
sw.getPossibleConfigurations().addAll(newConfigs);
Set<PathNode> nodesToAdd = new HashSet<>(newNodes);
nodesToAdd.removeAll(p.getConnectedNodes());
// A switch's connectedNodes are derived from its set of possible configurations.
// So now we need to update all connected nodes to match the set of configurations.
updateConnectedNodes(sw, connectedNodes);
}
p.getConnectedNodes().removeAll(nodesToRemove);
p.getConnectedNodes().addAll(nodesToAdd);
for (var node : nodesToRemove) {
node.getConnectedNodes().remove(p);
private void updateConnectedNodes(PathNode owner, Set<PathNode> newNodes) {
Set<PathNode> disconnected = new HashSet<>(owner.getConnectedNodes());
disconnected.removeAll(newNodes);
Set<PathNode> connected = new HashSet<>(newNodes);
connected.removeAll(owner.getConnectedNodes());
owner.getConnectedNodes().retainAll(newNodes);
owner.getConnectedNodes().addAll(newNodes);
for (var node : disconnected) {
node.getConnectedNodes().remove(owner);
componentRepository.save(node);
}
for (var node : nodesToAdd) {
node.getConnectedNodes().add(p);
for (var node : connected) {
node.getConnectedNodes().add(owner);
componentRepository.save(node);
}
componentRepository.saveAll(nodesToRemove);
componentRepository.saveAll(nodesToAdd);
p = componentRepository.save(p);
return ComponentResponse.of(p);
}
}