ContentCanonicalizationMaintenanceService.java

package net.sasasin.sreader.service.canonicalization;

import net.sasasin.sreader.domain.ContentCanonicalizationPlan;
import net.sasasin.sreader.domain.ContentCanonicalizationResult;
import net.sasasin.sreader.repository.ContentCanonicalizationMaintenanceRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class ContentCanonicalizationMaintenanceService {

  private static final Logger logger =
      LoggerFactory.getLogger(ContentCanonicalizationMaintenanceService.class);

  private final ContentCanonicalizationCandidateScanner candidateScanner;
  private final ContentCanonicalizationPlanner planner;
  private final ContentCanonicalizationExecutor executor;
  private final ContentCanonicalizationFileCleaner fileCleaner;

  ContentCanonicalizationMaintenanceService(
      ContentCanonicalizationCandidateScanner candidateScanner,
      ContentCanonicalizationPlanner planner,
      ContentCanonicalizationExecutor executor,
      ContentCanonicalizationFileCleaner fileCleaner) {
    this.candidateScanner = candidateScanner;
    this.planner = planner;
    this.executor = executor;
    this.fileCleaner = fileCleaner;
  }

  public ContentCanonicalizationResult canonicalize(Options options) {
    ContentCanonicalizationResultAccumulator result =
        new ContentCanonicalizationResultAccumulator();
    ContentCanonicalizationCandidateScanner.Session scanner =
        candidateScanner.start(options.host(), options.batchSize());
    while (options.limit() == null || result.processedGroups() < options.limit()) {
      ContentCanonicalizationCandidateScanner.Page page = scanner.next();
      if (page.isFinished()) {
        return result.snapshot();
      }
      result.addScannedRows(page.scannedRows());
      for (ContentCanonicalizationCandidateScanner.GroupCandidate candidate : page.groups()) {
        String normalized = candidate.normalizedUrl();
        var group = candidate.group();
        if (!planner.needsChange(group)) {
          result.addUnchangedRows(group.members().size());
          continue;
        }
        ContentCanonicalizationPlan plan = planner.plan(group);
        result.recordPlannedGroup(plan);
        if (options.apply()) {
          try {
            ContentCanonicalizationMaintenanceRepository.MergeCounts counts =
                executor.execute(plan);
            result.addMergeCounts(counts);
            result.addFileSummary(fileCleaner.clean(plan));
          } catch (RuntimeException e) {
            logger.error("Could not canonicalize group {}", normalized, e);
            result.recordFailedGroup();
          }
        }
        if (options.limit() != null && result.processedGroups() >= options.limit()) {
          return result.snapshot();
        }
      }
    }
    return result.snapshot();
  }

  public record Options(String host, int batchSize, Integer limit, boolean apply) {
    public Options {
      if (host != null && host.isBlank()) {
        host = null;
      }
      if (batchSize <= 0) {
        throw new IllegalArgumentException("batchSize must be positive");
      }
      if (limit != null && limit <= 0) {
        throw new IllegalArgumentException("limit must be positive");
      }
    }
  }
}