diff --git a/src/main/java/org/gridsuite/study/server/controller/StudyController.java b/src/main/java/org/gridsuite/study/server/controller/StudyController.java index 75f33edeb..bf18f028a 100644 --- a/src/main/java/org/gridsuite/study/server/controller/StudyController.java +++ b/src/main/java/org/gridsuite/study/server/controller/StudyController.java @@ -650,6 +650,26 @@ public ResponseEntity moveModification(@PathVariable("studyUuid") UUID stu return ResponseEntity.ok().build(); } + @PutMapping(value = "/studies/{studyUuid}/nodes/{nodeUuid}/composite-sub-modification/{modificationUuid}") + @Operation(summary = "Move a composite sub-modification within/between composites or to/from root level") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The sub-modification order has been updated")}) + public ResponseEntity moveSubModification( + @PathVariable("studyUuid") UUID studyUuid, + @PathVariable("nodeUuid") UUID nodeUuid, + @PathVariable("modificationUuid") UUID modificationUuid, + @Nullable @Parameter(description = "Source composite UUID; absent when moving from root level") @RequestParam(value = "sourceCompositeUuid", required = false) UUID sourceCompositeUuid, + @Nullable @Parameter(description = "Target composite UUID; absent when moving to root level") @RequestParam(value = "targetCompositeUuid", required = false) UUID targetCompositeUuid, + @Nullable @Parameter(description = "Insert before this UUID; absent means append at end") @RequestParam(value = "beforeUuid", required = false) UUID beforeUuid, + @RequestHeader(HEADER_USER_ID) String userId) { + studyService.assertCanUpdateNodeInStudy(studyUuid, nodeUuid); + studyService.assertNoBlockedNodeInStudy(studyUuid, nodeUuid); + rebuildNodeService.moveSubModification( + studyUuid, nodeUuid, + sourceCompositeUuid, targetCompositeUuid, + modificationUuid, beforeUuid, userId); + return ResponseEntity.ok().build(); + } + @PutMapping(value = "/studies/{studyUuid}/nodes/{nodeUuid}", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "For a list of network modifications passed in body, copy or cut, then append them to target node") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The modification list has been updated.")}) diff --git a/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java b/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java index ad2eb3a5a..251d713fc 100644 --- a/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java +++ b/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java @@ -406,4 +406,46 @@ public Map searchModifications(UUID networkUuid, String userInput) }).getBody(); } + + public void moveSubModification( + UUID groupUuid, + UUID sourceCompositeUuid, + UUID targetCompositeUuid, + UUID modificationUuid, + UUID beforeUuid) { + + var path = UriComponentsBuilder.fromPath( + COMPOSITE_PATH + "groups" + DELIMITER + + "{groupUuid}" + DELIMITER + + "sub-modifications" + DELIMITER + + "{modificationUuid}"); + + if (sourceCompositeUuid != null) { + path.queryParam("sourceCompositeUuid", sourceCompositeUuid); + } + if (targetCompositeUuid != null) { + path.queryParam("targetCompositeUuid", targetCompositeUuid); + } + if (beforeUuid != null) { + path.queryParam("beforeUuid", beforeUuid); + } + + restTemplate.put( + getNetworkModificationServerURI(false) + + path.buildAndExpand(groupUuid, modificationUuid).toUriString(), + null); + } + + public Set expandToLeafUuids(List modificationUuids) { + var path = UriComponentsBuilder.fromPath(COMPOSITE_PATH + "leaf-uuids") + .queryParam("uuids", modificationUuids) + .toUriString(); + + return restTemplate.exchange( + getNetworkModificationServerURI(false) + path, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() { } + ).getBody(); + } } diff --git a/src/main/java/org/gridsuite/study/server/service/RebuildNodeService.java b/src/main/java/org/gridsuite/study/server/service/RebuildNodeService.java index 7364b4a5d..66f141fa1 100644 --- a/src/main/java/org/gridsuite/study/server/service/RebuildNodeService.java +++ b/src/main/java/org/gridsuite/study/server/service/RebuildNodeService.java @@ -6,6 +6,7 @@ */ package org.gridsuite.study.server.service; +import lombok.NonNull; import org.gridsuite.study.server.dto.modification.NetworkModificationMetadata; import org.springframework.stereotype.Service; @@ -84,6 +85,38 @@ public void moveNetworkModifications(UUID studyUuid, UUID targetNodeUuid, UUID o () -> handleMoveNetworkModifications(studyUuid, targetNodeUuid, originNodeUuid, modificationsToCopyUuidList, userId)); } + public void moveSubModification( + UUID studyUuid, + UUID nodeUuid, + UUID sourceCompositeUuid, + UUID targetCompositeUuid, + UUID modificationUuid, + UUID beforeUuid, + String userId) { + handleRebuildNode(studyUuid, nodeUuid, userId, + () -> handleMoveNetworkSubmodification( + studyUuid, nodeUuid, + sourceCompositeUuid, targetCompositeUuid, + modificationUuid, beforeUuid, userId)); + } + + private void handleMoveNetworkSubmodification(@NonNull UUID studyUuid, + @NonNull UUID nodeUuid, + UUID sourceCompositeUuid, + UUID targetCompositeUuid, + @NonNull UUID modificationUuid, + UUID beforeUuid, + String userId) { + studyService.invalidateNodeTreeWhenMoveModification(studyUuid, nodeUuid); + try { + studyService.moveSubModification(studyUuid, nodeUuid, + sourceCompositeUuid, targetCompositeUuid, + modificationUuid, beforeUuid, userId); + } finally { + studyService.unblockNodeTree(studyUuid, nodeUuid); + } + } + private void handleMoveNetworkModifications(UUID studyUuid, UUID targetNodeUuid, UUID originNodeUuid, List modificationsToCopyUuidList, String userId) { boolean isTargetInDifferentNodeTree = studyService.invalidateNodeTreeWhenMoveModifications(studyUuid, targetNodeUuid, originNodeUuid); try { diff --git a/src/main/java/org/gridsuite/study/server/service/StudyService.java b/src/main/java/org/gridsuite/study/server/service/StudyService.java index c124713d7..7742ab63f 100644 --- a/src/main/java/org/gridsuite/study/server/service/StudyService.java +++ b/src/main/java/org/gridsuite/study/server/service/StudyService.java @@ -2171,7 +2171,9 @@ public void updateNetworkModificationsActivationInRootNetwork(UUID studyUuid, UU if (!networkModificationTreeService.getStudyUuidForNodeId(nodeUuid).equals(studyUuid)) { throw new StudyException(NOT_ALLOWED); } - rootNetworkNodeInfoService.updateModificationsToExclude(nodeUuid, rootNetworkUuid, modificationsUuids, activated); + Set modificationsToExclude = new HashSet<>(modificationsUuids); + modificationsToExclude.addAll(networkModificationService.expandToLeafUuids(new ArrayList<>(modificationsUuids))); + rootNetworkNodeInfoService.updateModificationsToExclude(nodeUuid, rootNetworkUuid, modificationsToExclude, activated); invalidateNodeTree(studyUuid, nodeUuid, rootNetworkUuid); } finally { notificationService.emitEndModificationEquipmentNotification(studyUuid, nodeUuid, Optional.of(rootNetworkUuid), childrenUuids); @@ -2391,6 +2393,28 @@ public void moveNetworkModifications( notificationService.emitElementUpdated(studyUuid, userId); } + public void moveSubModification( + @NonNull UUID studyUuid, + @NonNull UUID nodeUuid, + UUID sourceCompositeUuid, + UUID targetCompositeUuid, + @NonNull UUID modificationUuid, + UUID beforeUuid, + String userId) { + + List childrenUuids = networkModificationTreeService.getChildrenUuids(nodeUuid); + try { + notificationService.emitStartModificationEquipmentNotification(studyUuid, nodeUuid, childrenUuids, NotificationService.MODIFICATIONS_UPDATING_IN_PROGRESS); + checkStudyContainsNode(studyUuid, nodeUuid); + UUID groupUuid = networkModificationTreeService.getModificationGroupUuid(nodeUuid); + networkModificationService.moveSubModification( + groupUuid, sourceCompositeUuid, targetCompositeUuid, modificationUuid, beforeUuid); + } finally { + notificationService.emitEndModificationEquipmentNotification(studyUuid, nodeUuid, childrenUuids); + } + notificationService.emitElementUpdated(studyUuid, userId); + } + private void emitNetworkModificationImpactsForAllRootNetworks(List> modificationResults, StudyEntity studyEntity, UUID impactedNode) { int index = 0; List rootNetworkEntities = studyEntity.getRootNetworks(); diff --git a/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java b/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java index 37e5e689b..94de8ffa4 100644 --- a/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java +++ b/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java @@ -3084,6 +3084,138 @@ private void checkElementUpdatedMessageSent(UUID elementUuid, String userId) { assertEquals(userId, message.getHeaders().get(NotificationService.HEADER_MODIFIED_BY)); } + @Test + void testMoveSubModification() throws Exception { + String userId = "userId"; + StudyEntity studyEntity = insertDummyStudy(UUID.fromString(NETWORK_UUID_STRING), CASE_UUID, "UCTE"); + UUID studyUuid = studyEntity.getId(); + UUID rootNodeUuid = getRootNode(studyUuid).getId(); + NetworkModificationNode node = createNetworkModificationNode(studyUuid, rootNodeUuid, + UUID.randomUUID(), VARIANT_ID, "node 1", userId); + UUID nodeUuid = node.getId(); + + UUID modificationUuid = UUID.randomUUID(); + UUID sourceCompositeUuid = UUID.randomUUID(); + UUID targetCompositeUuid = UUID.randomUUID(); + UUID beforeUuid = UUID.randomUUID(); + + String moveSubModifUrlPattern = "/v1/network-composite-modifications/groups/" + node.getModificationGroupUuid() + + "/sub-modifications/" + modificationUuid; + + // --- Case 1: move between two composites with a beforeUuid --- + wireMockServer.stubFor(WireMock.put(WireMock.urlPathEqualTo(moveSubModifUrlPattern)) + .willReturn(WireMock.ok())); + + mockMvc.perform(put("/v1/studies/{studyUuid}/nodes/{nodeUuid}/composite-sub-modification/{modificationUuid}", + studyUuid, nodeUuid, modificationUuid) + .queryParam("sourceCompositeUuid", sourceCompositeUuid.toString()) + .queryParam("targetCompositeUuid", targetCompositeUuid.toString()) + .queryParam("beforeUuid", beforeUuid.toString()) + .header(USER_ID_HEADER, userId)) + .andExpect(status().isOk()); + checkUpdateStatusMessagesReceived(studyUuid, nodeUuid, output); + checkEquipmentUpdatingMessagesReceived(studyUuid, nodeUuid); + checkEquipmentUpdatingFinishedMessagesReceived(studyUuid, nodeUuid); + checkElementUpdatedMessageSent(studyUuid, userId); + WireMockUtilsCriteria.verifyPutRequest(wireMockServer, moveSubModifUrlPattern, false, Map.of( + "sourceCompositeUuid", WireMock.equalTo(sourceCompositeUuid.toString()), + "targetCompositeUuid", WireMock.equalTo(targetCompositeUuid.toString()), + "beforeUuid", WireMock.equalTo(beforeUuid.toString())), null); + + // --- Case 2: move from root level into a composite (no sourceCompositeUuid) --- + wireMockServer.stubFor(WireMock.put(WireMock.urlPathEqualTo(moveSubModifUrlPattern)) + .willReturn(WireMock.ok())); + + mockMvc.perform(put("/v1/studies/{studyUuid}/nodes/{nodeUuid}/composite-sub-modification/{modificationUuid}", + studyUuid, nodeUuid, modificationUuid) + .queryParam("targetCompositeUuid", targetCompositeUuid.toString()) + .header(USER_ID_HEADER, userId)) + .andExpect(status().isOk()); + checkUpdateStatusMessagesReceived(studyUuid, nodeUuid, output); + checkEquipmentUpdatingMessagesReceived(studyUuid, nodeUuid); + checkEquipmentUpdatingFinishedMessagesReceived(studyUuid, nodeUuid); + checkElementUpdatedMessageSent(studyUuid, userId); + WireMockUtilsCriteria.verifyPutRequest(wireMockServer, moveSubModifUrlPattern, false, Map.of( + "targetCompositeUuid", WireMock.equalTo(targetCompositeUuid.toString())), null); + + // --- Case 3: move from inside a composite to root level (no targetCompositeUuid) --- + wireMockServer.stubFor(WireMock.put(WireMock.urlPathEqualTo(moveSubModifUrlPattern)) + .willReturn(WireMock.ok())); + + mockMvc.perform(put("/v1/studies/{studyUuid}/nodes/{nodeUuid}/composite-sub-modification/{modificationUuid}", + studyUuid, nodeUuid, modificationUuid) + .queryParam("sourceCompositeUuid", sourceCompositeUuid.toString()) + .header(USER_ID_HEADER, userId)) + .andExpect(status().isOk()); + checkUpdateStatusMessagesReceived(studyUuid, nodeUuid, output); + checkEquipmentUpdatingMessagesReceived(studyUuid, nodeUuid); + checkEquipmentUpdatingFinishedMessagesReceived(studyUuid, nodeUuid); + checkElementUpdatedMessageSent(studyUuid, userId); + WireMockUtilsCriteria.verifyPutRequest(wireMockServer, moveSubModifUrlPattern, false, Map.of( + "sourceCompositeUuid", WireMock.equalTo(sourceCompositeUuid.toString())), null); + } + + @Test + void testUpdateNetworkModificationsActivationExpandsCompositeToLeafUuids() throws Exception { + String userId = "userId"; + StudyEntity studyEntity = insertDummyStudy(UUID.fromString(NETWORK_UUID_STRING), CASE_UUID, "UCTE"); + UUID studyUuid = studyEntity.getId(); + UUID rootNetworkUuid = studyTestUtils.getOneRootNetworkUuid(studyUuid); + UUID rootNodeUuid = getRootNode(studyUuid).getId(); + NetworkModificationNode node = createNetworkModificationNode(studyUuid, rootNodeUuid, + UUID.randomUUID(), VARIANT_ID, "node 1", userId); + UUID nodeUuid = node.getId(); + + UUID compositeUuid = UUID.randomUUID(); + UUID leafUuid1 = UUID.randomUUID(); + UUID leafUuid2 = UUID.randomUUID(); + Set expandedLeafUuids = Set.of(leafUuid1, leafUuid2); + + // Stub verifyModifications + wireMockServer.stubFor(WireMock.get(WireMock.urlPathEqualTo( + "/v1/groups/" + node.getModificationGroupUuid() + "/network-modifications/verify")) + .willReturn(WireMock.ok())); + + // Stub expandToLeafUuids + wireMockServer.stubFor(WireMock.get(WireMock.urlPathEqualTo( + "/v1/network-composite-modifications/leaf-uuids")) + .willReturn(WireMock.ok() + .withBody(mapper.writeValueAsString(expandedLeafUuids)) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))); + + // PUT .../network-modifications?activated=false&uuids= + mockMvc.perform(put("/v1/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/network-modifications", + studyUuid, rootNetworkUuid, nodeUuid) + .queryParam("activated", "false") + .queryParam("uuids", compositeUuid.toString()) + .header(USER_ID_HEADER, userId)) + .andExpect(status().isOk()); + + checkEquipmentUpdatingMessagesReceived(studyUuid, nodeUuid); + checkUpdateStatusMessagesReceived(studyUuid, nodeUuid, output); + checkEquipmentUpdatingFinishedMessagesReceived(studyUuid, nodeUuid); + checkElementUpdatedMessageSent(studyUuid, userId); + + // Verify that verifyModifications was called with the composite UUID + WireMockUtilsCriteria.verifyGetRequest(wireMockServer, + "/v1/groups/" + node.getModificationGroupUuid() + "/network-modifications/verify", + Map.of("uuids", WireMock.equalTo(compositeUuid.toString()))); + + // Verify that expandToLeafUuids was called with the composite UUID + WireMockUtilsCriteria.verifyGetRequest(wireMockServer, + "/v1/network-composite-modifications/leaf-uuids", + Map.of("uuids", WireMock.equalTo(compositeUuid.toString()))); + + // Verify that the set passed to updateModificationsToExclude contains both + // the original composite UUID and the expanded leaf UUIDs + verify(rootNetworkNodeInfoService, times(1)).updateModificationsToExclude( + eq(nodeUuid), + eq(rootNetworkUuid), + argThat((Set uuids) -> uuids.contains(compositeUuid) + && uuids.containsAll(expandedLeafUuids)), + eq(false)); + } + @AfterEach void tearDown() { studyRepository.findAll().forEach(s -> networkModificationTreeService.doDeleteTree(s.getId()));