diff --git a/src/main/java/org/gridsuite/modification/server/CompositeController.java b/src/main/java/org/gridsuite/modification/server/CompositeController.java index 89fd56789..8405071c4 100644 --- a/src/main/java/org/gridsuite/modification/server/CompositeController.java +++ b/src/main/java/org/gridsuite/modification/server/CompositeController.java @@ -78,6 +78,16 @@ public ResponseEntity moveSubModification( return ResponseEntity.ok().build(); } + @PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Assemble some network modifications into a new composite modification") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The composite modification has been created")}) + public ResponseEntity assembleNetworkModificationsIntoNewComposite( + @RequestBody List assembledModificationsUuids) { + return ResponseEntity.ok().body( + networkModificationService.assembleNetworkModificationsIntoNewComposite(assembledModificationsUuids) + ); + } + @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..20d44279c 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,76 @@ public List insertCompositeModifications( return newEntities.stream().map(ModificationEntity::toModificationInfos).toList(); } + @Transactional + public CompositeModificationEntity assembleNetworkModificationsIntoNewComposite(List assembledModificationsUuids) { + // get the target (groupUuid or composite Uuid of the first assembled modification + its index in this target) + final UUID firstModifUuid = assembledModificationsUuids.getFirst(); + final ModificationEntity firstModificationEntity = getModificationEntity(firstModifUuid); + final 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 assembled, remove previous assignment + List assembledModifications = assembledModificationsUuids.stream() + .map(modificationRepository::findById).filter(Optional::isPresent).map(Optional::get).toList(); + // 1. cleans and reorders the origin group if there is one : + ModificationGroupEntity originGroup = assembledModifications.stream() + .map(ModificationEntity::getGroup) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + if (originGroup != null) { + List originGroupModifications = originGroup.getModifications(); + originGroupModifications.removeIf(mod -> assembledModificationsUuids.contains(mod.getId())); + originGroup.setModifications(originGroupModifications); + assembledModifications.forEach(modificationEntity -> modificationEntity.setGroup(null)); + } + // 2. cleans the composites whose submodifications are assembled into a new one + for (ModificationEntity assembledModification : assembledModifications.stream().filter(mod -> mod.getGroup() == null).toList()) { + UUID compositeUuid = modificationRepository.findCompositeIdByContainedModificationId(assembledModification.getId()); + if (compositeUuid != null) { + CompositeModificationEntity previousOwner = compositeModificationRepository.findById(compositeUuid).orElse(null); + if (previousOwner != null) { + List modificationsLeft = previousOwner.getModifications() + .stream() + .filter(mod -> !assembledModificationsUuids.contains(mod.getId())) + .toList(); + previousOwner.setModifications(modificationsLeft); + } + } + } + + // create the new composite + CompositeModificationInfos newCompositeInfos = CompositeModificationInfos.builder() + .modificationsInfos(List.of()) + .name("Composite modification") + .build(); + CompositeModificationEntity newCompositeEntity = (CompositeModificationEntity) ModificationEntity.fromDTO(newCompositeInfos); + newCompositeEntity.setModificationsOrder(targetIndex); + + // assign modifications + newCompositeEntity.setModifications(assembledModifications); + // 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); + for (int i = 0; i < targetComposite.getModifications().size(); i++) { + targetComposite.getModifications().get(i).setModificationsOrder(i); + } + } + + return modificationRepository.save(newCompositeEntity); + } + @Transactional public void moveSubModification(@NonNull UUID groupUuid, UUID sourceCompositeUuid, UUID targetCompositeUuid, @NonNull UUID modificationUuid, UUID beforeUuid) { @@ -959,10 +1029,6 @@ public void moveSubModification(@NonNull UUID groupUuid, UUID sourceCompositeUui group.setModifications(notMovedMods); movedMods.forEach(entity -> entity.setGroup(null)); } - // reordering the composite modifications left (not moved) - for (int i = 0; i < notMovedMods.size(); i++) { - notMovedMods.get(i).setModificationsOrder(i); - } if (targetCompositeUuid != null) { // Check if targeted composite isn't already inside modificationUuid 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..8cd8b4f44 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,14 @@ public CompletableFuture insertCompositeModification new NetworkModificationsResult(ids, result)); } + @Transactional + public UUID assembleNetworkModificationsIntoNewComposite(@NonNull List assembledModificationsUuids) { + CompositeModificationInfos newComposite = + networkModificationRepository.assembleNetworkModificationsIntoNewComposite(assembledModificationsUuids).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..1369c0d8e 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 testAssembleNetworkModificationsIntoNewComposite() throws Exception { + // Create 3 root-level modifications in the group + List rootMods = createSomeSwitchModifications(TEST_GROUP_ID, 3); + final List originalRootModUuids = rootMods.stream().map(ModificationInfos::getUuid).toList(); + + assertEquals(3, modificationRepository.getModifications(TEST_GROUP_ID, true, true).size()); + + // ---- 1. Assemble the first 2 root-level modifications into a new composite + List assembledModificationUuids = originalRootModUuids.subList(0, 2); + MvcResult mvcResult = mockMvc.perform(post(URI_COMPOSITE_NETWORK_MODIF_BASE + "/") + .content(mapper.writeValueAsString(assembledModificationUuids)) + .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-assembled modification + List rootModificationsAfterAssemble = modificationRepository.getModifications(TEST_GROUP_ID, true, true); + assertEquals(2, rootModificationsAfterAssemble.size()); + assertEquals(firstCompositeUuid, rootModificationsAfterAssemble.getFirst().getUuid()); + assertEquals(COMPOSITE_MODIFICATION, rootModificationsAfterAssemble.getFirst().getType()); + assertEquals(originalRootModUuids.get(2), rootModificationsAfterAssemble.get(1).getUuid()); + + // The new composite should contain the assembled 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(originalRootModUuids.get(0), compositeContent.get(0).getUuid()); + assertEquals(originalRootModUuids.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 assembled modifications must no longer belong directly to the group + ModificationEntity firstAssembledEntity = modificationRepository.getModificationEntity(originalRootModUuids.get(0)); + ModificationEntity secondAssembledEntity = modificationRepository.getModificationEntity(originalRootModUuids.get(1)); + assertNull(firstAssembledEntity.getGroup()); + assertNull(secondAssembledEntity.getGroup()); + + // The non-assembled modification must still belong to TEST_GROUP_ID + ModificationEntity remainingInGroupEntity = modificationRepository.getModificationEntity(originalRootModUuids.get(2)); + assertNotNull(remainingInGroupEntity.getGroup()); + assertEquals(TEST_GROUP_ID, remainingInGroupEntity.getGroup().getId()); + + // ---- 2. now assembles a modification which is inside a composite with something that is outside : + assembledModificationUuids = List.of(compositeContent.getFirst().getUuid(), remainingInGroupEntity.getId()); + mvcResult = mockMvc.perform(post(URI_COMPOSITE_NETWORK_MODIF_BASE + "/") + .content(mapper.writeValueAsString(assembledModificationUuids)) + .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 + rootModificationsAfterAssemble = modificationRepository.getModifications(TEST_GROUP_ID, true, true); + assertEquals(1, rootModificationsAfterAssemble.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(twodepthCompositeUuid, compositeContent.get(0).getUuid()); + assertEquals(originalRootModUuids.get(1), 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]