UniqueValuesHelper.java

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

import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.Set;
import java.util.TreeSet;
import org.geotools.api.data.Query;
import org.geotools.api.data.SimpleFeatureSource;
import org.geotools.api.filter.Filter;
import org.geotools.api.filter.FilterFactory;
import org.geotools.api.filter.expression.Function;
import org.geotools.api.filter.sort.SortOrder;
import org.geotools.filter.text.cql2.CQLException;
import org.geotools.filter.text.ecql.ECQL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
import org.tailormap.api.geotools.featuresources.FeatureSourceFactoryHelper;
import org.tailormap.api.persistence.TMFeatureType;
import org.tailormap.api.viewer.model.UniqueValuesResponse;

public class UniqueValuesHelper {
  private static final Logger logger =
      LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  public static UniqueValuesResponse getUniqueValues(
      TMFeatureType tmft,
      String attributeName,
      String filter,
      FilterFactory ff,
      FeatureSourceFactoryHelper featureSourceFactoryHelper,
      Boolean useGeotoolsUniqueFunction) {
    final UniqueValuesResponse uniqueValuesResponse = new UniqueValuesResponse().filterApplied(false);
    SimpleFeatureSource fs = null;
    try {
      Filter existingFilter = null;
      if (null != filter) {
        existingFilter = ECQL.toFilter(filter);
      }
      logger.trace("existingFilter: {}", existingFilter);

      Filter notNull = ff.not(ff.isNull(ff.property(attributeName)));
      Filter f = notNull;
      if (null != existingFilter) {
        f = ff.and(notNull, existingFilter);
        uniqueValuesResponse.filterApplied(true);
      }

      Query q = new Query(tmft.getName(), f);
      q.setPropertyNames(attributeName);
      q.setSortBy(ff.sort(attributeName, SortOrder.ASCENDING));
      logger.trace("Unique values query: {}", q);

      fs = featureSourceFactoryHelper.openGeoToolsFeatureSource(tmft);
      // and then there are 2 scenarios:
      // there might be a performance benefit for one or the other
      if (!useGeotoolsUniqueFunction) {
        // #1 use a feature visitor to get the unique values
        // not recommended, as it may not be performant
        logger.trace("Using feature visitor to get unique values");
        fs.getFeatures(q)
            .accepts(
                feature -> uniqueValuesResponse.addValuesItem(
                    feature.getProperty(attributeName).getValue()),
                null);
      } else {
        // #2 or use a Function to get the unique values
        // this is the recommended way, uses SQL "distinct"
        logger.trace("Using geotools unique collection function to get unique values");
        Function unique = ff.function("Collection_Unique", ff.property(attributeName));
        Object o = unique.evaluate(fs.getFeatures(q));
        if (o instanceof Set<?> uniqueValues) {
          uniqueValuesResponse.setValues(new TreeSet<>(uniqueValues));
        }
      }
    } catch (CQLException e) {
      logger.error("Could not parse requested filter", e);
      throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Could not parse requested filter");
    } catch (IOException e) {
      logger.error("Could not retrieve attribute data", e);
    } finally {
      if (fs != null) {
        fs.getDataStore().dispose();
      }
    }

    return uniqueValuesResponse;
  }
}