FeedReaderProperties.java

package net.sasasin.sreader.config;

import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import net.sasasin.sreader.service.autopagerize.PaginationPolicy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@Validated
@ConfigurationProperties(prefix = "sreader")
public record FeedReaderProperties(
    Scheduler scheduler,
    Job job,
    Http http,
    Playwright playwright,
    TextExport textExport,
    Autopagerize autopagerize,
    List<String> seedFeedUrls) {

  public FeedReaderProperties {
    scheduler = scheduler == null ? Scheduler.defaults() : scheduler;
    job = job == null ? Job.defaults() : job;
    http = http == null ? Http.defaults() : http;
    playwright = playwright == null ? Playwright.defaults() : playwright;
    textExport = textExport == null ? TextExport.defaults() : textExport;
    autopagerize = autopagerize == null ? Autopagerize.defaults() : autopagerize;
    seedFeedUrls = seedFeedUrls == null ? List.of() : seedFeedUrls;
  }

  public record Scheduler(Boolean enabled, String cron) {
    static Scheduler defaults() {
      return new Scheduler(true, "0 */15 * * * *");
    }

    public Scheduler {
      enabled = enabled == null ? true : enabled;
      cron = requiredText(cron, "sreader.scheduler.cron", "0 */15 * * * *");
    }
  }

  public record Job(Boolean runOnce) {
    static Job defaults() {
      return new Job(false);
    }

    public Job {
      runOnce = runOnce == null ? false : runOnce;
    }
  }

  public record Http(
      String userAgent, Duration connectTimeout, Duration readTimeout, Integer retryCount) {
    static Http defaults() {
      return new Http("SReader/0.1", Duration.ofSeconds(5), Duration.ofSeconds(20), 1);
    }

    public Http {
      userAgent = requiredText(userAgent, "sreader.http.user-agent", "SReader/0.1");
      connectTimeout =
          positiveDuration(connectTimeout, "sreader.http.connect-timeout", Duration.ofSeconds(5));
      readTimeout =
          positiveDuration(readTimeout, "sreader.http.read-timeout", Duration.ofSeconds(20));
      retryCount = nonNegative(retryCount, "sreader.http.retry-count", 1);
    }
  }

  public record TextExport(Boolean enabled, Path outputDir, Integer batchSize) {
    static TextExport defaults() {
      return new TextExport(false, Path.of("/var/lib/sreader/content-text"), 100);
    }

    public TextExport {
      enabled = enabled == null ? false : enabled;
      outputDir = outputDir == null ? Path.of("/var/lib/sreader/content-text") : outputDir;
      batchSize = positive(batchSize, "sreader.text-export.batch-size", 100);
    }
  }

  public record Playwright(
      Boolean enabled,
      Boolean headless,
      Integer viewportWidth,
      Integer viewportHeight,
      Duration navigationTimeout,
      Duration networkIdleTimeout) {
    static Playwright defaults() {
      return new Playwright(false, true, 1280, 1600, Duration.ofSeconds(60), Duration.ofSeconds(5));
    }

    public Playwright {
      enabled = enabled == null ? false : enabled;
      headless = headless == null ? true : headless;
      viewportWidth = positive(viewportWidth, "sreader.playwright.viewport-width", 1280);
      viewportHeight = positive(viewportHeight, "sreader.playwright.viewport-height", 1600);
      navigationTimeout =
          positiveDuration(
              navigationTimeout, "sreader.playwright.navigation-timeout", Duration.ofSeconds(60));
      networkIdleTimeout =
          positiveDuration(
              networkIdleTimeout, "sreader.playwright.network-idle-timeout", Duration.ofSeconds(5));
    }
  }

  /**
   * AutoPagerize pagination limits shared by HTTP and Playwright adapters. Defaults: 20 pages, 5
   * MiB per page, 20 MiB total, 120 s, same-origin only.
   */
  public record Autopagerize(
      Integer maxPages,
      Long maxPageBytes,
      Long maxTotalBytes,
      Duration totalTimeout,
      Boolean sameOriginOnly) {

    static Autopagerize defaults() {
      return new Autopagerize(
          20, 5L * 1024 * 1024, 20L * 1024 * 1024, Duration.ofSeconds(120), true);
    }

    public Autopagerize {
      maxPages = positive(maxPages, "sreader.autopagerize.max-pages", 20);
      maxPageBytes =
          positiveLong(maxPageBytes, "sreader.autopagerize.max-page-bytes", 5L * 1024 * 1024);
      maxTotalBytes =
          positiveLong(maxTotalBytes, "sreader.autopagerize.max-total-bytes", 20L * 1024 * 1024);
      totalTimeout =
          positiveDuration(
              totalTimeout, "sreader.autopagerize.total-timeout", Duration.ofSeconds(120));
      sameOriginOnly = sameOriginOnly == null ? true : sameOriginOnly;
      if (maxTotalBytes < maxPageBytes) {
        throw new IllegalArgumentException(
            "sreader.autopagerize.max-total-bytes must be >= max-page-bytes");
      }
    }

    public PaginationPolicy toPaginationPolicy() {
      return new PaginationPolicy(
          maxPages, maxPageBytes, maxTotalBytes, totalTimeout, sameOriginOnly);
    }
  }

  private static String requiredText(String value, String path, String defaultValue) {
    if (value == null) {
      return defaultValue;
    }
    if (value.isBlank()) {
      throw new IllegalArgumentException(path + " must not be blank");
    }
    return value;
  }

  private static Integer positive(Integer value, String path, int defaultValue) {
    if (value == null) {
      return defaultValue;
    }
    if (value <= 0) {
      throw new IllegalArgumentException(path + " must be positive");
    }
    return value;
  }

  private static Long positiveLong(Long value, String path, long defaultValue) {
    if (value == null) {
      return defaultValue;
    }
    if (value <= 0) {
      throw new IllegalArgumentException(path + " must be positive");
    }
    return value;
  }

  private static Integer nonNegative(Integer value, String path, int defaultValue) {
    if (value == null) {
      return defaultValue;
    }
    if (value < 0) {
      throw new IllegalArgumentException(path + " must be zero or positive");
    }
    return value;
  }

  private static Duration positiveDuration(Duration value, String path, Duration defaultValue) {
    if (value == null) {
      return defaultValue;
    }
    if (value.isZero() || value.isNegative()) {
      throw new IllegalArgumentException(path + " must be positive");
    }
    return value;
  }
}