View Javadoc
1   /*
2    * Copyright (C) 2026 B3Partners B.V.
3    *
4    * SPDX-License-Identifier: MIT
5    */
6   package org.tailormap.api.controller.admin;
7   
8   import io.swagger.v3.oas.annotations.Operation;
9   import io.swagger.v3.oas.annotations.media.Content;
10  import io.swagger.v3.oas.annotations.media.Schema;
11  import io.swagger.v3.oas.annotations.responses.ApiResponse;
12  import java.io.IOException;
13  import org.apache.commons.lang3.StringUtils;
14  import org.geotools.api.data.SimpleFeatureSource;
15  import org.springframework.http.HttpStatus;
16  import org.springframework.http.MediaType;
17  import org.springframework.http.ResponseEntity;
18  import org.springframework.web.bind.annotation.ExceptionHandler;
19  import org.springframework.web.bind.annotation.GetMapping;
20  import org.springframework.web.bind.annotation.PathVariable;
21  import org.springframework.web.bind.annotation.RequestParam;
22  import org.springframework.web.bind.annotation.RestController;
23  import org.springframework.web.server.ResponseStatusException;
24  import org.tailormap.api.geotools.featuresources.FeatureSourceFactoryHelper;
25  import org.tailormap.api.geotools.featuresources.FeatureSourceStatistics;
26  import org.tailormap.api.persistence.TMFeatureType;
27  import org.tailormap.api.repository.FeatureTypeRepository;
28  import org.tailormap.api.viewer.model.AttributeStatisticsResponse;
29  import org.tailormap.api.viewer.model.ErrorResponse;
30  
31  @RestController
32  public class AttributeStatisticsAdminController {
33    private final FeatureTypeRepository featureTypeRepository;
34    private final FeatureSourceFactoryHelper featureSourceFactoryHelper;
35  
36    public AttributeStatisticsAdminController(
37        FeatureTypeRepository featureTypeRepository, FeatureSourceFactoryHelper featureSourceFactoryHelper) {
38      this.featureTypeRepository = featureTypeRepository;
39      this.featureSourceFactoryHelper = featureSourceFactoryHelper;
40    }
41  
42    @ExceptionHandler({ResponseStatusException.class})
43    public ResponseEntity<?> handleException(ResponseStatusException ex) {
44      // wrap the exception in a proper json response
45      return ResponseEntity.status(ex.getStatusCode())
46          .contentType(MediaType.APPLICATION_JSON)
47          .body(new ErrorResponse()
48              .message(
49                  ex.getReason() != null
50                      ? ex.getReason()
51                      : ex.getBody().getTitle())
52              .code(ex.getStatusCode().value()));
53    }
54  
55    /**
56     * Retrieve attribute statistics for a feature type.
57     *
58     * @param featureTypeId the feature type id for which to retrieve statistics
59     * @param attributeName the name of the attribute
60     * @param filter an optional CQL filter to apply to the features before calculating statistics
61     * @return @return the calculated statistics; fields may be null when not applicable or when no features match
62     * @throws ResponseStatusException if the feature type or attribute does not exist, or if a bad request was
63     *     submitted
64     */
65    @Operation(
66        summary = "Retrieve attribute statistics for a feature type",
67        description = "Retrieve statistics (min, max, average, sum, count) for a given attribute of a feature type."
68            + " Optionally, a CQL filter can be applied to filter the features before calculating the"
69            + " statistics.")
70    @GetMapping(
71        path = "${tailormap-api.admin.base-path}/statistics/{featureTypeId}/{attributeName}",
72        produces = MediaType.APPLICATION_JSON_VALUE)
73    @ApiResponse(
74        responseCode = "200",
75        description = "Statistics retrieved successfully",
76        content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(example = """
77  {"min":85746,"max":100000,"average":92873,"sum":371492,"count":4}
78  """)))
79    public ResponseEntity<AttributeStatisticsResponse> getStatistics(
80        @PathVariable Long featureTypeId,
81        @PathVariable String attributeName,
82        @RequestParam(required = false) String filter) {
83  
84      if (StringUtils.isBlank(attributeName)) {
85        throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Attribute name is required");
86      }
87  
88      TMFeatureType tmft = this.featureTypeRepository
89          .findById(featureTypeId)
90          .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Feature type not found"));
91  
92      if (tmft.getAttributeByName(attributeName).isEmpty()) {
93        throw new ResponseStatusException(
94            HttpStatus.BAD_REQUEST, "Attribute does not exist in feature type: " + attributeName);
95      }
96      SimpleFeatureSource featureSource = null;
97      try {
98        featureSource = featureSourceFactoryHelper.openGeoToolsFeatureSource(tmft);
99        return ResponseEntity.ok()
100           .body(FeatureSourceStatistics.getFeatureSourceStatistics(
101               featureSource, attributeName, filter, 10, null));
102     } catch (IllegalArgumentException e) {
103       throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage(), e);
104     } catch (IOException e) {
105       throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Error accessing feature source", e);
106     } finally {
107       if (featureSource != null) {
108         featureSource.getDataStore().dispose();
109       }
110     }
111   }
112 }