UniqueValuesAdminController.java

/*
 * Copyright (C) 2025 B3Partners B.V.
 *
 * SPDX-License-Identifier: MIT
 */
package org.tailormap.api.controller.admin;

import java.io.Serializable;
import org.apache.commons.lang3.StringUtils;
import org.geotools.api.filter.FilterFactory;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.util.factory.GeoTools;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import org.tailormap.api.geotools.featuresources.FeatureSourceFactoryHelper;
import org.tailormap.api.persistence.TMFeatureType;
import org.tailormap.api.persistence.helper.UniqueValuesHelper;
import org.tailormap.api.persistence.json.TMAttributeDescriptor;
import org.tailormap.api.repository.FeatureTypeRepository;
import org.tailormap.api.viewer.model.ErrorResponse;
import org.tailormap.api.viewer.model.UniqueValuesResponse;

@RestController
public class UniqueValuesAdminController {
  private final FeatureTypeRepository featureTypeRepository;

  private final FeatureSourceFactoryHelper featureSourceFactoryHelper;

  @Value("${tailormap-api.unique.use_geotools_unique_function:true}")
  private boolean useGeotoolsUniqueFunction;

  private final FilterFactory ff = CommonFactoryFinder.getFilterFactory(GeoTools.getDefaultHints());

  public UniqueValuesAdminController(
      FeatureTypeRepository featureTypeRepository, FeatureSourceFactoryHelper featureSourceFactoryHelper) {
    this.featureTypeRepository = featureTypeRepository;
    this.featureSourceFactoryHelper = featureSourceFactoryHelper;
  }

  @ExceptionHandler({ResponseStatusException.class})
  public ResponseEntity<?> handleException(ResponseStatusException ex) {
    // wrap the exception in a proper json response
    return ResponseEntity.status(ex.getStatusCode())
        .contentType(MediaType.APPLICATION_JSON)
        .body(new ErrorResponse()
            .message(
                ex.getReason() != null
                    ? ex.getReason()
                    : ex.getBody().getTitle())
            .code(ex.getStatusCode().value()));
  }

  @GetMapping(
      path = "${tailormap-api.admin.base-path}/unique-values/{featureTypeId}/{attributeName}",
      produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<Serializable> getUniqueValues(
      @PathVariable Long featureTypeId,
      @PathVariable String attributeName,
      @RequestParam(required = false) String filter)
      throws ResponseStatusException {
    if (StringUtils.isBlank(attributeName)) {
      throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Attribute name is required");
    }

    TMFeatureType tmft = this.featureTypeRepository
        .findById(featureTypeId)
        .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Feature type not found"));

    if (tmft.getAttributeByName(attributeName).isEmpty()) {
      throw new ResponseStatusException(
          HttpStatus.BAD_REQUEST,
          "Attribute does not exist, available attributes: "
              + tmft.getAttributes().stream()
                  .map(TMAttributeDescriptor::getName)
                  .toList());
    }

    UniqueValuesResponse response = UniqueValuesHelper.getUniqueValues(
        tmft, attributeName, filter, ff, featureSourceFactoryHelper, useGeotoolsUniqueFunction);
    return ResponseEntity.status(HttpStatus.OK).body(response);
  }
}