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))