diff --git a/pom.xml b/pom.xml index b851df1c00..0e2a545105 100644 --- a/pom.xml +++ b/pom.xml @@ -44,9 +44,10 @@ 43.0.0 + 1.12.0 org.gridsuite.study.server 5.0.0-alpha.14 - 1.0.5 + 1.0.5 gridsuite org.gridsuite:study-server @@ -144,6 +145,11 @@ com.powsybl powsybl-ws-commons + + org.gridsuite + gridsuite-filter + ${gridsuite-filter.version} + io.projectreactor reactor-core diff --git a/src/main/java/org/gridsuite/study/server/StudyConstants.java b/src/main/java/org/gridsuite/study/server/StudyConstants.java index 82046a6397..983c242584 100644 --- a/src/main/java/org/gridsuite/study/server/StudyConstants.java +++ b/src/main/java/org/gridsuite/study/server/StudyConstants.java @@ -46,6 +46,7 @@ private StudyConstants() { public static final String QUERY_PARAM_VARIANT_ID = "variantId"; public static final String QUERY_PARAM_NETWORK_UUID = "networkUuid"; public static final String QUERY_PARAM_EQUIPMENT_TYPE = "equipmentType"; + public static final String QUERY_PARAM_EQUIPMENT_TYPES = "equipmentTypes"; public static final String QUERY_PARAM_ELEMENT_TYPE = "elementType"; public static final String QUERY_PARAM_SIDE = "side"; public static final String QUERY_PARAM_NOMINAL_VOLTAGES = "nominalVoltages"; 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 b56f32837c..2af0a6f12a 100644 --- a/src/main/java/org/gridsuite/study/server/controller/StudyController.java +++ b/src/main/java/org/gridsuite/study/server/controller/StudyController.java @@ -16,6 +16,8 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; +import org.gridsuite.filter.globalfilter.GlobalFilter; +import org.gridsuite.filter.utils.EquipmentType; import org.gridsuite.study.server.StudyApi; import org.gridsuite.study.server.StudyConstants.ModificationsActionType; import org.gridsuite.study.server.StudyConstants.SldDisplayMode; @@ -54,6 +56,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.lang.NonNull; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; @@ -2256,6 +2259,20 @@ public ResponseEntity evaluateFilter( return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(studyService.evaluateFilter(nodeUuid, rootNetworkUuid, inUpstreamBuiltParentNode, filter)); } + @PostMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/global-filter/evaluate", + produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Evaluate a global filter to get matched elements") + @ApiResponse(responseCode = "200", description = "The list of matched elements") + public ResponseEntity> evaluateGlobalFilter( + @Parameter(description = "Study uuid") @PathVariable("studyUuid") UUID studyUuid, + @Parameter(description = "Root network uuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "Node uuid") @PathVariable("nodeUuid") UUID nodeUuid, + @Parameter(description = "The equipments types to filter and return") @RequestParam(name = "equipmentTypes") @NonNull final List equipmentTypes, + @RequestBody @NonNull GlobalFilter filter) { + this.studyService.assertIsRootNetworkAndNodeInStudy(studyUuid, rootNetworkUuid, nodeUuid); + return ResponseEntity.ok(studyService.evaluateGlobalFilter(nodeUuid, rootNetworkUuid, equipmentTypes, filter)); + } + @GetMapping(value = "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/filters/{filterUuid}/elements") @Operation(summary = "Evaluate a filter on root node to get matched elements") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The list of matched elements")}) diff --git a/src/main/java/org/gridsuite/study/server/service/FilterService.java b/src/main/java/org/gridsuite/study/server/service/FilterService.java index f93a61a927..bb35a77d31 100644 --- a/src/main/java/org/gridsuite/study/server/service/FilterService.java +++ b/src/main/java/org/gridsuite/study/server/service/FilterService.java @@ -7,16 +7,20 @@ package org.gridsuite.study.server.service; +import lombok.Getter; +import lombok.NonNull; +import org.apache.commons.lang3.StringUtils; +import org.gridsuite.filter.globalfilter.GlobalFilter; +import org.gridsuite.filter.utils.EquipmentType; import org.gridsuite.study.server.RemoteServicesProperties; import org.gridsuite.study.server.StudyException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; import java.util.List; @@ -25,6 +29,10 @@ import static org.gridsuite.study.server.StudyConstants.DELIMITER; import static org.gridsuite.study.server.StudyConstants.FILTER_API_VERSION; +import static org.gridsuite.study.server.StudyConstants.IDS; +import static org.gridsuite.study.server.StudyConstants.NETWORK_UUID; +import static org.gridsuite.study.server.StudyConstants.QUERY_PARAM_EQUIPMENT_TYPES; +import static org.gridsuite.study.server.StudyConstants.QUERY_PARAM_VARIANT_ID; import static org.gridsuite.study.server.StudyException.Type.EVALUATE_FILTER_FAILED; import static org.gridsuite.study.server.StudyException.Type.NETWORK_NOT_FOUND; import static org.gridsuite.study.server.utils.StudyUtils.handleHttpError; @@ -42,13 +50,9 @@ public class FilterService { private final RestTemplate restTemplate; + @Getter // getter to facilitate to mock private final String baseUri; - // getter to facilitate to mock - public String getBaseUri() { - return baseUri; - } - @Autowired public FilterService(RemoteServicesProperties remoteServicesProperties, RestTemplate restTemplate) { this.baseUri = remoteServicesProperties.getServiceUri("filter-server"); @@ -60,12 +64,11 @@ public String evaluateFilter(UUID networkUuid, String variantId, String filter) String endPointUrl = getBaseUri() + DELIMITER + FILTER_API_VERSION + FILTER_END_POINT_EVALUATE; UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(endPointUrl); - uriComponentsBuilder.queryParam("networkUuid", networkUuid); - if (variantId != null && !variantId.isBlank()) { - uriComponentsBuilder.queryParam("variantId", variantId); + uriComponentsBuilder.queryParam(NETWORK_UUID, networkUuid); + if (!StringUtils.isBlank(variantId)) { + uriComponentsBuilder.queryParam(QUERY_PARAM_VARIANT_ID, variantId); } - var uriComponent = uriComponentsBuilder - .build(); + var uriComponent = uriComponentsBuilder.build(); var headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); @@ -83,13 +86,35 @@ public String evaluateFilter(UUID networkUuid, String variantId, String filter) } } + public List evaluateGlobalFilter(@NonNull final UUID networkUuid, @NonNull final String variantId, + @NonNull final List equipmentTypes, @NonNull final GlobalFilter filter) { + final UriComponents uriComponent = UriComponentsBuilder.fromHttpUrl(getBaseUri()) + .pathSegment(FILTER_API_VERSION, "global-filter") + .queryParam(NETWORK_UUID, networkUuid) + .queryParam(QUERY_PARAM_VARIANT_ID, variantId) + .queryParam(QUERY_PARAM_EQUIPMENT_TYPES, equipmentTypes) + .build(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + try { + return restTemplate.exchange(uriComponent.toUri(), HttpMethod.POST, new HttpEntity<>(filter, headers), new ParameterizedTypeReference>() { }) + .getBody(); + } catch (final HttpStatusCodeException ex) { + if (HttpStatus.NOT_FOUND.equals(ex.getStatusCode())) { + throw new StudyException(NETWORK_NOT_FOUND); + } else { + throw handleHttpError(ex, EVALUATE_FILTER_FAILED); + } + } + } + public String exportFilter(UUID networkUuid, UUID filterUuid) { Objects.requireNonNull(networkUuid); Objects.requireNonNull(filterUuid); String endPointUrl = getBaseUri() + DELIMITER + FILTER_API_VERSION + FILTER_END_POINT_EXPORT; UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(endPointUrl); - uriComponentsBuilder.queryParam("networkUuid", networkUuid); + uriComponentsBuilder.queryParam(NETWORK_UUID, networkUuid); var uriComponent = uriComponentsBuilder.buildAndExpand(filterUuid); return restTemplate.getForObject(uriComponent.toUriString(), String.class); @@ -101,11 +126,11 @@ public String exportFilters(UUID networkUuid, List filtersUuid, String var String endPointUrl = getBaseUri() + DELIMITER + FILTER_API_VERSION + FILTERS_END_POINT_EXPORT; UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(endPointUrl); - uriComponentsBuilder.queryParam("networkUuid", networkUuid); - if (variantId != null && !variantId.isBlank()) { - uriComponentsBuilder.queryParam("variantId", variantId); + uriComponentsBuilder.queryParam(NETWORK_UUID, networkUuid); + if (!StringUtils.isBlank(variantId)) { + uriComponentsBuilder.queryParam(QUERY_PARAM_VARIANT_ID, variantId); } - uriComponentsBuilder.queryParam("ids", filtersUuid); + uriComponentsBuilder.queryParam(IDS, filtersUuid); var uriComponent = uriComponentsBuilder.buildAndExpand(); return restTemplate.getForObject(uriComponent.toUriString(), String.class); @@ -117,7 +142,7 @@ public String evaluateFilters(UUID networkUuid, String filters) { String endPointUrl = getBaseUri() + DELIMITER + FILTER_API_VERSION + FILTER_END_POINT_EVALUATE_IDS; UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(endPointUrl); - uriComponentsBuilder.queryParam("networkUuid", networkUuid); + uriComponentsBuilder.queryParam(NETWORK_UUID, networkUuid); var uriComponent = uriComponentsBuilder.buildAndExpand(); var headers = new HttpHeaders(); 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 ec75331e2f..78fc03dd0b 100644 --- a/src/main/java/org/gridsuite/study/server/service/StudyService.java +++ b/src/main/java/org/gridsuite/study/server/service/StudyService.java @@ -14,6 +14,8 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.NonNull; import org.apache.commons.collections4.CollectionUtils; +import org.gridsuite.filter.globalfilter.GlobalFilter; +import org.gridsuite.filter.utils.EquipmentType; import org.gridsuite.study.server.StudyConstants; import org.gridsuite.study.server.StudyException; import org.gridsuite.study.server.dto.*; @@ -296,6 +298,14 @@ public BasicStudyInfos createStudy(UUID caseUuid, String userId, UUID studyUuid, return basicStudyInfos; } + @Transactional(readOnly = true) + public void assertIsRootNetworkAndNodeInStudy(@NonNull final UUID studyUuid, @NonNull final UUID rootNetworkId, @NonNull final UUID nodeUuid) { + this.rootNetworkService.assertIsRootNetworkInStudy(studyUuid, rootNetworkId); + if (!studyUuid.equals(this.networkModificationTreeService.getStudyUuidForNodeId(nodeUuid))) { + throw new StudyException(NODE_NOT_FOUND); + } + } + @Transactional public void deleteRootNetworks(UUID studyUuid, List rootNetworksUuids) { assertIsStudyExist(studyUuid); @@ -3202,6 +3212,17 @@ public String evaluateFilter(UUID nodeUuid, UUID rootNetworkUuid, boolean inUpst return filterService.evaluateFilter(rootNetworkService.getNetworkUuid(rootNetworkUuid), networkModificationTreeService.getVariantId(nodeUuidToSearchIn, rootNetworkUuid), filter); } + @Transactional(readOnly = true) + public List evaluateGlobalFilter(@NonNull final UUID nodeUuid, @NonNull final UUID rootNetworkUuid, + @NonNull final List equipmentTypes, @NonNull final GlobalFilter filter) { + return filterService.evaluateGlobalFilter( + rootNetworkService.getNetworkUuid(rootNetworkUuid), + networkModificationTreeService.getVariantId(getNodeUuidToSearchIn(nodeUuid, rootNetworkUuid, true), rootNetworkUuid), + equipmentTypes, + filter + ); + } + @Transactional(readOnly = true) public String exportFilter(UUID rootNetworkUuid, UUID filterUuid) { return filterService.exportFilter(rootNetworkService.getNetworkUuid(rootNetworkUuid), filterUuid); diff --git a/src/test/java/org/gridsuite/study/server/FilterServiceTest.java b/src/test/java/org/gridsuite/study/server/FilterServiceTest.java index 0eceb20b81..3b95d56885 100644 --- a/src/test/java/org/gridsuite/study/server/FilterServiceTest.java +++ b/src/test/java/org/gridsuite/study/server/FilterServiceTest.java @@ -10,6 +10,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.github.tomakehurst.wiremock.WireMockServer; import com.powsybl.commons.exceptions.UncheckedInterruptedException; +import org.gridsuite.filter.utils.EquipmentType; +import org.gridsuite.study.server.networkmodificationtree.dto.InsertMode; +import org.gridsuite.study.server.networkmodificationtree.dto.NetworkModificationNode; import org.gridsuite.study.server.networkmodificationtree.dto.RootNode; import org.gridsuite.study.server.repository.StudyEntity; import org.gridsuite.study.server.repository.StudyRepository; @@ -38,6 +41,7 @@ import java.util.stream.Collectors; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.gridsuite.study.server.utils.TestUtils.createModificationNodeInfo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -58,6 +62,7 @@ class FilterServiceTest { private static final String FILTER_UUID_STRING = "c6c15d08-81e9-47a1-9cdb-7be22f017ad5"; private static final List FILTERS_UUID_STRING = List.of("fc3aa057-5fa4-4173-b1a8-16028f5eefd1", "f8773f32-f77c-4126-8c6f-a4af8bf6f788"); private static final UUID CASE_UUID = UUID.fromString(CASE_UUID_STRING); + private static final String NODE_1_NAME = "node1"; @Autowired private MockMvc mockMvc; @@ -126,6 +131,7 @@ void testEvaluateFilter() throws Exception { UUID studyNameUserIdUuid = studyEntity.getId(); UUID firstRootNetworkUuid = studyTestUtils.getOneRootNetworkUuid(studyNameUserIdUuid); UUID rootNodeUuid = getRootNode(studyNameUserIdUuid).getId(); + NetworkModificationNode firstNode = networkModificationTreeService.createNode(studyEntity, rootNodeUuid, createModificationNodeInfo(NODE_1_NAME), InsertMode.AFTER, null); // whatever string is allowed but given here a json string for more expressive final String sendBody = """ @@ -165,6 +171,49 @@ void testEvaluateFilter() throws Exception { assertEquals(responseBody, resultAsString); wireMockUtils.verifyFilterEvaluate(stubUuid, NETWORK_UUID_STRING); + + // evaluate on first node + mvcResult = mockMvc.perform(post("/v1/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/filters/evaluate", + studyNameUserIdUuid, firstRootNetworkUuid, firstNode.getId()) + .content(sendBody).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + resultAsString = mvcResult.getResponse().getContentAsString(); + assertEquals(responseBody, resultAsString); + + wireMockUtils.verifyFilterEvaluate(stubUuid, NETWORK_UUID_STRING); + } + + @Test + void testEvaluateGlobalFilter() throws Exception { + StudyEntity studyEntity = insertDummyStudy(UUID.fromString(NETWORK_UUID_STRING), CASE_UUID); + UUID studyNameUserIdUuid = studyEntity.getId(); + UUID firstRootNetworkUuid = studyTestUtils.getOneRootNetworkUuid(studyNameUserIdUuid); + UUID rootNodeUuid = getRootNode(studyNameUserIdUuid).getId(); + + // whatever string is allowed but given here a json string for more expressive + final String sendBody = """ + { + "nominalV" : ["380", "225"], + "countryCode": ["FR", "BE"], + "genericFilter": ["c6c15d08-81e9-47a1-9cdb-7be22f017ad5"], + "substationProperty": {} + } + """; + + String responseBody = "[\"GEN\"]"; + + UUID stubUuid = wireMockUtils.stubGlobalFilterEvaluate(NETWORK_UUID_STRING, List.of(EquipmentType.GENERATOR), responseBody); + + MvcResult mvcResult = mockMvc.perform(post("/v1/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/global-filter/evaluate?equipmentTypes=GENERATOR", + studyNameUserIdUuid, firstRootNetworkUuid, rootNodeUuid) + .content(sendBody).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + String resultAsString = mvcResult.getResponse().getContentAsString(); + assertEquals(responseBody, resultAsString); + + wireMockUtils.verifyGlobalFilterEvaluate(stubUuid, NETWORK_UUID_STRING, List.of(EquipmentType.GENERATOR)); } @Test @@ -330,6 +379,8 @@ void testExportFilters() throws Exception { UUID firstRootNetworkUuid = studyTestUtils.getOneRootNetworkUuid(studyEntity.getId()); UUID studyUuid = studyEntity.getId(); UUID rootNodeUuid = getRootNode(studyUuid).getId(); + NetworkModificationNode firstNode = networkModificationTreeService.createNode(studyEntity, rootNodeUuid, createModificationNodeInfo(NODE_1_NAME), InsertMode.AFTER, null); + String responseBody = """ [ { @@ -371,5 +422,15 @@ void testExportFilters() throws Exception { assertEquals(responseBody, resultAsString); wireMockUtils.verifyFiltersExport(stubUuid, FILTERS_UUID_STRING, NETWORK_UUID_STRING); + + // export on first node + mvcResult = mockMvc.perform(get("/v1/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/filters/elements?filtersUuid=" + FILTERS_UUID_STRING.stream().collect(Collectors.joining(",")), + studyUuid, firstRootNetworkUuid, firstNode.getId())) + .andExpect(status().isOk()) + .andReturn(); + resultAsString = mvcResult.getResponse().getContentAsString(); + assertEquals(responseBody, resultAsString); + + wireMockUtils.verifyFiltersExport(stubUuid, FILTERS_UUID_STRING, NETWORK_UUID_STRING); } } diff --git a/src/test/java/org/gridsuite/study/server/utils/WireMockUtils.java b/src/test/java/org/gridsuite/study/server/utils/WireMockUtils.java index 81cc3aa773..1d162b8dce 100644 --- a/src/test/java/org/gridsuite/study/server/utils/WireMockUtils.java +++ b/src/test/java/org/gridsuite/study/server/utils/WireMockUtils.java @@ -15,6 +15,7 @@ import com.github.tomakehurst.wiremock.matching.StringValuePattern; import com.github.tomakehurst.wiremock.stubbing.ServeEvent; import com.powsybl.iidm.network.TwoSides; +import org.gridsuite.filter.utils.EquipmentType; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -524,6 +525,15 @@ public UUID stubFilterEvaluate(String networkUuid, String responseBody) { ).getId(); } + public UUID stubGlobalFilterEvaluate(String networkUuid, List equipmentTypes, String responseBody) { + return wireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/v1/global-filter")) + .withQueryParam(NETWORK_UUID, WireMock.equalTo(networkUuid)) + .withQueryParam(QUERY_PARAM_VARIANT_ID, WireMock.equalTo("")) + .withQueryParam(QUERY_PARAM_EQUIPMENT_TYPES, WireMock.equalTo(String.join(",", equipmentTypes.stream().map(EquipmentType::name).toList()))) + .willReturn(WireMock.ok().withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).withBody(responseBody)) + ).getId(); + } + public UUID stubFilterEvaluateNotFoundError(String networkUuid) { return wireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/v1/filters/evaluate")) .withQueryParam(NETWORK_UUID, WireMock.equalTo(networkUuid)) @@ -543,6 +553,13 @@ public void verifyFilterEvaluate(UUID stubUuid, String networkUuid) { Map.of(NETWORK_UUID, WireMock.equalTo(networkUuid))); } + public void verifyGlobalFilterEvaluate(UUID stubUuid, String networkUuid, List equipmentTypes) { + verifyPostRequest(stubUuid, "/v1/global-filter", + Map.of(NETWORK_UUID, WireMock.equalTo(networkUuid), + QUERY_PARAM_VARIANT_ID, WireMock.equalTo(""), + QUERY_PARAM_EQUIPMENT_TYPES, WireMock.equalTo(String.join(",", equipmentTypes.stream().map(EquipmentType::name).toList())))); + } + public UUID stubFilterExport(String networkUuid, String filterUuid, String responseBody) { return wireMock.stubFor(WireMock.get(WireMock.urlPathEqualTo("/v1/filters/" + filterUuid + "/export")) .withQueryParam(NETWORK_UUID, WireMock.equalTo(networkUuid))