PaginationUriSupport.java
package net.sasasin.sreader.service.autopagerize;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
/**
* URI normalization, origin comparison, and next-link resolution for AutoPagerize pagination.
* Comparison canonical forms are separate from fetch URIs (fetch URIs are not rewritten).
*/
public final class PaginationUriSupport {
private PaginationUriSupport() {}
public static boolean isAllowedScheme(URI uri) {
Objects.requireNonNull(uri, "uri must not be null");
String scheme = uri.getScheme();
return "http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme);
}
public static boolean hasUserInfo(URI uri) {
Objects.requireNonNull(uri, "uri must not be null");
String userInfo = uri.getUserInfo();
return userInfo != null && !userInfo.isBlank();
}
/**
* Canonical form for visited-set comparison: lower-case host, effective port, normalized path, no
* fragment. Query is preserved.
*/
public static URI forVisitedComparison(URI uri) {
Objects.requireNonNull(uri, "uri must not be null");
try {
URI normalized = uri.normalize();
String scheme = lower(normalized.getScheme());
String host = lower(normalized.getHost());
int port = effectivePort(scheme, normalized.getPort());
String path = normalized.getPath();
if (path == null || path.isEmpty()) {
path = "/";
}
return new URI(scheme, null, host, port, path, normalized.getQuery(), null);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Cannot canonicalize URI: " + uri, e);
}
}
public static boolean sameOrigin(URI left, URI right) {
Objects.requireNonNull(left, "left must not be null");
Objects.requireNonNull(right, "right must not be null");
if (!Objects.equals(lower(left.getScheme()), lower(right.getScheme()))) {
return false;
}
if (!Objects.equals(lower(left.getHost()), lower(right.getHost()))) {
return false;
}
return effectivePort(left.getScheme(), left.getPort())
== effectivePort(right.getScheme(), right.getPort());
}
public static int effectivePort(String scheme, int port) {
if (port >= 0) {
return port;
}
if ("http".equalsIgnoreCase(scheme)) {
return 80;
}
if ("https".equalsIgnoreCase(scheme)) {
return 443;
}
return -1;
}
/**
* Resolves a next-link candidate against {@code <base href>} when present, otherwise against the
* page final URI.
*/
public static Optional<URI> resolveNextCandidate(
URI pageFinalUri, Document document, String raw) {
Objects.requireNonNull(pageFinalUri, "pageFinalUri must not be null");
Objects.requireNonNull(document, "document must not be null");
if (raw == null || raw.isBlank()) {
return Optional.empty();
}
try {
return Optional.of(resolveDocumentBase(pageFinalUri, document).resolve(raw.trim()));
} catch (IllegalArgumentException e) {
return Optional.empty();
}
}
public static URI resolveDocumentBase(URI pageFinalUri, Document document) {
Objects.requireNonNull(pageFinalUri, "pageFinalUri must not be null");
Objects.requireNonNull(document, "document must not be null");
Element base = document.selectFirst("base[href]");
if (base == null) {
return pageFinalUri;
}
String href = base.attr("href");
if (href.isBlank()) {
return pageFinalUri;
}
try {
return pageFinalUri.resolve(href.trim());
} catch (IllegalArgumentException e) {
return pageFinalUri;
}
}
public static Optional<String> firstNonBlankAttribute(Element element) {
Objects.requireNonNull(element, "element must not be null");
for (String attr : new String[] {"href", "action", "value"}) {
String value = element.attr(attr);
if (!value.isBlank()) {
return Optional.of(value.trim());
}
}
return Optional.empty();
}
private static String lower(String value) {
return value == null ? null : value.toLowerCase(Locale.ROOT);
}
}