diff --git a/pom.xml b/pom.xml index 073d7ab..e267412 100644 --- a/pom.xml +++ b/pom.xml @@ -107,6 +107,12 @@ + + org.gridsuite + gridsuite-filter + + 1.9.0-SNAPSHOT + com.fasterxml.jackson.core jackson-databind @@ -214,5 +220,10 @@ spring-boot-starter-test test + + org.wiremock + wiremock-jetty12 + test + diff --git a/src/main/java/org/gridsuite/actions/server/ContingencyListController.java b/src/main/java/org/gridsuite/actions/server/ContingencyListController.java index 8f229d7..36cbba3 100644 --- a/src/main/java/org/gridsuite/actions/server/ContingencyListController.java +++ b/src/main/java/org/gridsuite/actions/server/ContingencyListController.java @@ -149,6 +149,54 @@ public ResponseEntity modifyIdentifierContingencyList( } } + @PostMapping(value = "/filters-contingency-lists", consumes = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Create an filter base contingency list") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The filter based contingency list has been created successfully")}) + public ResponseEntity createFilterBasedContingencyList(@RequestParam(required = false, value = "id") UUID id, + @RequestBody FilterBasedContingencyList filterBasedContingencyList) { + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(service.createFilterBasedContingencyList(id, filterBasedContingencyList)); + } + + @PutMapping(value = "/filters-contingency-lists/{id}", consumes = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Modify a filter based contingency list") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The filter based contingency list have been modified successfully")}) + public ResponseEntity modifyFilterBasedContingencyList( + @PathVariable UUID id, + @RequestBody FilterBasedContingencyList contingencyList, + @RequestHeader("userId") String userId) { + try { + service.modifyFilterBasedContingencyList(id, contingencyList, userId); + return ResponseEntity.ok().build(); + } catch (EntityNotFoundException ignored) { + return ResponseEntity.notFound().build(); + } + } + + @PostMapping(value = "/filters-contingency-lists", params = "duplicateFrom") + @Operation(summary = "Create a filter based contingency list from existing one") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The filter based contingency list have been duplicated successfully"), + @ApiResponse(responseCode = "404", description = "Source filter based contingency list not found")}) + public ResponseEntity duplicateFilterBasedContingencyList(@RequestParam("duplicateFrom") UUID contingencyListsId) { + return service.duplicateFilterBasedContingencyList(contingencyListsId).map(contingencyList -> ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(contingencyList)) + .orElse(ResponseEntity.notFound().build()); + } + + @GetMapping(value = "/filters-contingency-lists/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Get filter based contingency list by id") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The filter based contingency list"), + @ApiResponse(responseCode = "404", description = "The filter based contingency list does not exists")}) + public ResponseEntity getFilterBasedContingencyList(@PathVariable("id") UUID id, + @RequestHeader("userId") String userId) { + return service.getFilterBasedContingencyList(id, userId).map(contingencyList -> ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(contingencyList)) + .orElse(ResponseEntity.notFound().build()); + } + @PostMapping(value = "/form-contingency-lists", params = "duplicateFrom") @Operation(summary = "Create a form contingency list from another existing one") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The form contingency list have been duplicated successfully"), diff --git a/src/main/java/org/gridsuite/actions/server/ContingencyListService.java b/src/main/java/org/gridsuite/actions/server/ContingencyListService.java index b07db64..9f166eb 100644 --- a/src/main/java/org/gridsuite/actions/server/ContingencyListService.java +++ b/src/main/java/org/gridsuite/actions/server/ContingencyListService.java @@ -7,8 +7,15 @@ package org.gridsuite.actions.server; import com.powsybl.commons.PowsyblException; +import com.powsybl.contingency.BatteryContingency; import com.powsybl.contingency.Contingency; import com.powsybl.contingency.ContingencyElement; +import com.powsybl.contingency.GeneratorContingency; +import com.powsybl.contingency.HvdcLineContingency; +import com.powsybl.contingency.LineContingency; +import com.powsybl.contingency.LoadContingency; +import com.powsybl.contingency.StaticVarCompensatorContingency; +import com.powsybl.contingency.TwoWindingsTransformerContingency; import com.powsybl.contingency.contingency.list.ContingencyList; import com.powsybl.contingency.contingency.list.IdentifierContingencyList; import com.powsybl.iidm.network.Connectable; @@ -22,9 +29,12 @@ import com.powsybl.network.store.iidm.impl.NetworkFactoryImpl; import org.gridsuite.actions.server.dto.*; import org.gridsuite.actions.server.entities.*; +import org.gridsuite.actions.server.repositories.FilterBasedContingencyListRepository; import org.gridsuite.actions.server.repositories.FormContingencyListRepository; import org.gridsuite.actions.server.repositories.IdBasedContingencyListRepository; +import org.gridsuite.actions.server.service.FilterService; import org.gridsuite.actions.server.utils.ContingencyListType; +import org.gridsuite.filter.identifierlistfilter.IdentifiableAttributes; import org.springframework.context.annotation.ComponentScan; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.http.HttpStatus; @@ -49,18 +59,26 @@ public class ContingencyListService { private final IdBasedContingencyListRepository idBasedContingencyListRepository; + private final FilterBasedContingencyListRepository filterBasedContingencyListRepository; + private final NetworkStoreService networkStoreService; private final NotificationService notificationService; + private final FilterService filterService; + public ContingencyListService(FormContingencyListRepository formContingencyListRepository, IdBasedContingencyListRepository idBasedContingencyListRepository, + FilterBasedContingencyListRepository filterBasedContingencyListRepository, NetworkStoreService networkStoreService, - NotificationService notificationService) { + NotificationService notificationService, + FilterService filterService) { this.formContingencyListRepository = formContingencyListRepository; this.idBasedContingencyListRepository = idBasedContingencyListRepository; + this.filterBasedContingencyListRepository = filterBasedContingencyListRepository; this.networkStoreService = networkStoreService; this.notificationService = notificationService; + this.filterService = filterService; } private static FormContingencyList fromFormContingencyListEntity(FormContingencyListEntity entity) { @@ -79,7 +97,9 @@ List getContingencyListsMetadata() { formContingencyListRepository.findAll().stream().map(formContingencyListEntity -> fromContingencyListEntity(formContingencyListEntity, ContingencyListType.FORM)), idBasedContingencyListRepository.findAll().stream().map(idBasedContingencyListEntity -> - fromContingencyListEntity(idBasedContingencyListEntity, ContingencyListType.IDENTIFIERS)) + fromContingencyListEntity(idBasedContingencyListEntity, ContingencyListType.IDENTIFIERS)), + filterBasedContingencyListRepository.findAll().stream().map(filterBasedContingencyListEntity -> + fromContingencyListEntity(filterBasedContingencyListEntity, ContingencyListType.FILTERS)) ).flatMap(Function.identity()).collect(Collectors.toList()); } @@ -88,7 +108,9 @@ List getContingencyListsMetadata(List ids) { formContingencyListRepository.findAllById(ids).stream().map(formContingencyListEntity -> fromContingencyListEntity(formContingencyListEntity, ContingencyListType.FORM)), idBasedContingencyListRepository.findAllById(ids).stream().map(idBasedContingencyListEntity -> - fromContingencyListEntity(idBasedContingencyListEntity, ContingencyListType.IDENTIFIERS)) + fromContingencyListEntity(idBasedContingencyListEntity, ContingencyListType.IDENTIFIERS)), + filterBasedContingencyListRepository.findAllById(ids).stream().map(filterBasedContingencyListEntity -> + fromContingencyListEntity(filterBasedContingencyListEntity, ContingencyListType.FILTERS)) ).flatMap(Function.identity()).collect(Collectors.toList()); } @@ -105,6 +127,10 @@ private Optional doGetFormContingencyListWithPreFetch }); } + public List evaluateFiltersNetwork(UUID networkUuid, String variantUuid, List filtersUuid) { + return filterService.evaluateFilters(networkUuid, variantUuid, filtersUuid); + } + @Transactional(readOnly = true) public Optional getFormContingencyList(UUID id) { return doGetFormContingencyList(id); @@ -125,8 +151,71 @@ private Optional doGetIdBasedContingencyList(UUID id, return idBasedContingencyListRepository.findById(id).map(idBasedContingencyListEntity -> fromIdBasedContingencyListEntity(idBasedContingencyListEntity, network)); } - private List getPowsyblContingencies(PersistentContingencyList contingencyList, Network network) { - ContingencyList powsyblContingencyList = contingencyList.toPowsyblContingencyList(network); + @Transactional + public Optional getFilterBasedContingencyList(UUID id, String userId) { + Optional entity = doGetFilterBasedContingencyListEntity(id); + if (entity.isEmpty()) { + return Optional.empty(); + } else { + List filterIds = entity.get().getFiltersIds(); + //get information from filterServer + List attributes = filterService.getFiltersAttributes(filterIds, userId); + return Optional.of(new FilterBasedContingencyList(entity.get().getId(), entity.get().getModificationDate(), attributes)); + } + } + + private Optional doGetFilterBasedContingencyList(UUID id) { + Objects.requireNonNull(id); + return filterBasedContingencyListRepository.findById(id).map(ContingencyListService::fromFilterBasedContingencyListEntity); + } + + private Optional doGetFilterBasedContingencyListEntity(UUID id) { + Objects.requireNonNull(id); + return filterBasedContingencyListRepository.findById(id); + } + + private static ContingencyElement toContingencyElement(IdentifiableAttributes id) { + switch (id.getType()) { + case LINE -> { + return new LineContingency(id.getId()); + } + case BATTERY -> { + return new BatteryContingency(id.getId()); + } + case LOAD -> { + return new LoadContingency(id.getId()); + } + case GENERATOR -> { + return new GeneratorContingency(id.getId()); + } + case TWO_WINDINGS_TRANSFORMER -> { + return new TwoWindingsTransformerContingency(id.getId()); + } + case HVDC_LINE -> { + return new HvdcLineContingency(id.getId()); + } + case STATIC_VAR_COMPENSATOR -> { + return new StaticVarCompensatorContingency(id.getId()); + } + default -> throw new IllegalStateException("Unexpected value: " + id.getType()); + } + } + + private List getPowsyblContingencies(PersistentContingencyList contingencyList, Network network, UUID networkUuid, String variantUuid) { + ContingencyList powsyblContingencyList; + if (Objects.requireNonNull(contingencyList.getMetadata().getType()) == ContingencyListType.FILTERS) { + FilterBasedContingencyList filterBasedContingencyList = (FilterBasedContingencyList) contingencyList; + List identifiers = evaluateFiltersNetwork(networkUuid, variantUuid, + filterBasedContingencyList.getFilters().stream().map(FilterAttributes::id).toList()); + powsyblContingencyList = ContingencyList.of(identifiers.stream() + .map(id -> + new Contingency(id.getId(), List.of(toContingencyElement(id)))) + .toArray(Contingency[]::new) + ); + } else { + powsyblContingencyList = contingencyList.toPowsyblContingencyList(network); + } + return powsyblContingencyList == null ? Collections.emptyList() : powsyblContingencyList.getContingencies(network); } @@ -136,7 +225,7 @@ public Integer getContingencyCount(List ids, UUID networkUuid, String vari return ids.stream() .map(uuid -> { Optional contingencyList = getAnyContingencyList(uuid, network); - return contingencyList.map(l -> getContingencies(l, network).size()).orElse(0); + return contingencyList.map(l -> getContingencies(l, network, networkUuid, variantId).size()).orElse(0); }) .reduce(0, Integer::sum); } @@ -150,15 +239,15 @@ public ContingencyListExportResult exportContingencyList(List contingencyL contingencyListIds.forEach(contingencyListId -> { Optional contingencyList = getAnyContingencyList(contingencyListId, network); contingencyList.ifPresentOrElse( - list -> contingencies.addAll(getContingencies(list, network)), + list -> contingencies.addAll(getContingencies(list, network, networkUuid, variantId)), () -> notFoundIds.add(contingencyListId) ); }); return new ContingencyListExportResult(contingencies, notFoundIds); } - private List getContingencies(PersistentContingencyList persistentContingencyList, Network network) { - return evaluateContingencyList(persistentContingencyList, network) + private List getContingencies(PersistentContingencyList persistentContingencyList, Network network, UUID networkUuid, String variantId) { + return evaluateContingencyList(persistentContingencyList, network, networkUuid, variantId) .stream() .map(ContingencyInfos::getContingency) .filter(Objects::nonNull) @@ -168,7 +257,7 @@ private List getContingencies(PersistentContingencyList persistentC @Transactional(readOnly = true) public List exportContingencyInfosList(List ids, UUID networkUuid, String variantId) { Network network = getNetworkFromUuid(networkUuid, variantId); - return ids.stream().map(id -> evaluateContingencyList(findContingencyList(id, network), network)).flatMap(Collection::stream).toList(); + return ids.stream().map(id -> evaluateContingencyList(findContingencyList(id, network), network, networkUuid, variantId)).flatMap(Collection::stream).toList(); } private PersistentContingencyList findContingencyList(UUID id, Network network) { @@ -179,11 +268,11 @@ private PersistentContingencyList findContingencyList(UUID id, Network network) private Optional getAnyContingencyList(UUID id, Network network) { return doGetFormContingencyList(id) - .or(() -> doGetIdBasedContingencyList(id, network)); + .or(() -> doGetIdBasedContingencyList(id, network).or(() -> doGetFilterBasedContingencyList(id))); } - private List evaluateContingencyList(PersistentContingencyList persistentContingencyList, Network network) { - List contingencies = getPowsyblContingencies(persistentContingencyList, network); + private List evaluateContingencyList(PersistentContingencyList persistentContingencyList, Network network, UUID networkUuid, String variantId) { + List contingencies = getPowsyblContingencies(persistentContingencyList, network, networkUuid, variantId); Map> notFoundElements = persistentContingencyList.getNotFoundElements(network); // For a gridsuite contingency with all equipments not found the powsybl contingency is not created @@ -262,6 +351,17 @@ public Optional duplicateFormContingencyList(UUID sourceListId) { } } + @Transactional + public Optional duplicateFilterBasedContingencyList(UUID sourceListId) { + Optional contingencyList = doGetFilterBasedContingencyList(sourceListId); + if (contingencyList.isEmpty()) { + throw createNotFoundException(sourceListId.toString(), "Form contingency list"); + } else { + FilterBasedContingencyList filterContingencyList = createFilterBasedContingencyList(null, (FilterBasedContingencyList) contingencyList.get()); + return Optional.of(filterContingencyList.getId()); + } + } + @Transactional public Optional duplicateIdentifierContingencyList(UUID sourceListId) { Optional idBasedContingencyList = doGetIdBasedContingencyList(sourceListId, null).map(s -> createIdBasedContingencyList(null, (IdBasedContingencyList) s)); @@ -286,14 +386,21 @@ public void modifyIdBasedContingencyList(UUID id, IdBasedContingencyList idBased notificationService.emitElementUpdated(id, userId); } + @Transactional + public void modifyFilterBasedContingencyList(UUID id, FilterBasedContingencyList contingencyList, String userId) { + // throw if not found + filterBasedContingencyListRepository.save(filterBasedContingencyListRepository.getReferenceById(id).update(contingencyList)); + notificationService.emitElementUpdated(id, userId); + } + @Transactional public void deleteContingencyList(UUID id) throws EmptyResultDataAccessException { Objects.requireNonNull(id); // if there is no form contingency list by this Id, deleted count == 0 - if (formContingencyListRepository.deleteFormContingencyListEntityById(id) == 0) { - if (idBasedContingencyListRepository.deleteIdBasedContingencyListEntityById(id) == 0) { - throw new EmptyResultDataAccessException("No element found", 1); - } + if (formContingencyListRepository.deleteFormContingencyListEntityById(id) == 0 + && idBasedContingencyListRepository.deleteIdBasedContingencyListEntityById(id) == 0 + && filterBasedContingencyListRepository.deleteFilterBasedContingencyListEntityById(id) == 0) { + throw new EmptyResultDataAccessException("No element found", 1); } } @@ -317,12 +424,23 @@ private static IdBasedContingencyList fromIdBasedContingencyListEntity(IdBasedCo notFoundElements); } + private static FilterBasedContingencyList fromFilterBasedContingencyListEntity(FilterBasedContingencyListEntity entity) { + return new FilterBasedContingencyList(entity.getId(), entity.getModificationDate(), + entity.getFiltersIds().stream().map(uuid -> new FilterAttributes(uuid, null, null)).toList()); + } + public IdBasedContingencyList createIdBasedContingencyList(UUID id, IdBasedContingencyList idBasedContingencyList) { IdBasedContingencyListEntity entity = new IdBasedContingencyListEntity(idBasedContingencyList); entity.setId(id == null ? UUID.randomUUID() : id); return fromIdBasedContingencyListEntity(idBasedContingencyListRepository.save(entity), null); } + public FilterBasedContingencyList createFilterBasedContingencyList(UUID id, FilterBasedContingencyList contingencyList) { + FilterBasedContingencyListEntity entity = new FilterBasedContingencyListEntity(contingencyList); + entity.setId(id == null ? UUID.randomUUID() : id); + return fromFilterBasedContingencyListEntity(filterBasedContingencyListRepository.save(entity)); + } + public ResponseStatusException createNotFoundException(String resourceId, String resourceType) { return new ResponseStatusException(HttpStatus.NOT_FOUND, String.format("%s %s not found", resourceType, resourceId)); } diff --git a/src/main/java/org/gridsuite/actions/server/dto/FilterAttributes.java b/src/main/java/org/gridsuite/actions/server/dto/FilterAttributes.java new file mode 100644 index 0000000..302d2b8 --- /dev/null +++ b/src/main/java/org/gridsuite/actions/server/dto/FilterAttributes.java @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2025, 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.actions.server.dto; +import org.gridsuite.filter.utils.EquipmentType; + +import java.util.UUID; + +// partial class from FilterAttributes (Filter-server) + +public record FilterAttributes(UUID id, EquipmentType equipmentType, String name) { +} diff --git a/src/main/java/org/gridsuite/actions/server/dto/FilterBasedContingencyList.java b/src/main/java/org/gridsuite/actions/server/dto/FilterBasedContingencyList.java new file mode 100644 index 0000000..eb5c435 --- /dev/null +++ b/src/main/java/org/gridsuite/actions/server/dto/FilterBasedContingencyList.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2025, 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.actions.server.dto; + +import com.powsybl.contingency.contingency.list.ContingencyList; +import com.powsybl.iidm.network.Network; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.gridsuite.actions.server.utils.ContingencyListType; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +@Getter +@NoArgsConstructor +@Schema(description = "filter based contingency list") +public class FilterBasedContingencyList extends AbstractContingencyList { + + @Schema(description = "filters list") + private List filters; + + public FilterBasedContingencyList(UUID uuid, Instant date, List filtersId) { + super(new ContingencyListMetadataImpl(uuid, ContingencyListType.FILTERS, date)); + this.filters = new ArrayList<>(); + this.filters.addAll(filtersId); + } + + @Override + public ContingencyList toPowsyblContingencyList(Network network) { + return null; + } + + @Override + public Map> getNotFoundElements(Network network) { + return Map.of(); + } +} diff --git a/src/main/java/org/gridsuite/actions/server/entities/FilterBasedContingencyListEntity.java b/src/main/java/org/gridsuite/actions/server/entities/FilterBasedContingencyListEntity.java new file mode 100644 index 0000000..f5bfaa2 --- /dev/null +++ b/src/main/java/org/gridsuite/actions/server/entities/FilterBasedContingencyListEntity.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2025, 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.actions.server.entities; + +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.gridsuite.actions.server.dto.FilterBasedContingencyList; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@NoArgsConstructor +@Getter +@Setter +@Entity +@Table(name = "filter_based_contingency_list") +public class FilterBasedContingencyListEntity extends AbstractContingencyEntity { + + @Column(name = "filter_ids") + @ElementCollection(targetClass = UUID.class) + @CollectionTable(name = "filter_based_contingency_list_filter", + joinColumns = @JoinColumn(name = "filter_based_contingency_list_id"), + foreignKey = @ForeignKey(name = "filter_based_contingency_list_id_fk")) + private List filtersIds; + + public FilterBasedContingencyListEntity(FilterBasedContingencyList contingencyList) { + super(); + if (CollectionUtils.isEmpty(contingencyList.getFilters())) { + return; + } + + init(contingencyList); + } + + private void init(FilterBasedContingencyList contingencyList) { + filtersIds = new ArrayList<>(); + contingencyList.getFilters().forEach(filterAttributes -> filtersIds.add(filterAttributes.id())); + } + + public FilterBasedContingencyListEntity update(FilterBasedContingencyList contingencyList) { + init(contingencyList); + return this; + } +} diff --git a/src/main/java/org/gridsuite/actions/server/repositories/FilterBasedContingencyListRepository.java b/src/main/java/org/gridsuite/actions/server/repositories/FilterBasedContingencyListRepository.java new file mode 100644 index 0000000..a64697a --- /dev/null +++ b/src/main/java/org/gridsuite/actions/server/repositories/FilterBasedContingencyListRepository.java @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2025, 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.actions.server.repositories; + +import org.gridsuite.actions.server.entities.FilterBasedContingencyListEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +public interface FilterBasedContingencyListRepository extends JpaRepository { + Integer deleteFilterBasedContingencyListEntityById(UUID id); +} diff --git a/src/main/java/org/gridsuite/actions/server/service/FilterService.java b/src/main/java/org/gridsuite/actions/server/service/FilterService.java new file mode 100644 index 0000000..a0fa269 --- /dev/null +++ b/src/main/java/org/gridsuite/actions/server/service/FilterService.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025, 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.actions.server.service; +import lombok.Getter; +import org.gridsuite.actions.server.dto.FilterAttributes; +import org.gridsuite.filter.identifierlistfilter.FilteredIdentifiables; +import org.gridsuite.filter.identifierlistfilter.IdentifiableAttributes; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +@Service +public class FilterService { + public static final String FILTER_END_POINT_EVALUATE_IDS = "/filters/evaluate/identifiables"; + public static final String FILTER_END_POINT_INFOS_IDS = "/filters/infos"; + public static final String DELIMITER = "/"; + public static final String FILTER_API_VERSION = "v1"; + + @Getter + private final String baseUri; + private final RestTemplate restTemplate; + + @Autowired + public FilterService(@Value("${gridsuite.services.filter-server.base-uri:http://filter-server/}") String baseUri, + RestTemplate restTemplate) { + this.baseUri = baseUri; + this.restTemplate = restTemplate; + } + + public List evaluateFilters(UUID networkUuid, String variantUuid, List filtersUuid) { + Objects.requireNonNull(networkUuid); + Objects.requireNonNull(filtersUuid); + String endPointUrl = getBaseUri() + DELIMITER + FILTER_API_VERSION + FILTER_END_POINT_EVALUATE_IDS; + + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(endPointUrl); + uriComponentsBuilder.queryParam("networkUuid", networkUuid); + uriComponentsBuilder.queryParam("variantUuid", variantUuid); + uriComponentsBuilder.queryParam("ids", filtersUuid); + var uriComponent = uriComponentsBuilder.buildAndExpand(); + + ResponseEntity response = restTemplate.getForEntity(uriComponent.toUriString(), FilteredIdentifiables.class); + return response.getBody() != null ? response.getBody().getEquipmentIds() : List.of(); + } + + public List getFiltersAttributes(List filtersUuid, String userId) { + if (filtersUuid.isEmpty()) { + return new ArrayList<>(); + } + String endPointUrl = getBaseUri() + DELIMITER + FILTER_API_VERSION + FILTER_END_POINT_INFOS_IDS; + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(endPointUrl); + uriComponentsBuilder.queryParam("filterUuids", filtersUuid); + var uriComponent = uriComponentsBuilder.buildAndExpand(); + + HttpHeaders headers = new HttpHeaders(); + headers.set("userId", userId); + + HttpEntity entity = new HttpEntity<>(headers); + ResponseEntity >response = restTemplate.exchange(uriComponent.toUriString(), + HttpMethod.GET, entity, new ParameterizedTypeReference<>() { }); + return response.getBody() != null ? response.getBody() : new ArrayList<>(); + } +} diff --git a/src/main/java/org/gridsuite/actions/server/utils/ContingencyListType.java b/src/main/java/org/gridsuite/actions/server/utils/ContingencyListType.java index 1c57f08..0ee3327 100644 --- a/src/main/java/org/gridsuite/actions/server/utils/ContingencyListType.java +++ b/src/main/java/org/gridsuite/actions/server/utils/ContingencyListType.java @@ -12,5 +12,6 @@ public enum ContingencyListType { FORM, - IDENTIFIERS + IDENTIFIERS, + FILTERS } diff --git a/src/main/java/org/gridsuite/actions/server/utils/RestTemplateConfig.java b/src/main/java/org/gridsuite/actions/server/utils/RestTemplateConfig.java new file mode 100644 index 0000000..3324e8a --- /dev/null +++ b/src/main/java/org/gridsuite/actions/server/utils/RestTemplateConfig.java @@ -0,0 +1,16 @@ + +package org.gridsuite.actions.server.utils; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} + diff --git a/src/main/resources/application-local.yaml b/src/main/resources/application-local.yaml index ce889c1..e12feb2 100644 --- a/src/main/resources/application-local.yaml +++ b/src/main/resources/application-local.yaml @@ -13,3 +13,8 @@ spring: powsybl-ws: database: host: localhost + +gridsuite: + services: + filter-server: + base-uri: http://localhost:5027 diff --git a/src/main/resources/db/changelog/changesets/changelog_20250918T115951Z.xml b/src/main/resources/db/changelog/changesets/changelog_20250918T115951Z.xml new file mode 100644 index 0000000..54055fd --- /dev/null +++ b/src/main/resources/db/changelog/changesets/changelog_20250918T115951Z.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 2d70543..e10a8a7 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -43,3 +43,7 @@ databaseChangeLog: - include: file: changesets/changelog_20250827T091725Z.xml relativeToChangelogFile: true + + - include: + file: changesets/changelog_20250918T115951Z.xml + relativeToChangelogFile: true \ No newline at end of file diff --git a/src/test/java/org/gridsuite/actions/server/ContingencyListControllerTest.java b/src/test/java/org/gridsuite/actions/server/ContingencyListControllerTest.java index cae44b7..9ed3e69 100644 --- a/src/test/java/org/gridsuite/actions/server/ContingencyListControllerTest.java +++ b/src/test/java/org/gridsuite/actions/server/ContingencyListControllerTest.java @@ -11,6 +11,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.MappingBuilder; +import com.github.tomakehurst.wiremock.client.WireMock; import com.powsybl.contingency.*; import com.powsybl.contingency.contingency.list.IdentifierContingencyList; import com.powsybl.contingency.json.ContingencyJsonModule; @@ -33,17 +36,20 @@ import org.gridsuite.actions.server.entities.NumericalFilterEntity; import org.gridsuite.actions.server.repositories.FormContingencyListRepository; import org.gridsuite.actions.server.repositories.IdBasedContingencyListRepository; +import org.gridsuite.actions.server.service.FilterService; import org.gridsuite.actions.server.utils.EquipmentType; import org.gridsuite.actions.server.utils.MatcherJson; import org.gridsuite.actions.server.utils.NumericalFilterOperator; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.cloud.stream.binder.test.OutputDestination; import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; import org.springframework.messaging.Message; @@ -53,9 +59,12 @@ import java.util.*; import java.util.stream.Collectors; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static com.powsybl.network.store.model.NetworkStoreApi.VERSION; import static org.apache.commons.lang3.StringUtils.join; import static org.gridsuite.actions.server.utils.NumericalFilterOperator.*; +import static org.gridsuite.filter.utils.EquipmentType.LINE; +import static org.gridsuite.filter.utils.EquipmentType.TWO_WINDINGS_TRANSFORMER; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.BDDMockito.given; import static org.springframework.http.MediaType.APPLICATION_JSON; @@ -103,6 +112,11 @@ class ContingencyListControllerTest { private ObjectMapper objectMapper; + private WireMockServer wireMockServer; + + @SpyBean + private FilterService filterService; + @AfterEach void tearDown() { formContingencyListRepository.deleteAll(); @@ -110,6 +124,7 @@ void tearDown() { List destinations = List.of(elementUpdateDestination); assertQueuesEmptyThenClear(destinations, output); + wireMockServer.stop(); } private static void assertQueuesEmptyThenClear(List destinations, OutputDestination output) { @@ -153,6 +168,12 @@ void setUp() { objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); objectMapper.registerModule(new ContingencyJsonModule()); + + wireMockServer = new WireMockServer(wireMockConfig().dynamicPort()); + wireMockServer.start(); + + // mock base url of filter server as one of wire mock server + Mockito.doAnswer(invocation -> wireMockServer.baseUrl()).when(filterService).getBaseUri(); } @Test @@ -276,8 +297,7 @@ private static String genFormContingencyList(EquipmentType type, Double nominalV return switch (type) { case LINE -> genFormContingencyListForLine(nominalVoltage, nominalVoltageOperator, countries); case HVDC_LINE -> genFormContingencyListForHVDC(nominalVoltage, nominalVoltageOperator, countries); - case TWO_WINDINGS_TRANSFORMER -> - genFormContingencyListFor2WT(nominalVoltage, nominalVoltageOperator, countries); + case TWO_WINDINGS_TRANSFORMER -> genFormContingencyListFor2WT(nominalVoltage, nominalVoltageOperator, countries); default -> genFormContingencyListForOthers(type, nominalVoltage, nominalVoltageOperator, countries); }; } @@ -347,6 +367,19 @@ private static String genFormContingencyList(EquipmentType type, return jsonData; } + private static String genFilterBasedContingencyList() { + String jsonData = "{\"filters\":[{\"equipmentType\":\"LINE\",\"id\":\"b45df471-ada2-4422-975b-d89b62192191\",\"name\":\"Filter1\"}"; + jsonData += ",{\"equipmentType\":\"LINE\",\"id\":\"d411e6b5-c1dc-49b4-9c17-4ef9a514196a\",\"name\":\"Filter2\"}"; + jsonData += ",{\"equipmentType\":\"TWO_WINDINGS_TRANSFORMER\",\"id\":\"2da834f8-6ab7-4781-b3ba-83f6f4a2f509\",\"name\":\"Filter3\"}]}"; + return jsonData; + } + + private static String genModifiedFilterBasedContingencyList() { + String jsonData = "{\"filters\":[{\"equipmentType\":\"LINE\",\"id\":\"b45df471-ada2-4422-975b-d89b62192191\",\"name\":\"Filter1\"}"; + jsonData += ",{\"equipmentType\":\"TWO_WINDINGS_TRANSFORMER\",\"id\":\"2da834f8-6ab7-4781-b3ba-83f6f4a2f509\",\"name\":\"Filter3\"}]}"; + return jsonData; + } + @Test void testDateFormContingencyList() throws Exception { String userId = "userId"; @@ -384,6 +417,24 @@ private UUID addNewFormContingencyList(String form) throws Exception { return list.getId(); } + private UUID addNewFilterBasedContingencyList(String filters) throws Exception { + + String res = mvc.perform(post("/" + VERSION + "/filters-contingency-lists") + .content(filters) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + + FilterBasedContingencyList list = objectMapper.readValue(res, FilterBasedContingencyList.class); + FilterBasedContingencyList original = objectMapper.readValue(filters, FilterBasedContingencyList.class); + compareFilterBasedContingencyList(original, list); + + // mandatory function but useless for this contingency list tests to increase coverage + assertEquals(null, list.toPowsyblContingencyList(network)); + assertEquals(Map.of(), list.getNotFoundElements(network)); + + return list.getId(); + } + @Test void testExportContingenciesLine() throws Exception { Set noCountries = Collections.emptySet(); @@ -620,6 +671,21 @@ private static void compareFormContingencyList(FormContingencyList expected, For } } + private static void compareFilterBasedContingencyList(FilterBasedContingencyList expected, FilterBasedContingencyList current) { + compareFiltersMetaDataLists(expected.getFilters(), current.getFilters()); + } + + private static void compareFiltersMetaDataLists(List expected, List current) { + assertEquals(expected.size(), current.size()); + + current.forEach(filter -> { + // find element in expected with same uuid + Optional expectedFilter = expected.stream().filter(f -> + f.id().equals(filter.id())).findFirst(); + assertTrue(expectedFilter.isPresent()); + }); + } + private void testExportContingencies(String content, String expectedContent, UUID networkId) throws Exception { testExportContingencies(content, expectedContent, networkId, null); } @@ -908,6 +974,93 @@ private static IdBasedContingencyList createIdBasedContingencyList(UUID listId, return new IdBasedContingencyList(listId, modificationDate, new IdentifierContingencyList(listId != null ? listId.toString() : "defaultName", networkElementIdentifiers)); } + @Test + void testFilterBasedContingencyList() throws Exception { + + List filters = List.of(UUID.fromString("b45df471-ada2-4422-975b-d89b62192191"), + UUID.fromString("d411e6b5-c1dc-49b4-9c17-4ef9a514196a"), + UUID.fromString("2da834f8-6ab7-4781-b3ba-83f6f4a2f509")); + + // create test + String list = genFilterBasedContingencyList(); + UUID id = addNewFilterBasedContingencyList(list); + + // test get + MappingBuilder requestPatternBuilder = WireMock.get(WireMock.urlPathEqualTo("/v1/filters/infos")) + .withHeader(USER_ID_HEADER, WireMock.equalTo(USER_ID_HEADER)); + + for (UUID filter : filters) { + requestPatternBuilder.withQueryParam("filterUuids", WireMock.equalTo(filter.toString())); + } + + wireMockServer.stubFor(requestPatternBuilder.willReturn(WireMock.ok())); + + mvc.perform(get("/" + VERSION + "/filters-contingency-lists/" + id) + .header(USER_ID_HEADER, USER_ID_HEADER) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(APPLICATION_JSON)); + + // test count + requestPatternBuilder = WireMock.get(WireMock.urlPathEqualTo("/v1/filters/evaluate/identifiables")) + .withQueryParam("networkUuid", WireMock.equalTo(NETWORK_UUID.toString())); + + for (UUID filter : filters) { + requestPatternBuilder.withQueryParam("ids", WireMock.equalTo(filter.toString())); + } + + wireMockServer.stubFor(requestPatternBuilder.willReturn(WireMock.ok())); + + String res = mvc.perform(get("/" + VERSION + "/contingency-lists/count?ids=" + id + "&networkUuid=" + NETWORK_UUID + "&variantId=" + VARIANT_ID_1) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + assertEquals(0, Integer.parseInt(res)); + + // duplicate test + String newUuid = mvc.perform(post("/" + VERSION + "/filters-contingency-lists?duplicateFrom=" + id)) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + assertNotNull(newUuid); + + mvc.perform(post("/" + VERSION + "/filters-contingency-lists?duplicateFrom=" + UUID.randomUUID())) + .andExpect(status().isNotFound()); + + // delete lists + mvc.perform(delete("/" + VERSION + "/contingency-lists/" + id)) + .andExpect(status().isOk()); + + newUuid = newUuid.replace("\"", ""); + mvc.perform(delete("/" + VERSION + "/contingency-lists/" + newUuid)) + .andExpect(status().isOk()); + } + + @Test + void modifyFilterBasedContingencyList() throws Exception { + String contingencyList = genFilterBasedContingencyList(); + + String res = mvc.perform(post("/" + VERSION + "/filters-contingency-lists") + .content(contingencyList) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + + UUID contingencyListId = objectMapper.readValue(res, FilterBasedContingencyList.class).getId(); + + String newList = genModifiedFilterBasedContingencyList(); + mvc.perform(put("/" + VERSION + "/filters-contingency-lists/" + contingencyListId) + .content(newList) + .contentType(APPLICATION_JSON) + .header(USER_ID_HEADER, USER_ID_HEADER)) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + + Message message = output.receive(TIMEOUT, elementUpdateDestination); + assertEquals(contingencyListId, message.getHeaders().get(NotificationService.HEADER_ELEMENT_UUID)); + assertEquals(USER_ID_HEADER, message.getHeaders().get(NotificationService.HEADER_MODIFIED_BY)); + + // delete lists + mvc.perform(delete("/" + VERSION + "/contingency-lists/" + contingencyListId)) + .andExpect(status().isOk()); + } + private int getContingencyListsCount() throws Exception { String res = mvc.perform(get("/" + VERSION + "/contingency-lists") .contentType(APPLICATION_JSON))