AbstractPagedRestApiSpliterator.java

package com.fwmotion.threescale.cms.support;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fwmotion.threescale.cms.exception.ThreescaleCmsApiException;
import com.fwmotion.threescale.cms.exception.ThreescaleCmsException;
import com.fwmotion.threescale.cms.exception.ThreescaleCmsUnexpectedPaginationException;
import com.redhat.threescale.rest.cms.ApiException;
import com.redhat.threescale.rest.cms.model.Error;
import com.redhat.threescale.rest.cms.model.ListPaginationMetadata;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;

import java.util.*;
import java.util.function.Consumer;

public abstract class AbstractPagedRestApiSpliterator<T> implements Spliterator<T> {

    protected static final int DEFAULT_REQUESTED_PAGE_SIZE = 20;

    private final int requestedPageSize;
    private final ObjectMapper objectMapper;
    private Iterator<T> currentPageIterator;
    private int currentPageSize;
    private int currentPageNumber;
    private boolean didSplit = false;

    protected AbstractPagedRestApiSpliterator(@Positive int requestedPageSize,
                                              @Nonnull ObjectMapper objectMapper,
                                              @Nonnull Collection<T> currentPage,
                                              @PositiveOrZero int currentPageNumber) {
        this.requestedPageSize = requestedPageSize;
        this.objectMapper = objectMapper;
        this.currentPageIterator = currentPage.iterator();
        this.currentPageSize = currentPage.size();
        this.currentPageNumber = currentPageNumber;
    }

    protected AbstractPagedRestApiSpliterator(@Nonnull Collection<T> currentPage,
                                              @Nonnull ObjectMapper objectMapper,
                                              @PositiveOrZero int currentPageNumber) {
        this(DEFAULT_REQUESTED_PAGE_SIZE, objectMapper, currentPage, currentPageNumber);
    }

    @Nonnull
    ObjectMapper getObjectMapper() {
        return objectMapper;
    }

    @Nullable
    abstract protected Collection<T> getPage(@PositiveOrZero int pageNumber,
                                             @Positive int pageSize);

    @Nonnull
    abstract protected AbstractPagedRestApiSpliterator<T> doSplit(
        @Positive int requestedPageSize,
        @Nonnull Collection<T> currentPage,
        @PositiveOrZero int currentPageNumber);

    @Nonnull
    @Override
    public abstract Comparator<? super T> getComparator();

    @Override
    public boolean tryAdvance(@Nonnull Consumer<? super T> action) {
        // Try to advance to next page if the current one doesn't exist
        if (!currentPageIterator.hasNext()) {
            if (didSplit) {
                return false;
            }

            Collection<T> nextPage = getPage(currentPageNumber + 1, requestedPageSize);

            if (nextPage == null || nextPage.isEmpty()) {
                didSplit = true;
                return false;
            }

            currentPageIterator = nextPage.iterator();
            currentPageSize = nextPage.size();
            currentPageNumber++;
        }

        action.accept(currentPageIterator.next());
        return true;
    }

    @Nullable
    @Override
    public AbstractPagedRestApiSpliterator<T> trySplit() {
        // Don't split again
        if (didSplit) {
            return null;
        }
        didSplit = true;

        // If at the end of the list, return null
        if (currentPageNumber > 0 && !currentPageIterator.hasNext() && currentPageSize < requestedPageSize) {
            return null;
        }

        // Get the next page
        Collection<T> nextPage = getPage(currentPageNumber + 1, requestedPageSize);

        if (nextPage == null || nextPage.isEmpty()) {
            return null;
        }

        return doSplit(requestedPageSize, nextPage, currentPageNumber + 1);
    }

    @PositiveOrZero
    @Override
    public long estimateSize() {
        return Long.MAX_VALUE;
    }

    protected void validateResultPageSize(@Nonnull String type,
                                          @PositiveOrZero int pageNumber,
                                          @Positive int pageSize,
                                          @Nonnull Collection<T> resultPage,
                                          @Nullable ListPaginationMetadata paginationMetadata) {
        int currentPage = Optional.ofNullable(paginationMetadata)
            .map(ListPaginationMetadata::getCurrentPage)
            .orElse(pageNumber);
        Optional<Integer> totalPagesOptional = Optional.ofNullable(paginationMetadata)
            .map(ListPaginationMetadata::getTotalPages);
        int totalPages = totalPagesOptional
            .orElse(Integer.MAX_VALUE);
        int perPage = Optional.ofNullable(paginationMetadata)
            .map(ListPaginationMetadata::getPerPage)
            .orElse(pageSize);

        int expectedPageSize;
        if (currentPage > totalPages) {
            expectedPageSize = 0;
        } else if (totalPagesOptional.isEmpty()
            || currentPage == totalPages) {
            expectedPageSize = Optional.ofNullable(paginationMetadata)
                .map(ListPaginationMetadata::getTotalEntries)
                .map(totalEntries -> totalEntries % perPage)
                .orElseGet(resultPage::size);
        } else {
            expectedPageSize = perPage;
        }

        if (resultPage.size() == expectedPageSize) {
            return;
        }

        throw new ThreescaleCmsUnexpectedPaginationException(type,
            pageNumber,
            pageSize,
            resultPage.size(),
            expectedPageSize);
    }

    protected ThreescaleCmsException handleApiException(
        @Nonnull ApiException e,
        @Nonnull String type,
        @PositiveOrZero int pageNumber,
        @Positive int pageSize
    ) {
        String errorMessage = "Unexpected exception while iterating CMS " + type
            + " page " + pageNumber
            + " (with page size of " + pageSize + ")";

        Error responseError;
        try {
            responseError = objectMapper.readValue(e.getResponseBody(), Error.class);
        } catch (JsonProcessingException ex) {
            return new ThreescaleCmsApiException(e.getCode(), errorMessage, e);
        }

        return new ThreescaleCmsApiException(e.getCode(),
            responseError,
            errorMessage,
            e);
    }

}