Skip to content

Commit e352c01

Browse files
committed
HTM-1963: Add Excel extract output and integration tests
1 parent f2e1022 commit e352c01

2 files changed

Lines changed: 108 additions & 4 deletions

File tree

src/main/java/org/tailormap/api/service/CreateLayerExtractService.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.springframework.stereotype.Service;
5252
import org.springframework.transaction.annotation.Transactional;
5353
import org.tailormap.api.controller.LayerExtractController;
54+
import org.tailormap.api.geotools.data.excel.ExcelDataStoreFactory;
5455
import org.tailormap.api.geotools.featuresources.FeatureSourceFactoryHelper;
5556
import org.tailormap.api.persistence.TMFeatureType;
5657
import org.tailormap.api.util.UUIDv7;
@@ -65,7 +66,6 @@ public class CreateLayerExtractService {
6566
private final SseEventBus eventBus;
6667
private final JsonMapper jsonMapper;
6768
private final FeatureSourceFactoryHelper featureSourceFactoryHelper;
68-
6969
private final FilterFactory ff = CommonFactoryFinder.getFilterFactory(GeoTools.getDefaultHints());
7070

7171
// we can safely use the tmp dir as a default here because we are running in a docker container so access is limited
@@ -223,7 +223,8 @@ public void createLayerExtract(
223223
logger.debug("Filtered source counts {}", featCount);
224224
final AtomicInteger featsAdded = new AtomicInteger();
225225

226-
FileDataStore outputDataStore = getExtractDataStore(extractOutputFormat, outputFileName, clientId);
226+
FileDataStore outputDataStore =
227+
getExtractDataStore(extractOutputFormat, outputFileName, clientId, inputTmFeatureType.getName());
227228
SimpleFeatureType fType =
228229
DataUtilities.createSubType(inputFeatureSource.getSchema(), attributes.toArray(new String[0]));
229230
outputDataStore.createSchema(fType);
@@ -268,7 +269,10 @@ public void createLayerExtract(
268269
}
269270

270271
private FileDataStore getExtractDataStore(
271-
LayerExtractController.ExtractOutputFormat extractOutputFormat, String outputFileName, String clientId)
272+
LayerExtractController.ExtractOutputFormat extractOutputFormat,
273+
String outputFileName,
274+
String clientId,
275+
String typeName)
272276
throws IOException {
273277

274278
final File outputFile = Files.createFile(Path.of(exportFilesLocation, outputFileName))
@@ -302,8 +306,17 @@ private FileDataStore getExtractDataStore(
302306
true);
303307
return (FileDataStore) new CSVDataStoreFactory().createNewDataStore(params);
304308
}
309+
case XLSX -> {
310+
Map<String, Serializable> params = Map.of(
311+
ExcelDataStoreFactory.FILE_PARAM.key,
312+
outputFile,
313+
ExcelDataStoreFactory.SHEET_PARAM.key,
314+
// typeName could hve a prefix; for Excel sheet names ':' is disallowed, max length is 31
315+
typeName.substring(typeName.lastIndexOf(":") + 1, Math.min(typeName.length(), 31)));
316+
return (FileDataStore) new ExcelDataStoreFactory().createNewDataStore(params);
317+
}
305318
// TODO implement
306-
case GEOJSON, XLSX, SHAPE -> {
319+
case GEOJSON, SHAPE -> {
307320
emitError(clientId, "Output format " + extractOutputFormat + " is not yet supported");
308321
logger.error("Output format {} is not yet supported", extractOutputFormat);
309322
throw new IOException("Unsupported output format: " + extractOutputFormat);

src/test/java/org/tailormap/api/controller/LayerExtractControllerIntegrationTest.java

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
1313
import static org.hamcrest.Matchers.not;
1414
import static org.hamcrest.Matchers.startsWith;
15+
import static org.junit.jupiter.api.Assertions.assertAll;
1516
import static org.junit.jupiter.api.Assertions.assertEquals;
1617
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
1718
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -22,8 +23,14 @@
2223
import static org.tailormap.api.controller.TestUrls.layerBegroeidTerreindeelPostgis;
2324
import static org.tailormap.api.controller.TestUrls.layerProxiedWithAuthInPublicApp;
2425

26+
import java.io.ByteArrayInputStream;
2527
import java.io.IOException;
28+
import java.io.InputStream;
2629
import java.nio.charset.StandardCharsets;
30+
import org.apache.poi.ss.usermodel.CellType;
31+
import org.apache.poi.ss.usermodel.Sheet;
32+
import org.apache.poi.ss.usermodel.Workbook;
33+
import org.apache.poi.ss.usermodel.WorkbookFactory;
2734
import org.awaitility.Awaitility;
2835
import org.junit.jupiter.api.BeforeEach;
2936
import org.junit.jupiter.api.MethodOrderer;
@@ -256,6 +263,90 @@ void should_export_wfs_to_csv_with_authentication() throws Exception {
256263
});
257264
}
258265

266+
@Test
267+
void should_export_large_filter_to_excel() throws Exception {
268+
final String extractUrl = apiBasePath + layerBegroeidTerreindeelPostgis + extractPath + sseClientId;
269+
mockMvc.perform(post(extractUrl)
270+
.accept(MediaType.APPLICATION_JSON)
271+
.with(setServletPath(extractUrl))
272+
.with(csrf())
273+
.param("attributes", "")
274+
.param("outputFormat", "xlsx")
275+
.param("filter", StaticTestData.get("large_cql_filter"))
276+
.acceptCharset(StandardCharsets.UTF_8)
277+
.characterEncoding(StandardCharsets.UTF_8)
278+
.contentType(MediaType.APPLICATION_FORM_URLENCODED))
279+
.andExpect(status().isAccepted());
280+
281+
// The SseEventBus may dispatch events slightly after the POST returns.
282+
// Awaitility polls the buffered SSE response until the expected content appears.
283+
Awaitility.await()
284+
.atMost(10, SECONDS)
285+
.untilAsserted(() -> assertThat(
286+
sseResult.getResponse().getContentAsString(), containsString("Extract task received")));
287+
288+
Awaitility.await().pollInterval(5, SECONDS).atMost(30, SECONDS).untilAsserted(() -> {
289+
final String stream = sseResult.getResponse().getContentAsString();
290+
assertThat(count_completed_messages(stream), greaterThanOrEqualTo(1));
291+
});
292+
293+
final String lastCompletedEventJson =
294+
getLastCompletedEventJson(sseResult.getResponse().getContentAsString());
295+
assertThat(lastCompletedEventJson.length(), greaterThanOrEqualTo(100));
296+
297+
final String extractedDownloadId = getDownloadId(lastCompletedEventJson);
298+
assertThat(extractedDownloadId, containsString(".xlsx"));
299+
300+
final String downloadUrl = apiBasePath + layerBegroeidTerreindeelPostgis + downloadPath + extractedDownloadId;
301+
MvcResult download = mockMvc.perform(get(downloadUrl).with(setServletPath(downloadUrl)))
302+
.andExpect(status().isOk())
303+
.andExpect(result -> {
304+
String contentType = result.getResponse().getContentType();
305+
assertThat(
306+
contentType,
307+
containsString("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
308+
309+
String contentDisposition = result.getResponse().getHeader("Content-Disposition");
310+
assertThat(contentDisposition, containsString("attachment; filename="));
311+
assertThat(contentDisposition, containsString(extractedDownloadId));
312+
})
313+
.andReturn();
314+
315+
// open the Excel file and check that we have the expected content
316+
try (InputStream inp = new ByteArrayInputStream(download.getResponse().getContentAsByteArray());
317+
Workbook wb = WorkbookFactory.create(inp)) {
318+
Sheet sheet = wb.getSheetAt(0);
319+
320+
assertEquals(
321+
18 + /*header row*/ 1,
322+
sheet.getPhysicalNumberOfRows(),
323+
() -> "Expected " + 18 + /*header row*/ 1
324+
+ " rows in the Excel sheet, including header and 18 data rows");
325+
326+
assertAll(
327+
"Check header and first data row",
328+
() -> assertEquals(
329+
"begroeidterreindeel",
330+
sheet.getSheetName(),
331+
"Expected sheet name to be begroeidterreindeel"),
332+
() -> assertEquals(
333+
14, sheet.getRow(0).getPhysicalNumberOfCells(), "Expected 14 columns in the header row"));
334+
335+
assertAll(
336+
"Check first data row",
337+
() -> assertEquals(
338+
CellType.NUMERIC,
339+
sheet.getRow(1).getCell(0).getCellType(),
340+
"Expected first cell in header to be numeric (with date format)"),
341+
() -> assertEquals(
342+
CellType.STRING,
343+
sheet.getRow(1).getCell(1).getCellType(),
344+
"Expected second cell in header to be a string"),
345+
() -> assertEquals("geenWaarde", sheet.getRow(1).getCell(1).getStringCellValue()),
346+
() -> assertEquals("G0344", sheet.getRow(1).getCell(2).getStringCellValue()));
347+
}
348+
}
349+
259350
/**
260351
* Parse the last non-empty line from the SSE stream that looks something like:
261352
* {@code data:{"details":{"message":"Extract task

0 commit comments

Comments
 (0)