GeoServiceValidator.java

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

package org.tailormap.api.repository.validation;

import static org.tailormap.api.persistence.json.GeoServiceProtocol.XYZ;
import static org.tailormap.api.util.TMExceptionUtils.joinAllThrowableMessages;

import java.lang.invoke.MethodHandles;
import java.net.URI;
import java.net.UnknownHostException;
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.Validator;
import org.tailormap.api.persistence.GeoService;
import org.tailormap.api.persistence.helper.GeoServiceHelper;

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

  private final GeoServiceHelper geoServiceHelper;

  public GeoServiceValidator(GeoServiceHelper geoServiceHelper) {
    this.geoServiceHelper = geoServiceHelper;
  }

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

  @Override
  public void validate(@NonNull Object target, @NonNull Errors errors) {
    GeoService service = (GeoService) target;
    logger.debug(
        "Validate service {}, refresh capabilities {}, url {}",
        service.getId(),
        service.isRefreshCapabilities(),
        service.getUrl());

    if (errors.getFieldError("url") != null) {
      return;
    }

    URI uri;
    try {
      // For XYZ URL templates, remove replacements
      if (service.getProtocol() == XYZ) {
        // Besides {x}, {y}, {z} also allow {-y} (for TMS) and {a-c} for domains
        uri = new URI(service.getUrl().replaceAll("\\{[a-z\\-]+}", ""));
      } else {
        uri = new URI(service.getUrl());
      }
    } catch (Exception e) {
      errors.rejectValue("url", "invalid", "Invalid URI");
      return;
    }
    if (!"https".equals(uri.getScheme()) && !"http".equals(uri.getScheme())) {
      errors.rejectValue("url", "invalid-scheme", "Invalid URI scheme");
      return;
    }

    // For XYZ make sure updated xyzCrs from settings is applied to layer
    if (service.isRefreshCapabilities() || XYZ.equals(service.getProtocol())) {
      try {
        geoServiceHelper.loadServiceCapabilities(service);
      } catch (UnknownHostException e) {
        errors.rejectValue("url", "unknown-host", "Unknown host: \"" + uri.getHost() + "\"");
      } catch (Exception e) {
        String msg = "Error loading capabilities from URL \"%s\": %s"
            .formatted(service.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", "loading-capabilities-failed", msg);
      }
    }
  }
}