TMFeatureTypeHelper.java

/*
 * Copyright (C) 2023 B3Partners B.V.
 *
 * SPDX-License-Identifier: MIT
 */

package org.tailormap.api.persistence.helper;

import jakarta.servlet.MultipartConfigElement;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import org.tailormap.api.persistence.Application;
import org.tailormap.api.persistence.TMFeatureType;
import org.tailormap.api.persistence.json.AppLayerSettings;
import org.tailormap.api.persistence.json.AppTreeLayerNode;
import org.tailormap.api.persistence.json.AttachmentAttributeType;
import org.tailormap.api.persistence.json.AttributeSettings;
import org.tailormap.api.persistence.json.TMAttributeDescriptor;

@Service
public class TMFeatureTypeHelper {
  private final MultipartConfigElement multipartConfigElement;

  public TMFeatureTypeHelper(MultipartConfigElement multipartConfigElement) {
    this.multipartConfigElement = multipartConfigElement;
  }

  public static boolean isEditable(
      Application application, AppTreeLayerNode appTreeLayerNode, TMFeatureType featureType) {
    if (featureType == null) {
      return false;
    }

    if (featureType.getInfo() == null
        || !Objects.equals(featureType.getInfo().getCrs(), application.getCrs())) {
      return false;
    }

    boolean editable = false;
    // Currently FeatureSourceHelper#loadCapabilities() sets editable to true when the datastore is
    // a JDBC DataStore (even when the database account can't write to the table, can't detect this
    // without trying). Other DataStore types we support (only WFS atm) we don't set as writeable

    // TODO: in the future, check for authorizations on editing. Currently you only need to be
    // logged in (the viewer frontend already checks this before showing editable layers so we don't
    // need to check for an authenticated user here).
    if (featureType.isWriteable()) {
      AppLayerSettings appLayerSettings = application.getAppLayerSettings(appTreeLayerNode);
      editable = Boolean.TRUE.equals(appLayerSettings.getEditable());
    }
    return editable;
  }

  public static Set<String> getHiddenAttributes(
      @NotNull TMFeatureType featureType, @NotNull AppLayerSettings appLayerSettings) {
    Set<String> hiddenAttributes = new HashSet<>();
    Optional.ofNullable(featureType.getSettings().getHideAttributes()).ifPresent(hiddenAttributes::addAll);
    Optional.ofNullable(appLayerSettings.getHideAttributes()).ifPresent(hiddenAttributes::addAll);
    return hiddenAttributes;
  }

  public static Set<String> getEditableAttributes(
      @NotNull TMFeatureType featureType, @NotNull AppLayerSettings appLayerSettings) {
    Set<String> editableAttributes = new HashSet<>();
    Optional.ofNullable(featureType.getSettings().getEditableAttributes()).ifPresent(editableAttributes::addAll);
    Optional.ofNullable(appLayerSettings.getReadOnlyAttributes()).ifPresent(editableAttributes::removeAll);
    Optional.ofNullable(featureType.getAllGeometryAttributeNames()).ifPresent(editableAttributes::addAll);
    return editableAttributes;
  }

  public record AttributeWithSettings(TMAttributeDescriptor attributeDescriptor, AttributeSettings settings) {}

  /**
   * Return a map of attribute names (in order, using a LinkedHashMap implementation) to an attribute descriptor with
   * configured settings, taking into account the configured attribute order and hidden attributes.
   *
   * @param featureType The feature type
   * @param appLayerSettings The app layer settings
   * @return A sorted map as described
   */
  public static Map<String, AttributeWithSettings> getConfiguredAttributes(
      @NotNull TMFeatureType featureType, @NotNull AppLayerSettings appLayerSettings) {
    LinkedHashMap<String, TMAttributeDescriptor> originalAttributesOrder = new LinkedHashMap<>();
    for (TMAttributeDescriptor attributeDescriptor : featureType.getAttributes()) {
      originalAttributesOrder.put(attributeDescriptor.getName(), attributeDescriptor);
    }

    // Order of attributes taking into account hidden attributes and configured attribute order
    LinkedHashSet<String> finalAttributeOrder;

    if (featureType.getSettings().getAttributeOrder().isEmpty()) {
      // Use original feature type order
      finalAttributeOrder = new LinkedHashSet<>(originalAttributesOrder.keySet());
    } else {
      finalAttributeOrder = new LinkedHashSet<>(featureType.getSettings().getAttributeOrder());
      // Remove once ordered attributes which no longer exist in the feature type as saved in the
      // configuration database
      finalAttributeOrder.retainAll(originalAttributesOrder.keySet());
      // Add attributes not named in attributeOrder in feature type order (added to the feature type
      // after an admin saved/changed the ordering of attributes -- these attributes should not be
      // hidden).
      if (finalAttributeOrder.size() != originalAttributesOrder.size()) {
        finalAttributeOrder.addAll(originalAttributesOrder.keySet());
      }
    }

    getHiddenAttributes(featureType, appLayerSettings).forEach(finalAttributeOrder::remove);

    Map<String, AttributeSettings> attributeSettings =
        featureType.getSettings().getAttributeSettings();
    LinkedHashMap<String, AttributeWithSettings> result = new LinkedHashMap<>();
    for (String attribute : finalAttributeOrder) {
      AttributeSettings settings =
          Optional.ofNullable(attributeSettings.get(attribute)).orElseGet(AttributeSettings::new);
      TMAttributeDescriptor attributeDescriptor = originalAttributesOrder.get(attribute);
      result.put(attribute, new AttributeWithSettings(attributeDescriptor, settings));
    }
    return result;
  }

  /**
   * Get the non-hidden attribute descriptors for a feature type.
   *
   * @param featureType The feature type
   * @param appLayerSettings The app layer settings
   * @return Unordered set of attribute descriptors
   */
  public static Set<TMAttributeDescriptor> getNonHiddenAttributes(
      @NotNull TMFeatureType featureType, @NotNull AppLayerSettings appLayerSettings) {
    Set<String> hiddenAttributes = getHiddenAttributes(featureType, appLayerSettings);
    return featureType.getAttributes().stream()
        .filter(attributeDescriptor -> !hiddenAttributes.contains(attributeDescriptor.getName()))
        .collect(Collectors.toSet());
  }

  /**
   * Get the non-hidden attribute names for a feature type.
   *
   * @param featureType The feature type
   * @param appLayerSettings The app layer settings
   * @return Unordered set of attribute names
   */
  public static Set<String> getNonHiddenAttributeNames(
      @NotNull TMFeatureType featureType, @NotNull AppLayerSettings appLayerSettings) {
    return getNonHiddenAttributes(featureType, appLayerSettings).stream()
        .map(TMAttributeDescriptor::getName)
        .collect(Collectors.toSet());
  }

  public Set<@Valid AttachmentAttributeType> getAttachmentAttributesWithMaxFileUploadSize(TMFeatureType featureType) {
    long maxFileSize = this.multipartConfigElement.getMaxFileSize();

    return featureType.getSettings().getAttachmentAttributes().stream()
        .map(att -> {
          long attMaxFileSize = att.getMaxAttachmentSize() == null
              ? maxFileSize
              : Math.min(att.getMaxAttachmentSize(), maxFileSize);
          return new AttachmentAttributeType(att.getAttributeName(), att.getMimeType(), attMaxFileSize);
        })
        .collect(Collectors.toCollection(LinkedHashSet::new));
  }
}