2121import java .util .Map ;
2222import java .util .Set ;
2323import 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 ;
2428import org .geotools .api .filter .sort .SortOrder ;
29+ import org .geotools .filter .text .cql2 .CQLException ;
30+ import org .geotools .filter .text .ecql .ECQL ;
2531import org .slf4j .Logger ;
2632import org .slf4j .LoggerFactory ;
2733import org .springframework .beans .factory .annotation .Value ;
4046import org .springframework .web .bind .annotation .RequestParam ;
4147import org .springframework .web .server .ResponseStatusException ;
4248import org .tailormap .api .annotation .AppRestController ;
49+ import org .tailormap .api .geotools .data .excel .ExcelDataStore ;
50+ import org .tailormap .api .geotools .featuresources .FeatureSourceFactoryHelper ;
4351import org .tailormap .api .persistence .Application ;
4452import org .tailormap .api .persistence .GeoService ;
4553import 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" ),
0 commit comments