FeedUrlRepository.java

package net.sasasin.sreader.repository;

import static net.sasasin.sreader.jooq.Tables.FEED_URL;

import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;
import net.sasasin.sreader.domain.FeedStatus;
import net.sasasin.sreader.domain.FeedUrl;
import net.sasasin.sreader.domain.FullTextMethod;
import net.sasasin.sreader.domain.UnsubscribeReason;
import org.jooq.Condition;
import org.jooq.DSLContext;
import org.jooq.Record;
import org.jooq.impl.DSL;
import org.springframework.stereotype.Repository;

@Repository
public class FeedUrlRepository {

  private final DSLContext dsl;

  public FeedUrlRepository(DSLContext dsl) {
    this.dsl = dsl;
  }

  public List<FeedUrl> findActiveForReading() {
    return dsl.select(
            FEED_URL.ID,
            FEED_URL.URL,
            FEED_URL.STATUS,
            FEED_URL.UNSUBSCRIBE_REASON,
            FEED_URL.UNSUBSCRIBED_AT,
            FEED_URL.NOTE,
            FEED_URL.FULL_TEXT_METHOD)
        .from(FEED_URL)
        .where(FEED_URL.STATUS.eq(FeedStatus.ACTIVE.value()))
        .orderBy(FEED_URL.URL)
        .fetch(this::mapFeedUrl);
  }

  public List<FeedUrl> findAllForExport(boolean activeOnly) {
    Condition condition =
        activeOnly ? FEED_URL.STATUS.eq(FeedStatus.ACTIVE.value()) : DSL.trueCondition();
    return dsl.select(
            FEED_URL.ID,
            FEED_URL.URL,
            FEED_URL.STATUS,
            FEED_URL.UNSUBSCRIBE_REASON,
            FEED_URL.UNSUBSCRIBED_AT,
            FEED_URL.NOTE,
            FEED_URL.FULL_TEXT_METHOD)
        .from(FEED_URL)
        .where(condition)
        .orderBy(FEED_URL.URL)
        .fetch(this::mapFeedUrl);
  }

  public Optional<FeedUrl> findByUrl(String url) {
    return dsl.select(
            FEED_URL.ID,
            FEED_URL.URL,
            FEED_URL.STATUS,
            FEED_URL.UNSUBSCRIBE_REASON,
            FEED_URL.UNSUBSCRIBED_AT,
            FEED_URL.NOTE,
            FEED_URL.FULL_TEXT_METHOD)
        .from(FEED_URL)
        .where(FEED_URL.URL.eq(url))
        .fetchOptional(this::mapFeedUrl);
  }

  public boolean insertIfAbsent(String id, String url) {
    return (dsl.insertInto(FEED_URL)
            .set(FEED_URL.ID, id)
            .set(FEED_URL.URL, url)
            .set(FEED_URL.STATUS, FeedStatus.ACTIVE.value())
            .set(FEED_URL.FULL_TEXT_METHOD, FullTextMethod.defaultMethod().value())
            .set(FEED_URL.CREATED_AT, OffsetDateTime.now())
            .set(FEED_URL.UPDATED_AT, OffsetDateTime.now())
            .onConflict(FEED_URL.ID)
            .doNothing()
            .execute()
        == 1);
  }

  public void insertFromImport(FeedUrl feedUrl) {
    OffsetDateTime now = OffsetDateTime.now();
    dsl.insertInto(FEED_URL)
        .set(FEED_URL.ID, feedUrl.id())
        .set(FEED_URL.URL, feedUrl.url())
        .set(FEED_URL.STATUS, feedUrl.status().value())
        .set(
            FEED_URL.UNSUBSCRIBE_REASON,
            feedUrl.unsubscribeReason() == null ? null : feedUrl.unsubscribeReason().value())
        .set(FEED_URL.UNSUBSCRIBED_AT, feedUrl.unsubscribedAt())
        .set(FEED_URL.NOTE, feedUrl.note())
        .set(FEED_URL.FULL_TEXT_METHOD, feedUrl.fullTextMethod().value())
        .set(FEED_URL.CREATED_AT, now)
        .set(FEED_URL.UPDATED_AT, now)
        .execute();
  }

  public void unsubscribe(
      String url, UnsubscribeReason reason, OffsetDateTime unsubscribedAt, String note) {
    dsl.update(FEED_URL)
        .set(FEED_URL.STATUS, FeedStatus.UNSUBSCRIBED.value())
        .set(FEED_URL.UNSUBSCRIBE_REASON, reason.value())
        .set(FEED_URL.UNSUBSCRIBED_AT, unsubscribedAt)
        .set(FEED_URL.NOTE, note)
        .set(FEED_URL.UPDATED_AT, OffsetDateTime.now())
        .where(FEED_URL.URL.eq(url))
        .execute();
  }

  public void updateUnsubscribedMetadata(
      String url, UnsubscribeReason reason, OffsetDateTime unsubscribedAt, String note) {
    dsl.update(FEED_URL)
        .set(FEED_URL.UNSUBSCRIBE_REASON, reason.value())
        .set(FEED_URL.UNSUBSCRIBED_AT, unsubscribedAt)
        .set(FEED_URL.NOTE, note)
        .set(FEED_URL.UPDATED_AT, OffsetDateTime.now())
        .where(FEED_URL.URL.eq(url))
        .execute();
  }

  public void resubscribe(String url) {
    dsl.update(FEED_URL)
        .set(FEED_URL.STATUS, FeedStatus.ACTIVE.value())
        .set(FEED_URL.UNSUBSCRIBE_REASON, (String) null)
        .set(FEED_URL.UNSUBSCRIBED_AT, (OffsetDateTime) null)
        .set(FEED_URL.NOTE, (String) null)
        .set(FEED_URL.UPDATED_AT, OffsetDateTime.now())
        .where(FEED_URL.URL.eq(url))
        .execute();
  }

  public void updateFullTextMethod(String url, FullTextMethod fullTextMethod) {
    dsl.update(FEED_URL)
        .set(FEED_URL.FULL_TEXT_METHOD, fullTextMethod.value())
        .set(FEED_URL.UPDATED_AT, OffsetDateTime.now())
        .where(FEED_URL.URL.eq(url))
        .execute();
  }

  private FullTextMethod toFullTextMethod(String value) {
    return value == null ? FullTextMethod.defaultMethod() : FullTextMethod.fromValue(value);
  }

  private UnsubscribeReason toUnsubscribeReason(String value) {
    return value == null ? null : UnsubscribeReason.fromValue(value);
  }

  /**
   * Maps a DB row to {@link FeedUrl}. Invalid status/reason strings fail via enum {@code
   * fromValue}. Rows that violate active/unsubscribed correlation fail-fast in the domain
   * constructor rather than silently normalizing corrupt data.
   */
  private FeedUrl mapFeedUrl(Record record) {
    return new FeedUrl(
        record.get(FEED_URL.ID),
        record.get(FEED_URL.URL),
        FeedStatus.fromValue(record.get(FEED_URL.STATUS)),
        toUnsubscribeReason(record.get(FEED_URL.UNSUBSCRIBE_REASON)),
        record.get(FEED_URL.UNSUBSCRIBED_AT),
        record.get(FEED_URL.NOTE),
        toFullTextMethod(record.get(FEED_URL.FULL_TEXT_METHOD)));
  }
}