FeatureSourceValidator.java

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

package org.tailormap.api.repository.validation;

import static org.tailormap.api.util.TMExceptionUtils.joinAllThrowableMessages;

import java.lang.invoke.MethodHandles;
import java.net.URI;
import java.net.UnknownHostException;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import org.tailormap.api.geotools.featuresources.JDBCFeatureSourceHelper;
import org.tailormap.api.geotools.featuresources.WFSFeatureSourceHelper;
import org.tailormap.api.persistence.TMFeatureSource;

@Component
public class FeatureSourceValidator implements Validator {
  private static final Logger logger =
      LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  @Override
  public boolean supports(@NonNull Class<?> clazz) {
    return TMFeatureSource.class.isAssignableFrom(clazz);
  }

  @Override
  public void validate(@NonNull Object target, @NonNull Errors errors) {
    TMFeatureSource featureSource = (TMFeatureSource) target;
    logger.debug("Validate {} feature source {}", featureSource.getProtocol(), featureSource);

    if (featureSource.getProtocol().equals(TMFeatureSource.Protocol.WFS)) {
      validateWFS(featureSource, errors);
    } else if (featureSource.getProtocol().equals(TMFeatureSource.Protocol.JDBC)) {
      validateJDBC(featureSource, errors);
    }
  }

  private void validateWFS(TMFeatureSource featureSource, Errors errors) {
    ValidationUtils.rejectIfEmptyOrWhitespace(errors, "url", "errors.required", "URL is required");

    if (errors.hasErrors()) {
      return;
    }

    URI uri;
    try {
      uri = new URI(featureSource.getUrl());
    } catch (Exception e) {
      errors.rejectValue("url", "errors.url.invalid", "Invalid URI");
      return;
    }
    if (!"https".equals(uri.getScheme()) && !"http".equals(uri.getScheme())) {
      errors.rejectValue("url", "errors.url.invalid-scheme", "Invalid URI scheme");
      return;
    }

    if (featureSource.isRefreshCapabilities()) {
      try {
        new WFSFeatureSourceHelper().loadCapabilities(featureSource);
      } catch (UnknownHostException e) {
        errors.rejectValue("url", "errors.unknown-host", "Unknown host: \"" + uri.getHost() + "\"");
      } catch (Exception e) {
        String msg = "Error loading WFS capabilities from URL \"%s\": %s"
            .formatted(featureSource.getUrl(), joinAllThrowableMessages(e));
        logger.info(
            "The following exception may not be an application error but could be a problem with an external service or user-entered data: {}",
            msg,
            e);
        errors.rejectValue("url", "errors.loading-capabilities-failed", msg);
      }
    }
  }

  private void validateJDBC(TMFeatureSource featureSource, Errors errors) {
    if (featureSource.getJdbcConnection() == null) {
      errors.rejectValue("jdbcConnection", "errors.required", "JDBC connection properties are required");
      return;
    }
    if (featureSource.getAuthentication() == null) {
      errors.rejectValue("authentication", "errors.required", "Database username and password are required");
      return;
    }

    if (errors.hasErrors()) {
      return;
    }

    if (featureSource.isRefreshCapabilities()) {
      try {
        new JDBCFeatureSourceHelper().loadCapabilities(featureSource);
      } catch (Exception e) {
        String msg = "Error loading capabilities from JDBC datastore: %s"
            .formatted(ExceptionUtils.getRootCauseMessage(e));
        logger.info(
            "The following exception may not be an application error but could be a problem with an external service or user-entered data: {}",
            msg,
            e);
        errors.rejectValue("url", "errors.loading-capabilities-failed", msg);
      }
    }
  }
}