AutoPagerizePageAnalyzer.java
package net.sasasin.sreader.service.autopagerize;
import java.net.URI;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.springframework.stereotype.Component;
/** Applies AutoPagerize nextLink / pageElement XPath to a loaded page. */
@Component
public class AutoPagerizePageAnalyzer {
public Document parse(PageSnapshot snapshot) {
Objects.requireNonNull(snapshot, "snapshot must not be null");
return Jsoup.parse(snapshot.html(), snapshot.finalUri().toString());
}
/**
* Returns whether the XPath selects at least one element. Distinguishes empty selection from
* invalid XPath evaluation.
*/
public XPathPresence presence(Document document, String xpath) {
Objects.requireNonNull(document, "document must not be null");
Objects.requireNonNull(xpath, "xpath must not be null");
try {
Elements elements = document.selectXpath(xpath);
if (elements.isEmpty()) {
return XPathPresence.EMPTY;
}
return XPathPresence.PRESENT;
} catch (RuntimeException e) {
return XPathPresence.INVALID;
}
}
public PageAnalysis analyze(PageSnapshot snapshot, CompiledAutoPagerizeRule rule) {
Objects.requireNonNull(snapshot, "snapshot must not be null");
Objects.requireNonNull(rule, "rule must not be null");
Document document = parse(snapshot);
Elements pageElements = document.selectXpath(rule.pageElementXpath());
if (pageElements.isEmpty()) {
throw new IllegalStateException("pageElement XPath matched zero elements");
}
String outerHtml = pageElements.stream().map(Element::outerHtml).collect(Collectors.joining());
String text =
pageElements.eachText().stream()
.filter(value -> !value.isBlank())
.collect(Collectors.joining("\n\n"));
String hash = PageElementContentHasher.sha256Hex(outerHtml);
Elements nextLinks = document.selectXpath(rule.nextLinkXpath());
if (nextLinks.isEmpty()) {
return new PageAnalysis(
outerHtml, text, hash, Optional.empty(), Optional.of(PageAnalysis.NextLinkIssue.MISSING));
}
Element first = nextLinks.first();
Optional<String> raw = PaginationUriSupport.firstNonBlankAttribute(first);
if (raw.isEmpty()) {
return new PageAnalysis(
outerHtml, text, hash, Optional.empty(), Optional.of(PageAnalysis.NextLinkIssue.MISSING));
}
Optional<URI> resolved =
PaginationUriSupport.resolveNextCandidate(snapshot.finalUri(), document, raw.get());
if (resolved.isEmpty()) {
return new PageAnalysis(
outerHtml,
text,
hash,
Optional.empty(),
Optional.of(PageAnalysis.NextLinkIssue.INVALID_URI));
}
URI next = resolved.get();
if (!PaginationUriSupport.isAllowedScheme(next)) {
return new PageAnalysis(
outerHtml,
text,
hash,
Optional.of(next),
Optional.of(PageAnalysis.NextLinkIssue.UNSUPPORTED_SCHEME));
}
if (PaginationUriSupport.hasUserInfo(next)) {
return new PageAnalysis(
outerHtml,
text,
hash,
Optional.of(next),
Optional.of(PageAnalysis.NextLinkIssue.USERINFO_REJECTED));
}
return new PageAnalysis(
outerHtml, text, hash, Optional.of(next), Optional.of(PageAnalysis.NextLinkIssue.NONE));
}
public enum XPathPresence {
PRESENT,
EMPTY,
INVALID
}
}