Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,21 @@ public ResponseEntity<Void> moveOrCopyModifications(@PathVariable("studyUuid") U
return ResponseEntity.ok().build();
}

@PostMapping(value = "/studies/{studyUuid}/nodes/{nodeUuid}/composite-modification", produces = MediaType.APPLICATION_JSON_VALUE)
@Operation(summary = "For a list of network modifications passed in body, merge them into a new composite modification")
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The composite modification has been created.")})
public ResponseEntity<UUID> mergeModificationsIntoNewComposite(
@PathVariable("studyUuid") UUID studyUuid,
@PathVariable("nodeUuid") UUID nodeUuid,
@RequestBody List<UUID> modificationsUuids,
@RequestHeader(HEADER_USER_ID) String userId) {
studyService.assertIsStudyAndNodeExist(studyUuid, nodeUuid);
studyService.assertCanUpdateNodeInStudy(studyUuid, nodeUuid);
studyService.assertNoBlockedNodeInStudy(studyUuid, nodeUuid);
UUID newCompositeUuid = rebuildNodeService.mergeModificationsIntoComposite(studyUuid, nodeUuid, modificationsUuids, userId);
return ResponseEntity.ok().body(newCompositeUuid);
}

/**
* @param modificationsToInsert pair of the composite uuid and its name
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,24 @@ public NetworkModificationsResult insertCompositeModifications(UUID groupUuid,
).getBody();
}

public UUID mergeModificationsIntoComposite(@NonNull List<UUID> modificationsUuids) {
var path = UriComponentsBuilder.fromPath(COMPOSITE_PATH);

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<List<UUID>> httpEntity = new HttpEntity<>(
modificationsUuids,
headers
);

return restTemplate.exchange(
getNetworkModificationServerURI(false) + path.toUriString(),
HttpMethod.POST,
httpEntity,
UUID.class
).getBody();
}

private NetworkModificationsResult handleModifications(UUID groupUuid, UUID originGroupUuid, ModificationsActionType action,
Pair<List<UUID>, List<ModificationApplicationContext>> modificationContextInfos) {
var path = UriComponentsBuilder.fromPath(GROUP_PATH)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.Set;
import java.util.UUID;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;

@Service
Expand Down Expand Up @@ -100,6 +101,23 @@ public void moveSubModification(
modificationUuid, beforeUuid, userId));
}

public UUID mergeModificationsIntoComposite(UUID studyUuid, UUID nodeUuid, List<UUID> modificationsUuids, String userId) {
return handleRebuildNodeSupply(
studyUuid,
nodeUuid,
userId,
() -> {
studyService.invalidateNodeTreeWhenMoveModification(studyUuid, nodeUuid);
UUID compositeUuid;
try {
compositeUuid = studyService.mergeModificationsIntoComposite(studyUuid, nodeUuid, modificationsUuids, userId);
} finally {
studyService.unblockNodeTree(studyUuid, nodeUuid);
}
return compositeUuid;
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private void handleMoveNetworkSubmodification(@NonNull UUID studyUuid,
@NonNull UUID nodeUuid,
UUID sourceCompositeUuid,
Expand Down Expand Up @@ -133,22 +151,32 @@ private void handleRebuildNode(UUID studyUuid, UUID nodeUuid, String userId, Run
handleRebuildNode(studyUuid, nodeUuid, nodeUuid, userId, action);
}

private <T> T handleRebuildNodeSupply(UUID studyUuid, UUID nodeUuid, String userId, Supplier<T> action) {
return handleRebuildNodeSupply(studyUuid, nodeUuid, nodeUuid, userId, action);
}

private void handleRebuildNode(UUID studyUuid, UUID node1Uuid, UUID node2Uuid, String userId, Runnable action) {
handleRebuildNodeSupply(studyUuid, node1Uuid, node2Uuid, userId, () -> {
action.run();
return null;
});
}

private <T> T handleRebuildNodeSupply(UUID studyUuid, UUID node1Uuid, UUID node2Uuid, String userId, Supplier<T> action) {
// if node 1 and 2 are in the same "subtree", rebuild only the highest one - otherwise, rebuild both
List<UUID> nodesToReBuild = networkModificationTreeService.getHighestNodeUuids(node1Uuid, node2Uuid).stream()
.filter(Predicate.not(networkModificationTreeService::isRootOrConstructionNode)).toList();

if (nodesToReBuild.isEmpty()) {
action.run();
return;
return action.get();
}

Map<UUID, Set<UUID>> rootNetworkUuidsByNodeBuilt = nodesToReBuild.stream().collect(Collectors.toMap(
nodeUuid -> nodeUuid,
nodeUuid -> getRootNetworkWhereNodeIsBuilt(studyUuid, nodeUuid)
));

action.run();
T result = action.get();

rootNetworkUuidsByNodeBuilt.forEach((nodeUuid, rootNetworkUuids) ->
rootNetworkUuids.stream().forEach(rootNetworkUuid ->
Expand All @@ -160,6 +188,8 @@ private void handleRebuildNode(UUID studyUuid, UUID node1Uuid, UUID node2Uuid, S
)
)
);

return result;
}

private Set<UUID> getRootNetworkWhereNodeIsBuilt(UUID studyUuid, UUID nodeUuid) {
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/org/gridsuite/study/server/service/StudyService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2449,6 +2449,25 @@ public void duplicateNetworkModifications(
userId);
}

@Transactional
public UUID mergeModificationsIntoComposite(
UUID targetStudyUuid,
UUID targetNodeUuid,
List<UUID> modificationsUuids,
String userId) {
UUID newCompositeUuid;
List<UUID> childrenUuids = networkModificationTreeService.getChildrenUuids(targetNodeUuid);
notificationService.emitStartModificationEquipmentNotification(targetStudyUuid, targetNodeUuid, childrenUuids, NotificationService.MODIFICATIONS_UPDATING_IN_PROGRESS);
try {
checkStudyContainsNode(targetStudyUuid, targetNodeUuid);
newCompositeUuid = networkModificationService.mergeModificationsIntoComposite(modificationsUuids);
} finally {
notificationService.emitEndModificationEquipmentNotification(targetStudyUuid, targetNodeUuid, childrenUuids);
}
notificationService.emitElementUpdated(targetStudyUuid, userId);
return newCompositeUuid;
}

@Transactional
public void insertCompositeNetworkModifications(
UUID targetStudyUuid,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2116,6 +2116,53 @@ void testInsertComposite() throws Exception {
expectedBody);
}

@Test
void testMergeModificationsIntoNewComposite() 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 node1 = createNetworkModificationNode(studyUuid, rootNodeUuid,
UUID.randomUUID(), VARIANT_ID, "New node 1", userId);
UUID nodeUuid1 = node1.getId();

UUID modification1 = UUID.randomUUID();
UUID modification2 = UUID.randomUUID();
List<UUID> modificationUuids = List.of(modification1, modification2);
String modificationsData = mapper.writeValueAsString(modificationUuids);

UUID newCompositeUuid = UUID.randomUUID();

wireMockServer.stubFor(WireMock.post(WireMock.urlPathMatching("/v1/network-composite-modifications/composite-modification"))
.willReturn(WireMock.ok()
.withBody(mapper.writeValueAsString(newCompositeUuid))
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)));

MvcResult mvcResult = mockMvc.perform(post("/v1/studies/{studyUuid}/nodes/{nodeUuid}/composite-modification",
studyUuid, nodeUuid1)
.contentType(MediaType.APPLICATION_JSON)
.content(modificationsData)
.header(USER_ID_HEADER, userId))
.andExpect(status().isOk())
.andReturn();

UUID resultUuid = mapper.readValue(mvcResult.getResponse().getContentAsString(), UUID.class);
assertEquals(newCompositeUuid, resultUuid);

checkUpdateStatusMessagesReceived(studyUuid, nodeUuid1, output);
checkEquipmentUpdatingMessagesReceived(studyUuid, nodeUuid1);
checkEquipmentUpdatingFinishedMessagesReceived(studyUuid, nodeUuid1);
checkElementUpdatedMessageSent(studyUuid, userId);

WireMockUtilsCriteria.verifyPostRequest(
wireMockServer,
"/v1/network-composite-modifications/composite-modification",
Map.of(),
1
);
Comment on lines +2158 to +2163
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Verify the request body sent to the composite-modification endpoint.

The test verifies that the WireMock POST endpoint was called, but does not verify that the correct modification UUIDs were sent in the request body. According to the service layer contract, NetworkModificationService.mergeModificationsIntoComposite should POST the modification UUIDs to the downstream API. Similar tests in this file (e.g., testInsertComposite at lines 2106-2116) construct an expected body and verify it was sent.

🧪 Suggested enhancement to verify request body
+        String expectedBody = mapper.writeValueAsString(modificationUuids);
+
         WireMockUtilsCriteria.verifyPostRequest(
                 wireMockServer,
                 "/v1/network-composite-modifications/composite-modification",
                 Map.of(),
+                expectedBody,
                 1
         );

Note: You may need to verify the actual signature of WireMockUtilsCriteria.verifyPostRequest to ensure it accepts a body parameter. If the current overload only accepts URL, query params, and count, you might need to use a different verification method that includes body matching, similar to how testInsertComposite uses WireMockUtilsCriteria.verifyPutRequest with an expectedBody parameter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/org/gridsuite/study/server/NetworkModificationTest.java` around
lines 2158 - 2163, The test currently only asserts the POST to
"/v1/network-composite-modifications/composite-modification" was called; update
the assertion to also verify the request body contains the expected modification
UUIDs produced by NetworkModificationService.mergeModificationsIntoComposite.
Build the same expectedBody map/JSON used in testInsertComposite (or mirror its
structure) and call the overload of WireMockUtilsCriteria.verifyPostRequest that
accepts an expected body (or use the equivalent body-verifying helper used for
verifyPutRequest) to assert the payload matches the expected modification UUID
list; ensure you reference the same endpoint and request-count arguments as
before.

}

@Test
void testDuplicateModification() throws Exception {
String userId = "userId";
Expand Down
Loading