Polishing Window scrolling API.
Moved general-purpose factory methods to the common ScrollPosition interface. Introduced a couple of domain specific methods to conveniently work with both Keyset- (to e.g. change direction) and OffsetScrollPosition (to e.g. advance the offset). Consolidated unit tests accordingly. Moved WindowIterator to ….data.support package. Fixed case of Direction enum values. Fixes #2824.
This commit is contained in:
@@ -18,27 +18,33 @@ package org.springframework.data.domain;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
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.
|
||||
* A {@link ScrollPosition} based on the last seen key set. Keyset scrolling must be associated with a {@link Sort
|
||||
* well-defined sort} to be able to extract the key set when resuming scrolling within the sorted result set.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Oliver Drotbohm
|
||||
* @since 3.1
|
||||
*/
|
||||
public final class KeysetScrollPosition implements ScrollPosition {
|
||||
|
||||
private static final KeysetScrollPosition initial = new KeysetScrollPosition(Collections.emptyMap(),
|
||||
Direction.Forward);
|
||||
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) {
|
||||
|
||||
Assert.notNull(keys, "Keys must not be null");
|
||||
Assert.notNull(direction, "Direction must not be null");
|
||||
|
||||
this.keys = keys;
|
||||
this.direction = direction;
|
||||
}
|
||||
@@ -46,44 +52,27 @@ public final class KeysetScrollPosition implements ScrollPosition {
|
||||
/**
|
||||
* Creates a new initial {@link KeysetScrollPosition} to start scrolling using keyset-queries.
|
||||
*
|
||||
* @return a new initial {@link KeysetScrollPosition} to start scrolling using keyset-queries.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static KeysetScrollPosition initial() {
|
||||
return initial;
|
||||
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}.
|
||||
* Creates a new {@link KeysetScrollPosition} from a key set 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}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static KeysetScrollPosition of(Map<String, ?> keys, Direction direction) {
|
||||
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 keys.isEmpty()
|
||||
? initial()
|
||||
: new KeysetScrollPosition(Collections.unmodifiableMap(new LinkedHashMap<>(keys)), direction);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,45 +89,78 @@ public final class KeysetScrollPosition implements ScrollPosition {
|
||||
return direction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current {@link KeysetScrollPosition} scrolls forward.
|
||||
*
|
||||
* @return whether the current {@link KeysetScrollPosition} scrolls forward.
|
||||
*/
|
||||
public boolean scrollsForward() {
|
||||
return direction == Direction.FORWARD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current {@link KeysetScrollPosition} scrolls backward.
|
||||
*
|
||||
* @return whether the current {@link KeysetScrollPosition} scrolls backward.
|
||||
*/
|
||||
public boolean scrollsBackward() {
|
||||
return direction == Direction.BACKWARD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link KeysetScrollPosition} based on the same keyset and scrolling forward.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public KeysetScrollPosition forward() {
|
||||
return direction == Direction.FORWARD ? this : new KeysetScrollPosition(keys, Direction.FORWARD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link KeysetScrollPosition} based on the same keyset and scrolling backward.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public KeysetScrollPosition backward() {
|
||||
return direction == Direction.BACKWARD ? this : new KeysetScrollPosition(keys, Direction.BACKWARD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link KeysetScrollPosition} with the direction reversed.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public KeysetScrollPosition reverse() {
|
||||
return new KeysetScrollPosition(keys, direction.reverse());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
public boolean isInitial() {
|
||||
return keys.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
|
||||
if (this == o) {
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
}
|
||||
|
||||
if (!(o instanceof KeysetScrollPosition that)) {
|
||||
return false;
|
||||
KeysetScrollPosition that = (KeysetScrollPosition) o;
|
||||
return ObjectUtils.nullSafeEquals(keys, that.keys) && direction == that.direction;
|
||||
}
|
||||
|
||||
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;
|
||||
return Objects.hash(keys, direction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("KeysetScrollPosition [%s, %s]", direction, keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyset scrolling direction.
|
||||
*/
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,49 +15,54 @@
|
||||
*/
|
||||
package org.springframework.data.domain;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* A {@link ScrollPosition} based on the offsets within query results.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Oliver Drotbohm
|
||||
* @since 3.1
|
||||
*/
|
||||
public final class OffsetScrollPosition implements ScrollPosition {
|
||||
|
||||
private static final OffsetScrollPosition initial = new OffsetScrollPosition(0);
|
||||
private static final OffsetScrollPosition INITIAL = new OffsetScrollPosition(0);
|
||||
|
||||
private final long offset;
|
||||
|
||||
/**
|
||||
* Creates a new {@link OffsetScrollPosition} for the given non-negative offset.
|
||||
*
|
||||
* @param offset must be greater or equal to zero.
|
||||
*/
|
||||
private OffsetScrollPosition(long offset) {
|
||||
|
||||
Assert.isTrue(offset >= 0, "Offset must not be negative");
|
||||
|
||||
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.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static OffsetScrollPosition initial() {
|
||||
return initial;
|
||||
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}.
|
||||
* @param offset the non-negative offset to start at.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static OffsetScrollPosition of(long offset) {
|
||||
|
||||
if (offset == 0) {
|
||||
return initial();
|
||||
}
|
||||
|
||||
return new OffsetScrollPosition(offset);
|
||||
static OffsetScrollPosition of(long offset) {
|
||||
return offset == 0 ? initial() : new OffsetScrollPosition(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,36 +78,51 @@ public final class OffsetScrollPosition implements ScrollPosition {
|
||||
return startOffset == 0 ? OffsetPositionFunction.ZERO : new OffsetPositionFunction(startOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInitial() {
|
||||
return offset == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The zero or positive offset.
|
||||
*
|
||||
* @return the offset.
|
||||
*/
|
||||
public long getOffset() {
|
||||
return offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link OffsetScrollPosition} that has been advanced by the given value. Negative deltas will be
|
||||
* constrained so that the new offset is at least zero.
|
||||
*
|
||||
* @param delta the value to advance the current offset by.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public OffsetScrollPosition advanceBy(long delta) {
|
||||
|
||||
var value = offset + delta;
|
||||
|
||||
return new OffsetScrollPosition(value < 0 ? 0 : value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
public boolean isInitial() {
|
||||
return offset == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
|
||||
if (this == o) {
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
}
|
||||
|
||||
if (!(o instanceof OffsetScrollPosition that)) {
|
||||
return false;
|
||||
OffsetScrollPosition that = (OffsetScrollPosition) o;
|
||||
}
|
||||
|
||||
return offset == that.offset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
|
||||
int result = 17;
|
||||
|
||||
result += 31 * ObjectUtils.nullSafeHashCode(offset);
|
||||
|
||||
return result;
|
||||
return Objects.hash(offset);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -174,7 +174,6 @@ public interface Pageable {
|
||||
throw new IllegalStateException("Cannot create OffsetScrollPosition from an unpaged instance");
|
||||
}
|
||||
|
||||
return OffsetScrollPosition.of(getOffset());
|
||||
return ScrollPosition.offset(getOffset());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.domain;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -30,4 +32,83 @@ public interface ScrollPosition {
|
||||
* @return
|
||||
*/
|
||||
boolean isInitial();
|
||||
|
||||
/**
|
||||
* Creates a new initial {@link ScrollPosition} to start scrolling using keyset-queries.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static KeysetScrollPosition keyset() {
|
||||
return KeysetScrollPosition.initial();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new initial {@link ScrollPosition} to start scrolling using offset / limit.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static OffsetScrollPosition offset() {
|
||||
return OffsetScrollPosition.initial();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ScrollPosition} from an {@code offset}.
|
||||
*
|
||||
* @param offset
|
||||
* @return a new {@link OffsetScrollPosition} with the given {@code offset}.
|
||||
*/
|
||||
public static OffsetScrollPosition offset(long offset) {
|
||||
return OffsetScrollPosition.of(offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ScrollPosition} from a key set scrolling forward.
|
||||
*
|
||||
* @param keys must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static KeysetScrollPosition forward(Map<String, ?> keys) {
|
||||
return of(keys, Direction.FORWARD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ScrollPosition} from a key set scrolling backward.
|
||||
*
|
||||
* @param keys must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static KeysetScrollPosition backward(Map<String, ?> keys) {
|
||||
return of(keys, Direction.BACKWARD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ScrollPosition} from a key set and {@link Direction}.
|
||||
*
|
||||
* @param keys must not be {@literal null}.
|
||||
* @param direction must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static KeysetScrollPosition of(Map<String, ?> keys, Direction direction) {
|
||||
return KeysetScrollPosition.of(keys, direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyset scrolling direction.
|
||||
*/
|
||||
public 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;
|
||||
|
||||
Direction reverse() {
|
||||
return this == FORWARD ? BACKWARD : FORWARD;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
/*
|
||||
* 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.NoSuchElementException;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An {@link Iterator} over multiple {@link Window Windows} obtained via a {@link Function window function}, that keeps
|
||||
* track of the current {@link ScrollPosition} allowing scrolling across all result elements.
|
||||
*
|
||||
* <pre class="code">
|
||||
* WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10By…("spring", position))
|
||||
* .startingAt(OffsetScrollPosition.initial());
|
||||
*
|
||||
* while (users.hasNext()) {
|
||||
* User u = users.next();
|
||||
* // consume user
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Mark Paluch
|
||||
* @since 3.1
|
||||
*/
|
||||
public class WindowIterator<T> implements Iterator<T> {
|
||||
|
||||
private final Function<ScrollPosition, Window<T>> windowFunction;
|
||||
|
||||
private ScrollPosition currentPosition;
|
||||
|
||||
private @Nullable Window<T> currentWindow;
|
||||
|
||||
private @Nullable Iterator<T> currentIterator;
|
||||
|
||||
/**
|
||||
* Entrypoint to create a new {@link WindowIterator} for the given windowFunction.
|
||||
*
|
||||
* @param windowFunction must not be {@literal null}.
|
||||
* @param <T>
|
||||
* @return new instance of {@link WindowIteratorBuilder}.
|
||||
*/
|
||||
public static <T> WindowIteratorBuilder<T> of(Function<ScrollPosition, Window<T>> windowFunction) {
|
||||
return new WindowIteratorBuilder<>(windowFunction);
|
||||
}
|
||||
|
||||
WindowIterator(Function<ScrollPosition, Window<T>> windowFunction, ScrollPosition position) {
|
||||
|
||||
this.windowFunction = windowFunction;
|
||||
this.currentPosition = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
|
||||
// use while loop instead of recursion to fetch the next window.
|
||||
do {
|
||||
if (currentWindow == null) {
|
||||
currentWindow = windowFunction.apply(currentPosition);
|
||||
}
|
||||
|
||||
if (currentIterator == null) {
|
||||
if (currentWindow != null) {
|
||||
currentIterator = currentWindow.iterator();
|
||||
}
|
||||
}
|
||||
|
||||
if (currentIterator != null) {
|
||||
|
||||
if (currentIterator.hasNext()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentWindow != null && currentWindow.hasNext()) {
|
||||
|
||||
currentPosition = currentWindow.positionAt(currentWindow.size() - 1);
|
||||
currentIterator = null;
|
||||
currentWindow = null;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} while (true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T next() {
|
||||
|
||||
if (!hasNext()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
|
||||
return currentIterator.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder API to construct a {@link WindowIterator}.
|
||||
*
|
||||
* @param <T>
|
||||
* @author Christoph Strobl
|
||||
* @since 3.1
|
||||
*/
|
||||
public static class WindowIteratorBuilder<T> {
|
||||
|
||||
private final Function<ScrollPosition, Window<T>> windowFunction;
|
||||
|
||||
WindowIteratorBuilder(Function<ScrollPosition, Window<T>> windowFunction) {
|
||||
|
||||
Assert.notNull(windowFunction, "WindowFunction must not be null");
|
||||
|
||||
this.windowFunction = windowFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link WindowIterator} given {@link ScrollPosition}.
|
||||
*
|
||||
* @param position
|
||||
* @return
|
||||
*/
|
||||
public WindowIterator<T> startingAt(ScrollPosition position) {
|
||||
|
||||
Assert.notNull(position, "ScrollPosition must not be null");
|
||||
|
||||
return new WindowIterator<>(windowFunction, position);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user