Skip to content

Commit e6a4eb8

Browse files
committed
HTM-1963: As defined in requirements: block export when Excel limits are exceeded
1 parent e352c01 commit e6a4eb8

2 files changed

Lines changed: 76 additions & 1 deletion

File tree

src/main/java/org/tailormap/api/controller/LayerExtractController.java

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@
2121
import java.util.Map;
2222
import java.util.Set;
2323
import java.util.regex.Pattern;
24+
import org.apache.commons.lang3.StringUtils;
25+
import org.geotools.api.data.Query;
26+
import org.geotools.api.data.SimpleFeatureSource;
27+
import org.geotools.api.filter.Filter;
2428
import org.geotools.api.filter.sort.SortOrder;
29+
import org.geotools.filter.text.cql2.CQLException;
30+
import org.geotools.filter.text.ecql.ECQL;
2531
import org.slf4j.Logger;
2632
import org.slf4j.LoggerFactory;
2733
import org.springframework.beans.factory.annotation.Value;
@@ -40,6 +46,8 @@
4046
import org.springframework.web.bind.annotation.RequestParam;
4147
import org.springframework.web.server.ResponseStatusException;
4248
import org.tailormap.api.annotation.AppRestController;
49+
import org.tailormap.api.geotools.data.excel.ExcelDataStore;
50+
import org.tailormap.api.geotools.featuresources.FeatureSourceFactoryHelper;
4351
import org.tailormap.api.persistence.Application;
4452
import org.tailormap.api.persistence.GeoService;
4553
import org.tailormap.api.persistence.TMFeatureType;
@@ -57,14 +65,18 @@ public class LayerExtractController {
5765
private static final Pattern SAFE_DOWNLOAD_ID = Pattern.compile("^[A-Za-z0-9._-]+$");
5866
private final FeatureSourceRepository featureSourceRepository;
5967
private final CreateLayerExtractService createLayerExtractService;
68+
private final FeatureSourceFactoryHelper featureSourceFactoryHelper;
6069

6170
@Value("#{'${tailormap-api.extract.allowed-outputformats}'.split(',')}")
6271
private List<ExtractOutputFormat> allowedExtractOutputFormats;
6372

6473
public LayerExtractController(
65-
FeatureSourceRepository featureSourceRepository, CreateLayerExtractService createLayerExtractService) {
74+
FeatureSourceRepository featureSourceRepository,
75+
CreateLayerExtractService createLayerExtractService,
76+
FeatureSourceFactoryHelper featureSourceFactoryHelper) {
6677
this.featureSourceRepository = featureSourceRepository;
6778
this.createLayerExtractService = createLayerExtractService;
79+
this.featureSourceFactoryHelper = featureSourceFactoryHelper;
6880
}
6981

7082
/**
@@ -186,6 +198,10 @@ public ResponseEntity<?> extract(
186198
attributes.add(sourceFT.getDefaultGeometryAttribute());
187199
}
188200

201+
if (outputFormat == ExtractOutputFormat.XLSX) {
202+
validateExcelLimits(sourceFT, attributes, filter);
203+
}
204+
189205
SortOrder sortingOrder = SortOrder.ASCENDING;
190206
if (null != sortOrder && (sortOrder.equalsIgnoreCase("desc") || sortOrder.equalsIgnoreCase("asc"))) {
191207
sortingOrder = SortOrder.valueOf(sortOrder.toUpperCase(Locale.ROOT));
@@ -204,6 +220,52 @@ public ResponseEntity<?> extract(
204220
.body(Map.of("message", "Extract request accepted", "downloadId", outputFileName));
205221
}
206222

223+
/**
224+
* Check that neither the number of columns nor the number of rows requested for the extract exceed the limits of
225+
* Excel format. This is required to block extract requests that would fail later on in the ExcelFeatureWriter when
226+
* the limits are exceeded. NOTE: cell size limits are handled in the ExcelFeatureWriter.
227+
*
228+
* @param featureType requested FT
229+
* @param attributes requested attributes
230+
* @param filterCQL requested filter
231+
*/
232+
private void validateExcelLimits(TMFeatureType featureType, Set<String> attributes, String filterCQL) {
233+
if (attributes.size() > ExcelDataStore.getMaxColumns()) {
234+
throw new ResponseStatusException(
235+
HttpStatus.BAD_REQUEST,
236+
"Excel format does not support more than " + ExcelDataStore.getMaxColumns() + " columns");
237+
}
238+
SimpleFeatureSource inputFeatureSource = null;
239+
try {
240+
// count all the features; this is expensive but required to block extract when the Excel limits for
241+
// row/columns are exceeded
242+
inputFeatureSource = featureSourceFactoryHelper.openGeoToolsFeatureSource(featureType);
243+
Query q = new Query(inputFeatureSource.getName().toString());
244+
if (!attributes.isEmpty()) {
245+
q.setPropertyNames(attributes.toArray(new String[0]));
246+
}
247+
248+
if (!StringUtils.isBlank(filterCQL)) {
249+
Filter filter = ECQL.toFilter(filterCQL);
250+
q.setFilter(filter);
251+
}
252+
final int featCount = inputFeatureSource.getCount(q);
253+
if (featCount >= ExcelDataStore.getMaxRows()) {
254+
throw new ResponseStatusException(
255+
HttpStatus.BAD_REQUEST,
256+
"Excel format does not support more than " + ExcelDataStore.getMaxRows() + " rows");
257+
}
258+
} catch (CQLException | IOException e) {
259+
throw new ResponseStatusException(
260+
HttpStatus.INTERNAL_SERVER_ERROR,
261+
"Failed to count all features for Excel extract: " + e.getMessage());
262+
} finally {
263+
if (inputFeatureSource != null) {
264+
inputFeatureSource.getDataStore().dispose();
265+
}
266+
}
267+
}
268+
207269
public enum ExtractOutputFormat {
208270
CSV("csv", "csv"),
209271
GEOJSON("geojson", "json"),

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@
5050
import org.springframework.scheduling.annotation.Scheduled;
5151
import org.springframework.stereotype.Service;
5252
import org.springframework.transaction.annotation.Transactional;
53+
import org.springframework.web.server.ResponseStatusException;
5354
import org.tailormap.api.controller.LayerExtractController;
55+
import org.tailormap.api.geotools.data.excel.ExcelDataStore;
5456
import org.tailormap.api.geotools.data.excel.ExcelDataStoreFactory;
5557
import org.tailormap.api.geotools.featuresources.FeatureSourceFactoryHelper;
5658
import org.tailormap.api.persistence.TMFeatureType;
@@ -221,6 +223,17 @@ public void createLayerExtract(
221223

222224
final int featCount = inputFeatureSource.getCount(q);
223225
logger.debug("Filtered source counts {}", featCount);
226+
if (featCount >= ExcelDataStore.getMaxRows()) {
227+
this.emitError(
228+
clientId,
229+
"Extract result contains %d features, which exceeds the maximum of %d for Excel output format. Please refine your filter or choose a different output format."
230+
.formatted(featCount, ExcelDataStore.getMaxRows()));
231+
throw new ResponseStatusException(
232+
org.springframework.http.HttpStatus.BAD_REQUEST,
233+
"Extract result contains %d features, which exceeds the maximum of %d for Excel output format. Please refine your filter or choose a different output format."
234+
.formatted(featCount, ExcelDataStore.getMaxRows()));
235+
}
236+
224237
final AtomicInteger featsAdded = new AtomicInteger();
225238

226239
FileDataStore outputDataStore =

0 commit comments

Comments
 (0)