diff --git a/src/main/java/org/gridsuite/modification/server/CompositeController.java b/src/main/java/org/gridsuite/modification/server/CompositeController.java index 89fd56789..71930f5df 100644 --- a/src/main/java/org/gridsuite/modification/server/CompositeController.java +++ b/src/main/java/org/gridsuite/modification/server/CompositeController.java @@ -78,6 +78,18 @@ public ResponseEntity moveSubModification( return ResponseEntity.ok().build(); } + @PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Merge some network modifications into a new composite modification") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The composite modification has been created")}) + public ResponseEntity mergeNetworkModificationsIntoNewComposite( + @RequestBody List mergedModificationsUuids) { + + return ResponseEntity.ok().body( + networkModificationService.mergeNetworkModificationsIntoNewComposite( + mergedModificationsUuids) + ); + } + @PostMapping(value = "", consumes = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Create a network composite modification") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The composite modification has been created")}) diff --git a/src/main/java/org/gridsuite/modification/server/repositories/ModificationRepository.java b/src/main/java/org/gridsuite/modification/server/repositories/ModificationRepository.java index fc21dbebb..3458f3346 100644 --- a/src/main/java/org/gridsuite/modification/server/repositories/ModificationRepository.java +++ b/src/main/java/org/gridsuite/modification/server/repositories/ModificationRepository.java @@ -71,6 +71,16 @@ SELECT CAST(sm.modification_id AS VARCHAR) """, nativeQuery = true) List findModificationIdsByCompositeModificationId(UUID uuid); + // return the uuid of the composite containing the modification sent as parameter + @Query(value = """ + SELECT CAST(sm.id AS VARCHAR) + FROM composite_modification_sub_modifications sm + INNER JOIN modification m ON sm.modification_id = m.id + WHERE sm.modification_id = :uuid + ORDER BY m.modifications_order + """, nativeQuery = true) + UUID findCompositeIdByContainedModificationId(UUID uuid); + @Query(value = """ SELECT CAST(sm.modification_id AS VARCHAR) FROM composite_modification_sub_modifications sm diff --git a/src/main/java/org/gridsuite/modification/server/repositories/NetworkModificationRepository.java b/src/main/java/org/gridsuite/modification/server/repositories/NetworkModificationRepository.java index 51d4594cf..0a37f2c0c 100644 --- a/src/main/java/org/gridsuite/modification/server/repositories/NetworkModificationRepository.java +++ b/src/main/java/org/gridsuite/modification/server/repositories/NetworkModificationRepository.java @@ -909,6 +909,75 @@ public List insertCompositeModifications( return newEntities.stream().map(ModificationEntity::toModificationInfos).toList(); } + @Transactional + public CompositeModificationEntity mergeNetworkModificationsIntoNewComposite( + List mergedModificationsUuids) { + // get the target (groupUuid or composite Uuid of the first merged modification + its index in this target) + UUID firstModifUuid = mergedModificationsUuids.getFirst(); + ModificationEntity firstModificationEntity = getModificationEntity(firstModifUuid); + int targetIndex = firstModificationEntity.getModificationsOrder(); + ModificationGroupEntity targetGroup = firstModificationEntity.getGroup(); + CompositeModificationEntity targetComposite = null; + if (targetGroup == null) { + // the first modification is inside a composite + UUID targetCompositeUuid = modificationRepository.findCompositeIdByContainedModificationId(firstModifUuid); + targetComposite = compositeModificationRepository.findById(targetCompositeUuid).orElse(null); + } + + // get all the modifications to be merged, remove previous assignment + List mergedModifications = mergedModificationsUuids.stream() + .map(modificationRepository::findById).filter(Optional::isPresent).map(Optional::get).toList(); + // remove previous assignments of the merged modifications + // 1. cleans and reorders the origin group if there is one : + ModificationGroupEntity originGroup = mergedModifications.stream() + .map(ModificationEntity::getGroup) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + if (originGroup != null) { + List originGroupModifications = originGroup.getModifications(); + originGroupModifications.removeIf(mod -> mergedModificationsUuids.contains(mod.getId())); + originGroup.setModifications(originGroupModifications); + mergedModifications.forEach(modificationEntity -> modificationEntity.setGroup(null)); + } + // 2. cleans the composites whose submodifications are merged into a new one + for (ModificationEntity mergedModification : mergedModifications.stream().filter(mod -> mod.getGroup() == null).toList()) { + UUID compositeUuid = modificationRepository.findCompositeIdByContainedModificationId(mergedModification.getId()); + if (compositeUuid != null) { + CompositeModificationEntity previousOwner = compositeModificationRepository.findById(compositeUuid).orElse(null); + if (previousOwner != null) { + List modificationsLeft = previousOwner.getModifications() + .stream() + .filter(mod -> !mergedModificationsUuids.contains(mod.getId())) + .toList(); + previousOwner.setModifications(modificationsLeft); + } + } + } + + // create the new composite + CompositeModificationInfos newCompositeInfos = CompositeModificationInfos.builder() + .modificationsInfos(List.of()) + .name("New composite modification") + .build(); + CompositeModificationEntity newCompositeEntity = (CompositeModificationEntity) ModificationEntity.fromDTO(newCompositeInfos); + newCompositeEntity.setModificationsOrder(targetIndex); + + // assign modifications + newCompositeEntity.setModifications(mergedModifications); + // put the new composite in the target group or composite + if (targetGroup != null) { + List modifications = targetGroup.getModifications(); + modifications.add(targetIndex, newCompositeEntity); + targetGroup.setModifications(modifications); + } else if (targetComposite != null) { + List modifications = targetComposite.getModifications(); + modifications.add(targetIndex, newCompositeEntity); + } + + return modificationRepository.save(newCompositeEntity); + } + @Transactional public void moveSubModification(@NonNull UUID groupUuid, UUID sourceCompositeUuid, UUID targetCompositeUuid, @NonNull UUID modificationUuid, UUID beforeUuid) { diff --git a/src/main/java/org/gridsuite/modification/server/service/NetworkModificationService.java b/src/main/java/org/gridsuite/modification/server/service/NetworkModificationService.java index bfcfb47b4..9e1573791 100644 --- a/src/main/java/org/gridsuite/modification/server/service/NetworkModificationService.java +++ b/src/main/java/org/gridsuite/modification/server/service/NetworkModificationService.java @@ -21,6 +21,7 @@ import org.gridsuite.filter.AbstractFilter; import org.gridsuite.modification.ModificationType; import org.gridsuite.modification.NetworkModificationException; +import org.gridsuite.modification.dto.CompositeModificationInfos; import org.gridsuite.modification.dto.EquipmentModificationInfos; import org.gridsuite.modification.dto.GenerationDispatchInfos; import org.gridsuite.modification.dto.ModificationInfos; @@ -474,6 +475,15 @@ public CompletableFuture insertCompositeModification new NetworkModificationsResult(ids, result)); } + @Transactional + public UUID mergeNetworkModificationsIntoNewComposite( + @NonNull List mergedModificationsUuids) { + CompositeModificationInfos newComposite = + networkModificationRepository.mergeNetworkModificationsIntoNewComposite(mergedModificationsUuids).toModificationInfos(); + + return newComposite.getUuid(); + } + @Transactional public UUID createNetworkCompositeModification(@NonNull List modificationUuids) { return networkModificationRepository.createNetworkCompositeModification(modificationUuids); diff --git a/src/test/java/org/gridsuite/modification/server/CompositeControllerTest.java b/src/test/java/org/gridsuite/modification/server/CompositeControllerTest.java index 64c693773..a97f6726f 100644 --- a/src/test/java/org/gridsuite/modification/server/CompositeControllerTest.java +++ b/src/test/java/org/gridsuite/modification/server/CompositeControllerTest.java @@ -20,7 +20,9 @@ import org.gridsuite.modification.dto.ModificationInfos; import org.gridsuite.modification.server.dto.NetworkModificationResult; import org.gridsuite.modification.server.dto.NetworkModificationsResult; +import org.gridsuite.modification.server.entities.CompositeModificationEntity; import org.gridsuite.modification.server.entities.ModificationEntity; +import org.gridsuite.modification.server.repositories.CompositeModificationRepository; import org.gridsuite.modification.server.repositories.NetworkModificationRepository; import org.gridsuite.modification.server.service.ReportService; import org.gridsuite.modification.server.utils.NetworkCreation; @@ -38,11 +40,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; +import java.util.*; import static org.gridsuite.modification.ModificationType.COMPOSITE_MODIFICATION; import static org.gridsuite.modification.server.utils.NetworkCreation.VARIANT_ID; @@ -76,6 +74,9 @@ class CompositeControllerTest { @Autowired private NetworkModificationRepository modificationRepository; + @Autowired + private CompositeModificationRepository compositeRepository; + @MockitoBean private ReportService reportService; @@ -276,7 +277,9 @@ void testDuplicateCompositeModification() throws Exception { Map returnedMap = mapper.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<>() { }); assertEquals(1, returnedMap.size()); - Map.Entry returnedIds = returnedMap.entrySet().stream().findFirst().get(); + Optional> first = returnedMap.entrySet().stream().findFirst(); + assertTrue(first.isPresent()); + Map.Entry returnedIds = first.get(); UUID returnedSourceId = returnedIds.getKey(); UUID returnedNewId = returnedIds.getValue(); assertNotEquals(returnedSourceId, returnedNewId); @@ -484,6 +487,96 @@ void testMoveSubModificationFromCompositeToRoot() throws Exception { assertNull(remainingEntity.getGroup()); } + @Test + void testMergeNetworkModificationsIntoNewComposite() throws Exception { + // Create 3 root-level modifications in the group + List rootMods = createSomeSwitchModifications(TEST_GROUP_ID, 3); + List rootModUuids = rootMods.stream().map(ModificationInfos::getUuid).toList(); + + assertEquals(3, modificationRepository.getModifications(TEST_GROUP_ID, true, true).size()); + + // ---- 1. Merge the first 2 root-level modifications into a new composite + List mergedModificationUuids = rootModUuids.subList(0, 2); + MvcResult mvcResult = mockMvc.perform(post(URI_COMPOSITE_NETWORK_MODIF_BASE + "/composite-modification") + .content(mapper.writeValueAsString(mergedModificationUuids)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andReturn(); + + UUID firstCompositeUuid = mapper.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<>() { }); + assertNotNull(firstCompositeUuid); + + // The root group should now contain the new composite and the remaining non-merged modification + List rootModificationsAfterMerge = modificationRepository.getModifications(TEST_GROUP_ID, true, true); + assertEquals(2, rootModificationsAfterMerge.size()); + assertEquals(firstCompositeUuid, rootModificationsAfterMerge.getFirst().getUuid()); + assertEquals(COMPOSITE_MODIFICATION, rootModificationsAfterMerge.getFirst().getType()); + assertEquals(rootModUuids.get(2), rootModificationsAfterMerge.get(1).getUuid()); + + // The new composite should contain the merged modifications in the same order + Map> compositeContentMap = mapper.readValue( + mockMvc.perform(get(URI_GET_COMPOSITE_NETWORK_MODIF_CONTENT + "/network-modifications?uuids={id}", firstCompositeUuid)) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(), + new TypeReference<>() { }); + List compositeContent = compositeContentMap.get(firstCompositeUuid); + + assertEquals(2, compositeContent.size()); + assertEquals(rootModUuids.get(0), compositeContent.get(0).getUuid()); + assertEquals(rootModUuids.get(1), compositeContent.get(1).getUuid()); + + // The new composite must belong to TEST_GROUP_ID at root level + CompositeModificationEntity firstComposite = compositeRepository.findById(firstCompositeUuid).orElseThrow(); + assertNotNull(firstComposite.getGroup()); + assertEquals(TEST_GROUP_ID, firstComposite.getGroup().getId()); + + // The merged modifications must no longer belong directly to the group + ModificationEntity firstMergedEntity = modificationRepository.getModificationEntity(rootModUuids.get(0)); + ModificationEntity secondMergedEntity = modificationRepository.getModificationEntity(rootModUuids.get(1)); + assertNull(firstMergedEntity.getGroup()); + assertNull(secondMergedEntity.getGroup()); + + // The non-merged modification must still belong to TEST_GROUP_ID + ModificationEntity remainingEntity = modificationRepository.getModificationEntity(rootModUuids.get(2)); + assertNotNull(remainingEntity.getGroup()); + assertEquals(TEST_GROUP_ID, remainingEntity.getGroup().getId()); + + // ---- 2. now merges a modification which is inside a composite with something that is outside : + mergedModificationUuids = List.of(compositeContent.get(0).getUuid(), rootModUuids.get(2)); + mvcResult = mockMvc.perform(post(URI_COMPOSITE_NETWORK_MODIF_BASE + "/composite-modification") + .content(mapper.writeValueAsString(mergedModificationUuids)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andReturn(); + + // this new composite will be generated inside the other composite because its first element was inside it + UUID twodepthCompositeUuid = mapper.readValue(mvcResult.getResponse().getContentAsString(), new TypeReference<>() { }); + assertNotNull(twodepthCompositeUuid); + + // The root group should now contain the new composite and nothing else + rootModificationsAfterMerge = modificationRepository.getModifications(TEST_GROUP_ID, true, true); + assertEquals(1, rootModificationsAfterMerge.size()); + + // The first composite should contain the new composite, then the other untouched modification + compositeContentMap = mapper.readValue( + mockMvc.perform(get(URI_GET_COMPOSITE_NETWORK_MODIF_CONTENT + "/network-modifications?uuids={id}", firstCompositeUuid)) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(), + new TypeReference<>() { }); + compositeContent = compositeContentMap.get(firstCompositeUuid); + + assertEquals(2, compositeContent.size()); + assertEquals(rootModUuids.get(1), compositeContent.get(0).getUuid()); + assertEquals(twodepthCompositeUuid, compositeContent.get(1).getUuid()); + + // The new 2 depth composite must now belong to the first composite, not to a group + CompositeModificationEntity twoDepthComposite = compositeRepository.findById(twodepthCompositeUuid).orElseThrow(); + assertNull(twoDepthComposite.getGroup()); + compositeContentMap = mapper.readValue( + mockMvc.perform(get(URI_GET_COMPOSITE_NETWORK_MODIF_CONTENT + "/network-modifications?uuids={id}", firstCompositeUuid)) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(), + new TypeReference<>() { }); + assertTrue(compositeContentMap.get(firstCompositeUuid).stream() + .map(ModificationInfos::getUuid) + .anyMatch(twodepthCompositeUuid::equals)); + } + @Test void testExpandToLeafUuidsNestedComposites() throws Exception { // Build nested structure: outerComposite → [innerComposite → [leaf1, leaf2], leaf3]