KeysetCursor.java

package com.ecommerce.platform.common.api;

import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.Base64;
import java.util.Objects;

public record KeysetCursor(Instant createdAt, long id) {

    private static final String VERSION = "v1";
    private static final String SEPARATOR = "|";

    public KeysetCursor {
        Objects.requireNonNull(createdAt, "createdAt must not be null");
        if (id <= 0) {
            throw new IllegalArgumentException("Cursor id must be positive");
        }
    }

    public String encode() {
        String payload = VERSION + SEPARATOR + createdAt + SEPARATOR + id;
        return Base64.getUrlEncoder().withoutPadding()
                .encodeToString(payload.getBytes(StandardCharsets.UTF_8));
    }

    public static KeysetCursor decode(String encoded) {
        if (encoded == null || encoded.isBlank()) {
            throw new IllegalArgumentException("Cursor must not be blank");
        }
        try {
            String payload = new String(
                    Base64.getUrlDecoder().decode(encoded),
                    StandardCharsets.UTF_8);
            String[] parts = payload.split("\\|", -1);
            if (parts.length != 3 || !VERSION.equals(parts[0])) {
                throw new IllegalArgumentException("Unsupported cursor payload");
            }
            return new KeysetCursor(Instant.parse(parts[1]), Long.parseLong(parts[2]));
        } catch (DateTimeParseException | NumberFormatException exception) {
            throw new IllegalArgumentException("Invalid cursor payload", exception);
        } catch (IllegalArgumentException exception) {
            throw new IllegalArgumentException("Invalid cursor", exception);
        }
    }
}