Skip to content

Commit c1d919f

Browse files
authored
add zipped export result csv (#74)
Signed-off-by: Ghazwa REHILI <[email protected]>
1 parent 6e9ef33 commit c1d919f

File tree

7 files changed

+208
-2
lines changed

7 files changed

+208
-2
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Copyright (c) 2024, RTE (http://www.rte-france.com)
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
package org.gridsuite.shortcircuit.server;
9+
10+
import org.springframework.http.HttpStatus;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.web.bind.annotation.ControllerAdvice;
13+
import org.springframework.web.bind.annotation.ExceptionHandler;
14+
15+
/**
16+
* @author Ghazwa Rehili <ghazwa.rehili at rte-france.com>
17+
*/
18+
@ControllerAdvice
19+
public class RestResponseEntityExceptionHandler {
20+
@ExceptionHandler(ShortCircuitException.class)
21+
protected ResponseEntity<Object> handleShortCircuitException(ShortCircuitException exception) {
22+
return switch (exception.getType()) {
23+
case RESULT_NOT_FOUND -> ResponseEntity.status(HttpStatus.NOT_FOUND).body(exception.getMessage());
24+
case INVALID_EXPORT_PARAMS -> ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exception.getMessage());
25+
case BUS_OUT_OF_VOLTAGE, FILE_EXPORT_ERROR ->
26+
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(exception.getMessage());
27+
};
28+
}
29+
}

src/main/java/org/gridsuite/shortcircuit/server/ShortCircuitController.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
import static com.powsybl.shortcircuit.Fault.FaultType;
3131
import static org.gridsuite.shortcircuit.server.service.NotificationService.HEADER_USER_ID;
32-
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
32+
import static org.springframework.http.MediaType.*;
3333

3434
/**
3535
* @author Etienne Homer <etienne.homer at rte-france.com>
@@ -82,6 +82,18 @@ public ResponseEntity<ShortCircuitAnalysisResult> getResult(@Parameter(descripti
8282
: ResponseEntity.notFound().build();
8383
}
8484

85+
@PostMapping(value = "/results/{resultUuid}/csv", produces = APPLICATION_OCTET_STREAM_VALUE)
86+
@Operation(summary = "Get a short circuit analysis csv result from the database")
87+
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The short circuit analysis csv export"),
88+
@ApiResponse(responseCode = "404", description = "Short circuit analysis result has not been found")})
89+
public ResponseEntity<byte[]> getZippedCsvExportFaultResult(
90+
@Parameter(description = "Result UUID") @PathVariable("resultUuid") UUID resultUuid,
91+
@Parameter(description = "Csv headers and translations payload") @RequestBody CsvTranslation csvTranslation) {
92+
return ResponseEntity.ok()
93+
.contentType(MediaType.parseMediaType(APPLICATION_OCTET_STREAM_VALUE))
94+
.body(shortCircuitService.getZippedCsvExportResult(resultUuid, csvTranslation));
95+
}
96+
8597
@GetMapping(value = "/results/{resultUuid}/fault_results/paged", produces = APPLICATION_JSON_VALUE)
8698
@Operation(summary = "Get a fault results page for a given short circuit analysis result")
8799
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The page of fault results"),

src/main/java/org/gridsuite/shortcircuit/server/ShortCircuitException.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
public class ShortCircuitException extends RuntimeException {
1818

1919
public enum Type {
20-
BUS_OUT_OF_VOLTAGE
20+
BUS_OUT_OF_VOLTAGE,
21+
RESULT_NOT_FOUND,
22+
INVALID_EXPORT_PARAMS,
23+
FILE_EXPORT_ERROR,
2124
}
2225

2326
private final Type type;
@@ -31,4 +34,8 @@ public ShortCircuitException(Type type, String message) {
3134
super(message);
3235
this.type = type;
3336
}
37+
38+
public Type getType() {
39+
return type;
40+
}
3441
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.gridsuite.shortcircuit.server.dto;
2+
3+
import lombok.Builder;
4+
5+
import java.util.List;
6+
import java.util.Map;
7+
8+
@Builder
9+
public record CsvTranslation(
10+
List<String> headersCsv,
11+
Map<String, String> enumValueTranslations
12+
) { }

src/main/java/org/gridsuite/shortcircuit/server/service/ShortCircuitService.java

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
import com.fasterxml.jackson.databind.ObjectMapper;
1010
import com.powsybl.ws.commons.LogUtils;
11+
import com.univocity.parsers.csv.CsvWriter;
12+
import com.univocity.parsers.csv.CsvWriterSettings;
13+
import org.gridsuite.shortcircuit.server.ShortCircuitException;
1114
import org.gridsuite.shortcircuit.server.dto.*;
1215
import org.gridsuite.shortcircuit.server.entities.*;
1316
import org.gridsuite.shortcircuit.server.repositories.ShortCircuitAnalysisResultRepository;
@@ -19,10 +22,15 @@
1922
import org.springframework.stereotype.Service;
2023
import org.springframework.transaction.annotation.Transactional;
2124

25+
import java.io.*;
2226
import java.util.*;
2327
import java.util.concurrent.TimeUnit;
2428
import java.util.concurrent.atomic.AtomicReference;
2529
import java.util.stream.Collectors;
30+
import java.util.zip.ZipEntry;
31+
import java.util.zip.ZipOutputStream;
32+
33+
import static org.gridsuite.shortcircuit.server.ShortCircuitException.Type.*;
2634

2735
/**
2836
* @author Etienne Homer <etienne.homer at rte-france.com>
@@ -110,6 +118,83 @@ private static ShortCircuitAnalysisResultEntity sortByElementId(ShortCircuitAnal
110118
return result;
111119
}
112120

121+
public byte[] exportToCsv(ShortCircuitAnalysisResult result, List<String> headersList, Map<String, String> enumValueTranslations) {
122+
List<FaultResult> faultResults = result.getFaults();
123+
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
124+
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
125+
CsvWriterSettings settings = new CsvWriterSettings();
126+
CsvWriter csvWriter = new CsvWriter(zipOutputStream, settings);
127+
zipOutputStream.putNextEntry(new ZipEntry("shortCircuit_result.csv"));
128+
csvWriter.writeHeaders(headersList);
129+
130+
for (FaultResult faultResult : faultResults) {
131+
// Process faultResult data
132+
List<String> faultRowData = new ArrayList<>(List.of(
133+
faultResult.getFault().getId(),
134+
enumValueTranslations.getOrDefault(faultResult.getFault().getFaultType(), ""),
135+
"",
136+
Double.toString(faultResult.getPositiveMagnitude())
137+
));
138+
139+
List<LimitViolation> limitViolations = faultResult.getLimitViolations();
140+
if (!limitViolations.isEmpty()) {
141+
String limitTypes = limitViolations.stream()
142+
.map(LimitViolation::getLimitType)
143+
.map(type -> enumValueTranslations.getOrDefault(type, ""))
144+
.collect(Collectors.joining(", "));
145+
faultRowData.add(limitTypes);
146+
} else {
147+
faultRowData.add("");
148+
}
149+
150+
ShortCircuitLimits shortCircuitLimits = faultResult.getShortCircuitLimits();
151+
faultRowData.addAll(List.of(
152+
Double.toString(shortCircuitLimits.getIpMin()),
153+
Double.toString(shortCircuitLimits.getIpMax()),
154+
Double.toString(faultResult.getShortCircuitPower()),
155+
Double.toString(shortCircuitLimits.getDeltaCurrentIpMin()),
156+
Double.toString(shortCircuitLimits.getDeltaCurrentIpMax())
157+
));
158+
159+
csvWriter.writeRow(faultRowData);
160+
161+
// Process feederResults data
162+
List<FeederResult> feederResults = faultResult.getFeederResults();
163+
if (!feederResults.isEmpty()) {
164+
for (FeederResult feederResult : feederResults) {
165+
List<String> feederRowData = new ArrayList<>(List.of(
166+
faultResult.getFault().getId(),
167+
"",
168+
feederResult.getConnectableId(),
169+
Double.toString(feederResult.getPositiveMagnitude())
170+
));
171+
csvWriter.writeRow(feederRowData);
172+
}
173+
} else {
174+
csvWriter.writeRow(List.of("", "", "", ""));
175+
}
176+
}
177+
178+
csvWriter.close();
179+
return outputStream.toByteArray();
180+
} catch (IOException e) {
181+
throw new ShortCircuitException(FILE_EXPORT_ERROR, e.getMessage());
182+
}
183+
}
184+
185+
public byte[] getZippedCsvExportResult(UUID resultUuid, CsvTranslation csvTranslation) {
186+
ShortCircuitAnalysisResult result = getResult(resultUuid, FaultResultsMode.FULL);
187+
if (result == null) {
188+
throw new ShortCircuitException(RESULT_NOT_FOUND, "The short circuit analysis result '" + resultUuid + "' does not exist");
189+
}
190+
if (Objects.isNull(csvTranslation) || Objects.isNull(csvTranslation.headersCsv()) || Objects.isNull(csvTranslation.enumValueTranslations())) {
191+
throw new ShortCircuitException(INVALID_EXPORT_PARAMS, "Missing information to export short-circuit result as csv: file headers and enum translation must be provided");
192+
}
193+
List<String> headersList = csvTranslation.headersCsv();
194+
Map<String, String> enumValueTranslations = csvTranslation.enumValueTranslations();
195+
return exportToCsv(result, headersList, enumValueTranslations);
196+
}
197+
113198
public ShortCircuitAnalysisResult getResult(UUID resultUuid, FaultResultsMode mode) {
114199
AtomicReference<Long> startTime = new AtomicReference<>();
115200
startTime.set(System.nanoTime());

src/test/java/org/gridsuite/shortcircuit/server/ShortCircuitAnalysisControllerTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.powsybl.security.LimitViolationType;
2525
import com.powsybl.shortcircuit.*;
2626
import lombok.SneakyThrows;
27+
import org.gridsuite.shortcircuit.server.dto.CsvTranslation;
2728
import org.gridsuite.shortcircuit.server.dto.ShortCircuitAnalysisStatus;
2829
import org.gridsuite.shortcircuit.server.entities.FaultEmbeddable;
2930
import org.gridsuite.shortcircuit.server.entities.FaultResultEntity;
@@ -52,13 +53,15 @@
5253
import org.springframework.test.web.servlet.MockMvc;
5354
import org.springframework.test.web.servlet.MvcResult;
5455

56+
import java.nio.charset.StandardCharsets;
5557
import java.time.LocalDateTime;
5658
import java.time.temporal.ChronoUnit;
5759
import java.util.*;
5860
import java.util.concurrent.CompletableFuture;
5961
import java.util.stream.Collectors;
6062

6163
import static com.powsybl.network.store.model.NetworkStoreApi.VERSION;
64+
import static org.gridsuite.shortcircuit.server.TestUtils.unzip;
6265
import static java.util.Comparator.comparing;
6366
import static java.util.Comparator.comparingDouble;
6467
import static org.gridsuite.shortcircuit.server.service.NotificationService.*;
@@ -96,6 +99,32 @@ public class ShortCircuitAnalysisControllerTest {
9699
private static final String VARIANT_4_ID = "variant_4";
97100
private static final String NODE_BREAKER_NETWORK_VARIANT_ID = "node_breaker_network_variant_id";
98101

102+
private static final List<String> CSV_HEADERS = List.of(
103+
"ID nœud",
104+
"Type",
105+
"Départs",
106+
"Icc (kA)",
107+
"Type de limite",
108+
"Icc min (kA)",
109+
"IMACC (kA)",
110+
"Pcc (MVA)",
111+
"Icc - Icc min (kA)",
112+
"Icc - IMACC (kA)"
113+
);
114+
115+
private final Map<String, String> enumTranslations = Map.of(
116+
"THREE_PHASE", "Triphasé",
117+
"SINGLE_PHASE", "Monophasé",
118+
"ACTIVE_POWER", "Puissance active",
119+
"APPARENT_POWER", "Puissance apparente",
120+
"CURRENT", "Intensité",
121+
"LOW_VOLTAGE", "Tension basse",
122+
"HIGH_VOLTAGE", "Tension haute",
123+
"LOW_SHORT_CIRCUIT_CURRENT", "Icc min",
124+
"HIGH_SHORT_CIRCUIT_CURRENT", "Icc max",
125+
"OTHER", "Autre"
126+
);
127+
99128
private static final int TIMEOUT = 1000;
100129

101130
private static final class ShortCircuitAnalysisResultMock {
@@ -422,6 +451,21 @@ public void runTest() throws Exception {
422451
expectedResultsIdInOrder = faultsFromDatabase.stream().sorted(comparatorByFaultIdDescAndResultUuid).map(faultResultEntity -> faultResultEntity.getFault().getId()).toList();
423452
assertPagedFaultResultsEquals(ShortCircuitAnalysisResultMock.RESULT_SORTED_PAGE_1, faultResultsPageDto1Full, expectedResultsIdInOrder);
424453

454+
// export zipped csv result
455+
result = mockMvc.perform(post(
456+
"/" + VERSION + "/results/{resultUuid}/csv", RESULT_UUID)
457+
.contentType(MediaType.APPLICATION_JSON)
458+
.content(mapper.writeValueAsString(CsvTranslation.builder().headersCsv(CSV_HEADERS).
459+
enumValueTranslations(enumTranslations).build())))
460+
.andExpectAll(status().isOk(), content().contentType(MediaType.APPLICATION_OCTET_STREAM))
461+
.andReturn();
462+
byte[] zipFile = result.getResponse().getContentAsByteArray();
463+
byte[] unzippedCsvFile = unzip(zipFile);
464+
String unzippedCsvFileAsString = new String(unzippedCsvFile, StandardCharsets.UTF_8);
465+
List<String> actualCsvLines = List.of(Arrays.asList(unzippedCsvFileAsString.split("\n")).get(0).split(","));
466+
List<String> expectedLines = new ArrayList<>(CSV_HEADERS);
467+
assertEquals(expectedLines, actualCsvLines);
468+
425469
// should throw not found if result does not exist
426470
mockMvc.perform(get("/" + VERSION + "/results/{resultUuid}", OTHER_RESULT_UUID))
427471
.andExpect(status().isNotFound());

src/test/java/org/gridsuite/shortcircuit/server/TestUtils.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616
import org.mockito.Mockito;
1717
import org.springframework.cloud.stream.binder.test.OutputDestination;
1818

19+
import java.io.ByteArrayInputStream;
20+
import java.io.ByteArrayOutputStream;
1921
import java.lang.reflect.Constructor;
2022
import java.util.List;
2123
import java.util.UUID;
24+
import java.util.zip.ZipInputStream;
2225

2326
import static com.vladmihalcea.sql.SQLStatementCountValidator.*;
2427
import static com.vladmihalcea.sql.SQLStatementCountValidator.assertDeleteCount;
@@ -86,4 +89,18 @@ public static void assertRequestsCount(long select, long insert, long update, lo
8689
assertUpdateCount(update);
8790
assertDeleteCount(delete);
8891
}
92+
93+
public static byte[] unzip(byte[] zippedBytes) throws Exception {
94+
var zipInputStream = new ZipInputStream(new ByteArrayInputStream(zippedBytes));
95+
var buff = new byte[1024];
96+
if (zipInputStream.getNextEntry() != null) {
97+
var outputStream = new ByteArrayOutputStream();
98+
int l;
99+
while ((l = zipInputStream.read(buff)) > 0) {
100+
outputStream.write(buff, 0, l);
101+
}
102+
return outputStream.toByteArray();
103+
}
104+
return new byte[0];
105+
}
89106
}

0 commit comments

Comments
 (0)