diff --git a/src/main/java/org/gridsuite/study/server/service/StudyExportService.java b/src/main/java/org/gridsuite/study/server/service/StudyExportService.java index 39785ae8b6..4c174a2758 100644 --- a/src/main/java/org/gridsuite/study/server/service/StudyExportService.java +++ b/src/main/java/org/gridsuite/study/server/service/StudyExportService.java @@ -27,6 +27,7 @@ import java.nio.file.attribute.PosixFilePermissions; import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.stream.Stream; @@ -44,6 +45,7 @@ public class StudyExportService { private static final Logger LOGGER = LoggerFactory.getLogger(StudyExportService.class); public static final String TREE_JSON_FILE_NAME = "tree.json"; public static final String CASES_FOLDER = "cases"; + public static final String PARAMETERS_FOLDER = "parameters"; private final StudyService studyService; private final CaseService caseService; @@ -68,7 +70,7 @@ public InputStreamResource exportStudy(UUID studyUuid, String userId) { Path tempDir = createTempWorkDir(studyUuid); Path zipFile = null; try { - zipFile = compressStudyToZip(studyUuid, tempDir); + zipFile = compressStudyToZip(studyUuid, userId, tempDir); InputStream stream = Files.newInputStream(zipFile, StandardOpenOption.DELETE_ON_CLOSE); zipFile = null; return new InputStreamResource(stream); @@ -93,7 +95,7 @@ public InputStreamResource exportStudy(UUID studyUuid, String userId) { /** * Build tree.json and the case files under tempDir, then compress them into a temp zip file */ - private Path compressStudyToZip(UUID studyUuid, Path tempDir) throws IOException { + private Path compressStudyToZip(UUID studyUuid, String userId, Path tempDir) throws IOException { TreeExportInfos treeExportInfos = studyService.buildTreeExport(studyUuid); Path studyJsonPath = tempDir.resolve(TREE_JSON_FILE_NAME); objectMapper.writerWithDefaultPrettyPrinter().writeValue(studyJsonPath.toFile(), treeExportInfos); @@ -103,6 +105,7 @@ private Path compressStudyToZip(UUID studyUuid, Path tempDir) throws IOException String caseName = rootNetworkInfos.caseInfos().getCaseName(); exportCaseFile(caseUuid, caseName, casesDir); } + exportComputationParameters(studyUuid, userId, tempDir); Path zipFile = createTempExportFile(studyUuid); try (OutputStream fos = Files.newOutputStream(zipFile); ZipOutputStream zipOut = new ZipOutputStream(fos)) { @@ -111,6 +114,17 @@ private Path compressStudyToZip(UUID studyUuid, Path tempDir) throws IOException return zipFile; } + private void exportComputationParameters(UUID studyUuid, String userId, Path tempDir) throws IOException { + Map parametersByFileName = studyService.exportComputationParameters(studyUuid, userId); + if (parametersByFileName.isEmpty()) { + return; + } + Path parametersDir = Files.createDirectories(tempDir.resolve(PARAMETERS_FOLDER)); + for (Map.Entry entry : parametersByFileName.entrySet()) { + Files.writeString(parametersDir.resolve(entry.getKey()), entry.getValue()); + } + } + private Path createTempWorkDir(UUID studyUuid) { return createTempPath(studyUuid, "temp directory", "rwx------", attr -> Files.createTempDirectory("study-export-" + studyUuid, attr)); 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 8a3c31bd29..4210cd49f4 100644 --- a/src/main/java/org/gridsuite/study/server/service/StudyService.java +++ b/src/main/java/org/gridsuite/study/server/service/StudyService.java @@ -3041,6 +3041,11 @@ public TreeExportInfos buildTreeExport(UUID studyUuid) { return new TreeExportInfos(studyUuid, rootNetworks, nodeTree); } + public Map exportComputationParameters(UUID studyUuid, String userId) { + StudyEntity studyEntity = getStudy(studyUuid); + return computationParametersService.exportParameters(studyEntity, userId); + } + private RootNetworkExportInfos toRootNetworkExportInfos(RootNetworkInfos rootNetworkInfos, int index) { return new RootNetworkExportInfos( rootNetworkInfos.getName(), diff --git a/src/main/java/org/gridsuite/study/server/service/common/ComputationParametersService.java b/src/main/java/org/gridsuite/study/server/service/common/ComputationParametersService.java index 737d758a87..af097fb9ce 100644 --- a/src/main/java/org/gridsuite/study/server/service/common/ComputationParametersService.java +++ b/src/main/java/org/gridsuite/study/server/service/common/ComputationParametersService.java @@ -6,9 +6,12 @@ */ package org.gridsuite.study.server.service.common; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import org.gridsuite.study.server.dto.ComputationType; import org.gridsuite.study.server.dto.UserProfileInfos; import org.gridsuite.study.server.dto.computation.ComputationParameterUUIDs; +import org.gridsuite.study.server.error.StudyException; import org.gridsuite.study.server.repository.StudyEntity; import org.gridsuite.study.server.service.*; import org.gridsuite.study.server.service.dynamicmargincalculation.DynamicMarginCalculationRestService; @@ -25,11 +28,17 @@ import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.UUID; import java.util.function.BiConsumer; +import java.util.function.BiFunction; import java.util.function.Function; +import static org.gridsuite.study.server.error.StudyBusinessErrorCode.EXPORT_STUDY_ERROR; + /** * @author Abdelsalem HEDHILI */ @@ -40,15 +49,17 @@ public class ComputationParametersService { private static final Logger LOGGER = LoggerFactory.getLogger(ComputationParametersService.class); private final UserAdminService userAdminService; + private final ObjectMapper objectMapper; private final List computationParametersDefinitions; - // this is useful to avoid repetitive calls when doing operation on all computation types (duplicate, delete) + // this is useful to avoid repetitive calls when doing operation on all computation types (duplicate, delete, export) private record ComputationParametersDefinition( ComputationType type, Function studyParameterGetter, Function profileParameterGetter, ComputationParameters service, - BiConsumer parametersSetter + BiConsumer parametersSetter, + BiFunction parametersFetcher ) { } @@ -62,70 +73,82 @@ public ComputationParametersService(SecurityAnalysisRestService securityAnalysis DynamicMarginCalculationRestService dynamicMarginCalculationRestService, StateEstimationRestService stateEstimationService, PccMinRestService pccMinService, - UserAdminService userAdminService) { + UserAdminService userAdminService, + ObjectMapper objectMapper) { this.userAdminService = userAdminService; + this.objectMapper = objectMapper; this.computationParametersDefinitions = List.of( new ComputationParametersDefinition( ComputationType.LOAD_FLOW, StudyEntity::getLoadFlowParametersUuid, UserProfileInfos::getLoadFlowParameterId, loadFlowRestService, - ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::loadFlowParametersUuid), + ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::loadFlowParametersUuid, + (uuid, userId) -> loadFlowRestService.getParameters(uuid)), new ComputationParametersDefinition( ComputationType.SHORT_CIRCUIT, StudyEntity::getShortCircuitParametersUuid, UserProfileInfos::getShortcircuitParameterId, shortCircuitService, - ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::shortCircuitParametersUuid), + ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::shortCircuitParametersUuid, + (uuid, userId) -> shortCircuitService.getParameters(uuid)), new ComputationParametersDefinition( ComputationType.DYNAMIC_SIMULATION, StudyEntity::getDynamicSimulationParametersUuid, UserProfileInfos::getDynamicSimulationParameterId, dynamicSimulationRestService, - ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::dynamicSimulationParametersUuid), + ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::dynamicSimulationParametersUuid, + (uuid, userId) -> dynamicSimulationRestService.getParameters(uuid)), new ComputationParametersDefinition( ComputationType.VOLTAGE_INITIALIZATION, StudyEntity::getVoltageInitParametersUuid, UserProfileInfos::getVoltageInitParameterId, voltageInitService, - ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::voltageInitParametersUuid), + ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::voltageInitParametersUuid, + (uuid, userId) -> voltageInitService.getParameters(uuid)), new ComputationParametersDefinition( ComputationType.SECURITY_ANALYSIS, StudyEntity::getSecurityAnalysisParametersUuid, UserProfileInfos::getSecurityAnalysisParameterId, securityAnalysisService, - ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::securityAnalysisParametersUuid), + ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::securityAnalysisParametersUuid, + (uuid, userId) -> securityAnalysisService.getParameters(uuid)), new ComputationParametersDefinition( ComputationType.SENSITIVITY_ANALYSIS, StudyEntity::getSensitivityAnalysisParametersUuid, UserProfileInfos::getSensitivityAnalysisParameterId, sensitivityAnalysisService, - ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::sensitivityAnalysisParametersUuid), + ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::sensitivityAnalysisParametersUuid, + (uuid, userId) -> sensitivityAnalysisService.getParameters(uuid)), new ComputationParametersDefinition( ComputationType.DYNAMIC_SECURITY_ANALYSIS, StudyEntity::getDynamicSecurityAnalysisParametersUuid, UserProfileInfos::getDynamicSecurityAnalysisParameterId, dynamicSecurityAnalysisRestService, - ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::dynamicSecurityAnalysisParametersUuid), + ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::dynamicSecurityAnalysisParametersUuid, + (uuid, userId) -> dynamicSecurityAnalysisRestService.getParameters(uuid)), new ComputationParametersDefinition( ComputationType.DYNAMIC_MARGIN_CALCULATION, StudyEntity::getDynamicMarginCalculationParametersUuid, UserProfileInfos::getDynamicMarginCalculationParameterId, dynamicMarginCalculationRestService, - ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::dynamicMarginCalculationParametersUuid), + ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::dynamicMarginCalculationParametersUuid, + dynamicMarginCalculationRestService::getParameters), new ComputationParametersDefinition( ComputationType.STATE_ESTIMATION, StudyEntity::getStateEstimationParametersUuid, userProfileInfos -> null, stateEstimationService, - ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::stateEstimationParametersUuid), + ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::stateEstimationParametersUuid, + (uuid, userId) -> stateEstimationService.getStateEstimationParameters(uuid)), new ComputationParametersDefinition( ComputationType.PCC_MIN, StudyEntity::getPccMinParametersUuid, UserProfileInfos::getPccMinParameterId, pccMinService, - ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::pccMinParametersUuid) + ComputationParameterUUIDs.ComputationParameterUUIDsBuilder::pccMinParametersUuid, + (uuid, userId) -> pccMinService.getParameters(uuid)) ); } @@ -220,4 +243,34 @@ public void createOrUpdateParameters( } } + public Map exportParameters(StudyEntity studyEntity, String userId) { + Map parametersByFileName = new LinkedHashMap<>(); + computationParametersDefinitions.forEach(definition -> { + UUID parametersUuid = definition.studyParameterGetter().apply(studyEntity); + if (parametersUuid == null) { + return; + } + try { + Object parameters = definition.parametersFetcher().apply(parametersUuid, userId); + String fileName = toFileName(definition.type()); + parametersByFileName.put(fileName, parameters instanceof String json ? json : writeAsJson(fileName, parameters)); + } catch (Exception e) { + LOGGER.warn("Failed to fetch {} parameters (uuid={}) for study export", definition.type().getLabel(), parametersUuid, e); + } + }); + return parametersByFileName; + } + + private static String toFileName(ComputationType type) { + return type.name().toLowerCase(Locale.ROOT).replace('_', '-') + ".json"; + } + + private String writeAsJson(String fileName, Object parameters) { + try { + return objectMapper.writeValueAsString(parameters); + } catch (JsonProcessingException e) { + throw new StudyException(EXPORT_STUDY_ERROR, "Failed to serialize computation parameters " + fileName); + } + } + } 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 b7e70195c4..2785c8f5c5 100644 --- a/src/test/java/org/gridsuite/study/server/studycontroller/TreeExportTest.java +++ b/src/test/java/org/gridsuite/study/server/studycontroller/TreeExportTest.java @@ -63,6 +63,8 @@ void testExportStudy() throws Exception { .willReturn(WireMock.aResponse().withStatus(200) .withHeader("Content-Type", "application/octet-stream") .withBody("dummy case content".getBytes()))); + // Stub the computation parameters fetches + computationServerStubs.stubGetParametersAny("{}"); // Export as zip MvcResult result = mockMvc.perform(get("/v1/studies/{studyUuid}/export/{studyName}", studyUuid, "studyName").header(HEADER_USER_ID, "testUser")) .andExpect(status().isOk()) @@ -103,6 +105,8 @@ void testExportStudy() throws Exception { // Verify the case content download call WireMockUtilsCriteria.verifyGetRequest(wireMockServer, "/v1/cases/" + CASE_UUID, false, Map.of(), 1); wireMockStubs.directoryServer.verifyCheckPermission(List.of(studyUuid), null, PermissionType.READ, false); + // Verify the computation parameters fetches + computationServerStubs.verifyParametersGetAny(10); } @Test @@ -125,6 +129,7 @@ void testExportStudyFailToDeleteTempZipFile() throws Exception { wireMockServer.stubFor(WireMock.get(WireMock.urlPathEqualTo("/v1/cases/" + CASE_UUID)) .willReturn(WireMock.aResponse().withStatus(200).withHeader("Content-Type", "application/octet-stream") .withBody("dummy case content".getBytes()))); + computationServerStubs.stubGetParametersAny("{}"); // Capture the real zip file path as it is matched, so the test can clean it up itself: // the service's own Files.deleteIfExists call on this path is mocked to fail below. AtomicReference capturedZipFile = new AtomicReference<>(); @@ -156,5 +161,6 @@ void testExportStudyFailToDeleteTempZipFile() throws Exception { } WireMockUtilsCriteria.verifyGetRequest(wireMockServer, "/v1/cases/" + CASE_UUID, false, Map.of(), 1); wireMockStubs.directoryServer.verifyCheckPermission(List.of(studyUuid), null, PermissionType.READ, false); + computationServerStubs.verifyParametersGetAny(10); } } diff --git a/src/test/java/org/gridsuite/study/server/utils/wiremock/ComputationServerStubs.java b/src/test/java/org/gridsuite/study/server/utils/wiremock/ComputationServerStubs.java index 8588636775..a9fb47d85a 100644 --- a/src/test/java/org/gridsuite/study/server/utils/wiremock/ComputationServerStubs.java +++ b/src/test/java/org/gridsuite/study/server/utils/wiremock/ComputationServerStubs.java @@ -154,6 +154,15 @@ public void verifyParametersGet(String paramUuid) { WireMockUtilsCriteria.verifyGetRequest(wireMock, "/v1/parameters/" + paramUuid, Map.of()); } + public void stubGetParametersAny(String responseBody) { + wireMock.stubFor(WireMock.get(WireMock.urlPathMatching("/v1/parameters/.*")) + .willReturn(WireMock.ok().withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).withBody(responseBody))); + } + + public void verifyParametersGetAny(int nbRequests) { + WireMockUtilsCriteria.verifyGetRequest(wireMock, "/v1/parameters/.*", true, Map.of(), nbRequests); + } + public void stubCreateParameter(String responseBody) { wireMock.stubFor(WireMock.post(WireMock.urlEqualTo("/v1/parameters")) .atPriority(10)