runContext, R result) {
+ Counter.Builder builder =
+ Counter.builder(COMPUTATION_TOTAL_COUNTER_NAME);
+ if (runContext.getProvider() != null) {
+ builder.tag(PROVIDER_TAG_NAME, runContext.getProvider());
+ }
+ builder.tag(TYPE_TAG_NAME, getComputationType())
+ .tag(STATUS_TAG_NAME, getResultStatus(result))
+ .register(meterRegistry)
+ .increment();
+ }
+
+ protected abstract String getResultStatus(R res);
+}
diff --git a/src/main/java/org/gridsuite/computation/service/AbstractComputationResultService.java b/src/main/java/org/gridsuite/computation/service/AbstractComputationResultService.java
new file mode 100644
index 0000000..c5424d6
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/service/AbstractComputationResultService.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2024, 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.computation.service;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * @author Mathieu Deharbe
+ * @param status specific to the computation
+ */
+public abstract class AbstractComputationResultService {
+
+ public abstract void insertStatus(List resultUuids, S status);
+
+ public abstract void delete(UUID resultUuid);
+
+ public abstract void deleteAll();
+
+ public abstract S findStatus(UUID resultUuid);
+}
diff --git a/src/main/java/org/gridsuite/computation/service/AbstractComputationRunContext.java b/src/main/java/org/gridsuite/computation/service/AbstractComputationRunContext.java
new file mode 100644
index 0000000..8cb4ca9
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/service/AbstractComputationRunContext.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2024, 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.computation.service;
+
+import com.powsybl.commons.report.ReportNode;
+import com.powsybl.iidm.network.Network;
+import lombok.Getter;
+import lombok.Setter;
+import org.gridsuite.computation.dto.ReportInfos;
+
+import java.util.UUID;
+
+/**
+ * @author Mathieu Deharbe
+ * @param
parameters structure specific to the computation
+ */
+@Getter
+@Setter
+public abstract class AbstractComputationRunContext
{
+ private final UUID networkUuid;
+ private final String variantId;
+ private final String receiver;
+ private final ReportInfos reportInfos;
+ private final String userId;
+ private String provider;
+ private P parameters;
+ private ReportNode reportNode;
+ private Network network;
+
+ protected AbstractComputationRunContext(UUID networkUuid, String variantId, String receiver, ReportInfos reportInfos,
+ String userId, String provider, P parameters) {
+ this.networkUuid = networkUuid;
+ this.variantId = variantId;
+ this.receiver = receiver;
+ this.reportInfos = reportInfos;
+ this.userId = userId;
+ this.provider = provider;
+ this.parameters = parameters;
+ this.reportNode = ReportNode.NO_OP;
+ this.network = null;
+ }
+}
diff --git a/src/main/java/org/gridsuite/computation/service/AbstractComputationService.java b/src/main/java/org/gridsuite/computation/service/AbstractComputationService.java
new file mode 100644
index 0000000..824f1ee
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/service/AbstractComputationService.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2024, 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.computation.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.Getter;
+import org.springframework.util.CollectionUtils;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * @author Mathieu Deharbe
+ * @param run context specific to a computation, including parameters
+ * @param run service specific to a computation
+ * @param enum status specific to a computation
+ */
+public abstract class AbstractComputationService, T extends AbstractComputationResultService, S> {
+
+ protected ObjectMapper objectMapper;
+ protected NotificationService notificationService;
+ protected UuidGeneratorService uuidGeneratorService;
+ protected T resultService;
+ @Getter
+ private final String defaultProvider;
+
+ protected AbstractComputationService(NotificationService notificationService,
+ T resultService,
+ ObjectMapper objectMapper,
+ UuidGeneratorService uuidGeneratorService,
+ String defaultProvider) {
+ this.notificationService = Objects.requireNonNull(notificationService);
+ this.objectMapper = objectMapper;
+ this.uuidGeneratorService = Objects.requireNonNull(uuidGeneratorService);
+ this.defaultProvider = defaultProvider;
+ this.resultService = Objects.requireNonNull(resultService);
+ }
+
+ public void stop(UUID resultUuid, String receiver) {
+ notificationService.sendCancelMessage(new CancelContext(resultUuid, receiver).toMessage());
+ }
+
+ public void stop(UUID resultUuid, String receiver, String userId) {
+ notificationService.sendCancelMessage(new CancelContext(resultUuid, receiver, userId).toMessage());
+ }
+
+ public abstract List getProviders();
+
+ public abstract UUID runAndSaveResult(C runContext);
+
+ public void deleteResult(UUID resultUuid) {
+ resultService.delete(resultUuid);
+ }
+
+ public void deleteResults(List resultUuids) {
+ if (!CollectionUtils.isEmpty(resultUuids)) {
+ resultUuids.forEach(resultService::delete);
+ } else {
+ deleteResults();
+ }
+ }
+
+ public void deleteResults() {
+ resultService.deleteAll();
+ }
+
+ public void setStatus(List resultUuids, S status) {
+ resultService.insertStatus(resultUuids, status);
+ }
+
+ public S getStatus(UUID resultUuid) {
+ return resultService.findStatus(resultUuid);
+ }
+}
diff --git a/src/main/java/org/gridsuite/computation/service/AbstractFilterService.java b/src/main/java/org/gridsuite/computation/service/AbstractFilterService.java
new file mode 100644
index 0000000..6558774
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/service/AbstractFilterService.java
@@ -0,0 +1,400 @@
+/**
+ * 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.computation.service;
+
+import com.powsybl.commons.PowsyblException;
+import com.powsybl.iidm.network.Country;
+import com.powsybl.iidm.network.Network;
+import com.powsybl.iidm.network.TwoSides;
+import com.powsybl.network.store.client.NetworkStoreService;
+import com.powsybl.network.store.client.PreloadingStrategy;
+import lombok.NonNull;
+import org.apache.commons.collections4.CollectionUtils;
+import org.gridsuite.computation.dto.GlobalFilter;
+import org.gridsuite.computation.dto.ResourceFilterDTO;
+import org.gridsuite.filter.AbstractFilter;
+import org.gridsuite.filter.FilterLoader;
+import org.gridsuite.filter.expertfilter.ExpertFilter;
+import org.gridsuite.filter.expertfilter.expertrule.*;
+import org.gridsuite.filter.identifierlistfilter.IdentifiableAttributes;
+import org.gridsuite.filter.utils.EquipmentType;
+import org.gridsuite.filter.utils.FilterServiceUtils;
+import org.gridsuite.filter.utils.expertfilter.CombinatorType;
+import org.gridsuite.filter.utils.expertfilter.FieldType;
+import org.gridsuite.filter.utils.expertfilter.OperatorType;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.client.HttpStatusCodeException;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.server.ResponseStatusException;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author Rehili Ghazwa
+ */
+public abstract class AbstractFilterService implements FilterLoader {
+ protected static final String FILTERS_NOT_FOUND = "Filters not found";
+ protected static final String FILTER_API_VERSION = "v1";
+ protected static final String DELIMITER = "/";
+
+ protected final RestTemplate restTemplate = new RestTemplate();
+ protected final NetworkStoreService networkStoreService;
+ protected final String filterServerBaseUri;
+ public static final String NETWORK_UUID = "networkUuid";
+
+ public static final String IDS = "ids";
+
+ protected AbstractFilterService(NetworkStoreService networkStoreService, String filterServerBaseUri) {
+ this.networkStoreService = networkStoreService;
+ this.filterServerBaseUri = filterServerBaseUri;
+ }
+
+ @Override
+ public List getFilters(List filtersUuids) {
+ if (CollectionUtils.isEmpty(filtersUuids)) {
+ return List.of();
+ }
+
+ String ids = filtersUuids.stream()
+ .map(UUID::toString)
+ .collect(Collectors.joining(","));
+
+ String path = UriComponentsBuilder
+ .fromPath(DELIMITER + FILTER_API_VERSION + "/filters/metadata")
+ .queryParam(IDS, ids)
+ .buildAndExpand()
+ .toUriString();
+
+ try {
+ return restTemplate.exchange(
+ filterServerBaseUri + path,
+ HttpMethod.GET,
+ null,
+ new ParameterizedTypeReference>() { }
+ ).getBody();
+ } catch (HttpStatusCodeException e) {
+ throw new PowsyblException(FILTERS_NOT_FOUND + " [" + filtersUuids + "]");
+ }
+ }
+
+ protected Network getNetwork(UUID networkUuid, String variantId) {
+ try {
+ Network network = networkStoreService.getNetwork(networkUuid, PreloadingStrategy.COLLECTION);
+ network.getVariantManager().setWorkingVariant(variantId);
+ return network;
+ } catch (PowsyblException e) {
+ throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage());
+ }
+ }
+
+ protected List filterNetwork(AbstractFilter filter, Network network) {
+ return FilterServiceUtils.getIdentifiableAttributes(filter, network, this)
+ .stream()
+ .map(IdentifiableAttributes::getId)
+ .toList();
+ }
+
+ public Optional getResourceFilter(@NonNull UUID networkUuid, @NonNull String variantId, @NonNull GlobalFilter globalFilter,
+ List equipmentTypes, String columnName) {
+
+ Network network = getNetwork(networkUuid, variantId);
+ List genericFilters = getFilters(globalFilter.getGenericFilter());
+
+ // Filter equipments by type
+ Map> subjectIdsByEquipmentType = filterEquipmentsByType(
+ network, globalFilter, genericFilters, equipmentTypes
+ );
+
+ // Combine all results into one list
+ List subjectIds = subjectIdsByEquipmentType.values().stream()
+ .filter(Objects::nonNull)
+ .flatMap(List::stream)
+ .toList();
+
+ return subjectIds.isEmpty() ? Optional.empty() :
+ Optional.of(new ResourceFilterDTO(
+ ResourceFilterDTO.DataType.TEXT,
+ ResourceFilterDTO.Type.IN,
+ subjectIds,
+ columnName
+ ));
+ }
+
+ protected List createNumberExpertRules(List values, FieldType fieldType) {
+ List rules = new ArrayList<>();
+ if (values != null) {
+ for (String value : values) {
+ rules.add(NumberExpertRule.builder()
+ .value(Double.valueOf(value))
+ .field(fieldType)
+ .operator(OperatorType.EQUALS)
+ .build());
+ }
+ }
+ return rules;
+ }
+
+ protected AbstractExpertRule createPropertiesRule(String property, List propertiesValues, FieldType fieldType) {
+ return PropertiesExpertRule.builder()
+ .combinator(CombinatorType.OR)
+ .operator(OperatorType.IN)
+ .field(fieldType)
+ .propertyName(property)
+ .propertyValues(propertiesValues)
+ .build();
+ }
+
+ protected List createEnumExpertRules(List values, FieldType fieldType) {
+ List rules = new ArrayList<>();
+ if (values != null) {
+ for (Country value : values) {
+ rules.add(EnumExpertRule.builder()
+ .value(value.toString())
+ .field(fieldType)
+ .operator(OperatorType.EQUALS)
+ .build());
+ }
+ }
+ return rules;
+ }
+
+ protected AbstractExpertRule createCombination(CombinatorType combinatorType, List rules) {
+ return CombinatorExpertRule.builder().combinator(combinatorType).rules(rules).build();
+ }
+
+ protected Optional createOrCombination(List rules) {
+ if (rules.isEmpty()) {
+ return Optional.empty();
+ }
+ return Optional.of(rules.size() > 1 ? createCombination(CombinatorType.OR, rules) : rules.getFirst());
+ }
+
+ /**
+ * Extracts equipment IDs from a generic filter based on equipment type
+ */
+ protected List extractEquipmentIdsFromGenericFilter(
+ AbstractFilter filter,
+ EquipmentType targetEquipmentType,
+ Network network) {
+
+ if (filter.getEquipmentType() == targetEquipmentType) {
+ return filterNetwork(filter, network);
+ } else if (filter.getEquipmentType() == EquipmentType.VOLTAGE_LEVEL) {
+ ExpertFilter voltageFilter = buildExpertFilterWithVoltageLevelIdsCriteria(
+ filter.getId(), targetEquipmentType);
+ return filterNetwork(voltageFilter, network);
+ }
+ return List.of();
+ }
+
+ /**
+ * Combines multiple filter results using AND or OR logic
+ */
+ protected List combineFilterResults(List> filterResults, boolean useAndLogic) {
+ if (filterResults.isEmpty()) {
+ return List.of();
+ }
+
+ if (filterResults.size() == 1) {
+ return filterResults.getFirst();
+ }
+
+ if (useAndLogic) {
+ // Intersection of all results
+ Set result = new HashSet<>(filterResults.getFirst());
+ for (int i = 1; i < filterResults.size(); i++) {
+ result.retainAll(filterResults.get(i));
+ }
+ return new ArrayList<>(result);
+ } else {
+ // Union of all results
+ Set result = new HashSet<>();
+ filterResults.forEach(result::addAll);
+ return new ArrayList<>(result);
+ }
+ }
+
+ /**
+ * Extracts filtered equipment IDs by applying expert and generic filters
+ */
+ protected List extractFilteredEquipmentIds(
+ Network network,
+ GlobalFilter globalFilter,
+ List genericFilters,
+ EquipmentType equipmentType) {
+
+ List> allFilterResults = new ArrayList<>();
+
+ // Extract IDs from expert filter
+ ExpertFilter expertFilter = buildExpertFilter(globalFilter, equipmentType);
+ if (expertFilter != null) {
+ allFilterResults.add(filterNetwork(expertFilter, network));
+ }
+
+ // Extract IDs from generic filters
+ for (AbstractFilter filter : genericFilters) {
+ List filterResult = extractEquipmentIdsFromGenericFilter(filter, equipmentType, network);
+ if (!filterResult.isEmpty()) {
+ allFilterResults.add(filterResult);
+ }
+ }
+
+ // Combine results with appropriate logic
+ // Expert filters use OR between them, generic filters use AND
+ return combineFilterResults(allFilterResults, !genericFilters.isEmpty());
+ }
+
+ /**
+ * Builds expert filter with voltage level IDs criteria
+ */
+ protected ExpertFilter buildExpertFilterWithVoltageLevelIdsCriteria(UUID filterUuid, EquipmentType equipmentType) {
+ AbstractExpertRule voltageLevelId1Rule = createVoltageLevelIdRule(filterUuid, TwoSides.ONE);
+ AbstractExpertRule voltageLevelId2Rule = createVoltageLevelIdRule(filterUuid, TwoSides.TWO);
+ AbstractExpertRule orCombination = createCombination(CombinatorType.OR,
+ List.of(voltageLevelId1Rule, voltageLevelId2Rule));
+ return new ExpertFilter(UUID.randomUUID(), new Date(), equipmentType, orCombination);
+ }
+
+ /**
+ * Creates voltage level ID rule for filtering
+ */
+ protected AbstractExpertRule createVoltageLevelIdRule(UUID filterUuid, TwoSides side) {
+ return FilterUuidExpertRule.builder()
+ .operator(OperatorType.IS_PART_OF)
+ .field(side == TwoSides.ONE ? FieldType.VOLTAGE_LEVEL_ID_1 : FieldType.VOLTAGE_LEVEL_ID_2)
+ .values(Set.of(filterUuid.toString()))
+ .build();
+ }
+
+ /**
+ * Builds all expert rules for a global filter and equipment type
+ */
+ protected List buildAllExpertRules(GlobalFilter globalFilter, EquipmentType equipmentType) {
+ List andRules = new ArrayList<>();
+
+ // Nominal voltage rules
+ buildNominalVoltageRules(globalFilter.getNominalV(), equipmentType)
+ .ifPresent(andRules::add);
+
+ // Country code rules
+ buildCountryCodeRules(globalFilter.getCountryCode(), equipmentType)
+ .ifPresent(andRules::add);
+
+ // Substation property rules
+ if (globalFilter.getSubstationProperty() != null) {
+ buildSubstationPropertyRules(globalFilter.getSubstationProperty(), equipmentType)
+ .ifPresent(andRules::add);
+ }
+
+ return andRules;
+ }
+
+ /**
+ * Builds nominal voltage rules combining all relevant field types
+ */
+ protected Optional buildNominalVoltageRules(
+ List nominalVoltages, EquipmentType equipmentType) {
+
+ List fieldTypes = getNominalVoltageFieldType(equipmentType);
+ List rules = fieldTypes.stream()
+ .flatMap(fieldType -> createNumberExpertRules(nominalVoltages, fieldType).stream())
+ .toList();
+
+ return createOrCombination(rules);
+ }
+
+ /**
+ * Builds country code rules combining all relevant field types
+ */
+ protected Optional buildCountryCodeRules(
+ List countryCodes, EquipmentType equipmentType) {
+
+ List fieldTypes = getCountryCodeFieldType(equipmentType);
+ List rules = fieldTypes.stream()
+ .flatMap(fieldType -> createEnumExpertRules(countryCodes, fieldType).stream())
+ .toList();
+
+ return createOrCombination(rules);
+ }
+
+ /**
+ * Builds substation property rules combining all relevant field types
+ */
+ protected Optional buildSubstationPropertyRules(
+ Map> properties, EquipmentType equipmentType) {
+
+ List fieldTypes = getSubstationPropertiesFieldTypes(equipmentType);
+ List rules = properties.entrySet().stream()
+ .flatMap(entry -> fieldTypes.stream()
+ .map(fieldType -> createPropertiesRule(
+ entry.getKey(), entry.getValue(), fieldType)))
+ .toList();
+
+ return createOrCombination(rules);
+ }
+
+ /**
+ * Filters equipments by type and returns map of IDs grouped by equipment type
+ */
+ protected Map> filterEquipmentsByType(
+ Network network,
+ GlobalFilter globalFilter,
+ List genericFilters,
+ List equipmentTypes) {
+
+ Map> result = new EnumMap<>(EquipmentType.class);
+
+ for (EquipmentType equipmentType : equipmentTypes) {
+ List filteredIds = extractFilteredEquipmentIds(network, globalFilter, genericFilters, equipmentType);
+ if (!filteredIds.isEmpty()) {
+ result.put(equipmentType, filteredIds);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Builds expert filter from global filter and equipment type
+ */
+ protected ExpertFilter buildExpertFilter(GlobalFilter globalFilter, EquipmentType equipmentType) {
+ List andRules = buildAllExpertRules(globalFilter, equipmentType);
+
+ return andRules.isEmpty() ? null :
+ new ExpertFilter(UUID.randomUUID(), new Date(), equipmentType,
+ createCombination(CombinatorType.AND, andRules));
+ }
+
+ protected List getNominalVoltageFieldType(EquipmentType equipmentType) {
+ return switch (equipmentType) {
+ case LINE, TWO_WINDINGS_TRANSFORMER -> List.of(FieldType.NOMINAL_VOLTAGE_1, FieldType.NOMINAL_VOLTAGE_2);
+ case VOLTAGE_LEVEL -> List.of(FieldType.NOMINAL_VOLTAGE);
+ default -> List.of();
+ };
+ }
+
+ protected List getCountryCodeFieldType(EquipmentType equipmentType) {
+ return switch (equipmentType) {
+ case VOLTAGE_LEVEL, TWO_WINDINGS_TRANSFORMER -> List.of(FieldType.COUNTRY);
+ case LINE -> List.of(FieldType.COUNTRY_1, FieldType.COUNTRY_2);
+ default -> List.of();
+ };
+ }
+
+ protected List getSubstationPropertiesFieldTypes(EquipmentType equipmentType) {
+ return equipmentType == EquipmentType.LINE ?
+ List.of(FieldType.SUBSTATION_PROPERTIES_1, FieldType.SUBSTATION_PROPERTIES_2) :
+ List.of(FieldType.SUBSTATION_PROPERTIES);
+ }
+}
+
+
+
diff --git a/src/main/java/org/gridsuite/computation/service/AbstractResultContext.java b/src/main/java/org/gridsuite/computation/service/AbstractResultContext.java
new file mode 100644
index 0000000..b80728d
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/service/AbstractResultContext.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2024, 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.computation.service;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.Data;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.support.MessageBuilder;
+
+import java.io.UncheckedIOException;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+
+import static org.gridsuite.computation.service.NotificationService.*;
+
+/**
+ * @author Mathieu Deharbe
+ * @param run context specific to a computation, including parameters
+ */
+@Data
+public abstract class AbstractResultContext> {
+
+ protected static final String RESULT_UUID_HEADER = "resultUuid";
+
+ protected static final String NETWORK_UUID_HEADER = "networkUuid";
+
+ protected static final String REPORT_UUID_HEADER = "reportUuid";
+
+ public static final String VARIANT_ID_HEADER = "variantId";
+
+ public static final String REPORTER_ID_HEADER = "reporterId";
+
+ public static final String REPORT_TYPE_HEADER = "reportType";
+
+ protected static final String MESSAGE_ROOT_NAME = "parameters";
+
+ private final UUID resultUuid;
+ private final C runContext;
+
+ protected AbstractResultContext(UUID resultUuid, C runContext) {
+ this.resultUuid = Objects.requireNonNull(resultUuid);
+ this.runContext = Objects.requireNonNull(runContext);
+ }
+
+ public Message toMessage(ObjectMapper objectMapper) {
+ String parametersJson = "";
+ if (objectMapper != null) {
+ try {
+ parametersJson = objectMapper.writeValueAsString(runContext.getParameters());
+ } catch (JsonProcessingException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+ return MessageBuilder.withPayload(parametersJson)
+ .setHeader(RESULT_UUID_HEADER, resultUuid.toString())
+ .setHeader(NETWORK_UUID_HEADER, runContext.getNetworkUuid().toString())
+ .setHeader(VARIANT_ID_HEADER, runContext.getVariantId())
+ .setHeader(HEADER_RECEIVER, runContext.getReceiver())
+ .setHeader(HEADER_PROVIDER, runContext.getProvider())
+ .setHeader(HEADER_USER_ID, runContext.getUserId())
+ .setHeader(REPORT_UUID_HEADER, runContext.getReportInfos().reportUuid() != null ? runContext.getReportInfos().reportUuid().toString() : null)
+ .setHeader(REPORTER_ID_HEADER, runContext.getReportInfos().reporterId())
+ .setHeader(REPORT_TYPE_HEADER, runContext.getReportInfos().computationType())
+ .copyHeaders(getSpecificMsgHeaders(objectMapper))
+ .build();
+ }
+
+ @SuppressWarnings("unused")
+ protected Map getSpecificMsgHeaders(ObjectMapper ignoredObjectMapper) {
+ return Map.of();
+ }
+}
diff --git a/src/main/java/org/gridsuite/computation/service/AbstractWorkerService.java b/src/main/java/org/gridsuite/computation/service/AbstractWorkerService.java
new file mode 100644
index 0000000..45d5c99
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/service/AbstractWorkerService.java
@@ -0,0 +1,260 @@
+/**
+ * Copyright (c) 2024, 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.computation.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.powsybl.commons.PowsyblException;
+import com.powsybl.commons.report.ReportNode;
+import com.powsybl.iidm.network.Network;
+import com.powsybl.iidm.network.VariantManagerConstants;
+import com.powsybl.network.store.client.NetworkStoreService;
+import com.powsybl.network.store.client.PreloadingStrategy;
+import org.apache.commons.lang3.StringUtils;
+import org.gridsuite.computation.ComputationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.messaging.Message;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Consumer;
+
+/**
+ * @author Mathieu Deharbe
+ * @param powsybl Result class specific to the computation
+ * @param Run context specific to a computation, including parameters
+ * @param
powsybl and gridsuite Parameters specifics to the computation
+ * @param result service specific to the computation
+ */
+public abstract class AbstractWorkerService, P, S extends AbstractComputationResultService>> {
+ private static final Logger LOGGER = LoggerFactory.getLogger(AbstractWorkerService.class);
+
+ protected final Lock lockRunAndCancel = new ReentrantLock();
+ protected final ObjectMapper objectMapper;
+ protected final NetworkStoreService networkStoreService;
+ protected final ReportService reportService;
+ protected final ExecutionService executionService;
+ protected final NotificationService notificationService;
+ protected final AbstractComputationObserver observer;
+ protected final Map> futures = new ConcurrentHashMap<>();
+ protected final Map cancelComputationRequests = new ConcurrentHashMap<>();
+ protected final S resultService;
+
+ protected AbstractWorkerService(NetworkStoreService networkStoreService,
+ NotificationService notificationService,
+ ReportService reportService,
+ S resultService,
+ ExecutionService executionService,
+ AbstractComputationObserver observer,
+ ObjectMapper objectMapper) {
+ this.networkStoreService = networkStoreService;
+ this.notificationService = notificationService;
+ this.reportService = reportService;
+ this.resultService = resultService;
+ this.executionService = executionService;
+ this.observer = observer;
+ this.objectMapper = objectMapper;
+ }
+
+ protected PreloadingStrategy getNetworkPreloadingStrategy() {
+ return PreloadingStrategy.COLLECTION;
+ }
+
+ protected Network getNetwork(UUID networkUuid, String variantId) {
+ try {
+ Network network = networkStoreService.getNetwork(networkUuid, getNetworkPreloadingStrategy());
+ String variant = StringUtils.isBlank(variantId) ? VariantManagerConstants.INITIAL_VARIANT_ID : variantId;
+ network.getVariantManager().setWorkingVariant(variant);
+ return network;
+ } catch (PowsyblException e) {
+ throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage());
+ }
+ }
+
+ protected void cleanResultsAndPublishCancel(UUID resultUuid, String receiver) {
+ resultService.delete(resultUuid);
+ notificationService.publishStop(resultUuid, receiver, getComputationType());
+ if (LOGGER.isInfoEnabled()) {
+ LOGGER.info("{} (resultUuid='{}')",
+ NotificationService.getCancelMessage(getComputationType()),
+ resultUuid);
+ }
+ }
+
+ private boolean cancelAsync(CancelContext cancelContext) {
+ lockRunAndCancel.lock();
+ boolean isCanceled = false;
+ try {
+ cancelComputationRequests.put(cancelContext.resultUuid(), cancelContext);
+
+ // find the completableFuture associated with result uuid
+ CompletableFuture future = futures.get(cancelContext.resultUuid());
+ if (future != null) {
+ isCanceled = future.cancel(true); // cancel computation in progress
+ if (isCanceled) {
+ cleanResultsAndPublishCancel(cancelContext.resultUuid(), cancelContext.receiver());
+ }
+ }
+ } finally {
+ lockRunAndCancel.unlock();
+ }
+ return isCanceled;
+ }
+
+ protected abstract AbstractResultContext fromMessage(Message message);
+
+ protected boolean resultCanBeSaved(R result) {
+ return result != null;
+ }
+
+ public Consumer> consumeRun() {
+ return message -> {
+ AbstractResultContext resultContext = fromMessage(message);
+ AtomicReference rootReporter = new AtomicReference<>(ReportNode.NO_OP);
+ try {
+ Network network = getNetwork(resultContext.getRunContext().getNetworkUuid(),
+ resultContext.getRunContext().getVariantId());
+ resultContext.getRunContext().setNetwork(network);
+ observer.observe("global.run", resultContext.getRunContext(), () -> {
+ long startTime = System.nanoTime();
+ R result = run(resultContext.getRunContext(), resultContext.getResultUuid(), rootReporter);
+
+ LOGGER.info("Just run in {}s", TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startTime));
+
+ if (resultCanBeSaved(result)) {
+ startTime = System.nanoTime();
+ observer.observe("results.save", resultContext.getRunContext(), () -> saveResult(network, resultContext, result));
+
+ LOGGER.info("Stored in {}s", TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startTime));
+
+ sendResultMessage(resultContext, result);
+ LOGGER.info("{} complete (resultUuid='{}')", getComputationType(), resultContext.getResultUuid());
+ }
+ });
+ } catch (CancellationException e) {
+ // Do nothing
+ } catch (Exception e) {
+ resultService.delete(resultContext.getResultUuid());
+ this.handleNonCancellationException(resultContext, e, rootReporter);
+ throw new ComputationException(String.format("%s: %s", NotificationService.getFailedMessage(getComputationType()), e.getMessage()), e.getCause());
+ } finally {
+ clean(resultContext);
+ }
+ };
+ }
+
+ /**
+ * Perform cleaning
+ * @param resultContext The context of the computation
+ */
+ protected void clean(AbstractResultContext resultContext) {
+ futures.remove(resultContext.getResultUuid());
+ cancelComputationRequests.remove(resultContext.getResultUuid());
+ }
+
+ /**
+ * Handle exception in consumeRun that is not a CancellationException
+ * @param resultContext The context of the computation
+ * @param exception The exception to handle
+ */
+ protected void handleNonCancellationException(AbstractResultContext resultContext, Exception exception, AtomicReference rootReporter) {
+ }
+
+ public Consumer> consumeCancel() {
+ return message -> {
+ CancelContext cancelContext = CancelContext.fromMessage(message);
+ boolean isCancelled = cancelAsync(cancelContext);
+ if (!isCancelled) {
+ notificationService.publishCancelFailed(cancelContext.resultUuid(), cancelContext.receiver(), getComputationType(), cancelContext.userId());
+ }
+ };
+ }
+
+ protected abstract void saveResult(Network network, AbstractResultContext resultContext, R result);
+
+ protected void sendResultMessage(AbstractResultContext resultContext, R ignoredResult) {
+ notificationService.sendResultMessage(resultContext.getResultUuid(), resultContext.getRunContext().getReceiver(),
+ resultContext.getRunContext().getUserId(), null);
+ }
+
+ /**
+ * Do some extra task before running the computation, e.g. print log or init extra data for the run context
+ * @param ignoredRunContext This context may be used for further computation in overriding classes
+ */
+ protected void preRun(C ignoredRunContext) {
+ LOGGER.info("Run {} computation...", getComputationType());
+ }
+
+ protected R run(C runContext, UUID resultUuid, AtomicReference rootReporter) {
+ String provider = runContext.getProvider();
+ ReportNode reportNode = ReportNode.NO_OP;
+
+ if (runContext.getReportInfos() != null && runContext.getReportInfos().reportUuid() != null) {
+ final String reportType = runContext.getReportInfos().computationType();
+ String rootReporterId = runContext.getReportInfos().reporterId();
+ ReportNode rootReporterNode = ReportNode.newRootReportNode()
+ .withAllResourceBundlesFromClasspath()
+ .withMessageTemplate("ws.commons.rootReporterId")
+ .withUntypedValue("rootReporterId", rootReporterId).build();
+ rootReporter.set(rootReporterNode);
+ reportNode = rootReporter.get().newReportNode().withMessageTemplate("ws.commons.reportType")
+ .withUntypedValue("reportType", reportType)
+ .withUntypedValue("optionalProvider", provider != null ? " (" + provider + ")" : "").add();
+ // Delete any previous computation logs
+ observer.observe("report.delete",
+ runContext, () -> reportService.deleteReport(runContext.getReportInfos().reportUuid()));
+ }
+ runContext.setReportNode(reportNode);
+
+ preRun(runContext);
+ CompletableFuture future = runAsync(runContext, provider, resultUuid);
+ R result = future == null ? null : observer.observeRun("run", runContext, future::join);
+ postRun(runContext, rootReporter, result);
+ return result;
+ }
+
+ /**
+ * Do some extra task after running the computation
+ * @param runContext This context may be used for extra task in overriding classes
+ * @param rootReportNode root of the reporter tree
+ * @param ignoredResult The result of the computation
+ */
+ protected void postRun(C runContext, AtomicReference rootReportNode, R ignoredResult) {
+ if (runContext.getReportInfos().reportUuid() != null) {
+ observer.observe("report.send", runContext, () -> reportService.sendReport(runContext.getReportInfos().reportUuid(), rootReportNode.get()));
+ }
+ }
+
+ protected CompletableFuture runAsync(
+ C runContext,
+ String provider,
+ UUID resultUuid) {
+ lockRunAndCancel.lock();
+ try {
+ if (resultUuid != null && cancelComputationRequests.get(resultUuid) != null) {
+ return null;
+ }
+ CompletableFuture future = getCompletableFuture(runContext, provider, resultUuid);
+ if (resultUuid != null) {
+ futures.put(resultUuid, future);
+ }
+ return future;
+ } finally {
+ lockRunAndCancel.unlock();
+ }
+ }
+
+ protected abstract String getComputationType();
+
+ protected abstract CompletableFuture getCompletableFuture(C runContext, String provider, UUID resultUuid);
+}
diff --git a/src/main/java/org/gridsuite/computation/service/CancelContext.java b/src/main/java/org/gridsuite/computation/service/CancelContext.java
new file mode 100644
index 0000000..e343b8f
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/service/CancelContext.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2024, 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.computation.service;
+
+import org.gridsuite.computation.utils.MessageUtils;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.messaging.support.MessageBuilder;
+
+import java.util.Objects;
+import java.util.UUID;
+
+import static org.gridsuite.computation.service.NotificationService.*;
+
+/**
+ * @author Anis Touri
+ */
+public record CancelContext(UUID resultUuid, String receiver, String userId) {
+
+ public CancelContext(UUID resultUuid, String receiver, String userId) {
+ this.resultUuid = Objects.requireNonNull(resultUuid);
+ this.receiver = Objects.requireNonNull(receiver);
+ this.userId = userId;
+ }
+
+ public CancelContext(UUID resultUuid, String receiver) {
+ this(resultUuid, receiver, null);
+ }
+
+ public static CancelContext fromMessage(Message message) {
+ Objects.requireNonNull(message);
+ MessageHeaders headers = message.getHeaders();
+ UUID resultUuid = UUID.fromString(MessageUtils.getNonNullHeader(headers, HEADER_RESULT_UUID));
+ String receiver = headers.get(HEADER_RECEIVER, String.class);
+ String userId = headers.get(HEADER_USER_ID, String.class);
+ return new CancelContext(resultUuid, receiver, userId);
+ }
+
+ public Message toMessage() {
+ return MessageBuilder.withPayload("")
+ .setHeader(HEADER_RESULT_UUID, resultUuid.toString())
+ .setHeader(HEADER_RECEIVER, receiver)
+ .setHeader(HEADER_USER_ID, userId)
+ .build();
+ }
+}
diff --git a/src/main/java/org/gridsuite/computation/service/ExecutionService.java b/src/main/java/org/gridsuite/computation/service/ExecutionService.java
new file mode 100644
index 0000000..9e97db8
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/service/ExecutionService.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2024, 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.computation.service;
+
+import com.powsybl.computation.ComputationManager;
+import com.powsybl.computation.local.LocalComputationManager;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import lombok.Getter;
+import lombok.SneakyThrows;
+import org.springframework.stereotype.Service;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+
+/**
+ * @author David Braquart
+ */
+@Service
+@Getter
+public class ExecutionService {
+
+ private ExecutorService executorService;
+
+ private ComputationManager computationManager;
+
+ @SneakyThrows
+ @PostConstruct
+ private void postConstruct() {
+ executorService = Executors.newCachedThreadPool();
+ computationManager = new LocalComputationManager(getExecutorService());
+ }
+
+ @PreDestroy
+ private void preDestroy() {
+ executorService.shutdown();
+ }
+}
diff --git a/src/main/java/org/gridsuite/computation/service/NotificationService.java b/src/main/java/org/gridsuite/computation/service/NotificationService.java
new file mode 100644
index 0000000..d3aa71d
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/service/NotificationService.java
@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2022, 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.computation.service;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.gridsuite.computation.utils.annotations.PostCompletion;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.stream.function.StreamBridge;
+import org.springframework.lang.Nullable;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.support.MessageBuilder;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * @author Etienne Homer message) {
+ RUN_MESSAGE_LOGGER.debug(SENDING_MESSAGE, message);
+ publisher.send(publishPrefix + "Run-out-0", message);
+ }
+
+ public void sendCancelMessage(Message message) {
+ CANCEL_MESSAGE_LOGGER.debug(SENDING_MESSAGE, message);
+ publisher.send(publishPrefix + "Cancel-out-0", message);
+ }
+
+ @PostCompletion
+ public void sendResultMessage(UUID resultUuid, String receiver, String userId, @Nullable Map additionalHeaders) {
+ MessageBuilder builder = MessageBuilder
+ .withPayload("")
+ .setHeader(HEADER_RESULT_UUID, resultUuid.toString())
+ .setHeader(HEADER_RECEIVER, receiver)
+ .setHeader(HEADER_USER_ID, userId)
+ .copyHeaders(additionalHeaders);
+ Message message = builder.build();
+ RESULT_MESSAGE_LOGGER.debug(SENDING_MESSAGE, message);
+ publisher.send(publishPrefix + "Result-out-0", message);
+ }
+
+ @PostCompletion
+ public void publishStop(UUID resultUuid, String receiver, String computationLabel) {
+ Message message = MessageBuilder
+ .withPayload("")
+ .setHeader(HEADER_RESULT_UUID, resultUuid.toString())
+ .setHeader(HEADER_RECEIVER, receiver)
+ .setHeader(HEADER_MESSAGE, getCancelMessage(computationLabel))
+ .build();
+ STOP_MESSAGE_LOGGER.debug(SENDING_MESSAGE, message);
+ publisher.send(publishPrefix + "Stopped-out-0", message);
+ }
+
+ @PostCompletion
+ public void publishCancelFailed(UUID resultUuid, String receiver, String computationLabel, String userId) {
+ Message message = MessageBuilder
+ .withPayload("")
+ .setHeader(HEADER_RESULT_UUID, resultUuid.toString())
+ .setHeader(HEADER_RECEIVER, receiver)
+ .setHeader(HEADER_USER_ID, userId)
+ .setHeader(HEADER_MESSAGE, getCancelFailedMessage(computationLabel))
+ .build();
+ CANCEL_FAILED_MESSAGE_LOGGER.info(SENDING_MESSAGE, message);
+ publisher.send(publishPrefix + "CancelFailed-out-0", message);
+ }
+
+ public static String getCancelMessage(String computationLabel) {
+ return computationLabel + " was canceled";
+ }
+
+ public static String getFailedMessage(String computationLabel) {
+ return computationLabel + " has failed";
+ }
+
+ public static String getCancelFailedMessage(String computationLabel) {
+ return computationLabel + " could not be cancelled";
+ }
+}
diff --git a/src/main/java/org/gridsuite/computation/service/ReportService.java b/src/main/java/org/gridsuite/computation/service/ReportService.java
new file mode 100644
index 0000000..7ebf468
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/service/ReportService.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2022, 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.computation.service;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.powsybl.commons.PowsyblException;
+import com.powsybl.commons.report.ReportNode;
+import lombok.Setter;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * @author Anis Touri
+ */
+@Service
+public class ReportService {
+
+ static final String REPORT_API_VERSION = "v1";
+ private static final String DELIMITER = "/";
+ private static final String QUERY_PARAM_REPORT_THROW_ERROR = "errorOnReportNotFound";
+ @Setter
+ private String reportServerBaseUri;
+
+ private final RestTemplate restTemplate;
+
+ private final ObjectMapper objectMapper;
+
+ public ReportService(ObjectMapper objectMapper,
+ @Value("${gridsuite.services.report-server.base-uri:http://report-server/}") String reportServerBaseUri,
+ RestTemplate restTemplate) {
+ this.reportServerBaseUri = reportServerBaseUri;
+ this.objectMapper = objectMapper;
+ this.restTemplate = restTemplate;
+ }
+
+ private String getReportServerURI() {
+ return this.reportServerBaseUri + DELIMITER + REPORT_API_VERSION + DELIMITER + "reports" + DELIMITER;
+ }
+
+ public void sendReport(UUID reportUuid, ReportNode reportNode) {
+ Objects.requireNonNull(reportUuid);
+
+ var path = UriComponentsBuilder.fromPath("{reportUuid}")
+ .buildAndExpand(reportUuid)
+ .toUriString();
+ var headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+
+ try {
+ String str = objectMapper.writeValueAsString(reportNode);
+ restTemplate.exchange(getReportServerURI() + path, HttpMethod.PUT, new HttpEntity<>(str, headers), ReportNode.class);
+ } catch (JsonProcessingException error) {
+ throw new PowsyblException("Error sending report", error);
+ }
+ }
+
+ public void deleteReport(UUID reportUuid) {
+ Objects.requireNonNull(reportUuid);
+
+ var path = UriComponentsBuilder.fromPath("{reportUuid}")
+ .queryParam(QUERY_PARAM_REPORT_THROW_ERROR, false)
+ .buildAndExpand(reportUuid)
+ .toUriString();
+ var headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ restTemplate.exchange(getReportServerURI() + path, HttpMethod.DELETE, new HttpEntity<>(headers), Void.class);
+ }
+}
diff --git a/src/main/java/org/gridsuite/computation/service/UuidGeneratorService.java b/src/main/java/org/gridsuite/computation/service/UuidGeneratorService.java
new file mode 100644
index 0000000..70f2ef7
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/service/UuidGeneratorService.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2024, 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.computation.service;
+
+import org.springframework.stereotype.Service;
+
+import java.util.UUID;
+
+/**
+ * @author Geoffroy Jamgotchian
+ */
+@Service
+public class UuidGeneratorService {
+
+ public UUID generate() {
+ return UUID.randomUUID();
+ }
+}
diff --git a/src/main/java/org/gridsuite/computation/specification/AbstractCommonSpecificationBuilder.java b/src/main/java/org/gridsuite/computation/specification/AbstractCommonSpecificationBuilder.java
new file mode 100644
index 0000000..aba1daf
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/specification/AbstractCommonSpecificationBuilder.java
@@ -0,0 +1,85 @@
+/**
+ * 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.computation.specification;
+
+import jakarta.persistence.criteria.Path;
+import jakarta.persistence.criteria.Root;
+import lombok.NoArgsConstructor;
+import org.gridsuite.computation.dto.ResourceFilterDTO;
+import org.gridsuite.computation.utils.SpecificationUtils;
+import org.springframework.data.jpa.domain.Specification;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * @author Kevin LE SAULNIER
+ */
+@NoArgsConstructor
+public abstract class AbstractCommonSpecificationBuilder {
+
+ public Specification resultUuidEquals(UUID value) {
+ return (root, cq, cb) -> cb.equal(getResultIdPath(root), value);
+ }
+
+ public Specification uuidIn(List uuids) {
+ return (root, cq, cb) -> root.get(getIdFieldName()).in(uuids);
+ }
+
+ /**
+ * @param distinct : true if you want to force the results to be distinct.
+ * Since sql joins generates duplicate results, we may need to use distinct here
+ * But can't use both distinct and sort on nested field (sql limitation)
+ */
+ public Specification buildSpecification(UUID resultUuid, List resourceFilters, boolean distinct) {
+ List childrenFilters = resourceFilters != null ? resourceFilters.stream().filter(this::isNotParentFilter).toList() : List.of();
+ // filter by resultUuid
+ Specification specification = Specification.where(resultUuidEquals(resultUuid));
+ if (distinct) {
+ specification = specification.and(SpecificationUtils.distinct());
+ }
+ if (childrenFilters.isEmpty()) {
+ Specification spec = addSpecificFilterWhenNoChildrenFilter();
+ if (spec != null) {
+ specification = specification.and(spec);
+ }
+ } else {
+ // needed here to filter main entities that would have empty collection when filters are applied
+ Specification spec = addSpecificFilterWhenChildrenFilters();
+ if (spec != null) {
+ specification = specification.and(spec);
+ }
+ }
+
+ return SpecificationUtils.appendFiltersToSpecification(specification, resourceFilters);
+ }
+
+ public Specification buildSpecification(UUID resultUuid, List resourceFilters) {
+ return buildSpecification(resultUuid, resourceFilters, true);
+ }
+
+ public Specification buildLimitViolationsSpecification(List uuids, List resourceFilters) {
+ List childrenFilters = resourceFilters.stream().filter(this::isNotParentFilter).toList();
+ Specification specification = Specification.where(uuidIn(uuids));
+
+ return SpecificationUtils.appendFiltersToSpecification(specification, childrenFilters);
+ }
+
+ public Specification addSpecificFilterWhenChildrenFilters() {
+ return null;
+ }
+
+ public Specification addSpecificFilterWhenNoChildrenFilter() {
+ return null;
+ }
+
+ public abstract boolean isNotParentFilter(ResourceFilterDTO filter);
+
+ public abstract String getIdFieldName();
+
+ public abstract Path getResultIdPath(Root root);
+}
diff --git a/src/main/java/org/gridsuite/computation/utils/ComputationResultUtils.java b/src/main/java/org/gridsuite/computation/utils/ComputationResultUtils.java
new file mode 100644
index 0000000..6a5ddc4
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/utils/ComputationResultUtils.java
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2024, 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.computation.utils;
+
+import com.powsybl.iidm.network.*;
+import com.powsybl.security.BusBreakerViolationLocation;
+import com.powsybl.security.LimitViolation;
+import com.powsybl.security.NodeBreakerViolationLocation;
+import com.powsybl.security.ViolationLocation;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static com.powsybl.iidm.network.IdentifiableType.BUSBAR_SECTION;
+import static com.powsybl.security.LimitViolationType.*;
+import static com.powsybl.security.ViolationLocation.Type.NODE_BREAKER;
+
+/**
+ * @author Jamal KHEYYAD
+ */
+public final class ComputationResultUtils {
+
+ private ComputationResultUtils() {
+ }
+
+ public static String getViolationLocationId(LimitViolation limitViolation, Network network) {
+ // LocationId only for voltage-based limit violations
+ if (!Set.of(LOW_VOLTAGE, HIGH_VOLTAGE, LOW_VOLTAGE_ANGLE, HIGH_VOLTAGE_ANGLE).contains(limitViolation.getLimitType())) {
+ return null;
+ }
+
+ Optional violationLocation = limitViolation.getViolationLocation();
+ if (violationLocation.isEmpty()) {
+ return limitViolation.getSubjectId();
+ }
+
+ ViolationLocation location = violationLocation.get();
+ if (location.getType() == NODE_BREAKER) {
+ return getNodeBreakerViolationLocationId((NodeBreakerViolationLocation) location, network);
+ } else {
+ return getBusBreakerViolationLocationId((BusBreakerViolationLocation) location, network, limitViolation.getSubjectId());
+ }
+ }
+
+ private static String getNodeBreakerViolationLocationId(NodeBreakerViolationLocation nodeBreakerViolationLocation, Network network) {
+ VoltageLevel vl = network.getVoltageLevel(nodeBreakerViolationLocation.getVoltageLevelId());
+
+ List busBarIds = nodeBreakerViolationLocation.getNodes().stream()
+ .map(node -> vl.getNodeBreakerView().getTerminal(node))
+ .filter(Objects::nonNull)
+ .map(Terminal::getConnectable)
+ .filter(t -> t.getType() == BUSBAR_SECTION)
+ .map(Identifiable::getId)
+ .distinct()
+ .toList();
+
+ String busId = null;
+ if (!busBarIds.isEmpty()) {
+ busId = getBusId(vl, new HashSet<>(busBarIds));
+ }
+ return formatViolationLocationId(busId != null ? List.of() : busBarIds, busId != null ? busId : nodeBreakerViolationLocation.getVoltageLevelId());
+ }
+
+ private static String getBusId(VoltageLevel voltageLevel, Set sjbIds) {
+ Optional bus = voltageLevel.getBusView()
+ .getBusStream()
+ .filter(b -> {
+ Set busSjbIds = b.getConnectedTerminalStream().map(Terminal::getConnectable).filter(c -> c.getType() == BUSBAR_SECTION).map(Connectable::getId).collect(Collectors.toSet());
+ return busSjbIds.equals(sjbIds);
+ })
+ .findFirst();
+ return bus.map(Identifiable::getId).orElse(null);
+ }
+
+ private static String formatViolationLocationId(List elementsIds, String subjectId) {
+ return !elementsIds.isEmpty() ?
+ subjectId + " (" + String.join(", ", elementsIds) + ")" :
+ subjectId;
+ }
+
+ private static String getBusBreakerViolationLocationId(BusBreakerViolationLocation busBreakerViolationLocation, Network network, String subjectId) {
+ List busBreakerIds = busBreakerViolationLocation
+ .getBusView(network)
+ .getBusStream()
+ .map(Identifiable::getId)
+ .distinct()
+ .toList();
+
+ return busBreakerIds.size() == 1 ? formatViolationLocationId(List.of(), busBreakerIds.getFirst()) : formatViolationLocationId(busBreakerIds, subjectId);
+ }
+
+}
diff --git a/src/main/java/org/gridsuite/computation/utils/FilterUtils.java b/src/main/java/org/gridsuite/computation/utils/FilterUtils.java
new file mode 100644
index 0000000..b503df5
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/utils/FilterUtils.java
@@ -0,0 +1,49 @@
+/**
+ * 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.computation.utils;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.gridsuite.computation.ComputationException;
+import org.gridsuite.computation.dto.GlobalFilter;
+import org.gridsuite.computation.dto.ResourceFilterDTO;
+
+import java.util.List;
+
+/**
+ * @author maissa Souissi
+ */
+public final class FilterUtils {
+
+ // Utility class, so no constructor
+ private FilterUtils() {
+ }
+
+ private static T fromStringToDTO(String jsonString, ObjectMapper objectMapper, TypeReference typeReference, T defaultValue) {
+ if (StringUtils.isEmpty(jsonString)) {
+ return defaultValue;
+ }
+ try {
+ return objectMapper.readValue(jsonString, typeReference);
+ } catch (JsonProcessingException e) {
+ throw new ComputationException(ComputationException.Type.INVALID_FILTER_FORMAT);
+ }
+ }
+
+ public static List fromStringFiltersToDTO(String stringFilters, ObjectMapper objectMapper) {
+ return fromStringToDTO(stringFilters, objectMapper, new TypeReference<>() {
+ }, List.of());
+ }
+
+ public static GlobalFilter fromStringGlobalFiltersToDTO(String stringGlobalFilters, ObjectMapper objectMapper) {
+ return fromStringToDTO(stringGlobalFilters, objectMapper, new TypeReference<>() {
+ }, null);
+ }
+}
+
diff --git a/src/main/java/org/gridsuite/computation/utils/MessageUtils.java b/src/main/java/org/gridsuite/computation/utils/MessageUtils.java
new file mode 100644
index 0000000..6fc5632
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/utils/MessageUtils.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2024, 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.computation.utils;
+
+import com.powsybl.commons.PowsyblException;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.lang.Nullable;
+import org.springframework.messaging.MessageHeaders;
+
+/**
+ * @author Thang PHAM
+ */
+public final class MessageUtils {
+ public static final int MSG_MAX_LENGTH = 256;
+
+ private MessageUtils() {
+ throw new AssertionError("Suppress default constructor for noninstantiability");
+ }
+
+ public static String getNonNullHeader(MessageHeaders headers, String name) {
+ String header = headers.get(name, String.class);
+ if (header == null) {
+ throw new PowsyblException("Header '" + name + "' not found");
+ }
+ return header;
+ }
+
+ /**
+ * Prevent the message from being too long for RabbitMQ.
+ * @apiNote the beginning and ending are both kept, it should make it easier to identify
+ */
+ public static String shortenMessage(@Nullable final String msg) {
+ return StringUtils.abbreviateMiddle(msg, " ... ", MSG_MAX_LENGTH);
+ }
+}
diff --git a/src/main/java/org/gridsuite/computation/utils/SpecificationUtils.java b/src/main/java/org/gridsuite/computation/utils/SpecificationUtils.java
new file mode 100644
index 0000000..74abf2c
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/utils/SpecificationUtils.java
@@ -0,0 +1,267 @@
+/**
+ * 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/.
+ */
+
+/**
+ * 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.computation.utils;
+
+import com.google.common.collect.Lists;
+import jakarta.persistence.criteria.*;
+import org.gridsuite.computation.dto.ResourceFilterDTO;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.data.jpa.repository.query.EscapeCharacter;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+
+import static org.springframework.data.jpa.domain.Specification.anyOf;
+import static org.springframework.data.jpa.domain.Specification.not;
+
+/**
+ * Utility class to create Spring Data JPA Specification (Spring interface for JPA Criteria API).
+ *
+ * @author Kevin Le Saulnier
+ */
+public final class SpecificationUtils {
+ /**
+ * Maximum values per IN clause chunk to avoid StackOverflow exceptions.
+ * Current value (500) is a safe default but can be changed
+ */
+ public static final int MAX_IN_CLAUSE_SIZE = 500;
+
+ public static final String FIELD_SEPARATOR = ".";
+
+ // Utility class, so no constructor
+ private SpecificationUtils() { }
+
+ // we use .as(String.class) to be able to works on enum fields
+ public static Specification equals(String field, String value) {
+ return (root, cq, cb) -> cb.equal(
+ cb
+ .upper(getColumnPath(root, field).as(String.class))
+ .as(String.class),
+ value.toUpperCase()
+ );
+ }
+
+ public static Specification notEqual(String field, String value) {
+ return (root, cq, cb) -> cb.notEqual(getColumnPath(root, field), value);
+ }
+
+ public static Specification contains(String field, String value) {
+ return (root, cq, cb) -> cb.like(cb.upper(getColumnPath(root, field).as(String.class)), "%" + EscapeCharacter.DEFAULT.escape(value).toUpperCase() + "%", EscapeCharacter.DEFAULT.getEscapeCharacter());
+ }
+
+ public static Specification startsWith(String field, String value) {
+ return (root, cq, cb) -> cb.like(cb.upper(getColumnPath(root, field).as(String.class)), EscapeCharacter.DEFAULT.escape(value).toUpperCase() + "%", EscapeCharacter.DEFAULT.getEscapeCharacter());
+ }
+
+ /**
+ * Returns a specification where the field value is not equal within the given tolerance.
+ */
+ public static Specification notEqual(String field, Double value, Double tolerance) {
+ return (root, cq, cb) -> {
+ Expression doubleExpression = getColumnPath(root, field).as(Double.class);
+ /*
+ * in order to be equal to doubleExpression, value has to fit :
+ * value - tolerance <= doubleExpression <= value + tolerance
+ * therefore in order to be different at least one of the opposite comparison needs to be true :
+ */
+ return cb.or(
+ cb.greaterThan(doubleExpression, value + tolerance),
+ cb.lessThan(doubleExpression, value - tolerance)
+ );
+ };
+ }
+
+ public static Specification lessThanOrEqual(String field, Double value, Double tolerance) {
+ return (root, cq, cb) -> {
+ Expression doubleExpression = getColumnPath(root, field).as(Double.class);
+ return cb.lessThanOrEqualTo(doubleExpression, value + tolerance);
+ };
+ }
+
+ public static Specification greaterThanOrEqual(String field, Double value, Double tolerance) {
+ return (root, cq, cb) -> {
+ Expression doubleExpression = getColumnPath(root, field).as(Double.class);
+ return cb.greaterThanOrEqualTo(doubleExpression, value - tolerance);
+ };
+ }
+
+ public static Specification isNotEmpty(String field) {
+ return (root, cq, cb) -> cb.isNotEmpty(getColumnPath(root, field));
+ }
+
+ public static Specification distinct() {
+ return (root, cq, cb) -> {
+ // to select distinct result, we need to set a "criteria query" param
+ // we don't need to return any predicate here
+ cq.distinct(true);
+ return null;
+ };
+ }
+
+ public static Specification appendFiltersToSpecification(Specification specification, List resourceFilters) {
+ Objects.requireNonNull(specification);
+
+ if (resourceFilters == null || resourceFilters.isEmpty()) {
+ return specification;
+ }
+
+ Specification completedSpecification = specification;
+
+ for (ResourceFilterDTO resourceFilter : resourceFilters) {
+ if (resourceFilter.dataType() == ResourceFilterDTO.DataType.TEXT) {
+ completedSpecification = appendTextFilterToSpecification(completedSpecification, resourceFilter);
+ } else if (resourceFilter.dataType() == ResourceFilterDTO.DataType.NUMBER) {
+ completedSpecification = appendNumberFilterToSpecification(completedSpecification, resourceFilter);
+ }
+ }
+
+ return completedSpecification;
+ }
+
+ @NotNull
+ private static Specification appendTextFilterToSpecification(Specification specification, ResourceFilterDTO resourceFilter) {
+ Specification completedSpecification = specification;
+
+ switch (resourceFilter.type()) {
+ case NOT_EQUAL, EQUALS, IN -> {
+ // this type can manage one value or a list of values (with OR)
+ if (resourceFilter.value() instanceof Collection> valueList) {
+ // implicitly an IN resourceFilter type because only IN may have value lists as filter value
+ List inValues = valueList.stream()
+ .map(Object::toString)
+ .toList();
+ completedSpecification = completedSpecification.and(
+ generateInSpecification(resourceFilter.column(), inValues)
+ );
+ } else if (resourceFilter.value() == null) {
+ // if the value is null, we build an impossible specification (trick to remove later on ?)
+ completedSpecification = completedSpecification.and(not(completedSpecification));
+ } else {
+ completedSpecification = completedSpecification.and(equals(resourceFilter.column(), resourceFilter.value().toString()));
+ }
+ }
+ case CONTAINS -> {
+ if (resourceFilter.value() instanceof Collection> valueList) {
+ completedSpecification = completedSpecification.and(
+ anyOf(
+ valueList
+ .stream()
+ .map(value -> SpecificationUtils.contains(resourceFilter.column(), value.toString()))
+ .toList()
+ ));
+ } else {
+ completedSpecification = completedSpecification.and(contains(resourceFilter.column(), resourceFilter.value().toString()));
+ }
+ }
+ case STARTS_WITH ->
+ completedSpecification = completedSpecification.and(startsWith(resourceFilter.column(), resourceFilter.value().toString()));
+ default -> throw new IllegalArgumentException("The filter type " + resourceFilter.type() + " is not supported with the data type " + resourceFilter.dataType());
+ }
+
+ return completedSpecification;
+ }
+
+ /**
+ * Generates a specification for IN clause with the given column and values.
+ * Handles large value lists by chunking them to avoid StackOverflow.
+ *
+ * @param column the column name to filter on
+ * @param inPossibleValues the list of values for the IN clause
+ * @return a specification for the IN clause
+ */
+ private static Specification generateInSpecification(String column, List inPossibleValues) {
+
+ if (inPossibleValues.size() > MAX_IN_CLAUSE_SIZE) {
+ // there are too many values for only one call to anyOf() : it might cause a StackOverflow
+ // => the specification is divided into several specifications which have an OR between them :
+ List> chunksOfInValues = Lists.partition(inPossibleValues, MAX_IN_CLAUSE_SIZE);
+ Specification containerSpec = null;
+ for (List chunk : chunksOfInValues) {
+ Specification multiOrEqualSpec = anyOf(
+ chunk
+ .stream()
+ .map(value -> SpecificationUtils.equals(column, value))
+ .toList()
+ );
+ if (containerSpec == null) {
+ containerSpec = multiOrEqualSpec;
+ } else {
+ containerSpec = containerSpec.or(multiOrEqualSpec);
+ }
+ }
+ return containerSpec;
+ }
+ return anyOf(inPossibleValues
+ .stream()
+ .map(value -> SpecificationUtils.equals(column, value))
+ .toList()
+ );
+ }
+
+ @NotNull
+ private static Specification appendNumberFilterToSpecification(Specification specification, ResourceFilterDTO resourceFilter) {
+ String filterValue = resourceFilter.value().toString();
+ double tolerance;
+ if (resourceFilter.tolerance() != null) {
+ tolerance = resourceFilter.tolerance();
+ } else {
+ // the reference for the comparison is the number of digits after the decimal point in filterValue
+ // extra digits are ignored, but the user may add '0's after the decimal point in order to get a better precision
+ String[] splitValue = filterValue.split("\\.");
+ int numberOfDecimalAfterDot = 0;
+ if (splitValue.length > 1) {
+ numberOfDecimalAfterDot = splitValue[1].length();
+ }
+ // tolerance is multiplied by 0.5 to simulate the fact that the database value is rounded (in the front, from the user viewpoint)
+ // more than 13 decimal after dot will likely cause rounding errors due to double precision
+ tolerance = Math.pow(10, -numberOfDecimalAfterDot) * 0.5;
+ }
+ Double valueDouble = Double.valueOf(filterValue);
+ return switch (resourceFilter.type()) {
+ case NOT_EQUAL -> specification.and(notEqual(resourceFilter.column(), valueDouble, tolerance));
+ case LESS_THAN_OR_EQUAL ->
+ specification.and(lessThanOrEqual(resourceFilter.column(), valueDouble, tolerance));
+ case GREATER_THAN_OR_EQUAL ->
+ specification.and(greaterThanOrEqual(resourceFilter.column(), valueDouble, tolerance));
+ default ->
+ throw new IllegalArgumentException("The filter type " + resourceFilter.type() + " is not supported with the data type " + resourceFilter.dataType());
+ };
+ }
+
+ /**
+ * This method allow to query eventually dot separated fields with the Criteria API
+ * Ex : from 'fortescueCurrent.positiveMagnitude' we create the query path
+ * root.get("fortescueCurrent").get("positiveMagnitude") to access to the correct nested field
+ *
+ * @param root the root entity
+ * @param dotSeparatedFields dot separated fields (can be only one field without any dot)
+ * @param the entity type referenced by the root
+ * @param the type referenced by the path
+ * @return path for the query
+ */
+ private static Path getColumnPath(Root root, String dotSeparatedFields) {
+ if (dotSeparatedFields.contains(SpecificationUtils.FIELD_SEPARATOR)) {
+ String[] fields = dotSeparatedFields.split("\\.");
+ Path path = root.get(fields[0]);
+ for (int i = 1; i < fields.length; i++) {
+ path = path.get(fields[i]);
+ }
+ return path;
+ } else {
+ return root.get(dotSeparatedFields);
+ }
+ }
+}
diff --git a/src/main/java/org/gridsuite/computation/utils/annotations/PostCompletion.java b/src/main/java/org/gridsuite/computation/utils/annotations/PostCompletion.java
new file mode 100644
index 0000000..1230927
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/utils/annotations/PostCompletion.java
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2023, 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.computation.utils.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author Anis Touri > RUNNABLE = new ThreadLocal<>();
+
+ // register a new runnable for post completion execution
+ public void execute(Runnable runnable) {
+ if (TransactionSynchronizationManager.isSynchronizationActive()) {
+ List runnables = RUNNABLE.get();
+ if (runnables == null) {
+ runnables = new LinkedList<>();
+ RUNNABLE.set(runnables);
+ }
+ runnables.add(runnable);
+ TransactionSynchronizationManager.registerSynchronization(this);
+ } else {
+ // if transaction synchronisation is not active
+ runnable.run();
+ }
+ }
+
+ @Override
+ public void afterCompletion(int status) {
+ List runnables = RUNNABLE.get();
+ runnables.forEach(Runnable::run);
+ RUNNABLE.remove();
+ }
+}
diff --git a/src/main/java/org/gridsuite/computation/utils/annotations/PostCompletionAnnotationAspect.java b/src/main/java/org/gridsuite/computation/utils/annotations/PostCompletionAnnotationAspect.java
new file mode 100644
index 0000000..1e82bef
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/utils/annotations/PostCompletionAnnotationAspect.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2023, 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.computation.utils.annotations;
+
+import lombok.AllArgsConstructor;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Anis Touri {
+ try {
+ pjp.proceed(pjp.getArgs());
+ } catch (Throwable e) {
+ throw new PostCompletionException(e);
+ }
+ });
+ return null;
+ }
+}
diff --git a/src/main/java/org/gridsuite/computation/utils/annotations/PostCompletionException.java b/src/main/java/org/gridsuite/computation/utils/annotations/PostCompletionException.java
new file mode 100644
index 0000000..0250eab
--- /dev/null
+++ b/src/main/java/org/gridsuite/computation/utils/annotations/PostCompletionException.java
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2024, 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.computation.utils.annotations;
+
+/**
+ * @author Slimane Amar
+ */
+public class PostCompletionException extends RuntimeException {
+ public PostCompletionException(Throwable t) {
+ super(t);
+ }
+}
diff --git a/src/test/java/org/gridsuite/computation/ComputationExceptionTest.java b/src/test/java/org/gridsuite/computation/ComputationExceptionTest.java
new file mode 100644
index 0000000..fb9b670
--- /dev/null
+++ b/src/test/java/org/gridsuite/computation/ComputationExceptionTest.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2024, 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.computation;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * @author Joris Mancini
+ */
+class ComputationExceptionTest {
+
+ @Test
+ void testMessageConstructor() {
+ var e = new ComputationException("test");
+ assertEquals("test", e.getMessage());
+ }
+
+ @Test
+ void testMessageAndThrowableConstructor() {
+ var cause = new RuntimeException("test");
+ var e = new ComputationException("test", cause);
+ assertEquals("test", e.getMessage());
+ assertEquals(cause, e.getCause());
+ }
+}
diff --git a/src/test/java/org/gridsuite/computation/ComputationTest.java b/src/test/java/org/gridsuite/computation/ComputationTest.java
new file mode 100644
index 0000000..5572102
--- /dev/null
+++ b/src/test/java/org/gridsuite/computation/ComputationTest.java
@@ -0,0 +1,335 @@
+/**
+ * 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.computation;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.powsybl.iidm.network.Network;
+import com.powsybl.iidm.network.VariantManager;
+import com.powsybl.network.store.client.NetworkStoreService;
+import com.powsybl.network.store.client.PreloadingStrategy;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import io.micrometer.observation.ObservationRegistry;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.assertj.core.api.WithAssertions;
+import org.gridsuite.computation.dto.ReportInfos;
+import org.gridsuite.computation.service.*;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.cloud.stream.function.StreamBridge;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.support.MessageBuilder;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+import static org.gridsuite.computation.service.NotificationService.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+@ExtendWith({ MockitoExtension.class })
+@Slf4j
+class ComputationTest implements WithAssertions {
+ private static final String COMPUTATION_TYPE = "mockComputation";
+ @Mock
+ private VariantManager variantManager;
+ @Mock
+ private NetworkStoreService networkStoreService;
+ @Mock
+ private ReportService reportService;
+ private final ExecutionService executionService = new ExecutionService();
+ private final UuidGeneratorService uuidGeneratorService = new UuidGeneratorService();
+ @Mock
+ private StreamBridge publisher;
+ private NotificationService notificationService;
+ @Mock
+ private ObjectMapper objectMapper;
+ @Mock
+ private Network network;
+
+ private enum MockComputationStatus {
+ NOT_DONE,
+ RUNNING,
+ COMPLETED
+ }
+
+ private static class MockComputationResultService extends AbstractComputationResultService {
+ Map mockDBStatus = new HashMap<>();
+
+ @Override
+ public void insertStatus(List resultUuids, MockComputationStatus status) {
+ resultUuids.forEach(uuid ->
+ mockDBStatus.put(uuid, status));
+ }
+
+ @Override
+ public void delete(UUID resultUuid) {
+ mockDBStatus.remove(resultUuid);
+ }
+
+ @Override
+ public void deleteAll() {
+ mockDBStatus.clear();
+ }
+
+ @Override
+ public MockComputationStatus findStatus(UUID resultUuid) {
+ return mockDBStatus.get(resultUuid);
+ }
+ }
+
+ private static class MockComputationObserver extends AbstractComputationObserver