committed by
Christoph Strobl
parent
a5de842460
commit
035965a3a2
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or 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 org.springframework.data.domain;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* A {@link ScrollPosition} based on the last seen keyset. Keyset scrolling must be associated with a {@link Sort
|
||||
* well-defined sort} to be able to extract the keyset when resuming scrolling within the sorted result set.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.1
|
||||
*/
|
||||
public final class KeysetScrollPosition implements ScrollPosition {
|
||||
|
||||
private static final KeysetScrollPosition initial = new KeysetScrollPosition(Collections.emptyMap(),
|
||||
Direction.Forward);
|
||||
|
||||
private final Map<String, Object> keys;
|
||||
|
||||
private final Direction direction;
|
||||
|
||||
private KeysetScrollPosition(Map<String, Object> keys, Direction direction) {
|
||||
this.keys = keys;
|
||||
this.direction = direction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new initial {@link KeysetScrollPosition} to start scrolling using keyset-queries.
|
||||
*
|
||||
* @return a new initial {@link KeysetScrollPosition} to start scrolling using keyset-queries.
|
||||
*/
|
||||
public static KeysetScrollPosition initial() {
|
||||
return initial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link KeysetScrollPosition} from a keyset.
|
||||
*
|
||||
* @param keys must not be {@literal null}.
|
||||
* @return a new {@link KeysetScrollPosition} for the given keyset.
|
||||
*/
|
||||
public static KeysetScrollPosition of(Map<String, ?> keys) {
|
||||
return of(keys, Direction.Forward);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link KeysetScrollPosition} from a keyset and {@link Direction}.
|
||||
*
|
||||
* @param keys must not be {@literal null}.
|
||||
* @param direction must not be {@literal null}.
|
||||
* @return a new {@link KeysetScrollPosition} for the given keyset and {@link Direction}.
|
||||
*/
|
||||
public static KeysetScrollPosition of(Map<String, ?> keys, Direction direction) {
|
||||
|
||||
Assert.notNull(keys, "Keys must not be null");
|
||||
Assert.notNull(direction, "Direction must not be null");
|
||||
|
||||
if (keys.isEmpty()) {
|
||||
return initial();
|
||||
}
|
||||
|
||||
return new KeysetScrollPosition(Collections.unmodifiableMap(new LinkedHashMap<>(keys)), direction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInitial() {
|
||||
return keys.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the keyset.
|
||||
*/
|
||||
public Map<String, Object> getKeys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the scroll direction.
|
||||
*/
|
||||
public Direction getDirection() {
|
||||
return direction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
KeysetScrollPosition that = (KeysetScrollPosition) o;
|
||||
return ObjectUtils.nullSafeEquals(keys, that.keys) && direction == that.direction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
|
||||
int result = 17;
|
||||
|
||||
result += 31 * ObjectUtils.nullSafeHashCode(keys);
|
||||
result += 31 * ObjectUtils.nullSafeHashCode(direction);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("KeysetScrollPosition [%s, %s]", direction, keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyset scrolling direction.
|
||||
*/
|
||||
enum Direction {
|
||||
|
||||
/**
|
||||
* Forward (default) direction to scroll from the beginning of the results to their end.
|
||||
*/
|
||||
Forward,
|
||||
|
||||
/**
|
||||
* Backward direction to scroll from the end of the results to their beginning.
|
||||
*/
|
||||
Backward;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or 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 org.springframework.data.domain;
|
||||
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* A {@link ScrollPosition} based on the offsets within query results.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.1
|
||||
*/
|
||||
public final class OffsetScrollPosition implements ScrollPosition {
|
||||
|
||||
private static final OffsetScrollPosition initial = new OffsetScrollPosition(0);
|
||||
|
||||
private final long offset;
|
||||
|
||||
private OffsetScrollPosition(long offset) {
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new initial {@link OffsetScrollPosition} to start scrolling using offset/limit.
|
||||
*
|
||||
* @return a new initial {@link OffsetScrollPosition} to start scrolling using offset/limit.
|
||||
*/
|
||||
public static OffsetScrollPosition initial() {
|
||||
return initial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link OffsetScrollPosition} from an {@code offset}.
|
||||
*
|
||||
* @param offset
|
||||
* @return a new {@link OffsetScrollPosition} with the given {@code offset}.
|
||||
*/
|
||||
public static OffsetScrollPosition of(long offset) {
|
||||
|
||||
if (offset == 0) {
|
||||
return initial();
|
||||
}
|
||||
|
||||
return new OffsetScrollPosition(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link IntFunction position function} to calculate.
|
||||
*
|
||||
* @param startOffset the start offset to be used. Must not be negative.
|
||||
* @return the offset-based position function.
|
||||
*/
|
||||
public static IntFunction<OffsetScrollPosition> positionFunction(long startOffset) {
|
||||
|
||||
Assert.isTrue(startOffset >= 0, "Start offset must not be negative");
|
||||
|
||||
return startOffset == 0 ? OffsetPositionFunction.ZERO : new OffsetPositionFunction(startOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInitial() {
|
||||
return offset == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the offset.
|
||||
*/
|
||||
public long getOffset() {
|
||||
return offset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
OffsetScrollPosition that = (OffsetScrollPosition) o;
|
||||
return offset == that.offset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
|
||||
int result = 17;
|
||||
|
||||
result += 31 * ObjectUtils.nullSafeHashCode(offset);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("OffsetScrollPosition [%s]", offset);
|
||||
}
|
||||
|
||||
private record OffsetPositionFunction(long startOffset) implements IntFunction<OffsetScrollPosition> {
|
||||
|
||||
static final OffsetPositionFunction ZERO = new OffsetPositionFunction(0);
|
||||
|
||||
@Override
|
||||
public OffsetScrollPosition apply(int offset) {
|
||||
|
||||
if (offset < 0) {
|
||||
throw new IndexOutOfBoundsException(offset);
|
||||
}
|
||||
|
||||
return of(startOffset + offset + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,4 +161,20 @@ public interface Pageable {
|
||||
return isUnpaged() ? Optional.empty() : Optional.of(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link OffsetScrollPosition} from this pageable if the page request {@link #isPaged() is paged}.
|
||||
*
|
||||
* @return
|
||||
* @throws IllegalStateException if the request is {@link #isUnpaged()}
|
||||
* @since 3.1
|
||||
*/
|
||||
default OffsetScrollPosition toScrollPosition() {
|
||||
|
||||
if (isUnpaged()) {
|
||||
throw new IllegalStateException("Cannot create OffsetScrollPosition from an unpaged instance");
|
||||
}
|
||||
|
||||
return OffsetScrollPosition.of(getOffset());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
167
src/main/java/org/springframework/data/domain/Scroll.java
Normal file
167
src/main/java/org/springframework/data/domain/Scroll.java
Normal file
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or 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 org.springframework.data.domain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
import org.springframework.data.util.Streamable;
|
||||
|
||||
/**
|
||||
* A scroll of data consumed from an underlying query result. A scroll is similar to {@link Slice} in the sense that it
|
||||
* contains a subset of the actual query results for easier consumption of large result sets. The scroll is less
|
||||
* opinionated about the actual data retrieval, whether the query has used index/offset, keyset-based pagination or
|
||||
* cursor resume tokens.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.1
|
||||
* @see ScrollPosition
|
||||
*/
|
||||
public interface Scroll<T> extends Streamable<T> {
|
||||
|
||||
/**
|
||||
* Construct a {@link Scroll}.
|
||||
*
|
||||
* @param items the list of data.
|
||||
* @param positionFunction the list of data.
|
||||
* @return the {@link Scroll}.
|
||||
* @param <T>
|
||||
*/
|
||||
static <T> Scroll<T> from(List<T> items, IntFunction<? extends ScrollPosition> positionFunction) {
|
||||
return new ScrollImpl<>(items, positionFunction, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a {@link Scroll}.
|
||||
*
|
||||
* @param items the list of data.
|
||||
* @param positionFunction the list of data.
|
||||
* @param hasNext
|
||||
* @return the {@link Scroll}.
|
||||
* @param <T>
|
||||
*/
|
||||
static <T> Scroll<T> from(List<T> items, IntFunction<? extends ScrollPosition> positionFunction, boolean hasNext) {
|
||||
return new ScrollImpl<>(items, positionFunction, hasNext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of elements in this scroll.
|
||||
*
|
||||
* @return the number of elements in this scroll.
|
||||
*/
|
||||
int size();
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this scroll contains no elements.
|
||||
*
|
||||
* @return {@code true} if this scroll contains no elements
|
||||
*/
|
||||
boolean isEmpty();
|
||||
|
||||
/**
|
||||
* Returns the scroll content as {@link List}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
List<T> getContent();
|
||||
|
||||
/**
|
||||
* Returns whether the current scroll is the last one.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
default boolean isLast() {
|
||||
return !hasNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if there is a next scroll.
|
||||
*
|
||||
* @return if there is a next scroll window.
|
||||
*/
|
||||
boolean hasNext();
|
||||
|
||||
/**
|
||||
* Returns the {@link ScrollPosition} at {@code index}.
|
||||
*
|
||||
* @param index
|
||||
* @return
|
||||
* @throws IndexOutOfBoundsException if the index is out of range ({@code index < 0 || index >= size()}).
|
||||
*/
|
||||
ScrollPosition positionAt(int index);
|
||||
|
||||
/**
|
||||
* Returns the {@link ScrollPosition} for {@code object}.
|
||||
*
|
||||
* @param object
|
||||
* @return
|
||||
* @throws NoSuchElementException if the object is not part of the result.
|
||||
*/
|
||||
default ScrollPosition positionAt(T object) {
|
||||
|
||||
int index = getContent().indexOf(object);
|
||||
|
||||
if (index == -1) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
|
||||
return positionAt(index);
|
||||
}
|
||||
|
||||
// TODO: First and last seem to conflict with first/last scroll or first/last position of elements.
|
||||
// these methods should rather express the position of the first element within this scroll and the scroll position
|
||||
// to be used to get the next Scroll.
|
||||
/**
|
||||
* Returns the first {@link ScrollPosition} or throw {@link NoSuchElementException} if the list is empty.
|
||||
*
|
||||
* @return the first {@link ScrollPosition}.
|
||||
* @throws NoSuchElementException if this result is empty.
|
||||
*/
|
||||
default ScrollPosition firstPosition() {
|
||||
|
||||
if (size() == 0) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
|
||||
return positionAt(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last {@link ScrollPosition} or throw {@link NoSuchElementException} if the list is empty.
|
||||
*
|
||||
* @return the last {@link ScrollPosition}.
|
||||
* @throws NoSuchElementException if this result is empty.
|
||||
*/
|
||||
default ScrollPosition lastPosition() {
|
||||
|
||||
if (size() == 0) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
|
||||
return positionAt(size() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link Scroll} with the content of the current one mapped by the given {@code converter}.
|
||||
*
|
||||
* @param converter must not be {@literal null}.
|
||||
* @return a new {@link Scroll} with the content of the current one mapped by the given {@code converter}.
|
||||
*/
|
||||
<U> Scroll<U> map(Function<? super T, ? extends U> converter);
|
||||
|
||||
}
|
||||
119
src/main/java/org/springframework/data/domain/ScrollImpl.java
Normal file
119
src/main/java/org/springframework/data/domain/ScrollImpl.java
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or 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 org.springframework.data.domain;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.IntFunction;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Default {@link Scroll} implementation.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.1
|
||||
*/
|
||||
class ScrollImpl<T> implements Scroll<T> {
|
||||
|
||||
private final List<T> items;
|
||||
private final IntFunction<? extends ScrollPosition> positionFunction;
|
||||
|
||||
private final boolean hasNext;
|
||||
|
||||
ScrollImpl(List<T> items, IntFunction<? extends ScrollPosition> positionFunction, boolean hasNext) {
|
||||
|
||||
Assert.notNull(items, "List of items must not be null");
|
||||
Assert.notNull(positionFunction, "Position function must not be null");
|
||||
|
||||
this.items = items;
|
||||
this.positionFunction = positionFunction;
|
||||
this.hasNext = hasNext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return items.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return items.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<T> getContent() {
|
||||
return items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return hasNext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScrollPosition positionAt(int index) {
|
||||
|
||||
if (index < 0 || index >= size()) {
|
||||
throw new IndexOutOfBoundsException(index);
|
||||
}
|
||||
|
||||
return positionFunction.apply(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <U> Scroll<U> map(Function<? super T, ? extends U> converter) {
|
||||
|
||||
Assert.notNull(converter, "Function must not be null");
|
||||
|
||||
return new ScrollImpl<>(stream().map(converter).collect(Collectors.toList()), positionFunction, hasNext);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Iterator<T> iterator() {
|
||||
return items.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
ScrollImpl<?> that = (ScrollImpl<?>) o;
|
||||
return ObjectUtils.nullSafeEquals(items, that.items)
|
||||
&& ObjectUtils.nullSafeEquals(positionFunction, that.positionFunction)
|
||||
&& ObjectUtils.nullSafeEquals(hasNext, that.hasNext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = ObjectUtils.nullSafeHashCode(items);
|
||||
result = 31 * result + ObjectUtils.nullSafeHashCode(positionFunction);
|
||||
result = 31 * result + ObjectUtils.nullSafeHashCode(hasNext);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Scroll " + items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or 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 org.springframework.data.domain;
|
||||
|
||||
/**
|
||||
* Interface to specify a position within a total query result. Scroll positions are used to start scrolling from the
|
||||
* beginning of a query result or to resume scrolling from a given position within the query result.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.1
|
||||
*/
|
||||
public interface ScrollPosition {
|
||||
|
||||
/**
|
||||
* Returns whether the current scroll position is the initial one.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
boolean isInitial();
|
||||
}
|
||||
Reference in New Issue
Block a user