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 bdd17914a..f160bd293 100644 --- a/src/main/java/org/gridsuite/study/server/controller/StudyController.java +++ b/src/main/java/org/gridsuite/study/server/controller/StudyController.java @@ -23,6 +23,7 @@ import org.gridsuite.study.server.dto.networkexport.ExportNetworkStatus; import org.gridsuite.study.server.dto.networkexport.NodeExportInfos; import org.gridsuite.study.server.dto.sequence.NodeSequenceType; +import org.gridsuite.study.server.dto.studyexport.TreeExportInfos; import org.gridsuite.study.server.elasticsearch.EquipmentInfosService; import org.gridsuite.study.server.error.StudyException; import org.gridsuite.study.server.exception.PartialResultException; @@ -70,6 +71,7 @@ public class StudyController { private final NodeActivityRunnerService nodeActivityRunnerService; private final NodeActivityService nodeActivityService; private final StudyExportService studyExportService; + private final StudyImportService studyImportService; public StudyController(StudyService studyService, NetworkService networkStoreService, @@ -82,7 +84,8 @@ public StudyController(StudyService studyService, RebuildNodeService rebuildNodeService, NodeActivityRunnerService nodeActivityRunnerService, NodeActivityService nodeActivityService, - StudyExportService studyExportService) { + StudyExportService studyExportService, + StudyImportService studyImportService) { this.nodeActivityService = nodeActivityService; this.studyService = studyService; this.networkModificationTreeService = networkModificationTreeService; @@ -95,6 +98,7 @@ public StudyController(StudyService studyService, this.rebuildNodeService = rebuildNodeService; this.nodeActivityRunnerService = nodeActivityRunnerService; this.studyExportService = studyExportService; + this.studyImportService = studyImportService; } @InitBinder @@ -1642,4 +1646,13 @@ public ResponseEntity exportStudy(@PathVariable("studyUuid") UUID stud headers.setContentType(MediaType.parseMediaType("application/zip")); return ResponseEntity.ok().headers(headers).body(studyExportService.exportStudy(studyUuid, userId)); } + + @PostMapping(value = "/studies/import") + @Operation(summary = "Create a study and its root networks from a previously exported study archive") + @ApiResponse(responseCode = "200", description = "Study import initiated successfully") + public ResponseEntity importStudy(@RequestBody TreeExportInfos treeExportInfos, + @RequestHeader(HEADER_USER_ID) String userId) { + studyImportService.importStudy(treeExportInfos, userId); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/org/gridsuite/study/server/service/ConsumerService.java b/src/main/java/org/gridsuite/study/server/service/ConsumerService.java index 3f418048a..3571a6c41 100644 --- a/src/main/java/org/gridsuite/study/server/service/ConsumerService.java +++ b/src/main/java/org/gridsuite/study/server/service/ConsumerService.java @@ -70,7 +70,6 @@ public class ConsumerService { private final CaseService caseService; private final LoadFlowRestService loadFlowRestService; private final NetworkModificationTreeService networkModificationTreeService; - private final StudyConfigService studyConfigService; private final RootNetworkNodeInfoService rootNetworkNodeInfoService; private final RootNetworkService rootNetworkService; private final DirectoryService directoryService; @@ -86,7 +85,6 @@ public ConsumerService(ObjectMapper objectMapper, CaseService caseService, LoadFlowRestService loadFlowRestService, NetworkModificationTreeService networkModificationTreeService, - StudyConfigService studyConfigService, RootNetworkNodeInfoService rootNetworkNodeInfoService, RootNetworkService rootNetworkService, DirectoryService directoryService, @@ -101,7 +99,6 @@ public ConsumerService(ObjectMapper objectMapper, this.caseService = caseService; this.loadFlowRestService = loadFlowRestService; this.networkModificationTreeService = networkModificationTreeService; - this.studyConfigService = studyConfigService; this.rootNetworkNodeInfoService = rootNetworkNodeInfoService; this.rootNetworkService = rootNetworkService; this.directoryService = directoryService; @@ -292,73 +289,15 @@ private void insertStudy(UUID studyUuid, String userId, NetworkInfos networkInfo UserProfileInfos userProfileInfos = studyService.getUserProfile(userId); ComputationParameterUUIDs computationParameterUUIDs = computationParametersService.createDefaultComputationParameters(userId, userProfileInfos); - UUID networkVisualizationParametersUuid = createDefaultNetworkVisualizationParameters(userId, userProfileInfos); - UUID spreadsheetConfigCollectionUuid = createDefaultSpreadsheetConfigCollection(userId, userProfileInfos); - UUID workspacesConfigUuid = createWorkspacesConfig(userProfileInfos); + UUID networkVisualizationParametersUuid = studyService.createDefaultNetworkVisualizationParameters(userId, userProfileInfos); + UUID spreadsheetConfigCollectionUuid = studyService.createDefaultSpreadsheetConfigCollection(userId, userProfileInfos); + UUID workspacesConfigUuid = studyService.createWorkspacesConfig(userProfileInfos); studyService.insertStudy(studyUuid, userId, networkInfos, caseInfos, computationParameterUUIDs, networkVisualizationParametersUuid, spreadsheetConfigCollectionUuid, workspacesConfigUuid, importParameters, importReportUuid); } - private UUID createDefaultNetworkVisualizationParameters(String userId, UserProfileInfos userProfileInfos) { - if (userProfileInfos != null && userProfileInfos.getNetworkVisualizationParameterId() != null) { - // try to access/duplicate the user profile network visualization parameters - try { - return studyConfigService.duplicateNetworkVisualizationParameters(userProfileInfos.getNetworkVisualizationParameterId()); - } catch (Exception e) { - // TODO try to report a log in Root subreporter ? - LOGGER.error(String.format("Could not duplicate network visualization parameters with id '%s' from user/profile '%s/%s'. Using default parameters", - userProfileInfos.getNetworkVisualizationParameterId(), userId, userProfileInfos.getName()), e); - } - } - // no profile, or no/bad network visualization parameters in profile => use default values - try { - return studyConfigService.createDefaultNetworkVisualizationParameters(); - } catch (final Exception e) { - LOGGER.error("Error while creating network visualization default parameters", e); - return null; - } - } - - private UUID createDefaultSpreadsheetConfigCollection(String userId, UserProfileInfos userProfileInfos) { - if (userProfileInfos != null && userProfileInfos.getSpreadsheetConfigCollectionId() != null) { - // try to access/duplicate the user profile spreadsheet config collection - try { - return studyConfigService.duplicateSpreadsheetConfigCollection(userProfileInfos.getSpreadsheetConfigCollectionId()); - } catch (Exception e) { - // TODO try to report a log in Root subreporter ? - LOGGER.error(String.format("Could not duplicate spreadsheet config collection with id '%s' from user/profile '%s/%s'. Using default spreadsheet config collection", - userProfileInfos.getSpreadsheetConfigCollectionId(), userId, userProfileInfos.getName()), e); - } - } - // no profile, or no/bad spreadsheet config collection in profile => use default values - try { - return studyConfigService.createDefaultSpreadsheetConfigCollection(); - } catch (final Exception e) { - LOGGER.error("Error while creating default spreadsheet config collection", e); - return null; - } - } - - @SuppressWarnings("checkstyle:LambdaBodyLength") - private UUID createWorkspacesConfig(UserProfileInfos userProfileInfos) { - try { - List workspaceIds = new ArrayList<>(); - if (userProfileInfos != null && userProfileInfos.getWorkspaceId() != null) { - // Create config with profile workspace as first, and two empty workspaces - workspaceIds.add(userProfileInfos.getWorkspaceId()); - workspaceIds.add(null); - workspaceIds.add(null); - } - // Empty list will create default config - return studyConfigService.createWorkspacesConfigFromWorkspaces(workspaceIds); - } catch (final Exception e) { - LOGGER.error("Error while creating workspace collection", e); - return null; - } - } - @Bean @SuppressWarnings("checkstyle:LambdaBodyLength") public Consumer> consumeCaseImportFailed() { diff --git a/src/main/java/org/gridsuite/study/server/service/StudyImportService.java b/src/main/java/org/gridsuite/study/server/service/StudyImportService.java new file mode 100644 index 000000000..2527b606d --- /dev/null +++ b/src/main/java/org/gridsuite/study/server/service/StudyImportService.java @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.study.server.service; + +import org.apache.commons.collections4.CollectionUtils; +import org.gridsuite.study.server.dto.CaseInfos; +import org.gridsuite.study.server.dto.NetworkInfos; +import org.gridsuite.study.server.dto.RootNetworkInfos; +import org.gridsuite.study.server.dto.RootNetworkLoadStatus; +import org.gridsuite.study.server.dto.studyexport.NodeTreeExportInfos; +import org.gridsuite.study.server.dto.studyexport.RootNetworkExportInfos; +import org.gridsuite.study.server.dto.studyexport.TreeExportInfos; +import org.gridsuite.study.server.error.StudyException; +import org.gridsuite.study.server.notification.NotificationService; +import org.gridsuite.study.server.repository.StudyEntity; +import org.gridsuite.study.server.repository.StudyRepository; +import org.gridsuite.study.server.repository.rootnetwork.RootNetworkEntity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.gridsuite.study.server.error.StudyBusinessErrorCode.NOT_FOUND; + +/** + * @author Ghazwa Rehili + */ +@Service +public class StudyImportService { + private static final Logger LOGGER = LoggerFactory.getLogger(StudyImportService.class); + + private final StudyService studyService; + private final StudyRepository studyRepository; + private final RootNetworkService rootNetworkService; + private final NetworkModificationService networkModificationService; + private final CaseService caseService; + private final NotificationService notificationService; + + public StudyImportService(StudyService studyService, StudyRepository studyRepository, RootNetworkService rootNetworkService, + NetworkModificationService networkModificationService, CaseService caseService, NotificationService notificationService) { + this.studyService = studyService; + this.studyRepository = studyRepository; + this.rootNetworkService = rootNetworkService; + this.networkModificationService = networkModificationService; + this.caseService = caseService; + this.notificationService = notificationService; + } + + @Transactional + public void importStudy(TreeExportInfos treeExportInfos, String userId) { + if (treeExportInfos.rootNetworks().isEmpty()) { + throw new StudyException(NOT_FOUND, "No root network found in import archive"); + } + Map modificationGroupUuidMapping = duplicateModificationGroups(treeExportInfos.nodeTree()); + StudyEntity studyEntity = studyService.createStudyEntityWithTree(treeExportInfos.studyUuid(), userId, treeExportInfos.nodeTree(), modificationGroupUuidMapping); + studyRepository.save(studyEntity); + List orderedRootNetworks = treeExportInfos.rootNetworks().stream() + .sorted(Comparator.comparing(RootNetworkExportInfos::index)) + .toList(); + List duplicatedCaseUuids = new ArrayList<>(); + try { + for (RootNetworkExportInfos rootNetworkInfos : orderedRootNetworks) { + UUID newCaseUuid = caseService.duplicateCase(rootNetworkInfos.caseInfos().getCaseUuid(), true); + duplicatedCaseUuids.add(newCaseUuid); + RootNetworkEntity rootNetworkEntity = rootNetworkService.createRootNetwork(studyEntity, RootNetworkInfos.builder() + .id(UUID.randomUUID()) + .name(rootNetworkInfos.name()) + .tag(rootNetworkInfos.tag()) + .caseInfos(new CaseInfos(newCaseUuid, rootNetworkInfos.caseInfos().getOriginalCaseUuid(), + rootNetworkInfos.caseInfos().getCaseName(), rootNetworkInfos.caseInfos().getCaseFormat())) + .importParameters(rootNetworkInfos.importParameters()) + .networkInfos(new NetworkInfos(UUID.randomUUID(), "")) + .build()); + rootNetworkService.updateNetworkLoadStatus(rootNetworkEntity.getId(), RootNetworkLoadStatus.UNLOADED); + } + } catch (Exception e) { + modificationGroupUuidMapping.values().forEach(newGroupUuid -> { + try { + networkModificationService.deleteModifications(newGroupUuid); + } catch (Exception exception) { + LOGGER.error(String.format("Could not clean up orphaned modification group '%s' after import failure", newGroupUuid), exception); + } + }); + duplicatedCaseUuids.forEach(caseUuid -> { + try { + caseService.deleteCase(caseUuid); + } catch (Exception exception) { + LOGGER.error(String.format("Could not clean up orphaned case '%s' after import failure", caseUuid), exception); + } + }); + throw e; + } + notificationService.emitStudyCreationFinished(studyEntity.getId(), userId); + } + + private Map duplicateModificationGroups(NodeTreeExportInfos nodeTree) { + Map modificationGroupUuidMapping = new HashMap<>(); + if (nodeTree == null) { + return modificationGroupUuidMapping; + } + try { + CollectionUtils.emptyIfNull(nodeTree.children()).forEach(child -> duplicateModificationGroupsRecursively(child, modificationGroupUuidMapping)); + } catch (Exception e) { + modificationGroupUuidMapping.values().forEach(newGroupUuid -> { + try { + networkModificationService.deleteModifications(newGroupUuid); + } catch (Exception cleanupException) { + LOGGER.error(String.format("Could not clean up orphaned modification group '%s' after import failure", newGroupUuid), cleanupException); + } + }); + throw e; + } + return modificationGroupUuidMapping; + } + + private void duplicateModificationGroupsRecursively(NodeTreeExportInfos exportNode, Map modificationGroupUuidMapping) { + studyService.toNetworkModificationNodeType(exportNode.nodeType()); + if (exportNode.modificationGroupUuid() != null) { + UUID newGroupUuid = UUID.randomUUID(); + networkModificationService.duplicateModificationsGroup(exportNode.modificationGroupUuid(), newGroupUuid); + modificationGroupUuidMapping.put(exportNode.modificationGroupUuid(), newGroupUuid); + } + CollectionUtils.emptyIfNull(exportNode.children()).forEach(child -> duplicateModificationGroupsRecursively(child, modificationGroupUuidMapping)); + } +} 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 7f19fdd37..aa7b2af4f 100644 --- a/src/main/java/org/gridsuite/study/server/service/StudyService.java +++ b/src/main/java/org/gridsuite/study/server/service/StudyService.java @@ -36,6 +36,7 @@ import org.gridsuite.study.server.error.StudyException; import org.gridsuite.study.server.networkmodificationtree.dto.*; import org.gridsuite.study.server.networkmodificationtree.entities.NetworkModificationNodeInfoEntity; +import org.gridsuite.study.server.networkmodificationtree.entities.NetworkModificationNodeType; import org.gridsuite.study.server.networkmodificationtree.entities.NodeEntity; import org.gridsuite.study.server.networkmodificationtree.entities.NodeType; import org.gridsuite.study.server.notification.NotificationService; @@ -2930,4 +2931,119 @@ private NodeTreeExportInfos toNodeTreeExportInfos(AbstractNode node) { children ); } + + NetworkModificationNodeType toNetworkModificationNodeType(String nodeType) { + if (nodeType == null) { + throw new StudyException(BAD_NODE_TYPE, "Missing node type in imported tree"); + } + try { + return NetworkModificationNodeType.valueOf(nodeType); + } catch (IllegalArgumentException e) { + throw new StudyException(BAD_NODE_TYPE, e.getMessage()); + } + } + + @Transactional + public StudyEntity createStudyEntityWithTree(UUID studyUuid, String userId, NodeTreeExportInfos nodeTree, Map modificationGroupUuidMapping) { + UserProfileInfos userProfileInfos = getUserProfile(userId); + ComputationParameterUUIDs computationParameterUUIDs = computationParametersService.createDefaultComputationParameters(userId, userProfileInfos); + UUID networkVisualizationParametersUuid = createDefaultNetworkVisualizationParameters(userId, userProfileInfos); + UUID spreadsheetConfigCollectionUuid = createDefaultSpreadsheetConfigCollection(userId, userProfileInfos); + UUID workspacesConfigUuid = createWorkspacesConfig(userProfileInfos); + + StudyEntity studyEntity = studyRepository.save(StudyEntity.builder() + .id(studyUuid) + .loadFlowParametersUuid(computationParameterUUIDs.loadFlowParametersUuid()) + .shortCircuitParametersUuid(computationParameterUUIDs.shortCircuitParametersUuid()) + .voltageInitParametersUuid(computationParameterUUIDs.voltageInitParametersUuid()) + .securityAnalysisParametersUuid(computationParameterUUIDs.securityAnalysisParametersUuid()) + .sensitivityAnalysisParametersUuid(computationParameterUUIDs.sensitivityAnalysisParametersUuid()) + .voltageInitParameters(new StudyVoltageInitParametersEntity()) + .networkVisualizationParametersUuid(networkVisualizationParametersUuid) + .dynamicSimulationParametersUuid(computationParameterUUIDs.dynamicSimulationParametersUuid()) + .dynamicSecurityAnalysisParametersUuid(computationParameterUUIDs.dynamicSecurityAnalysisParametersUuid()) + .dynamicMarginCalculationParametersUuid(computationParameterUUIDs.dynamicMarginCalculationParametersUuid()) + .stateEstimationParametersUuid(computationParameterUUIDs.stateEstimationParametersUuid()) + .pccMinParametersUuid(computationParameterUUIDs.pccMinParametersUuid()) + .spreadsheetConfigCollectionUuid(spreadsheetConfigCollectionUuid) + .workspacesConfigUuid(workspacesConfigUuid) + .monoRoot(true) + .build()); + + UUID rootNodeUuid = networkModificationTreeService.createRoot(studyEntity).getIdNode(); + if (nodeTree != null && !nodeTree.children().isEmpty()) { + nodeTree.children().forEach(child -> createNodeRecursively(studyEntity, rootNodeUuid, child, userId, modificationGroupUuidMapping)); + } + + studyInfosService.add(toCreatedStudyBasicInfos(studyEntity)); + + return studyEntity; + } + + private void createNodeRecursively(StudyEntity studyEntity, UUID parentNodeUuid, NodeTreeExportInfos exportNode, String userId, Map modificationGroupUuidMapping) { + NetworkModificationNodeType nodeType = toNetworkModificationNodeType(exportNode.nodeType()); + UUID newGroupUuid = exportNode.modificationGroupUuid() != null ? modificationGroupUuidMapping.get(exportNode.modificationGroupUuid()) : null; + NetworkModificationNode newNode = networkModificationTreeService.createNode( + studyEntity, + parentNodeUuid, + NetworkModificationNode.builder() + .name(exportNode.name()) + .nodeType(nodeType) + // buildStatus intentionally left by default (NOT_BUILT): + .modificationGroupUuid(newGroupUuid) + .build(), + InsertMode.CHILD, + userId + ); + CollectionUtils.emptyIfNull(exportNode.children()).forEach(child -> createNodeRecursively(studyEntity, newNode.getId(), child, userId, modificationGroupUuidMapping)); + } + + UUID createDefaultNetworkVisualizationParameters(String userId, UserProfileInfos userProfileInfos) { + if (userProfileInfos != null && userProfileInfos.getNetworkVisualizationParameterId() != null) { + try { + return studyConfigService.duplicateNetworkVisualizationParameters(userProfileInfos.getNetworkVisualizationParameterId()); + } catch (Exception e) { + LOGGER.error(String.format("Could not duplicate network visualization parameters with id '%s' from user/profile '%s/%s'. Using default parameters", + userProfileInfos.getNetworkVisualizationParameterId(), userId, userProfileInfos.getName()), e); + } + } + try { + return studyConfigService.createDefaultNetworkVisualizationParameters(); + } catch (final Exception e) { + LOGGER.error("Error while creating network visualization default parameters", e); + return null; + } + } + + UUID createDefaultSpreadsheetConfigCollection(String userId, UserProfileInfos userProfileInfos) { + if (userProfileInfos != null && userProfileInfos.getSpreadsheetConfigCollectionId() != null) { + try { + return studyConfigService.duplicateSpreadsheetConfigCollection(userProfileInfos.getSpreadsheetConfigCollectionId()); + } catch (Exception e) { + LOGGER.error(String.format("Could not duplicate spreadsheet config collection with id '%s' from user/profile '%s/%s'. Using default spreadsheet config collection", + userProfileInfos.getSpreadsheetConfigCollectionId(), userId, userProfileInfos.getName()), e); + } + } + try { + return studyConfigService.createDefaultSpreadsheetConfigCollection(); + } catch (final Exception e) { + LOGGER.error("Error while creating default spreadsheet config collection", e); + return null; + } + } + + UUID createWorkspacesConfig(UserProfileInfos userProfileInfos) { + try { + List workspaceIds = new ArrayList<>(); + if (userProfileInfos != null && userProfileInfos.getWorkspaceId() != null) { + workspaceIds.add(userProfileInfos.getWorkspaceId()); + workspaceIds.add(null); + workspaceIds.add(null); + } + return studyConfigService.createWorkspacesConfigFromWorkspaces(workspaceIds); + } catch (final Exception e) { + LOGGER.error("Error while creating workspace collection", e); + return null; + } + } } diff --git a/src/test/java/org/gridsuite/study/server/studycontroller/ImportStudyTest.java b/src/test/java/org/gridsuite/study/server/studycontroller/ImportStudyTest.java new file mode 100644 index 000000000..aa053894c --- /dev/null +++ b/src/test/java/org/gridsuite/study/server/studycontroller/ImportStudyTest.java @@ -0,0 +1,233 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.study.server.studycontroller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.powsybl.ws.commons.error.PowsyblWsProblemDetail; +import org.gridsuite.study.server.dto.CaseInfos; +import org.gridsuite.study.server.dto.RootNetworkLoadStatus; +import org.gridsuite.study.server.dto.studyexport.NodeTreeExportInfos; +import org.gridsuite.study.server.dto.studyexport.RootNetworkExportInfos; +import org.gridsuite.study.server.dto.studyexport.TreeExportInfos; +import org.gridsuite.study.server.networkmodificationtree.dto.AbstractNode; +import org.gridsuite.study.server.networkmodificationtree.dto.NetworkModificationNode; +import org.gridsuite.study.server.networkmodificationtree.dto.RootNode; +import org.gridsuite.study.server.notification.NotificationService; +import org.gridsuite.study.server.repository.rootnetwork.RootNetworkEntity; +import org.gridsuite.study.server.repository.rootnetwork.RootNetworkRequestRepository; +import org.gridsuite.study.server.utils.TestUtils; +import org.gridsuite.study.server.utils.wiremock.WireMockUtilsCriteria; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.Message; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MvcResult; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.gridsuite.study.server.StudyConstants.HEADER_USER_ID; +import static org.gridsuite.study.server.error.StudyBusinessErrorCode.BAD_NODE_TYPE; +import static org.gridsuite.study.server.error.StudyBusinessErrorCode.NOT_FOUND; +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Ghazwa Rehili + */ +class ImportStudyTest extends StudyTestBase { + + private static final String IMPORT_URL = "/v1/studies/import"; + private static final String USER_ID = "testUser"; + + @Autowired + private ObjectMapper objectMapper; + @Autowired + private RootNetworkRequestRepository rootNetworkRequestRepository; + + @Test + void testImportStudy() throws Exception { + UUID studyUuid = UUID.randomUUID(); + UUID caseUuid1 = UUID.randomUUID(); + UUID caseUuid2 = UUID.randomUUID(); + UUID duplicatedCaseUuid1 = UUID.randomUUID(); + UUID duplicatedCaseUuid2 = UUID.randomUUID(); + UUID modificationGroupUuid1 = UUID.randomUUID(); + UUID modificationGroupUuid2 = UUID.randomUUID(); + + stubDefaultParametersCreation(); + wireMockStubs.caseServer.stubDuplicateCaseWithBody(caseUuid1.toString(), objectMapper.writeValueAsString(duplicatedCaseUuid1)); + wireMockStubs.caseServer.stubDuplicateCaseWithBody(caseUuid2.toString(), objectMapper.writeValueAsString(duplicatedCaseUuid2)); + UUID stubDuplicateModificationGroupId = wireMockStubs.stubDuplicateModificationGroup(); + + NodeTreeExportInfos nodeTree = new NodeTreeExportInfos("Root", "ROOT", null, null, List.of( + new NodeTreeExportInfos("N1", "NETWORK_MODIFICATION", modificationGroupUuid1, "SECURITY", List.of( + new NodeTreeExportInfos("N2", "NETWORK_MODIFICATION", modificationGroupUuid2, "CONSTRUCTION", List.of()) + )) + )); + TreeExportInfos treeExportInfos = new TreeExportInfos(studyUuid, List.of( + rootNetworkExportInfos("rn1", "1", 0, caseUuid1), + rootNetworkExportInfos("rn2", "2", 1, caseUuid2) + ), nodeTree); + + mockMvc.perform(post(IMPORT_URL).header(HEADER_USER_ID, USER_ID) + .contentType("application/json") + .content(objectMapper.writeValueAsString(treeExportInfos))) + .andExpect(status().isOk()); + + // Import is fully synchronous, the only notification sent is the study creation finished one + Message message = TestUtils.receiveStudyUpdate(output, studyUpdateDestination); + assertNotNull(message); + assertEquals(studyUuid, message.getHeaders().get(NotificationService.HEADER_STUDY_UUID)); + assertEquals(USER_ID, message.getHeaders().get(HEADER_USER_ID)); + assertEquals(NotificationService.UPDATE_TYPE_STUDY_CREATION_FINISHED, message.getHeaders().get(NotificationService.HEADER_UPDATE_TYPE)); + assertNull(output.receive(TIMEOUT, studyUpdateDestination)); + assertNull(output.receive(TIMEOUT, elementUpdateDestination)); + + assertTrue(studyRepository.findById(studyUuid).isPresent()); + RootNode rootNode = networkModificationTreeService.getStudyTree(studyUuid, null); + assertNotNull(rootNode); + assertEquals(1, rootNode.getChildren().size()); + AbstractNode n1 = rootNode.getChildren().getFirst(); + assertEquals("N1", n1.getName()); + assertInstanceOf(NetworkModificationNode.class, n1); + assertEquals("SECURITY", ((NetworkModificationNode) n1).getNodeType().name()); + assertEquals(1, n1.getChildren().size()); + AbstractNode n2 = n1.getChildren().getFirst(); + assertEquals("N2", n2.getName()); + assertEquals("CONSTRUCTION", ((NetworkModificationNode) n2).getNodeType().name()); + assertNotEquals(modificationGroupUuid1, ((NetworkModificationNode) n1).getModificationGroupUuid()); + assertNotEquals(modificationGroupUuid2, ((NetworkModificationNode) n2).getModificationGroupUuid()); + wireMockStubs.verifyDuplicateModificationGroup(stubDuplicateModificationGroupId, 2); + + // Root networks are created directly synchronously + assertEquals(0, rootNetworkRequestRepository.countAllByStudyUuid(studyUuid)); + List rootNetworks = rootNetworkRepository.findAllByStudyId(studyUuid); + assertEquals(2, rootNetworks.size()); + + RootNetworkEntity rn1 = rootNetworkRepository.findByNameAndStudyId("rn1", studyUuid).orElseThrow(); + assertEquals("1", rn1.getTag()); + assertEquals(duplicatedCaseUuid1, rn1.getCaseUuid()); + assertNull(rn1.getOriginalCaseUuid()); + assertEquals(RootNetworkLoadStatus.UNLOADED, rn1.getLoadStatus()); + // Network is not actually imported during a study import: networkInfos is only a placeholder, + // the real network will be loaded later on demand to recreate network + assertNotNull(rn1.getNetworkUuid()); + assertEquals("", rn1.getNetworkId()); + + RootNetworkEntity rn2 = rootNetworkRepository.findByNameAndStudyId("rn2", studyUuid).orElseThrow(); + assertEquals("2", rn2.getTag()); + assertEquals(duplicatedCaseUuid2, rn2.getCaseUuid()); + assertNull(rn2.getOriginalCaseUuid()); + assertEquals(RootNetworkLoadStatus.UNLOADED, rn2.getLoadStatus()); + + verifyDuplicateCaseRequest(caseUuid1); + verifyDuplicateCaseRequest(caseUuid2); + verifyDefaultParametersCreation(); + } + + @Test + void testImportStudyWithModificationGroupOnFailure() throws Exception { + UUID studyUuid = UUID.randomUUID(); + UUID caseUuid = UUID.randomUUID(); + UUID modificationGroupUuid1 = UUID.randomUUID(); + UUID modificationGroupUuid2 = UUID.randomUUID(); + + UUID stubDuplicateModificationGroupId = wireMockStubs.stubDuplicateModificationGroup(); + UUID stubDeleteGroupId = wireMockStubs.stubNetworkModificationDeleteGroup(); + + NodeTreeExportInfos nodeTree = new NodeTreeExportInfos("Root", "ROOT", null, null, List.of( + new NodeTreeExportInfos("N1", "NETWORK_MODIFICATION", modificationGroupUuid1, "SECURITY", List.of( + new NodeTreeExportInfos("N2", "NETWORK_MODIFICATION", modificationGroupUuid2, null, List.of()) + )) + )); + TreeExportInfos treeExportInfos = new TreeExportInfos(studyUuid, List.of( + rootNetworkExportInfos("rn1", "1", 0, caseUuid) + ), nodeTree); + + MvcResult result = mockMvc.perform(post(IMPORT_URL).header(HEADER_USER_ID, USER_ID) + .contentType("application/json") + .content(objectMapper.writeValueAsString(treeExportInfos))) + .andExpect(status().isForbidden()) + .andReturn(); + PowsyblWsProblemDetail problemDetail = objectMapper.readValue(result.getResponse().getContentAsString(), PowsyblWsProblemDetail.class); + assertEquals(BAD_NODE_TYPE.value(), problemDetail.getBusinessErrorCode()); + wireMockStubs.verifyDuplicateModificationGroup(stubDuplicateModificationGroupId, 1); + wireMockStubs.verifyNetworkModificationDeleteGroup(stubDeleteGroupId, false); + + assertTrue(studyRepository.findById(studyUuid).isEmpty()); + assertEquals(0, rootNetworkRequestRepository.countAllByStudyUuid(studyUuid)); + } + + @Test + void testImportStudyWithInvalidNodeType() throws Exception { + UUID studyUuid = UUID.randomUUID(); + UUID caseUuid = UUID.randomUUID(); + UUID modificationGroupUuid = UUID.randomUUID(); + + NodeTreeExportInfos nodeTree = new NodeTreeExportInfos("Root", "ROOT", null, null, List.of( + new NodeTreeExportInfos("N1", "NETWORK_MODIFICATION", modificationGroupUuid, null, List.of()) + )); + TreeExportInfos treeExportInfos = new TreeExportInfos(studyUuid, List.of( + rootNetworkExportInfos("rn1", "1", 0, caseUuid) + ), nodeTree); + + MvcResult result = mockMvc.perform(post(IMPORT_URL).header(HEADER_USER_ID, USER_ID) + .contentType("application/json") + .content(objectMapper.writeValueAsString(treeExportInfos))) + .andExpect(status().isForbidden()) + .andReturn(); + PowsyblWsProblemDetail problemDetail = objectMapper.readValue(result.getResponse().getContentAsString(), PowsyblWsProblemDetail.class); + assertEquals(BAD_NODE_TYPE.value(), problemDetail.getBusinessErrorCode()); + + assertTrue(studyRepository.findById(studyUuid).isEmpty()); + assertEquals(0, rootNetworkRequestRepository.countAllByStudyUuid(studyUuid)); + wireMockServer.verify(0, WireMock.postRequestedFor(WireMock.urlPathEqualTo("/v1/cases/" + caseUuid + "/duplicate"))); + wireMockServer.verify(0, WireMock.getRequestedFor(WireMock.urlPathEqualTo("/v1/cases/" + caseUuid + "/exists"))); + wireMockServer.verify(0, WireMock.postRequestedFor(WireMock.urlPathMatching("/v1/groups/.*/duplicate"))); + wireMockServer.verify(0, WireMock.getRequestedFor(WireMock.urlPathEqualTo("/v1/users/" + USER_ID + "/profile"))); + } + + @Test + void testImportStudyWithNoRootNetworks() throws Exception { + UUID studyUuid = UUID.randomUUID(); + TreeExportInfos treeExportInfos = new TreeExportInfos(studyUuid, List.of(), new NodeTreeExportInfos("Root", "ROOT", null, null, List.of())); + + MvcResult result = mockMvc.perform(post(IMPORT_URL).header(HEADER_USER_ID, USER_ID) + .contentType("application/json") + .content(objectMapper.writeValueAsString(treeExportInfos))) + .andExpect(status().isNotFound()) + .andReturn(); + PowsyblWsProblemDetail problemDetail = objectMapper.readValue(result.getResponse().getContentAsString(), PowsyblWsProblemDetail.class); + assertEquals(NOT_FOUND.value(), problemDetail.getBusinessErrorCode()); + + assertTrue(studyRepository.findById(studyUuid).isEmpty()); + } + + private RootNetworkExportInfos rootNetworkExportInfos(String name, String tag, int index, UUID caseUuid) { + return new RootNetworkExportInfos(name, tag, index, new CaseInfos(caseUuid, null, "caseName", "UCTE"), Map.of()); + } + + private void stubDefaultParametersCreation() throws Exception { + ReflectionTestUtils.setField(caseService, "caseServerBaseUri", wireMockServer.baseUrl()); + wireMockStubs.userAdminServer.stubGetUserProfile(USER_ID); + setupCreateParametersStubs(); + } + + private void verifyDefaultParametersCreation() { + wireMockStubs.userAdminServer.verifyGetUserProfile(USER_ID); + verifyCreateParameters(1, 9, 1, 1, 1); + } + + private void verifyDuplicateCaseRequest(UUID caseUuid) { + WireMockUtilsCriteria.verifyPostRequest(wireMockServer, "/v1/cases/" + caseUuid + "/duplicate", + Map.of("withExpiration", WireMock.matching(".*"))); + } +} diff --git a/src/test/java/org/gridsuite/study/server/studycontroller/TreeExportTest.java b/src/test/java/org/gridsuite/study/server/studycontroller/TreeExportTest.java index b7e70195c..1a1b2191e 100644 --- a/src/test/java/org/gridsuite/study/server/studycontroller/TreeExportTest.java +++ b/src/test/java/org/gridsuite/study/server/studycontroller/TreeExportTest.java @@ -20,7 +20,9 @@ import org.springframework.test.web.servlet.MvcResult; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -29,6 +31,7 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; +import java.util.zip.GZIPOutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -105,6 +108,41 @@ void testExportStudy() throws Exception { wireMockStubs.directoryServer.verifyCheckPermission(List.of(studyUuid), null, PermissionType.READ, false); } + @Test + void testExportStudyDecompressesGzipCaseContent() throws Exception { + UUID studyUuid = createStudyWithStubs("testUser", CASE_UUID); + ReflectionTestUtils.setField(caseService, "caseServerBaseUri", wireMockServer.baseUrl()); + wireMockStubs.directoryServer.stubCheckPermission(List.of(studyUuid), null, "testUser", PermissionType.READ, false, HttpStatus.OK.value()); + String caseContent = "dummy case content"; + ByteArrayOutputStream gzipped = new ByteArrayOutputStream(); + try (GZIPOutputStream gzipOut = new GZIPOutputStream(gzipped)) { + gzipOut.write(caseContent.getBytes(StandardCharsets.UTF_8)); + } + wireMockServer.stubFor(WireMock.get(WireMock.urlPathEqualTo("/v1/cases/" + CASE_UUID)) + .willReturn(WireMock.aResponse().withStatus(200) + .withHeader("Content-Type", "application/octet-stream") + .withHeader("Content-Encoding", "gzip") + .withBody(gzipped.toByteArray()))); + + MvcResult result = mockMvc.perform(get("/v1/studies/{studyUuid}/export/{studyName}", studyUuid, "studyName").header(HEADER_USER_ID, "testUser")) + .andExpect(status().isOk()) + .andReturn(); + + byte[] archiveContent = result.getResponse().getContentAsByteArray(); + String extractedCaseContent = null; + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(archiveContent))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.getName().startsWith("cases/")) { + extractedCaseContent = new String(zis.readAllBytes(), StandardCharsets.UTF_8); + } + } + } + assertEquals(caseContent, extractedCaseContent); + WireMockUtilsCriteria.verifyGetRequest(wireMockServer, "/v1/cases/" + CASE_UUID, false, Map.of(), 1); + wireMockStubs.directoryServer.verifyCheckPermission(List.of(studyUuid), null, PermissionType.READ, false); + } + @Test void testExportStudyFailNoPermission() throws Exception { UUID studyUuid = createStudyWithStubs("testUser", CASE_UUID);