FullTextMethod.java
package net.sasasin.sreader.domain;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Canonical catalog for full-text extraction methods.
*
* <p>Each constant holds its wire value (DB / TOML / CLI) and a sealed {@link Definition} that
* describes runtime extraction behavior. Boundaries that persist or parse methods convert only via
* {@link #value()} and {@link #fromValue(String)}.
*
* <p>Adding a wire value also requires updating the database check constraint and user-facing
* documentation.
*/
public enum FullTextMethod {
FEED("feed", new Definition.FeedEntry()),
HTTP("http", new Definition.HttpArticle(PaginationMode.NONE, HtmlExtractor.XPATH_OR_BODY_TEXT)),
HTTP_READABILITY(
"http_readability",
new Definition.HttpArticle(PaginationMode.NONE, HtmlExtractor.READABILITY)),
HTTP_AUTOPAGERIZE(
"http_autopagerize",
new Definition.HttpArticle(PaginationMode.AUTOPAGERIZE, HtmlExtractor.XPATH_OR_BODY_TEXT)),
HTTP_AUTOPAGERIZE_READABILITY(
"http_autopagerize_readability",
new Definition.HttpArticle(PaginationMode.AUTOPAGERIZE, HtmlExtractor.READABILITY)),
PLAYWRIGHT(
"playwright",
new Definition.PlaywrightArticle(PaginationMode.NONE, HtmlExtractor.XPATH_OR_BODY_TEXT)),
PLAYWRIGHT_READABILITY(
"playwright_readability",
new Definition.PlaywrightArticle(PaginationMode.NONE, HtmlExtractor.READABILITY)),
PLAYWRIGHT_AUTOPAGERIZE(
"playwright_autopagerize",
new Definition.PlaywrightArticle(
PaginationMode.AUTOPAGERIZE, HtmlExtractor.XPATH_OR_BODY_TEXT)),
PLAYWRIGHT_AUTOPAGERIZE_READABILITY(
"playwright_autopagerize_readability",
new Definition.PlaywrightArticle(PaginationMode.AUTOPAGERIZE, HtmlExtractor.READABILITY));
private static final FullTextMethod DEFAULT = HTTP;
private static final Map<String, FullTextMethod> BY_VALUE =
Arrays.stream(values())
.collect(
Collectors.toUnmodifiableMap(
FullTextMethod::value,
Function.identity(),
(left, right) -> {
throw new IllegalStateException(
"Duplicate full text method wire value: " + left.value());
}));
private static final List<String> WIRE_VALUES =
Arrays.stream(values()).map(FullTextMethod::value).toList();
private final String value;
private final Definition definition;
FullTextMethod(String value, Definition definition) {
this.value = Objects.requireNonNull(value, "wire value must not be null");
if (this.value.isBlank()) {
throw new IllegalArgumentException("wire value must not be blank");
}
this.definition = Objects.requireNonNull(definition, "definition must not be null");
}
public String value() {
return value;
}
public Definition definition() {
return definition;
}
/**
* Default method used when TOML omits {@code full_text_method}, when DB null compatibility
* mapping applies, and when extraction is invoked without an explicit method.
*/
public static FullTextMethod defaultMethod() {
return DEFAULT;
}
/** Immutable wire values in enum declaration order. */
public static List<String> wireValues() {
return WIRE_VALUES;
}
/** Comma-separated wire values for CLI / validation error messages. */
public static String supportedValues() {
return String.join(", ", WIRE_VALUES);
}
public static FullTextMethod fromValue(String value) {
if (value == null) {
throw new IllegalArgumentException("Unsupported full text method: null");
}
FullTextMethod method = BY_VALUE.get(value);
if (method == null) {
throw new IllegalArgumentException("Unsupported full text method: " + value);
}
return method;
}
/** Whether this method uses feed entry body content (no article fetch). */
public boolean usesFeedEntryContent() {
return definition instanceof Definition.FeedEntry;
}
/** Whether an entry link is required for feed-entry probe / article URL resolution. */
public boolean requiresEntryLink() {
return !usesFeedEntryContent();
}
/** Whether article URL probing is supported. */
public boolean supportsArticleProbe() {
return !usesFeedEntryContent();
}
/** Whether an explicit XPath override is applicable. */
public boolean supportsXpathOverride() {
return !usesFeedEntryContent();
}
/** Whether Playwright rendering is required. */
public boolean requiresPlaywright() {
return definition instanceof Definition.PlaywrightArticle;
}
/** Whether AutoPagerize multi-page tracking is required. */
public boolean usesAutopagerize() {
return switch (definition) {
case Definition.HttpArticle http -> http.pagination() == PaginationMode.AUTOPAGERIZE;
case Definition.PlaywrightArticle playwright ->
playwright.pagination() == PaginationMode.AUTOPAGERIZE;
case Definition.FeedEntry ignored -> false;
};
}
public Optional<Definition.ArticleDefinition> articleDefinition() {
return definition instanceof Definition.ArticleDefinition article
? Optional.of(article)
: Optional.empty();
}
/** HTML extractor applied to a fetched or rendered article document. */
public enum HtmlExtractor {
XPATH_OR_BODY_TEXT,
READABILITY
}
/** Pagination strategy for article methods. */
public enum PaginationMode {
NONE,
AUTOPAGERIZE
}
/**
* Sealed runtime definition for a full-text method. Invalid combinations such as FEED +
* READABILITY cannot be constructed.
*/
public sealed interface Definition permits Definition.FeedEntry, Definition.ArticleDefinition {
/** Feed entry body content; no article document fetch. */
record FeedEntry() implements Definition {}
/** Article methods that apply an {@link HtmlExtractor} to an HTML document. */
sealed interface ArticleDefinition extends Definition permits HttpArticle, PlaywrightArticle {
HtmlExtractor extractor();
}
/**
* HTTP fetch of the article URL, then HTML extraction. {@link PaginationMode#AUTOPAGERIZE}
* tracks multi-page chains through a cookie-isolated session.
*/
record HttpArticle(PaginationMode pagination, HtmlExtractor extractor)
implements ArticleDefinition {
public HttpArticle {
Objects.requireNonNull(pagination, "pagination must not be null");
Objects.requireNonNull(extractor, "extractor must not be null");
}
}
/**
* Playwright render of the article URL, then HTML extraction. {@link
* PaginationMode#AUTOPAGERIZE} tracks multi-page chains through a short-lived standard browser
* context (no browser extension / persistent profile).
*/
record PlaywrightArticle(PaginationMode pagination, HtmlExtractor extractor)
implements ArticleDefinition {
public PlaywrightArticle {
Objects.requireNonNull(pagination, "pagination must not be null");
Objects.requireNonNull(extractor, "extractor must not be null");
}
}
}
}