Skip to content

Commit

Permalink
Add CursoredPage and interceptor
Browse files Browse the repository at this point in the history
  • Loading branch information
andriy-dmytruk committed Apr 23, 2024
1 parent b6278a9 commit 8aa2078
Show file tree
Hide file tree
Showing 17 changed files with 361 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.micronaut.data.annotation;

import io.micronaut.data.model.CursoredPage;
import io.micronaut.data.model.Page;
import io.micronaut.data.model.Pageable;
import io.micronaut.data.model.Slice;
Expand Down Expand Up @@ -59,7 +60,8 @@ TypeRole[] typeRoles() default {
@TypeRole(role = TypeRole.PAGEABLE, type = Pageable.class),
@TypeRole(role = TypeRole.SORT, type = Sort.class),
@TypeRole(role = TypeRole.SLICE, type = Slice.class),
@TypeRole(role = TypeRole.PAGE, type = Page.class)
@TypeRole(role = TypeRole.PAGE, type = Page.class),
@TypeRole(role = TypeRole.CURSORED_PAGE, type = CursoredPage.class)
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@
*/
String PAGE = "page";

/**
* The parameter that is used to represent a {@link io.micronaut.data.model.CursoredPage}.
*/
String CURSORED_PAGE = "cursoredPage";

/**
* The name of the role.
* @return The role name
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2017-2020 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.data.intercept;

/**
* An interceptor that handles a return type of {@link io.micronaut.data.model.CursoredPage}.
*
* @author Andriy Dmytruk
* @param <T> The declaring type
* @param <R> The return type
* @since 4.8.0
*/
public interface FindCursoredPageInterceptor<T, R> extends DataInterceptor<T, R> {
}
182 changes: 182 additions & 0 deletions data-model/src/main/java/io/micronaut/data/model/CursoredPage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
* Copyright 2017-2020 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.data.model;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import io.micronaut.context.annotation.DefaultImplementation;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.annotation.ReflectiveAccess;
import io.micronaut.core.annotation.TypeHint;
import io.micronaut.data.model.Pageable.Cursor;
import io.micronaut.data.model.Pageable.Mode;
import io.micronaut.serde.annotation.Serdeable;

import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
* Inspired by the Jakarta's {@code CursoredPage}, this models a type that supports
* pagination operations with cursors.
*
* <p>A CursoredPage is a result set associated with a particular {@link Pageable} that includes
* a calculation of the total size of page of records.</p>
*
* @param <T> The generic type
* @author Andriy Dmytruk
* @since 4.8.0
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@TypeHint(CursoredPage.class)
@JsonDeserialize(as = DefaultCursoredPage.class)
@Serdeable
@DefaultImplementation(DefaultCursoredPage.class)
public interface CursoredPage<T> extends Page<T> {

CursoredPage<?> EMPTY = new DefaultCursoredPage<>(Collections.emptyList(), Pageable.unpaged(), Collections.emptyList(), null);

/**
* @return Whether this {@link CursoredPage} contains the total count of the records
* @since 4.8.0
*/
boolean hasTotalSize();

/**
* Get the total count of all the records that can be given by this query.
* The method may produce a {@link IllegalStateException} if the {@link Pageable} request
* did not ask for total size.
*
* @return The total size of the all records.
*/
long getTotalSize();

/**
* Get the total count of pages that can be given by this query.
* The method may produce a {@link IllegalStateException} if the {@link Pageable} request
* did not ask for total size.
*
* @return The total page of pages
*/
default int getTotalPages() {
int size = getSize();
return size == 0 ? 1 : (int) Math.ceil((double) getTotalSize() / (double) size);
}

@Override
default boolean hasNext() {
Pageable pageable = getPageable();
if (pageable.getMode() == Mode.CURSOR_NEXT) {
return getContent().size() == pageable.getSize();
} else {
return true;
}
}

@Override
default boolean hasPrevious() {
Pageable pageable = getPageable();
if (pageable.getMode() == Mode.CURSOR_PREVIOUS) {
return getContent().size() == pageable.getSize();
} else {
return true;
}
}

@Override
default CursoredPageable nextPageable() {
Pageable pageable = getPageable();
Cursor cursor = getCursor(getCursors().size() - 1).orElse(pageable.cursor().orElse(null));
return Pageable.afterCursor(cursor, pageable.getNumber() + 1, pageable.getSize(), pageable.getSort());
}

@Override
default CursoredPageable previousPageable() {
Pageable pageable = getPageable();
Cursor cursor = getCursor(0).orElse(pageable.cursor().orElse(null));
return Pageable.beforeCursor(cursor, Math.max(0, pageable.getNumber() - 1), pageable.getSize(), pageable.getSort());
}


/**
* Maps the content with the given function.
*
* @param function The function to apply to each element in the content.
* @param <T2> The type returned by the function
* @return A new slice with the mapped content
*/
@Override
default @NonNull <T2> CursoredPage<T2> map(Function<T, T2> function) {
List<T2> content = getContent().stream().map(function).collect(Collectors.toList());
return new DefaultCursoredPage<>(content, getPageable(), getCursors(), getTotalSize());
}

/**
* Creates a cursored page from the given content, pageable, cursors and totalSize.
*
* @param content The content
* @param pageable The pageable
* @param cursors The cursors for cursored pagination
* @param totalSize The total size
* @param <T> The generic type
* @return The slice
*/
@JsonCreator
@ReflectiveAccess
static @NonNull <T> CursoredPage<T> of(
@JsonProperty("content") @NonNull List<T> content,
@JsonProperty("pageable") @NonNull Pageable pageable,
@JsonProperty("cursors") @Nullable List<Cursor> cursors,
@JsonProperty("totalSize") @Nullable Long totalSize
) {
return new DefaultCursoredPage<>(content, pageable, cursors, totalSize);
}

/**
* Get cursor at the given position or empty if no such cursor exists.
* There must be a cursor for each of the data entities in the same order.
* To start pagination after or before a cursor create a pageable from it using the
* same sorting as before.
*
* @param i The index of cursor to retrieve.
* @return The cursor at the provided index.
*/
Optional<Cursor> getCursor(int i);

/**
* Get all the cursors.
*
* @see #getCursor(int) getCursor(i) for more details.
* @return All the cursors
*/
List<Cursor> getCursors();

/**
* Creates an empty page object.
* @param <T2> The generic type
* @return The slice
*/
@SuppressWarnings("unchecked")
static @NonNull <T2> CursoredPage<T2> empty() {
return (CursoredPage<T2>) EMPTY;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import io.micronaut.serde.annotation.Serdeable;

/**
* Models pageable data that uses a currentCursor.
* Models a pageable request that uses a cursor.
*
* @author Andriy Dmytruk
* @since 4.8.0
Expand All @@ -35,13 +35,6 @@
@JsonIgnoreProperties(ignoreUnknown = true)
public interface CursoredPageable extends Pageable {

/**
* Constant for no pagination.
*/
CursoredPageable UNPAGED = new DefaultCursoredPageable(
-1, null, Mode.CURSOR_NEXT, 0, Sort.UNSORTED, true
);

/**
* Whether the pageable is traversing backwards.
*
Expand All @@ -62,7 +55,7 @@ default Mode getMode() {
*/
static @NonNull CursoredPageable from(Sort sort) {
if (sort == null) {
return UNPAGED;
sort = Sort.UNSORTED;
}
return new DefaultCursoredPageable(
-1, null, Mode.CURSOR_NEXT, 0, sort, true
Expand Down Expand Up @@ -113,11 +106,4 @@ default Mode getMode() {
return new DefaultCursoredPageable(size, cursor, mode, page, sort, requestTotal);
}

/**
* @return A new instance without paging data.
*/
static @NonNull CursoredPageable unpaged() {
return UNPAGED;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import io.micronaut.core.annotation.Creator;
import io.micronaut.core.annotation.ReflectiveAccess;
import io.micronaut.data.model.Pageable.Cursor;
import io.micronaut.data.model.Pageable.Mode;
import io.micronaut.serde.annotation.Serdeable;

import java.util.List;
Expand All @@ -35,7 +34,7 @@
* @param <T> The generic type
*/
@Serdeable
class DefaultCursoredPage<T> extends DefaultPage<T> {
class DefaultCursoredPage<T> extends DefaultPage<T> implements CursoredPage<T> {

private final List<Cursor> cursors;

Expand Down Expand Up @@ -78,41 +77,12 @@ public boolean equals(Object o) {

@Override
public Optional<Cursor> getCursor(int i) {
return i >= cursors.size() ? Optional.empty() : Optional.of(cursors.get(i));
return i >= cursors.size() || i < 0 ? Optional.empty() : Optional.of(cursors.get(i));
}

@Override
public boolean hasNext() {
Pageable pageable = getPageable();
if (pageable.getMode() == Mode.CURSOR_NEXT) {
return cursors.size() == pageable.getSize();
} else {
return true;
}
}

@Override
public boolean hasPrevious() {
Pageable pageable = getPageable();
if (pageable.getMode() == Mode.CURSOR_PREVIOUS) {
return cursors.size() == pageable.getSize();
} else {
return true;
}
}

@Override
public Pageable nextPageable() {
Pageable pageable = getPageable();
Cursor cursor = cursors.isEmpty() ? pageable.cursor().orElse(null) : cursors.get(cursors.size() - 1);
return Pageable.afterCursor(cursor, pageable.getNumber() + 1, pageable.getSize(), pageable.getSort());
}

@Override
public Pageable previousPageable() {
Pageable pageable = getPageable();
Cursor cursor = cursors.isEmpty() ? pageable.cursor().orElse(null) : cursors.get(0);
return Pageable.beforeCursor(cursor, Math.max(0, pageable.getNumber() - 1), pageable.getSize(), pageable.getSort());
public List<Cursor> getCursors() {
return cursors;
}

@Override
Expand Down
Loading

0 comments on commit 8aa2078

Please sign in to comment.