From 2cc3c2259a2491a841c70c72ed2df5807c7c2efd Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Mon, 23 May 2022 12:45:28 +0200 Subject: [PATCH] Updated component creation and update workflow. --- .../dao/ComponentRepository.java | 1 + .../model/component/Component.java | 1 + .../railsignalapi/model/component/Label.java | 8 +- .../railsignalapi/model/component/Signal.java | 2 + .../rest/ComponentsApiController.java | 10 +- .../rest/dto/PathNodeUpdatePayload.java | 10 -- .../component/in/SegmentBoundaryPayload.java | 8 ++ .../service/ComponentCreationService.java | 34 ++++- .../service/ComponentService.java | 133 ++++++++++++++---- 9 files changed, 155 insertions(+), 52 deletions(-) delete mode 100644 src/main/java/nl/andrewl/railsignalapi/rest/dto/PathNodeUpdatePayload.java diff --git a/src/main/java/nl/andrewl/railsignalapi/dao/ComponentRepository.java b/src/main/java/nl/andrewl/railsignalapi/dao/ComponentRepository.java index 805c7d2..3b89c6d 100644 --- a/src/main/java/nl/andrewl/railsignalapi/dao/ComponentRepository.java +++ b/src/main/java/nl/andrewl/railsignalapi/dao/ComponentRepository.java @@ -17,6 +17,7 @@ public interface ComponentRepository extends JpaRepository< Optional 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 " + 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 ac05695..56a3895 100644 --- a/src/main/java/nl/andrewl/railsignalapi/model/component/Component.java +++ b/src/main/java/nl/andrewl/railsignalapi/model/component/Component.java @@ -39,6 +39,7 @@ public abstract class Component { * components in the rail system. */ @Column(nullable = false) + @Setter private String name; /** diff --git a/src/main/java/nl/andrewl/railsignalapi/model/component/Label.java b/src/main/java/nl/andrewl/railsignalapi/model/component/Label.java index 0fcbac2..37b0261 100644 --- a/src/main/java/nl/andrewl/railsignalapi/model/component/Label.java +++ b/src/main/java/nl/andrewl/railsignalapi/model/component/Label.java @@ -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) { diff --git a/src/main/java/nl/andrewl/railsignalapi/model/component/Signal.java b/src/main/java/nl/andrewl/railsignalapi/model/component/Signal.java index 2e1446f..4a78150 100644 --- a/src/main/java/nl/andrewl/railsignalapi/model/component/Signal.java +++ b/src/main/java/nl/andrewl/railsignalapi/model/component/Signal.java @@ -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) { diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/ComponentsApiController.java b/src/main/java/nl/andrewl/railsignalapi/rest/ComponentsApiController.java index fc1ce5a..e7c35c2 100644 --- a/src/main/java/nl/andrewl/railsignalapi/rest/ComponentsApiController.java +++ b/src/main/java/nl/andrewl/railsignalapi/rest/ComponentsApiController.java @@ -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); } } diff --git a/src/main/java/nl/andrewl/railsignalapi/rest/dto/PathNodeUpdatePayload.java b/src/main/java/nl/andrewl/railsignalapi/rest/dto/PathNodeUpdatePayload.java deleted file mode 100644 index 2315742..0000000 --- a/src/main/java/nl/andrewl/railsignalapi/rest/dto/PathNodeUpdatePayload.java +++ /dev/null @@ -1,10 +0,0 @@ -package nl.andrewl.railsignalapi.rest.dto; - -import java.util.List; - -public class PathNodeUpdatePayload { - public List connectedNodes; - public static class NodeIdObj { - public long id; - } -} 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 index 30f38cd..0c71017 100644 --- 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 @@ -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; + } } diff --git a/src/main/java/nl/andrewl/railsignalapi/service/ComponentCreationService.java b/src/main/java/nl/andrewl/railsignalapi/service/ComponentCreationService.java index 73e0017..31a518e 100644 --- a/src/main/java/nl/andrewl/railsignalapi/service/ComponentCreationService.java +++ b/src/main/java/nl/andrewl/railsignalapi/service/ComponentCreationService.java @@ -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 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 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 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; } } diff --git a/src/main/java/nl/andrewl/railsignalapi/service/ComponentService.java b/src/main/java/nl/andrewl/railsignalapi/service/ComponentService.java index 39227f4..8e2bb34 100644 --- a/src/main/java/nl/andrewl/railsignalapi/service/ComponentService.java +++ b/src/main/java/nl/andrewl/railsignalapi/service/ComponentService.java @@ -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 componentRepository; private final RailSystemRepository railSystemRepository; private final SwitchConfigurationRepository switchConfigurationRepository; + private final SegmentRepository segmentRepository; @Transactional(readOnly = true) public List 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 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 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 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 newConfigs = new HashSet<>(); + Set connectedNodes = new HashSet<>(); + for (var configData : sp.possibleConfigurations) { + Set 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 nodesToRemove = new HashSet<>(p.getConnectedNodes()); - nodesToRemove.removeAll(newNodes); + sw.getPossibleConfigurations().retainAll(newConfigs); + sw.getPossibleConfigurations().addAll(newConfigs); - Set 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 newNodes) { + Set disconnected = new HashSet<>(owner.getConnectedNodes()); + disconnected.removeAll(newNodes); + + Set 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); } }