diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/AllDocsBasePageIterator.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/AllDocsBasePageIterator.java new file mode 100644 index 000000000..f0fbd27fc --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/AllDocsBasePageIterator.java @@ -0,0 +1,59 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.List; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.AllDocsResult; +import com.ibm.cloud.cloudant.v1.model.DocsResultRow; + +abstract class AllDocsBasePageIterator extends KeyPageIterator { + + AllDocsBasePageIterator(Cloudant client, O options, OptionsHandler optsHandler) { + super(client, options, optsHandler); + } + + @Override + final Function> itemsGetter() { + return AllDocsResult::getRows; + } + + @Override + final Function nextKeyGetter() { + return DocsResultRow::getKey; + } + + @Override + final Function nextKeyIdGetter() { + return DocsResultRow::getId; + } + + /** + * Setting start key doc ID is a no-op for all_docs based paging where + * key is the same as id. + */ + @Override + final Optional> nextKeyIdSetter() { + return Optional.empty(); + } + + @Override + final Optional checkBoundary(DocsResultRow penultimateItem, DocsResultRow lastItem) { + // AllDocs and DesignDocs pagers always have unique keys (because they are document IDs) + return Optional.empty(); + } +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/AllDocsPageIterator.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/AllDocsPageIterator.java new file mode 100644 index 000000000..02871a546 --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/AllDocsPageIterator.java @@ -0,0 +1,55 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.function.BiFunction; +import java.util.function.Function; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.AllDocsResult; +import com.ibm.cloud.cloudant.v1.model.PostAllDocsOptions; +import com.ibm.cloud.cloudant.v1.model.PostAllDocsOptions.Builder; +import com.ibm.cloud.sdk.core.http.ServiceCall; + +final class AllDocsPageIterator extends AllDocsBasePageIterator { + + AllDocsPageIterator(Cloudant client, PostAllDocsOptions options) { + super(client, options, OptionsHandler.POST_ALL_DOCS); + } + + @Override + Function optionsToBuilderFunction() { + return PostAllDocsOptions::newBuilder; + } + + @Override + Function builderToOptionsFunction() { + return Builder::build; + } + + @Override + BiFunction> nextRequestFunction() { + return Cloudant::postAllDocs; + } + + @Override + BiFunction nextKeySetter() { + return Builder::startKey; + } + + @Override + Function limitGetter() { + return PostAllDocsOptions::limit; + } + +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/AllDocsPartitionPageIterator.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/AllDocsPartitionPageIterator.java new file mode 100644 index 000000000..f48fa8534 --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/AllDocsPartitionPageIterator.java @@ -0,0 +1,56 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.function.BiFunction; +import java.util.function.Function; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.AllDocsResult; +import com.ibm.cloud.cloudant.v1.model.PostPartitionAllDocsOptions; +import com.ibm.cloud.cloudant.v1.model.PostPartitionAllDocsOptions.Builder; +import com.ibm.cloud.sdk.core.http.ServiceCall; + +final class AllDocsPartitionPageIterator extends AllDocsBasePageIterator { + + AllDocsPartitionPageIterator(Cloudant client, PostPartitionAllDocsOptions options) { + super(client, options, OptionsHandler.POST_PARTITION_ALL_DOCS); + } + + @Override + Function optionsToBuilderFunction() { + return PostPartitionAllDocsOptions::newBuilder; + } + + @Override + Function builderToOptionsFunction() { + return Builder::build; + } + + @Override + BiFunction> nextRequestFunction() { + return Cloudant::postPartitionAllDocs; + } + + + @Override + BiFunction nextKeySetter() { + return Builder::startKey; + } + + @Override + Function limitGetter() { + return PostPartitionAllDocsOptions::limit; + } + +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/BasePageIterator.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/BasePageIterator.java new file mode 100644 index 000000000..fe3930d53 --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/BasePageIterator.java @@ -0,0 +1,92 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import java.util.function.Function; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.sdk.core.http.ServiceCall; + +abstract class BasePageIterator implements Iterator> { + + protected final Cloudant client; + protected final long pageSize; + protected final OptionsHandler optsHandler; + protected final AtomicReference nextPageOptionsRef = new AtomicReference<>(); + protected volatile boolean hasNext = true; + + BasePageIterator(Cloudant client, O options, OptionsHandler optsHandler) { + this.client = client; + this.optsHandler = optsHandler; + // Set the page size from the supplied options limit + this.pageSize = getPageSizeFromOptionsLimit(options); + // Set the first page options + buildAndSetOptions(optsHandler.builderFromOptions(options)); + } + + @Override + public final boolean hasNext() { + return this.hasNext; + } + + @Override + public List next() { + if (this.hasNext()) { + return Collections.unmodifiableList(this.nextRequest()); + } else { + throw new NoSuchElementException(); + } + } + + List nextRequest() { + ServiceCall request = nextRequestFunction().apply(this.client, this.nextPageOptionsRef.get()); + R result = request.execute().getResult(); + List items = itemsGetter().apply(result); + if (items.size() < this.pageSize) { + this.hasNext = false; + } else { + B optionsBuilder = optsHandler.builderFromOptions(this.nextPageOptionsRef.get()); + setNextPageOptions(optionsBuilder, result); + buildAndSetOptions(optionsBuilder); + } + return items; + } + + private void buildAndSetOptions(B optionsBuilder) { + this.nextPageOptionsRef.set(optsHandler.optionsFromBuilder(optionsBuilder)); + } + + abstract Function optionsToBuilderFunction(); + + abstract Function builderToOptionsFunction(); + + abstract Function> itemsGetter(); + + abstract void setNextPageOptions(B builder, R result); + + abstract BiFunction> nextRequestFunction(); + + abstract Function limitGetter(); + + Long getPageSizeFromOptionsLimit(O opts) { + return Optional.ofNullable(limitGetter().apply(opts)).orElse(200L); + } + +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/BookmarkPageIterator.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/BookmarkPageIterator.java new file mode 100644 index 000000000..6662a709b --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/BookmarkPageIterator.java @@ -0,0 +1,36 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.function.BiFunction; +import java.util.function.Function; +import com.ibm.cloud.cloudant.v1.Cloudant; + +abstract class BookmarkPageIterator extends BasePageIterator { + + BookmarkPageIterator(Cloudant client, O options, OptionsHandler optsHandler) { + super(client, options, optsHandler); + } + + abstract Function bookmarkGetter(); + + abstract BiFunction bookmarkSetter(); + + @Override + final void setNextPageOptions(B builder, R result) { + String bookmark = bookmarkGetter().apply(result); + bookmarkSetter().apply(builder, bookmark); + } + +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/DesignDocsPageIterator.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/DesignDocsPageIterator.java new file mode 100644 index 000000000..c593d5a41 --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/DesignDocsPageIterator.java @@ -0,0 +1,55 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.function.BiFunction; +import java.util.function.Function; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.AllDocsResult; +import com.ibm.cloud.cloudant.v1.model.PostDesignDocsOptions; +import com.ibm.cloud.cloudant.v1.model.PostDesignDocsOptions.Builder; +import com.ibm.cloud.sdk.core.http.ServiceCall; + +public class DesignDocsPageIterator extends AllDocsBasePageIterator { + + DesignDocsPageIterator(Cloudant client, PostDesignDocsOptions options) { + super(client, options, OptionsHandler.POST_DESIGN_DOCS); + } + + @Override + BiFunction nextKeySetter() { + return Builder::key; + } + + @Override + Function optionsToBuilderFunction() { + return PostDesignDocsOptions::newBuilder; + } + + @Override + Function builderToOptionsFunction() { + return Builder::build; + } + + @Override + BiFunction> nextRequestFunction() { + return Cloudant::postDesignDocs; + } + + @Override + Function limitGetter() { + return PostDesignDocsOptions::limit; + } + +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/FindBasePageIterator.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/FindBasePageIterator.java new file mode 100644 index 000000000..0dbdffead --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/FindBasePageIterator.java @@ -0,0 +1,38 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.List; +import java.util.function.Function; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.Document; +import com.ibm.cloud.cloudant.v1.model.FindResult; + +abstract class FindBasePageIterator extends BookmarkPageIterator { + + FindBasePageIterator(Cloudant client, O options, OptionsHandler optsHandler) { + super(client, options, optsHandler); + } + + @Override + final Function> itemsGetter() { + return FindResult::getDocs; + } + + @Override + final Function bookmarkGetter() { + return FindResult::getBookmark; + } + +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/FindPager.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/FindPager.java new file mode 100644 index 000000000..d05a462e4 --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/FindPager.java @@ -0,0 +1,55 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.function.BiFunction; +import java.util.function.Function; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.FindResult; +import com.ibm.cloud.cloudant.v1.model.PostFindOptions; +import com.ibm.cloud.cloudant.v1.model.PostFindOptions.Builder; +import com.ibm.cloud.sdk.core.http.ServiceCall; + +final class FindPager extends FindBasePageIterator { + + FindPager(Cloudant client, PostFindOptions options) { + super(client, options, OptionsHandler.POST_FIND); + } + + @Override + Function optionsToBuilderFunction() { + return PostFindOptions::newBuilder; + } + + @Override + Function builderToOptionsFunction() { + return Builder::build; + } + + @Override + BiFunction bookmarkSetter() { + return Builder::bookmark; + } + + @Override + BiFunction> nextRequestFunction() { + return Cloudant::postFind; + } + + @Override + Function limitGetter() { + return PostFindOptions::limit; + } + +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/FindPartitionPager.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/FindPartitionPager.java new file mode 100644 index 000000000..ca2467306 --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/FindPartitionPager.java @@ -0,0 +1,55 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.function.BiFunction; +import java.util.function.Function; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.FindResult; +import com.ibm.cloud.cloudant.v1.model.PostPartitionFindOptions; +import com.ibm.cloud.cloudant.v1.model.PostPartitionFindOptions.Builder; +import com.ibm.cloud.sdk.core.http.ServiceCall; + +final class FindPartitionPager extends FindBasePageIterator { + + FindPartitionPager(Cloudant client, PostPartitionFindOptions options) { + super(client, options, OptionsHandler.POST_PARTITION_FIND); + } + + @Override + Function optionsToBuilderFunction() { + return PostPartitionFindOptions::newBuilder; + } + + @Override + Function builderToOptionsFunction() { + return Builder::build; + } + + @Override + BiFunction bookmarkSetter() { + return Builder::bookmark; + } + + @Override + BiFunction> nextRequestFunction() { + return Cloudant::postPartitionFind; + } + + @Override + Function limitGetter() { + return PostPartitionFindOptions::limit; + } + +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/IteratorPager.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/IteratorPager.java new file mode 100644 index 000000000..71f7f5146 --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/IteratorPager.java @@ -0,0 +1,77 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +final class IteratorPager implements Pager { + + private State state = State.NEW; + private final Iterable> pageIterable; + private final Iterator> pageIterator; + + enum State { + NEW, GET_NEXT, GET_ALL, CONSUMED + } + + IteratorPager(Iterable> pageIterable) { + this.pageIterable = pageIterable; + this.pageIterator = pageIterable.iterator(); + } + + @Override + public boolean hasNext() { + return this.pageIterator.hasNext(); + } + + @Override + public List getNext() { + checkState(State.GET_NEXT); + List page = this.pageIterator.next(); + if (!this.hasNext()) { + state = State.CONSUMED; + } + return page; + } + + @Override + public List getAll() { + checkState(State.GET_ALL); + List allRows = StreamSupport.stream(this.pageIterable.spliterator(), false) + .flatMap(List::stream).collect(Collectors.toList()); + // If it didn't throw we can set the consumed state + state = State.CONSUMED; + return allRows; + } + + private void checkState(State mode) { + if (state == mode) { + return; + } + switch (state) { + case NEW: + state = mode; + break; + case CONSUMED: + throw new IllegalStateException("This pager has been consumed use a new Pager."); + default: + throw new IllegalStateException( + "Cannot mix getAll() and getNext() use only one method or get a a new Pager."); + } + } +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/KeyPageIterator.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/KeyPageIterator.java new file mode 100644 index 000000000..8f3e8be7d --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/KeyPageIterator.java @@ -0,0 +1,73 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.List; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import com.ibm.cloud.cloudant.v1.Cloudant; + +abstract class KeyPageIterator extends BasePageIterator { + + private Optional boundaryFailure = Optional.empty(); + + KeyPageIterator(Cloudant client, O options, OptionsHandler optsHandler) { + super(client, options, optsHandler); + } + + abstract BiFunction nextKeySetter(); + + abstract Optional> nextKeyIdSetter(); + + abstract Function nextKeyGetter(); + + abstract Function nextKeyIdGetter(); + + List nextRequest() { + // If the previous request had the duplicate boundary error + // throw it now because we cannot safely get the next page. + if (boundaryFailure.isPresent()) { + throw new UnsupportedOperationException(boundaryFailure.get()); + } + List items = super.nextRequest(); + if (this.hasNext()) { + I lastItem = items.remove(items.size() - 1); + if (items.size() > 0) { + I penultimateItem = items.get(items.size() - 1); + // Defer a throw for a boundary failure to the next request + boundaryFailure = checkBoundary(penultimateItem, lastItem); + } + } + return items; + } + + @Override + final void setNextPageOptions(B builder, R result) { + List items = itemsGetter().apply(result); + I lastItem = items.get(items.size() - 1); + K nextKey = nextKeyGetter().apply(lastItem); + String nextId = nextKeyIdGetter().apply(lastItem); + nextKeySetter().apply(builder, nextKey); + nextKeyIdSetter().ifPresent(f -> f.apply(builder, nextId)); + } + + @Override + Long getPageSizeFromOptionsLimit(O opts) { + return super.getPageSizeFromOptionsLimit(opts) + 1; + } + + abstract Optional checkBoundary(I penultimateItem, I lastItem); + +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/OptionsHandler.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/OptionsHandler.java new file mode 100644 index 000000000..13523e438 --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/OptionsHandler.java @@ -0,0 +1,283 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import com.ibm.cloud.cloudant.v1.model.PostAllDocsOptions; +import com.ibm.cloud.cloudant.v1.model.PostDesignDocsOptions; +import com.ibm.cloud.cloudant.v1.model.PostFindOptions; +import com.ibm.cloud.cloudant.v1.model.PostPartitionAllDocsOptions; +import com.ibm.cloud.cloudant.v1.model.PostPartitionFindOptions; +import com.ibm.cloud.cloudant.v1.model.PostPartitionSearchOptions; +import com.ibm.cloud.cloudant.v1.model.PostPartitionViewOptions; +import com.ibm.cloud.cloudant.v1.model.PostSearchOptions; +import com.ibm.cloud.cloudant.v1.model.PostViewOptions; + +abstract class OptionsHandler { + + static final OptionsHandler POST_ALL_DOCS = + new AllDocsOptionsHandler(); + static final OptionsHandler POST_DESIGN_DOCS = + new DesignDocsOptionsHandler(); + static final OptionsHandler POST_FIND = + new FindOptionsHandler(); + static final OptionsHandler POST_PARTITION_ALL_DOCS = + new PartitionAllDocsOptionsHandler(); + static final OptionsHandler POST_PARTITION_FIND = + new PartitionFindOptionsHandler(); + static final OptionsHandler POST_PARTITION_SEARCH = + new PartitionSearchOptionsHandler(); + static final OptionsHandler POST_PARTITION_VIEW = + new PartitionViewOptionsHandler(); + static final OptionsHandler POST_SEARCH = + new SearchOptionsHandler(); + static final OptionsHandler POST_VIEW = + new ViewOptionsHandler(); + + // The maximum and minimum limit values (i.e. page size) + static final Long MAX_LIMIT = 200L; + static final Long MIN_LIMIT = 1L; + + private final Function builderToOptions; + private final Function optionsToBuilder; + + private OptionsHandler(Function builderToOptions, Function optionsToBuilder) { + this.builderToOptions = builderToOptions; + this.optionsToBuilder = optionsToBuilder; + } + + B builderFromOptions(O options) { + return this.optionsToBuilder.apply(options); + } + + O optionsFromBuilder(B builder) { + return this.builderToOptions.apply(builder); + } + + abstract void validate(O options); + + O clone(O options) { + return this.optionsFromBuilder(this.builderFromOptions(options)); + } + + static final PostAllDocsOptions duplicate(PostAllDocsOptions opts) { + return POST_ALL_DOCS.clone(opts); + } + + static final PostDesignDocsOptions duplicate(PostDesignDocsOptions opts) { + return POST_DESIGN_DOCS.clone(opts); + } + + static final PostFindOptions duplicate(PostFindOptions opts) { + return POST_FIND.clone(opts); + } + + static final PostPartitionAllDocsOptions duplicate(PostPartitionAllDocsOptions opts) { + return POST_PARTITION_ALL_DOCS.clone(opts); + } + + static final PostPartitionFindOptions duplicate(PostPartitionFindOptions opts) { + return POST_PARTITION_FIND.clone(opts); + } + + static final PostPartitionSearchOptions duplicate(PostPartitionSearchOptions opts) { + return POST_PARTITION_SEARCH.clone(opts); + } + + static final PostPartitionViewOptions duplicate(PostPartitionViewOptions opts) { + return POST_PARTITION_VIEW.clone(opts); + } + + static final PostSearchOptions duplicate(PostSearchOptions opts) { + return POST_SEARCH.clone(opts); + } + + static final PostViewOptions duplicate(PostViewOptions opts) { + return POST_VIEW.clone(opts); + } + + private static void validateLimit(Supplier limitSupplier) { + // If limit is set check it is within range + // Else it is unset and we will set the valid default value later + if (optionIsPresent(limitSupplier)) { + Long limit = limitSupplier.get(); + if (limit > MAX_LIMIT) { + throw new IllegalArgumentException(String.format( + "The provided limit %d exceeds the maximum page size value of %d.", limit, MAX_LIMIT)); + } + if (limit < MIN_LIMIT) { + throw new IllegalArgumentException( + String.format("The provided limit %d is lower than the minimum page size value of %d.", + limit, MIN_LIMIT)); + } + } + } + + private static boolean optionIsPresent(final Supplier optionSupplier) { + return Optional.ofNullable(optionSupplier.get()).isPresent(); + } + + private static void validateOptionsAbsent(final Map> options) { + for (Map.Entry> option : options.entrySet()) { + if (optionIsPresent(option.getValue())) { + throw new IllegalArgumentException( + String.format("The option '%s' is invalid when using pagination.", option.getKey())); + } + } + } + + private static final class AllDocsOptionsHandler + extends OptionsHandler { + + private AllDocsOptionsHandler() { + super(PostAllDocsOptions.Builder::build, PostAllDocsOptions::newBuilder); + } + + @Override + void validate(PostAllDocsOptions options) { + validateLimit(options::limit); + validateOptionsAbsent(Collections.singletonMap("keys", options::keys)); + } + + } + + private static final class DesignDocsOptionsHandler + extends OptionsHandler { + + private DesignDocsOptionsHandler() { + super(PostDesignDocsOptions.Builder::build, PostDesignDocsOptions::newBuilder); + } + + @Override + void validate(PostDesignDocsOptions options) { + validateLimit(options::limit); + validateOptionsAbsent(Collections.singletonMap("keys", options::keys)); + } + + } + + private static final class FindOptionsHandler + extends OptionsHandler { + + private FindOptionsHandler() { + super(PostFindOptions.Builder::build, PostFindOptions::newBuilder); + } + + @Override + void validate(PostFindOptions options) { + validateLimit(options::limit); + } + + } + + private static final class PartitionAllDocsOptionsHandler + extends OptionsHandler { + + private PartitionAllDocsOptionsHandler() { + super(PostPartitionAllDocsOptions.Builder::build, PostPartitionAllDocsOptions::newBuilder); + } + + @Override + void validate(PostPartitionAllDocsOptions options) { + validateLimit(options::limit); + validateOptionsAbsent(Collections.singletonMap("keys", options::keys)); + } + + } + + private static final class PartitionFindOptionsHandler + extends OptionsHandler { + + private PartitionFindOptionsHandler() { + super(PostPartitionFindOptions.Builder::build, PostPartitionFindOptions::newBuilder); + } + + @Override + void validate(PostPartitionFindOptions options) { + validateLimit(options::limit); + } + + } + + private static final class PartitionSearchOptionsHandler + extends OptionsHandler { + + private PartitionSearchOptionsHandler() { + super(PostPartitionSearchOptions.Builder::build, PostPartitionSearchOptions::newBuilder); + } + + @Override + void validate(PostPartitionSearchOptions options) { + validateLimit(options::limit); + } + + } + + private static final class PartitionViewOptionsHandler + extends OptionsHandler { + + private PartitionViewOptionsHandler() { + super(PostPartitionViewOptions.Builder::build, PostPartitionViewOptions::newBuilder); + } + + @Override + void validate(PostPartitionViewOptions options) { + validateLimit(options::limit); + validateOptionsAbsent(Collections.singletonMap("keys", options::keys)); + } + + } + + private static final class SearchOptionsHandler + extends OptionsHandler { + + private SearchOptionsHandler() { + super(PostSearchOptions.Builder::build, PostSearchOptions::newBuilder); + } + + @Override + void validate(PostSearchOptions options) { + validateLimit(options::limit); + Map> invalidOptions = new HashMap<>(5); + invalidOptions.put("counts", options::counts); + invalidOptions.put("groupField", options::groupField); + invalidOptions.put("groupLimit", options::groupLimit); + invalidOptions.put("groupSort", options::groupSort); + invalidOptions.put("ranges", options::ranges); + validateOptionsAbsent(invalidOptions); + } + + } + + private static final class ViewOptionsHandler + extends OptionsHandler { + + private ViewOptionsHandler() { + super(PostViewOptions.Builder::build, PostViewOptions::newBuilder); + } + + @Override + void validate(PostViewOptions options) { + validateLimit(options::limit); + validateOptionsAbsent(Collections.singletonMap("keys", options::keys)); + } + + } + +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/Pager.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/Pager.java new file mode 100644 index 000000000..48203c503 --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/Pager.java @@ -0,0 +1,52 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.List; + +/** + * Interface for pagination of database operations. + * + * Use the static methods to instantiate a Pager instance for + * the required operation. + * + * @param the item type of the page rows + */ +public interface Pager { + + /** + * Returns {@code true} if there may be another page. + * + * @return {@code false} if there are no more pages + */ + boolean hasNext(); + + /** + * Get the next page in the sequence. + * + * @return java.util.List of the rows from the next page + */ + List getNext(); + + /** + * Get all the avaialble pages and collect them into a + * single java.util.List. This operation is not lazy and + * may consume significant memory to hold the entire + * results collection for large queries. + * + * @return java.util.List of the rows from all the pages + */ + List getAll(); + +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/Pagination.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/Pagination.java new file mode 100644 index 000000000..9271c704d --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/Pagination.java @@ -0,0 +1,297 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.BiFunction; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.DocsResultRow; +import com.ibm.cloud.cloudant.v1.model.Document; +import com.ibm.cloud.cloudant.v1.model.PostAllDocsOptions; +import com.ibm.cloud.cloudant.v1.model.PostDesignDocsOptions; +import com.ibm.cloud.cloudant.v1.model.PostFindOptions; +import com.ibm.cloud.cloudant.v1.model.PostPartitionAllDocsOptions; +import com.ibm.cloud.cloudant.v1.model.PostPartitionFindOptions; +import com.ibm.cloud.cloudant.v1.model.PostPartitionSearchOptions; +import com.ibm.cloud.cloudant.v1.model.PostPartitionViewOptions; +import com.ibm.cloud.cloudant.v1.model.PostSearchOptions; +import com.ibm.cloud.cloudant.v1.model.PostViewOptions; +import com.ibm.cloud.cloudant.v1.model.SearchResultRow; +import com.ibm.cloud.cloudant.v1.model.ViewResultRow; + +/** + * {@link Pagination} is the entry point for pagination features. + * + * Use the static methods to create new {@link Pagination} instances. The instances in turn can be + * used to create: + *
    + *
  • {@link Stream} of result rows via {@link #rowStream()} + *
  • {@link Stream} of pages via {@link #pageStream()} + *
  • {@link Iterator}s of result rows via the {@link Iterable} {@link #rows()} + *
  • {@link Iterator}s of pages via the {@link Iterable}{@link #pages()} + *
  • IBM Cloud SDK style {@link Pager}s via {@link #pager()} + *
+ * + * @param the type of the options object used to configure the operation. + * @param the reslt row type of the operation. + */ +public class Pagination { + + private final Cloudant client; + private final O opts; + private final BiFunction> iteratorCtor; + private final Iterable> pageIterable = new PageIterable(); + private final Iterable rowIterable = new RowIterable(); + + Pagination(Cloudant client, O opts, BiFunction> iteratorCtor) { + this.client = client; + this.opts = opts; + this.iteratorCtor = iteratorCtor; + } + + private final class PageIterable implements Iterable> { + + /** + * Makes a new lazy {@link Iterator} over the operation result pages as defined by the options. + * + * @return new {@link Iterator} over the pages + */ + @Override + public Iterator> iterator() { + return Pagination.this.iteratorCtor.apply(Pagination.this.client, Pagination.this.opts); + } + + /** + * Makes a new lazy {@link Spliterator.ORDERED} {@link Spliterator.NONNULL} + * {@link Spliterator.IMMUTABLE} {@link Spliterator} over the operation result pages as defined + * by the options. + * + * @return new {@link Spliterator} over the pages + */ + @Override + public Spliterator> spliterator() { + return Spliterators.spliteratorUnknownSize(this.iterator(), + Spliterator.ORDERED | Spliterator.NONNULL | Spliterator.IMMUTABLE); + } + } + + private final class RowIterable implements Iterable { + + /** + * Makes a new lazy (page at a time) {@link Iterator} over the operation result rows from all + * the pages as defined by the options. + * + * @return new {@link Iterator} over the result rows + */ + @Override + public Iterator iterator() { + return Pagination.this.rowStream().iterator(); + } + } + + /** + * Get a new IBM Cloud SDK style Pager for the operation. + * + * This type is useful for retrieving one page at a time from a method call. + * + * @return a new IBM Cloud SDK style Pager + */ + public Pager pager() { + return new IteratorPager(this.pages()); + } + + /** + * Get an Iterable for all the pages. + * + * This method is useful for handling pages in an enhanced for loop e.g. {@code + * for (List page : Pagination.newPagination(client, allDocsOptions).pages()) { + * } } + * + * @return an {@link Iterable} over all the pages + */ + public Iterable> pages() { + return this.pageIterable; + } + + /** + * Get a page by page stream of all the pages. + * + * @return a {@link Stream} of all the pages + */ + public Stream> pageStream() { + return StreamSupport.stream(this.pages().spliterator(), false); + } + + /** + * Get an Iterable for all the rows from all the pages. + * + * This type is useful for handling rows in an enhanced for loop e.g. {@code + * for (AllDocsResultRow row : Pagination.newPagination(client, allDocsOptions).rows()) { } } + * + * @return an {@link Iterable} over all the result rows + */ + public Iterable rows() { + return this.rowIterable; + } + + /** + * Get a row by row stream of all the rows from all the pages. + * + * @return a {@link Stream} of all the result rows + */ + public Stream rowStream() { + return this.pageStream().flatMap(List::stream); + } + + /** + * Get a Pagination for the postAllDocs operation. The page size is configured with the limit + * paramater of the options. + * + * @param client com.ibm.cloud.cloudant.v1.Cloudant instance to make page requests + * @param options com.ibm.cloud.cloudant.v1.model.PostAllDocsOptions for the query + * @return a Pagination for all the documents in the database + */ + public static Pagination newPagination(Cloudant client, + PostAllDocsOptions options) { + OptionsHandler.POST_ALL_DOCS.validate(options); + return new Pagination(client, + OptionsHandler.duplicate(options), AllDocsPageIterator::new); + } + + /** + * Get a Pagination for the postPartitionAllDocs operation. The page size is configured with the + * limit paramater of the options. + * + * @param client com.ibm.cloud.cloudant.v1.Cloudant instance to make page requests + * @param options com.ibm.cloud.cloudant.v1.model.PostPartitionAllDocsOptions for the query + * @return a Pagination for all the documents in a database partition + */ + public static Pagination newPagination( + Cloudant client, PostPartitionAllDocsOptions options) { + OptionsHandler.POST_PARTITION_ALL_DOCS.validate(options); + return new Pagination(client, + OptionsHandler.duplicate(options), AllDocsPartitionPageIterator::new); + } + + /** + * Get a Pagination for the postPartitionAllDocs operation. The page size is configured with the + * limit paramater of the options. + * + * @param client com.ibm.cloud.cloudant.v1.Cloudant instance to make page requests + * @param options com.ibm.cloud.cloudant.v1.model.PostPartitionAllDocsOptions for the query + * @return a Pagination for all the documents in a database partition + */ + public static Pagination newPagination( + Cloudant client, PostDesignDocsOptions options) { + OptionsHandler.POST_DESIGN_DOCS.validate(options); + return new Pagination(client, + OptionsHandler.duplicate(options), DesignDocsPageIterator::new); + } + + /** + * Get a Pagination for the postFind operation. The page size is configured with the limit + * paramater of the options. + * + * @param client com.ibm.cloud.cloudant.v1.Cloudant instance to make page requests + * @param options com.ibm.cloud.cloudant.v1.model.PostFindOptions for the query + * @return a Pagination for the result of a find query + */ + public static Pagination newPagination(Cloudant client, + PostFindOptions options) { + OptionsHandler.POST_FIND.validate(options); + return new Pagination(client, OptionsHandler.duplicate(options), + FindPager::new); + } + + /** + * Get a Pagination for the postPartitionFind operation. + * + * @param client com.ibm.cloud.cloudant.v1.Cloudant instance to make page requests + * @param options com.ibm.cloud.cloudant.v1.model.PostPartitionFindOptions for the query + * @return a Pagination for the result of a partition find query + */ + public static Pagination newPagination(Cloudant client, + PostPartitionFindOptions options) { + OptionsHandler.POST_PARTITION_FIND.validate(options); + return new Pagination(client, + OptionsHandler.duplicate(options), FindPartitionPager::new); + } + + /** + * Get a Pagination for the postSearch operation. The page size is configured with the limit + * paramater of the options. + * + * @param client com.ibm.cloud.cloudant.v1.Cloudant instance to make page requests + * @param options com.ibm.cloud.cloudant.v1.model.PostSearchOptions for the query + * @return a Pagination for the result of a search query + */ + public static Pagination newPagination(Cloudant client, + PostSearchOptions options) { + OptionsHandler.POST_SEARCH.validate(options); + return new Pagination(client, + OptionsHandler.duplicate(options), SearchPageIterator::new); + } + + /** + * Get a Pagination for the postPartitionSearch operation. The page size is configured with the + * limit paramater of the options. + * + * @param client com.ibm.cloud.cloudant.v1.Cloudant instance to make page requests + * @param options com.ibm.cloud.cloudant.v1.model.PostPartitionSearchOptions for the query + * @return a Pagination for the result of a partition search query + */ + public static Pagination newPagination( + Cloudant client, PostPartitionSearchOptions options) { + OptionsHandler.POST_PARTITION_SEARCH.validate(options); + return new Pagination(client, + OptionsHandler.duplicate(options), SearchPartitionPageIterator::new); + } + + /** + * Get a Pagination for the postView operation. The page size is configured with the limit + * paramater of the options. + * + * @param client com.ibm.cloud.cloudant.v1.Cloudant instance to make page requests + * @param options com.ibm.cloud.cloudant.v1.model.PostViewOptions for the query + * @return a Pagination for the result of a view query + */ + public static Pagination newPagination(Cloudant client, + PostViewOptions options) { + OptionsHandler.POST_VIEW.validate(options); + return new Pagination(client, OptionsHandler.duplicate(options), + ViewPageIterator::new); + } + + /** + * Get a Pagination for the postPartitionView operation. The page size is configured with the + * limit paramater of the options. + * + * @param client com.ibm.cloud.cloudant.v1.Cloudant instance to make page requests + * @param options com.ibm.cloud.cloudant.v1.model.PostPartitionViewOptions for the query + * @return a Pagination for the result of a partition view query + */ + public static Pagination newPagination(Cloudant client, + PostPartitionViewOptions options) { + OptionsHandler.POST_PARTITION_VIEW.validate(options); + return new Pagination(client, + OptionsHandler.duplicate(options), ViewPartitionPageIterator::new); + } + +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/SearchBasePageIterator.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/SearchBasePageIterator.java new file mode 100644 index 000000000..09d1365d1 --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/SearchBasePageIterator.java @@ -0,0 +1,38 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.List; +import java.util.function.Function; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.SearchResult; +import com.ibm.cloud.cloudant.v1.model.SearchResultRow; + +abstract class SearchBasePageIterator extends BookmarkPageIterator { + + SearchBasePageIterator(Cloudant client, O options, OptionsHandler optsHandler) { + super(client, options, optsHandler); + } + + @Override + final Function> itemsGetter() { + return SearchResult::getRows; + } + + @Override + final Function bookmarkGetter() { + return SearchResult::getBookmark; + } + +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/SearchPageIterator.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/SearchPageIterator.java new file mode 100644 index 000000000..c7ac7a858 --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/SearchPageIterator.java @@ -0,0 +1,55 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.function.BiFunction; +import java.util.function.Function; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.PostSearchOptions; +import com.ibm.cloud.cloudant.v1.model.PostSearchOptions.Builder; +import com.ibm.cloud.cloudant.v1.model.SearchResult; +import com.ibm.cloud.sdk.core.http.ServiceCall; + +final class SearchPageIterator extends SearchBasePageIterator { + + SearchPageIterator(Cloudant client, PostSearchOptions options) { + super(client, options, OptionsHandler.POST_SEARCH); + } + + @Override + Function optionsToBuilderFunction() { + return PostSearchOptions::newBuilder; + } + + @Override + Function builderToOptionsFunction() { + return Builder::build; + } + + @Override + BiFunction bookmarkSetter() { + return Builder::bookmark; + } + + @Override + BiFunction> nextRequestFunction() { + return Cloudant::postSearch; + } + + @Override + Function limitGetter() { + return PostSearchOptions::limit; + } + +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/SearchPartitionPageIterator.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/SearchPartitionPageIterator.java new file mode 100644 index 000000000..6b599d734 --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/SearchPartitionPageIterator.java @@ -0,0 +1,55 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.function.BiFunction; +import java.util.function.Function; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.PostPartitionSearchOptions; +import com.ibm.cloud.cloudant.v1.model.PostPartitionSearchOptions.Builder; +import com.ibm.cloud.cloudant.v1.model.SearchResult; +import com.ibm.cloud.sdk.core.http.ServiceCall; + +final class SearchPartitionPageIterator extends SearchBasePageIterator { + + SearchPartitionPageIterator(Cloudant client, PostPartitionSearchOptions options) { + super(client, options, OptionsHandler.POST_PARTITION_SEARCH); + } + + @Override + Function optionsToBuilderFunction() { + return PostPartitionSearchOptions::newBuilder; + } + + @Override + Function builderToOptionsFunction() { + return Builder::build; + } + + @Override + BiFunction bookmarkSetter() { + return Builder::bookmark; + } + + @Override + BiFunction> nextRequestFunction() { + return Cloudant::postPartitionSearch; + } + + @Override + Function limitGetter() { + return PostPartitionSearchOptions::limit; + } + +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/ViewBasePageIterator.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/ViewBasePageIterator.java new file mode 100644 index 000000000..ab15baa2d --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/ViewBasePageIterator.java @@ -0,0 +1,66 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.ViewResult; +import com.ibm.cloud.cloudant.v1.model.ViewResultRow; + +abstract class ViewBasePageIterator extends KeyPageIterator { + + ViewBasePageIterator(Cloudant client, O options, OptionsHandler optsHandler) { + super(client, options, optsHandler); + } + + @Override + final Function> itemsGetter() { + return ViewResult::getRows; + } + + @Override + final Function nextKeyGetter() { + return ViewResultRow::getKey; + } + + @Override + final Function nextKeyIdGetter() { + return ViewResultRow::getId; + } + + @Override + final Optional checkBoundary(ViewResultRow penultimateItem, ViewResultRow lastItem) { + String pId = penultimateItem.getId(); + String lId = lastItem.getId(); + if (pId.equals(lId)) { + // ID's are the same, check the keys + Object pKey = penultimateItem.getKey(); + Object lKey = lastItem.getKey(); + // Check reference equality first (e.g. null) + // Then check values + if (pKey == lKey || pKey != null && pKey.equals(lKey)) { + // Identical keys, set an error message + return Optional.of( + String.format( + "Cannot paginate on a boundary containing identical keys '%s' and document IDs '%s'", + String.valueOf(lKey), + lId)); + } + } + return Optional.empty(); + } + +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/ViewPageIterator.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/ViewPageIterator.java new file mode 100644 index 000000000..2bb58f78e --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/ViewPageIterator.java @@ -0,0 +1,61 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.PostViewOptions; +import com.ibm.cloud.cloudant.v1.model.PostViewOptions.Builder; +import com.ibm.cloud.cloudant.v1.model.ViewResult; +import com.ibm.cloud.sdk.core.http.ServiceCall; + +final class ViewPageIterator extends ViewBasePageIterator { + + ViewPageIterator(Cloudant client, PostViewOptions options) { + super(client, options, OptionsHandler.POST_VIEW); + } + + @Override + Function optionsToBuilderFunction() { + return PostViewOptions::newBuilder; + } + + @Override + Function builderToOptionsFunction() { + return Builder::build; + } + + @Override + BiFunction> nextRequestFunction() { + return Cloudant::postView; + } + + @Override + BiFunction nextKeySetter() { + return Builder::startKey; + } + + @Override + Optional> nextKeyIdSetter() { + return Optional.of(Builder::startKeyDocId); + } + + @Override + Function limitGetter() { + return PostViewOptions::limit; + } + +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/ViewPartitionPageIterator.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/ViewPartitionPageIterator.java new file mode 100644 index 000000000..3663aa957 --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/ViewPartitionPageIterator.java @@ -0,0 +1,61 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.PostPartitionViewOptions; +import com.ibm.cloud.cloudant.v1.model.PostPartitionViewOptions.Builder; +import com.ibm.cloud.cloudant.v1.model.ViewResult; +import com.ibm.cloud.sdk.core.http.ServiceCall; + +final class ViewPartitionPageIterator extends ViewBasePageIterator { + + ViewPartitionPageIterator(Cloudant client, PostPartitionViewOptions options) { + super(client, options, OptionsHandler.POST_PARTITION_VIEW); + } + + @Override + Function optionsToBuilderFunction() { + return PostPartitionViewOptions::newBuilder; + } + + @Override + Function builderToOptionsFunction() { + return Builder::build; + } + + @Override + BiFunction> nextRequestFunction() { + return Cloudant::postPartitionView; + } + + @Override + BiFunction nextKeySetter() { + return Builder::startKey; + } + + @Override + Optional> nextKeyIdSetter() { + return Optional.of(Builder::startKeyDocId); + } + + @Override + Function limitGetter() { + return PostPartitionViewOptions::limit; + } + +} diff --git a/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/package-info.java b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/package-info.java new file mode 100644 index 000000000..289e2f0c2 --- /dev/null +++ b/modules/cloudant/src/main/java/com/ibm/cloud/cloudant/features/pagination/package-info.java @@ -0,0 +1,20 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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. + */ + +/** + * Feature for paginating operation requests. + * + * Accessed via the {@link Pagination} class. + * + */ +package com.ibm.cloud.cloudant.features.pagination; diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/ChangesFollowerTest.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/ChangesFollowerTest.java index 3a58f71a8..7ab1b72b7 100644 --- a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/ChangesFollowerTest.java +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/ChangesFollowerTest.java @@ -23,11 +23,12 @@ import java.util.function.Supplier; import java.util.stream.Stream; import com.ibm.cloud.cloudant.features.ChangesRequestMockClient.LimitExposingException; -import com.ibm.cloud.cloudant.features.ChangesRequestMockClient.MockError; -import com.ibm.cloud.cloudant.features.ChangesRequestMockClient.MockInstruction; import com.ibm.cloud.cloudant.features.ChangesRequestMockClient.PerpetualSupplier; -import com.ibm.cloud.cloudant.features.ChangesRequestMockClient.QueuedSupplier; +import com.ibm.cloud.cloudant.features.MockCloudant.MockError; +import com.ibm.cloud.cloudant.features.MockCloudant.MockInstruction; +import com.ibm.cloud.cloudant.features.MockCloudant.QueuedSupplier; import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.ChangesResult; import com.ibm.cloud.cloudant.v1.model.ChangesResultItem; import org.testng.Assert; import org.testng.annotations.DataProvider; @@ -36,16 +37,6 @@ public class ChangesFollowerTest { - Object[][] errorsAsTestObjectArray(Collection errors) { - Object[][] tests = new Object[errors.size()][]; - int index = 0; - for (MockError e : errors) { - tests[index] = new Object[]{e}; - index++; - } - return tests; - } - /** * Make a collection of mock instructions that alternate between * successful batches and errors. @@ -53,21 +44,21 @@ Object[][] errorsAsTestObjectArray(Collection errors) { * @param batches * @return */ - Collection getAlternatingBatchesAndErrors(int batches) { - Collection instructions = new ArrayList<>(batches*2); + Collection> getAlternatingBatchesAndErrors(int batches) { + Collection> instructions = new ArrayList<>(batches*2); Object[] errors = MockError.getTransientErrors().toArray(); - Supplier qs = QueuedSupplier.makeBatchSupplier(batches); + Supplier> qs = ChangesRequestMockClient.makeBatchSupplier(batches); for (int i = 0; i < batches; i++) { // Add a successful batch instructions.add(qs.get()); // Add a transient error - instructions.add(new MockInstruction((MockError) errors[i % errors.length])); + instructions.add(new MockInstruction((MockError) errors[i % errors.length])); } return instructions; } - Supplier getAlternatingBatchErrorSupplier(int batches) { - return new QueuedSupplier(getAlternatingBatchesAndErrors(batches)); + Supplier> getAlternatingBatchErrorSupplier(int batches) { + return new QueuedSupplier(getAlternatingBatchesAndErrors(batches)); } Cloudant getClientWithTimeouts(Duration callTimeout, Duration readTimeout) { @@ -90,18 +81,18 @@ Cloudant getClientWithTimeouts(Duration callTimeout, Duration readTimeout) { * @param batches * @return */ - Supplier getAlternatingBatchErrorThenPerpetualSupplier(int batches) { + Supplier> getAlternatingBatchErrorThenPerpetualSupplier(int batches) { return new PerpetualSupplier(getAlternatingBatchesAndErrors(batches), true); } @DataProvider(name = "terminalErrors") Object[][] getTerminalErrors() { - return errorsAsTestObjectArray(MockError.getTerminalErrors()); + return MockError.errorsAsTestObjectArray(MockError.getTerminalErrors()); } @DataProvider(name = "transientErrors") Object[][] getTransientErrors() { - return errorsAsTestObjectArray(MockError.getTransientErrors()); + return MockError.errorsAsTestObjectArray(MockError.getTransientErrors()); } @DataProvider(name = "invalidTimeoutClients") @@ -265,7 +256,7 @@ long testFollowerOnThread(ChangesFollower testFollower, ChangesFollower.Mode mod @Test void testStartOneOff() { int batches = 6; - Cloudant mockClient = new ChangesRequestMockClient(QueuedSupplier.makeBatchSupplier(batches)); + Cloudant mockClient = new ChangesRequestMockClient(ChangesRequestMockClient.makeBatchSupplier(batches)); ChangesFollower testFollower = new ChangesFollower(mockClient, TestOptions.MINIMUM.getOptions()); Assert.assertEquals(testFollower.startOneOff().count(), batches*ChangesFollower.BATCH_SIZE, "There should be the expected number of changes."); } @@ -275,7 +266,7 @@ void testStartOneOff() { */ @Test(dataProvider = "terminalErrors") void testStartOneOffTerminalErrors(MockError error) { - Cloudant mockClient = new ChangesRequestMockClient(() -> new MockInstruction(error)); + Cloudant mockClient = new ChangesRequestMockClient(() -> new MockInstruction(error)); ChangesFollower testFollower = new ChangesFollower(mockClient, TestOptions.MINIMUM.getOptions()); RuntimeException e = Assert.expectThrows(error.getExceptionClass(), () -> { testFollower.startOneOff().count(); @@ -288,7 +279,7 @@ void testStartOneOffTerminalErrors(MockError error) { */ @Test(dataProvider = "transientErrors") void testStartOneOffTransientErrorsNoSuppression(MockError error) { - Cloudant mockClient = new ChangesRequestMockClient(() -> new MockInstruction(error)); + Cloudant mockClient = new ChangesRequestMockClient(() -> new MockInstruction(error)); ChangesFollower testFollower = new ChangesFollower(mockClient, TestOptions.MINIMUM.getOptions(), Duration.ZERO); RuntimeException e = Assert.expectThrows(error.getExceptionClass(), () -> { testFollower.startOneOff().count(); @@ -303,7 +294,7 @@ void testStartOneOffTransientErrorsNoSuppression(MockError error) { */ @Test(dataProvider = "transientErrors") void testStartOneOffTransientErrorsWithSuppressionDuration(MockError error) { - Cloudant mockClient = new ChangesRequestMockClient(() -> new MockInstruction(error)); + Cloudant mockClient = new ChangesRequestMockClient(() -> new MockInstruction(error)); ChangesFollower testFollower = new ChangesFollower(mockClient, TestOptions.MINIMUM.getOptions(), Duration.ofMillis(100L)); RuntimeException e = Assert.expectThrows(error.getExceptionClass(), () -> { testFollower.startOneOff().count(); @@ -330,7 +321,7 @@ void testStartOneOffTransientErrorsWithSuppressionDurationCompletes() { */ @Test(dataProvider = "transientErrors") void testStartOneOffTransientErrorsMaxSuppressionDoesNotComplete(MockError error) { - Cloudant mockClient = new ChangesRequestMockClient(() -> new MockInstruction(error)); + Cloudant mockClient = new ChangesRequestMockClient(() -> new MockInstruction(error)); ChangesFollower testFollower = new ChangesFollower(mockClient, TestOptions.MINIMUM.getOptions()); try { long count = testFollowerOnThread(testFollower, ChangesFollower.Mode.FINITE, false, Duration.ofMillis(500L)); @@ -380,7 +371,7 @@ void testStart() { */ @Test(dataProvider = "terminalErrors") void testStartTerminalErrors(MockError error) { - Cloudant mockClient = new ChangesRequestMockClient(() -> new MockInstruction(error)); + Cloudant mockClient = new ChangesRequestMockClient(() -> new MockInstruction(error)); ChangesFollower testFollower = new ChangesFollower(mockClient, TestOptions.MINIMUM.getOptions()); RuntimeException e = Assert.expectThrows(error.getExceptionClass(), () -> { testFollowerOnThread(testFollower, ChangesFollower.Mode.LISTEN, true, Duration.ofSeconds(1L)); @@ -393,7 +384,7 @@ void testStartTerminalErrors(MockError error) { */ @Test(dataProvider = "transientErrors") void testStartTransientErrorsNoSuppression(MockError error) { - Cloudant mockClient = new ChangesRequestMockClient(() -> new MockInstruction(error)); + Cloudant mockClient = new ChangesRequestMockClient(() -> new MockInstruction(error)); ChangesFollower testFollower = new ChangesFollower(mockClient, TestOptions.MINIMUM.getOptions(), Duration.ZERO); RuntimeException e = Assert.expectThrows(error.getExceptionClass(), () -> { testFollowerOnThread(testFollower, ChangesFollower.Mode.LISTEN, true, Duration.ofSeconds(1L)); @@ -406,7 +397,7 @@ void testStartTransientErrorsNoSuppression(MockError error) { */ @Test(dataProvider = "transientErrors") void testStartTransientErrorsWithSuppressionDurationErrorTermination(MockError error) { - Cloudant mockClient = new ChangesRequestMockClient(() -> new MockInstruction(error)); + Cloudant mockClient = new ChangesRequestMockClient(() -> new MockInstruction(error)); ChangesFollower testFollower = new ChangesFollower(mockClient, TestOptions.MINIMUM.getOptions(), Duration.ofMillis(100L)); RuntimeException e = Assert.expectThrows(error.getExceptionClass(), () -> { testFollowerOnThread(testFollower, ChangesFollower.Mode.LISTEN, true, Duration.ofSeconds(1L)); @@ -436,7 +427,7 @@ void testStartTransientErrorsWithSuppressionDurationAllChanges() { */ @Test(dataProvider = "transientErrors") void testStartTransientErrorsWithMaxSuppression(MockError error) { - Cloudant mockClient = new ChangesRequestMockClient(() -> new MockInstruction(error)); + Cloudant mockClient = new ChangesRequestMockClient(() -> new MockInstruction(error)); ChangesFollower testFollower = new ChangesFollower(mockClient, TestOptions.MINIMUM.getOptions()); try { long count = testFollowerOnThread(testFollower, ChangesFollower.Mode.LISTEN, false, Duration.ofSeconds(1L)); @@ -539,7 +530,7 @@ void testLimit(ChangesFollower.Mode mode, long limit) { */ @Test void testBatchSize() { - Cloudant mockClient = new ChangesRequestMockClient(QueuedSupplier.makeBatchSupplier(1)); + Cloudant mockClient = new ChangesRequestMockClient(ChangesRequestMockClient.makeBatchSupplier(1)); // Use no error tolerance to ensure we get our special test exception ChangesFollower testFollower = new ChangesFollower(mockClient, TestOptions.MINIMUM.getBuilder().includeDocs(true).build(), Duration.ZERO); LimitExposingException lee = Assert.expectThrows(LimitExposingException.class, () -> { @@ -558,7 +549,7 @@ void testBatchSize() { */ @Test void testBatchSizeMinimum() { - ChangesRequestMockClient mockClient = new ChangesRequestMockClient(QueuedSupplier.makeBatchSupplier(1)); + ChangesRequestMockClient mockClient = new ChangesRequestMockClient(ChangesRequestMockClient.makeBatchSupplier(1)); mockClient.setDatabaseInfoDocCount(1L); mockClient.setDatabaseInfoDocSize(5L * 1024L * 1024L - 1L); // Use no error tolerance to ensure we get our special test exception @@ -580,7 +571,7 @@ void testBatchSizeMinimum() { */ @Test void testBatchSizeLimit() { - Cloudant mockClient = new ChangesRequestMockClient(QueuedSupplier.makeBatchSupplier(1)); + Cloudant mockClient = new ChangesRequestMockClient(ChangesRequestMockClient.makeBatchSupplier(1)); // Use no error tolerance to ensure we get our special test exception ChangesFollower testFollower = new ChangesFollower(mockClient, TestOptions.MINIMUM.getBuilder().limit(1000L).includeDocs(true).build(), Duration.ZERO); diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/ChangesRequestMockClient.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/ChangesRequestMockClient.java index 587793b28..a57de1ce4 100644 --- a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/ChangesRequestMockClient.java +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/ChangesRequestMockClient.java @@ -13,22 +13,16 @@ package com.ibm.cloud.cloudant.features; -import java.io.IOException; -import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.EnumSet; import java.util.List; import java.util.NoSuchElementException; -import java.util.Queue; import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.LongStream; -import com.google.gson.stream.MalformedJsonException; -import com.ibm.cloud.cloudant.v1.Cloudant; import com.ibm.cloud.cloudant.v1.model.Change; import com.ibm.cloud.cloudant.v1.model.ChangesResult; import com.ibm.cloud.cloudant.v1.model.ChangesResultItem; @@ -39,149 +33,19 @@ import com.ibm.cloud.sdk.core.http.Response; import com.ibm.cloud.sdk.core.http.ServiceCall; import com.ibm.cloud.sdk.core.http.ServiceCallback; -import com.ibm.cloud.sdk.core.security.NoAuthAuthenticator; -import com.ibm.cloud.sdk.core.service.exception.BadRequestException; -import com.ibm.cloud.sdk.core.service.exception.ForbiddenException; -import com.ibm.cloud.sdk.core.service.exception.InternalServerErrorException; -import com.ibm.cloud.sdk.core.service.exception.InvalidServiceResponseException; -import com.ibm.cloud.sdk.core.service.exception.NotFoundException; -import com.ibm.cloud.sdk.core.service.exception.ServiceResponseException; -import com.ibm.cloud.sdk.core.service.exception.ServiceUnavailableException; -import com.ibm.cloud.sdk.core.service.exception.TooManyRequestsException; -import com.ibm.cloud.sdk.core.service.exception.UnauthorizedException; import org.testng.Assert; import io.reactivex.Single; -import okhttp3.MediaType; -import okhttp3.Protocol; -import okhttp3.RequestBody; -import okhttp3.ResponseBody; /** * Mock client that simulates changes responses */ -public class ChangesRequestMockClient extends Cloudant { +public class ChangesRequestMockClient extends MockCloudant { static final Random r = new Random(); - private static final okhttp3.Request MOCK_REQUEST = new okhttp3.Request.Builder() - .url("https://test.example/foo/_changes") - .method("POST", - RequestBody.create("{}", - MediaType.get("application/json"))) - .build(); - private static final okhttp3.Response SUCCESS_RESPONSE = new okhttp3.Response.Builder() - .code(200) - .message("OK") - .protocol(Protocol.HTTP_2) - .request(MOCK_REQUEST) - .build(); - - private final Supplier mocks; - final List> cancelledRequests = Collections.synchronizedList(new ArrayList<>()); + private boolean batchSizeCalculation = false; private Long databaseInfoDocCount = 500_000L; private Long databaseInfoDocSize = 523L; - - /** - * Mock errors - */ - enum MockError { - TERMINAL_400, - TERMINAL_401, - TERMINAL_403, - TERMINAL_404, - TRANSIENT_429, - TRANSIENT_500, - TRANSIENT_502, - TRANSIENT_503, - TRANSIENT_504, - TRANSIENT_BAD_JSON, - TRANSIENT_IO; - - static EnumSet getTerminalErrors() { - return EnumSet.of( - TERMINAL_400, - TERMINAL_401, - TERMINAL_403, - TERMINAL_404); - } - - static EnumSet getTransientErrors() { - return EnumSet.of( - TRANSIENT_429, - TRANSIENT_500, - TRANSIENT_502, - TRANSIENT_503, - TRANSIENT_504, - TRANSIENT_BAD_JSON, - TRANSIENT_IO); - } - - private okhttp3.Response makeResponse(okhttp3.Response.Builder rb, int code, String error) { - rb.code(code) - .protocol(Protocol.HTTP_2) - .request(MOCK_REQUEST); - switch (code) { - case 200: - rb.message("OK"); - // make some invalid JSON - rb.body(ResponseBody.create("{", MediaType.get("application/json"))); - break; - case 502: - rb.message("BAD_GATEWAY"); - rb.body(ResponseBody.create("", MediaType.get("text/plain"))); - break; - case 504: - rb.message("GATEWAY_TIMEOUT"); - rb.body(ResponseBody.create("", MediaType.get("text/plain"))); - break; - default: - rb.message(error); - rb.body(ResponseBody.create( - String.format("{\"error\":\"%s\", \"reason\":\"test error\"}", error), - MediaType.get("application/json"))); - break; - } - return rb.build(); - } - - Class getExceptionClass() { - return getException().getClass(); - } - - RuntimeException getException() { - okhttp3.Response.Builder rb = new okhttp3.Response.Builder(); - switch(this) { - case TERMINAL_400: - return new BadRequestException(makeResponse(rb, 400, "bad_request")); - case TERMINAL_401: - return new UnauthorizedException(makeResponse(rb, 401, "unauthorized")); - case TERMINAL_403: - return new ForbiddenException(makeResponse(rb, 403, "forbidden")); - case TERMINAL_404: - return new NotFoundException(makeResponse(rb, 404, "not_found")); - case TRANSIENT_429: - return new TooManyRequestsException(makeResponse(rb, 429, "too_many_requests")); - case TRANSIENT_500: - return new InternalServerErrorException(makeResponse(rb, 500, "internal_server_error")); - case TRANSIENT_503: - return new ServiceUnavailableException(makeResponse(rb, 503, "service_unavailable")); - case TRANSIENT_502: - return new ServiceResponseException(502, makeResponse(rb, 502, null)); - case TRANSIENT_504: - return new ServiceResponseException(504, makeResponse(rb, 504, null)); - case TRANSIENT_BAD_JSON: - return new InvalidServiceResponseException(makeResponse(rb, 200, null), "Bad request body", new MalformedJsonException("test bad json")); - case TRANSIENT_IO: - return new RuntimeException("Bad IO", new IOException("test bad IO")); - default: - return new RuntimeException("Unimplemented test exception"); - } - } - - void throwException() throws RuntimeException { - throw getException(); - } - } static String generateAlphanumString(final int size) { return r.ints(48 /* i.e. 0 */, 122 + 1 /* i.e. z +1 code point */) @@ -201,9 +65,8 @@ static String generateSeqLikeString(final long gen, final int size) { * Make a mock client that supplies results or exceptions as indicated by the supplier. * @param instructionSupplier */ - ChangesRequestMockClient(Supplier instructionSupplier) { - super(null, new NoAuthAuthenticator()); - this.mocks = instructionSupplier; + ChangesRequestMockClient(Supplier> instructionSupplier) { + super(instructionSupplier); } void setDatabaseInfoDocCount(Long count) { @@ -290,34 +153,6 @@ public void cancel() { }; } - /** - * An instruction to return a result or an error - */ - static class MockInstruction { - - private final MockError e; - private final ChangesResult result; - - MockInstruction(MockError e) { - this.e = e; - this.result = null; - } - - MockInstruction(ChangesResult result) { - this.result = result; - this.e = null; - } - - ChangesResult getResultOrThrow() { - if (result != null) { - return result; - } else { - e.throwException(); - return null; - } - } - } - static final class MockChangesResult extends ChangesResult { static ChangesResult EMPTY_RESULT = new MockChangesResult(Collections.emptyList(), 0L); @@ -358,86 +193,11 @@ static final class MockChange extends Change { } } - final class MockServiceCall implements ServiceCall { - - private final MockInstruction mockI; - private volatile boolean isCancelled = false; - - MockServiceCall(MockInstruction mockI) { - this.mockI = mockI; - } - - @Override - public ServiceCall addHeader(String name, String value) { - throw new UnsupportedOperationException("NOT MOCKED"); - } - - @Override - public Response execute() throws RuntimeException { - if (isCancelled) { - throw new RuntimeException("Execution of MockServiceCall after cancellation."); - } - return new Response<>(mockI.getResultOrThrow(), SUCCESS_RESPONSE); - } - - @Override - public void enqueue(ServiceCallback callback) { - throw new UnsupportedOperationException("NOT MOCKED"); - } - - @Override - public Single> reactiveRequest() { - throw new UnsupportedOperationException("NOT MOCKED"); - } - - @Override - public void cancel() { - isCancelled = true; - cancelledRequests.add(this); - } - } - - /** - * A MockInstruction supplier that uses a queue. - */ - static class QueuedSupplier implements Supplier { - - protected final Queue q; - - QueuedSupplier(Collection instructions) { - q = new ArrayDeque<>(instructions); - } - - @Override - public MockInstruction get() { - try { - return q.remove(); - } catch(NoSuchElementException e) { - Assert.fail("Test error: no mock instruction available for request."); - // The assert throw should take care of the exit, but the compiler - // doesn't seem to notice. - return null; - } - } - - static Supplier makeBatchSupplier(int numberOfBatches) { - long pending = numberOfBatches * ChangesFollower.BATCH_SIZE; - List mocks = new ArrayList<>(); - for (int i=1; i <= numberOfBatches; i++) { - pending -= ChangesFollower.BATCH_SIZE; - mocks.add(new MockInstruction(new MockChangesResult(((i-1) * ChangesFollower.BATCH_SIZE) + 1, pending))); - } - // Add a final empty result (the only result for 0 batch case) - mocks.add(new MockInstruction(MockChangesResult.EMPTY_RESULT)); - return new QueuedSupplier(mocks); - } - } - /** * A MockInstruction supplier that adds a new result instruction for * each one taken, thereby providing results perpetually. */ - static class PerpetualSupplier extends QueuedSupplier { + static class PerpetualSupplier extends QueuedSupplier { final boolean emptyFeed; final AtomicInteger counter = new AtomicInteger(0); @@ -450,7 +210,7 @@ static class PerpetualSupplier extends QueuedSupplier { this(Collections.emptyList(), emptyFeed); } - PerpetualSupplier(Collection initialMocks, boolean emptyFeed) { + PerpetualSupplier(Collection> initialMocks, boolean emptyFeed) { super(initialMocks); this.emptyFeed = emptyFeed; // Add an initial batch if there wasn't one @@ -463,11 +223,11 @@ boolean add() { ChangesResult mockResult = emptyFeed ? MockChangesResult.EMPTY_RESULT : new MockChangesResult((counter.getAndIncrement() * ChangesFollower.BATCH_SIZE) + 1, Long.MAX_VALUE); - return q.add(new MockInstruction(mockResult)); + return q.add(new MockInstruction(mockResult)); } @Override - public MockInstruction get() { + public MockInstruction get() { add(); return super.get(); } @@ -483,4 +243,16 @@ long getRequestLimit() { return this.limit; } } + + static Supplier> makeBatchSupplier(int numberOfBatches) { + long pending = numberOfBatches * ChangesFollower.BATCH_SIZE; + List> mocks = new ArrayList<>(); + for (int i=1; i <= numberOfBatches; i++) { + pending -= ChangesFollower.BATCH_SIZE; + mocks.add(new MockInstruction(new MockChangesResult(((i-1) * ChangesFollower.BATCH_SIZE) + 1, pending))); + } + // Add a final empty result (the only result for 0 batch case) + mocks.add(new MockInstruction(MockChangesResult.EMPTY_RESULT)); + return new QueuedSupplier(mocks); + } } diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/ChangesResultSpliteratorTest.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/ChangesResultSpliteratorTest.java index 2d1f7a24a..b907e6715 100644 --- a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/ChangesResultSpliteratorTest.java +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/ChangesResultSpliteratorTest.java @@ -26,10 +26,10 @@ import java.util.function.Consumer; import java.util.function.Supplier; import com.ibm.cloud.cloudant.features.ChangesRequestMockClient.MockChangesResult; -import com.ibm.cloud.cloudant.features.ChangesRequestMockClient.MockError; -import com.ibm.cloud.cloudant.features.ChangesRequestMockClient.MockInstruction; import com.ibm.cloud.cloudant.features.ChangesRequestMockClient.PerpetualSupplier; -import com.ibm.cloud.cloudant.features.ChangesRequestMockClient.QueuedSupplier; +import com.ibm.cloud.cloudant.features.MockCloudant.MockError; +import com.ibm.cloud.cloudant.features.MockCloudant.MockInstruction; +import com.ibm.cloud.cloudant.features.MockCloudant.QueuedSupplier; import com.ibm.cloud.cloudant.v1.Cloudant; import com.ibm.cloud.cloudant.v1.model.ChangesResult; import com.ibm.cloud.cloudant.v1.model.PostChangesOptions; @@ -66,7 +66,7 @@ enum Action { * @return */ WrappedTestSpliterator makeFixedSizeSpliterator(int batches, ChangesFollower.Mode mode, Duration tolerance) { - return makeSpliterator(QueuedSupplier.makeBatchSupplier(batches), mode, tolerance); + return makeSpliterator(ChangesRequestMockClient.makeBatchSupplier(batches), mode, tolerance); } /** @@ -91,7 +91,7 @@ WrappedTestSpliterator makePerpetualSpliterator(boolean empty, ChangesFollower.M * @param tolerance * @return */ - WrappedTestSpliterator makeSpliterator(Supplier mockSupplier, ChangesFollower.Mode mode, Duration tolerance) { + WrappedTestSpliterator makeSpliterator(Supplier> mockSupplier, ChangesFollower.Mode mode, Duration tolerance) { return new WrappedTestSpliterator( new ChangesRequestMockClient(mockSupplier), DEFAULT_OPTIONS, @@ -134,7 +134,7 @@ Object[][] getModesWithErrors() { int index = 0; for (ChangesFollower.Mode mode : modes) { for (ChangesRequestMockClient.MockError error : errors) { - tests[index] = new Object[]{mode, error, new ChangesRequestMockClient(() -> new MockInstruction(error))}; + tests[index] = new Object[]{mode, error, new ChangesRequestMockClient(() -> new MockInstruction(error))}; index++; } } @@ -169,19 +169,19 @@ Object[][] getSuppressionSequences() { for (ChangesFollower.Mode mode : modes) { for (ChangesRequestMockClient.MockError error : errors) { for(Action[] sequence : sequences) { - List instructions = new ArrayList<>(3); + List> instructions = new ArrayList<>(3); for (Action a: sequence) { switch(a) { case SUCCESS: - instructions.add(new MockInstruction(new MockChangesResult(1,1))); + instructions.add(new MockInstruction(new MockChangesResult(1,1))); break; case SUPPRESS: case THROW: - instructions.add(new MockInstruction(error)); + instructions.add(new MockInstruction(error)); break; } } - tests[index] = new Object[]{sequence, mode, error, new ChangesRequestMockClient(new QueuedSupplier(instructions))}; + tests[index] = new Object[]{sequence, mode, error, new ChangesRequestMockClient(new QueuedSupplier(instructions))}; index++; } } @@ -270,12 +270,12 @@ void testEstimateSize(ChangesFollower.Mode mode) { */ @Test void testEstimateSizePartialBatch() { - List instructions = new ArrayList<>(); + List> instructions = new ArrayList<>(); long partialBatchSize = 1844; - instructions.add(new MockInstruction(new MockChangesResult(1, ChangesFollower.BATCH_SIZE, ChangesFollower.BATCH_SIZE + partialBatchSize))); - instructions.add(new MockInstruction(new MockChangesResult(ChangesFollower.BATCH_SIZE + 1, ChangesFollower.BATCH_SIZE, partialBatchSize))); - instructions.add(new MockInstruction(new MockChangesResult(2*ChangesFollower.BATCH_SIZE + 1, partialBatchSize, 0L))); - ChangesResultSpliterator testSpliterator = makeSpliterator(new QueuedSupplier(instructions), ChangesFollower.Mode.FINITE, Duration.ZERO); + instructions.add(new MockInstruction(new MockChangesResult(1, ChangesFollower.BATCH_SIZE, ChangesFollower.BATCH_SIZE + partialBatchSize))); + instructions.add(new MockInstruction(new MockChangesResult(ChangesFollower.BATCH_SIZE + 1, ChangesFollower.BATCH_SIZE, partialBatchSize))); + instructions.add(new MockInstruction(new MockChangesResult(2*ChangesFollower.BATCH_SIZE + 1, partialBatchSize, 0L))); + ChangesResultSpliterator testSpliterator = makeSpliterator(new QueuedSupplier(instructions), ChangesFollower.Mode.FINITE, Duration.ZERO); // Initial estimate is always Long.MAX_VALUE Assert.assertEquals(testSpliterator.estimateSize(), Long.MAX_VALUE, "The initial estimated size should be Long.MAX_VALUE."); // Advance the spliterator @@ -292,7 +292,7 @@ void testEstimateSizePartialBatch() { void testNext(ChangesFollower.Mode mode) { ChangesResult expectedResult = new MockChangesResult(1, 0); ChangesResultSpliterator testSpliterator = makeSpliterator( - new QueuedSupplier(Collections.singletonList(new MockInstruction(expectedResult))), + new QueuedSupplier(Collections.singletonList(new MockInstruction(expectedResult))), mode, Duration.ZERO); ChangesResult actualResult = testSpliterator.next(); @@ -409,7 +409,7 @@ void testStop(ChangesFollower.Mode mode) throws Exception { @Test(dataProvider = "modes") void testStopDuringSuppression(ChangesFollower.Mode mode) throws Exception { executeTestStop(makeSpliterator( - () -> new MockInstruction(MockError.TRANSIENT_429), + () -> new MockInstruction(MockError.TRANSIENT_429), mode, ChronoUnit.FOREVER.getDuration())); } @@ -423,11 +423,11 @@ void testStopDuringSuppression(ChangesFollower.Mode mode) throws Exception { */ @Test(dataProvider = "modes") void testStopWithCancelledCall(ChangesFollower.Mode mode) throws Exception { - Collection instructions = Collections.singletonList( - new MockInstruction(MockError.TRANSIENT_IO) + Collection> instructions = Collections.singletonList( + new MockInstruction(MockError.TRANSIENT_IO) ); ChangesResultSpliterator testSpliterator = makeSpliterator( - new QueuedSupplier(instructions), + new QueuedSupplier(instructions), mode, Duration.ZERO); testSpliterator.stop(); @@ -573,7 +573,7 @@ void testTrySplit(ChangesFollower.Mode mode) { @Test(dataProvider = "modes") void testRetryDelay(ChangesFollower.Mode mode) { // Make a spliterator with 429 responses - ChangesResultSpliterator testSpliterator = makeSpliterator(() -> new MockInstruction(MockError.TRANSIENT_429), mode, ChronoUnit.FOREVER.getDuration()); + ChangesResultSpliterator testSpliterator = makeSpliterator(() -> new MockInstruction(MockError.TRANSIENT_429), mode, ChronoUnit.FOREVER.getDuration()); // Iterate for at least 300 ms (i.e. minimum 2 delays i.e. 100, 200 but could be more because of jitter) int requestCounter = 0; long startTime = System.currentTimeMillis(); diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/MockCloudant.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/MockCloudant.java new file mode 100644 index 000000000..46e908d16 --- /dev/null +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/MockCloudant.java @@ -0,0 +1,283 @@ +/** + * © Copyright IBM Corporation 2022, 2023. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Queue; +import java.util.function.Supplier; +import com.google.gson.stream.MalformedJsonException; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.sdk.core.http.Response; +import com.ibm.cloud.sdk.core.http.ServiceCall; +import com.ibm.cloud.sdk.core.http.ServiceCallback; +import com.ibm.cloud.sdk.core.security.NoAuthAuthenticator; +import com.ibm.cloud.sdk.core.service.exception.BadRequestException; +import com.ibm.cloud.sdk.core.service.exception.ForbiddenException; +import com.ibm.cloud.sdk.core.service.exception.InternalServerErrorException; +import com.ibm.cloud.sdk.core.service.exception.InvalidServiceResponseException; +import com.ibm.cloud.sdk.core.service.exception.NotFoundException; +import com.ibm.cloud.sdk.core.service.exception.ServiceResponseException; +import com.ibm.cloud.sdk.core.service.exception.ServiceUnavailableException; +import com.ibm.cloud.sdk.core.service.exception.TooManyRequestsException; +import com.ibm.cloud.sdk.core.service.exception.UnauthorizedException; +import org.testng.Assert; +import io.reactivex.Single; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.RequestBody; +import okhttp3.ResponseBody; + +/** + * Mock client that simulates changes responses + */ +public class MockCloudant extends Cloudant { + + /** + * Make a mock client that supplies results or exceptions as indicated by the supplier. + * @param instructionSupplier + */ + public MockCloudant(Supplier> instructionSupplier) { + super(null, new NoAuthAuthenticator()); + this.mocks = instructionSupplier; + } + + protected static final okhttp3.Request MOCK_REQUEST = new okhttp3.Request.Builder() + .url("https://test.example/foo/bar/baz") + .method("POST", + RequestBody.create("{}", + MediaType.get("application/json"))) + .build(); + protected static final okhttp3.Response SUCCESS_RESPONSE = new okhttp3.Response.Builder() + .code(200) + .message("OK") + .protocol(Protocol.HTTP_2) + .request(MOCK_REQUEST) + .build(); + + protected final Supplier> mocks; + protected final List> cancelledRequests = Collections.synchronizedList(new ArrayList<>()); + + /** + * Mock errors + */ + public enum MockError { + TERMINAL_400, + TERMINAL_401, + TERMINAL_403, + TERMINAL_404, + TRANSIENT_429, + TRANSIENT_500, + TRANSIENT_502, + TRANSIENT_503, + TRANSIENT_504, + TRANSIENT_BAD_JSON, + TRANSIENT_IO; + + static EnumSet getTerminalErrors() { + return EnumSet.of( + TERMINAL_400, + TERMINAL_401, + TERMINAL_403, + TERMINAL_404); + } + + static EnumSet getTransientErrors() { + return EnumSet.of( + TRANSIENT_429, + TRANSIENT_500, + TRANSIENT_502, + TRANSIENT_503, + TRANSIENT_504, + TRANSIENT_BAD_JSON, + TRANSIENT_IO); + } + + private okhttp3.Response makeResponse(okhttp3.Response.Builder rb, int code, String error) { + rb.code(code) + .protocol(Protocol.HTTP_2) + .request(MOCK_REQUEST); + switch (code) { + case 200: + rb.message("OK"); + // make some invalid JSON + rb.body(ResponseBody.create("{", MediaType.get("application/json"))); + break; + case 502: + rb.message("BAD_GATEWAY"); + rb.body(ResponseBody.create("", MediaType.get("text/plain"))); + break; + case 504: + rb.message("GATEWAY_TIMEOUT"); + rb.body(ResponseBody.create("", MediaType.get("text/plain"))); + break; + default: + rb.message(error); + rb.body(ResponseBody.create( + String.format("{\"error\":\"%s\", \"reason\":\"test error\"}", error), + MediaType.get("application/json"))); + break; + } + return rb.build(); + } + + public Class getExceptionClass() { + return getException().getClass(); + } + + RuntimeException getException() { + okhttp3.Response.Builder rb = new okhttp3.Response.Builder(); + switch(this) { + case TERMINAL_400: + return new BadRequestException(makeResponse(rb, 400, "bad_request")); + case TERMINAL_401: + return new UnauthorizedException(makeResponse(rb, 401, "unauthorized")); + case TERMINAL_403: + return new ForbiddenException(makeResponse(rb, 403, "forbidden")); + case TERMINAL_404: + return new NotFoundException(makeResponse(rb, 404, "not_found")); + case TRANSIENT_429: + return new TooManyRequestsException(makeResponse(rb, 429, "too_many_requests")); + case TRANSIENT_500: + return new InternalServerErrorException(makeResponse(rb, 500, "internal_server_error")); + case TRANSIENT_503: + return new ServiceUnavailableException(makeResponse(rb, 503, "service_unavailable")); + case TRANSIENT_502: + return new ServiceResponseException(502, makeResponse(rb, 502, null)); + case TRANSIENT_504: + return new ServiceResponseException(504, makeResponse(rb, 504, null)); + case TRANSIENT_BAD_JSON: + return new InvalidServiceResponseException(makeResponse(rb, 200, null), "Bad request body", new MalformedJsonException("test bad json")); + case TRANSIENT_IO: + return new RuntimeException("Bad IO", new IOException("test bad IO")); + default: + return new RuntimeException("Unimplemented test exception"); + } + } + + void throwException() throws RuntimeException { + throw getException(); + } + + public static Object[][] errorsAsTestObjectArray(Collection errors) { + Object[][] tests = new Object[errors.size()][]; + int index = 0; + for (MockError e : errors) { + tests[index] = new Object[]{e}; + index++; + } + return tests; + } + } + + /** + * An instruction to return a result or an error + */ + public static class MockInstruction { + + private final MockError e; + private final R result; + + public MockInstruction(MockError e) { + this.e = e; + this.result = null; + } + + public MockInstruction(R result) { + this.result = result; + this.e = null; + } + + R getResultOrThrow() { + if (result != null) { + return result; + } else { + e.throwException(); + return null; + } + } + } + + protected final class MockServiceCall implements ServiceCall { + + private final MockInstruction mockI; + private volatile boolean isCancelled = false; + + public MockServiceCall(MockInstruction mockI) { + this.mockI = mockI; + } + + @Override + public ServiceCall addHeader(String name, String value) { + throw new UnsupportedOperationException("NOT MOCKED"); + } + + @Override + public Response execute() throws RuntimeException { + if (isCancelled) { + throw new RuntimeException("Execution of MockServiceCall after cancellation."); + } + return new Response(mockI.getResultOrThrow(), SUCCESS_RESPONSE); + } + + @Override + public void enqueue(ServiceCallback callback) { + throw new UnsupportedOperationException("NOT MOCKED"); + } + + @Override + public Single> reactiveRequest() { + throw new UnsupportedOperationException("NOT MOCKED"); + } + + @Override + public void cancel() { + isCancelled = true; + cancelledRequests.add(this); + } + } + + /** + * A MockInstruction supplier that uses a queue. + */ + public static class QueuedSupplier implements Supplier> { + + protected final Collection> instructions; + protected final Queue> q; + + public QueuedSupplier(Collection> instructions) { + this.instructions = instructions; + this.q = new ArrayDeque<>(instructions); + } + + @Override + public MockInstruction get() { + try { + return q.remove(); + } catch(NoSuchElementException e) { + Assert.fail("Test error: no mock instruction available for request."); + // The assert throw should take care of the exit, but the compiler + // doesn't seem to notice. + return null; + } + } + } + +} diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/AllDocsTest.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/AllDocsTest.java new file mode 100644 index 000000000..7269ae538 --- /dev/null +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/AllDocsTest.java @@ -0,0 +1,41 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import com.ibm.cloud.cloudant.features.MockCloudant; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.OptionsWrapper; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.PageSupplierFactory; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestAllDocsResult; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestDocsResultRow; +import com.ibm.cloud.cloudant.v1.model.AllDocsResult; +import com.ibm.cloud.cloudant.v1.model.DocsResultRow; +import com.ibm.cloud.cloudant.v1.model.PostAllDocsOptions; + +public class AllDocsTest extends + PaginationOperationTest { + + AllDocsTest() { + super(new PageSupplierFactory(TestAllDocsResult::new, TestDocsResultRow::new, + true), OptionsWrapper.POST_ALL_DOCS.newProvider(), true); + } + + // New Pagination + @Override + protected Pagination makeNewPagination( + MockCloudant cloudant, PostAllDocsOptions options) { + return Pagination.newPagination(cloudant, options); + } + +} diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/BasePageIteratorTest.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/BasePageIteratorTest.java new file mode 100644 index 000000000..d8bb10fff --- /dev/null +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/BasePageIteratorTest.java @@ -0,0 +1,570 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; +import java.util.function.Function; +import org.testng.Assert; +import org.testng.annotations.Test; +import com.ibm.cloud.cloudant.features.MockCloudant.MockInstruction; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.MockPagerClient; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.PageSupplier; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestResult; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.PostViewOptions; +import com.ibm.cloud.cloudant.v1.model.PostViewOptions.Builder; +import com.ibm.cloud.sdk.core.http.ServiceCall; + +import static com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.getDefaultTestOptions; +import static com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.getRequiredTestOptionsBuilder; +import static com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.newPageSupplierFromList; + +public class BasePageIteratorTest { + + private Cloudant mockClient = new MockPagerClient(null); + + /** + * This test sub-class of BasePager implicitly tests that various abstract methods are correctly + * called by non-abstract methods in the BasePager. + */ + private static class TestPager extends BasePageIterator { + + protected TestPager(Cloudant client, PostViewOptions options) { + super(client, options, OptionsHandler.POST_VIEW); + } + + Cloudant getClient() { + return this.client; + } + + /** + * Implicitly tests that limit gets applied per page, otherwise the default would be used and + * page counts would be wrong. + */ + @Override + protected Function optionsToBuilderFunction() { + return PostViewOptions::newBuilder; + } + + @Override + protected Function builderToOptionsFunction() { + return Builder::build; + } + + @Override + protected Function> itemsGetter() { + return TestResult::getRows; + } + + /** + * These tests don't actually use the options, but we set a startKey so we can validate calls to + * setNextPageOptions. + */ + @Override + protected void setNextPageOptions(Builder builder, TestResult result) { + List rows = result.getRows(); + if (rows.isEmpty()) { + throw new IllegalStateException("Test failure: tried to setNextPageOptions on empty page."); + } else { + Integer i = rows.get(rows.size() - 1); + builder.startKey(i); + } + } + + /** + * Delegates to our next mock. If the BasePager didn't correctly call this the mocks wouldn't + * work. + */ + @Override + protected BiFunction> nextRequestFunction() { + return (c, o) -> { + return ((MockPagerClient) c).testCall(); + }; + } + + @Override + protected Function limitGetter() { + return PostViewOptions::limit; + } + + } + + private static class ThrowingTestPager extends TestPager { + + private int callCounter = 0; + + protected ThrowingTestPager(Cloudant client, PostViewOptions options) { + super(client, options); + } + + @Override + List nextRequest() { + callCounter++; + switch (callCounter) { + case 2: + throw new RuntimeException("Test issue with request"); + default: + return super.nextRequest(); + } + } + + } + + private Pagination newPagination(Cloudant client, + PostViewOptions options) { + return new Pagination<>(client, options, TestPager::new); + } + + private Pagination newThrowingPagination(Cloudant client, + PostViewOptions options) { + return new Pagination<>(client, options, ThrowingTestPager::new); + } + + // test constructor + @Test + void testConstructor() { + TestPager pager = new TestPager(mockClient, getDefaultTestOptions(42)); + // Assert the client + Assert.assertEquals(((TestPager) pager).getClient(), mockClient, + "The client should be the supplied one."); + } + + // test page size default + @Test + void testDefaultPageSize() { + TestPager pager = new TestPager(mockClient, getRequiredTestOptionsBuilder().build()); + // Assert the default page size + Assert.assertEquals(pager.pageSize, 200, "The page size should be the default."); + } + + // test page size limit + @Test + void testLimitPageSize() { + TestPager pager = new TestPager(mockClient, getDefaultTestOptions(42)); + // Assert the limit provided as page size + Assert.assertEquals(pager.pageSize, 42, "The page size should match the limit."); + } + + // test hasNext + @Test + void testHasNextIsTrueInitially() { + TestPager pager = new TestPager(mockClient, getDefaultTestOptions(42)); + Assert.assertEquals(pager.hasNext(), true, "hasNext() should initially return true."); + } + + @Test + void testHasNextIsTrueForResultEqualToLimit() { + // Mock a one element page + MockPagerClient c = new MockPagerClient(() -> { + return new MockInstruction(new TestResult(Collections.singletonList(1))); + }); + TestPager pager = new TestPager(c, getDefaultTestOptions(1)); + // Get the first page (size 1) + pager.next(); + // hasNext should return true because results size is the same as limit + Assert.assertEquals(pager.hasNext(), true, "hasNext() should return true."); + } + + @Test + void testHasNextIsFalseForResultLessThanLimit() { + // Mock a zero size page + MockPagerClient c = new MockPagerClient(() -> { + return new MockInstruction(new TestResult(Collections.emptyList())); + }); + TestPager pager = new TestPager(c, getDefaultTestOptions(1)); + // Get the first page (size 0) + pager.next(); + // hasNext should return false because results size smaller than limit + Assert.assertEquals(pager.hasNext(), false, "hasNext() should return false."); + } + + // test next + @Test + void testNextFirstPage() { + int pageSize = 25; + PageSupplier pageSupplier = + PaginationTestHelpers.newBasePageSupplier(pageSize, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + TestPager pager = new TestPager(c, getDefaultTestOptions(pageSize)); + List actualPage = pager.next(); + // Assert first page + Assert.assertEquals(actualPage, pageSupplier.pages.get(0), + "The actual page should match the expected page."); + } + + @Test + void testNextTwoPages() { + int pageSize = 3; + PageSupplier pageSupplier = + PaginationTestHelpers.newBasePageSupplier(2 * pageSize, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + TestPager pager = new TestPager(c, getDefaultTestOptions(pageSize)); + Assert.assertEquals(pager.hasNext(), true, "hasNext() should return true."); + List actualPage1 = pager.next(); + // Assert pages + Assert.assertEquals(actualPage1, pageSupplier.pages.get(0), + "The actual page should match the expected page."); + Assert.assertEquals(pager.hasNext(), true, "hasNext() should return true."); + List actualPage2 = pager.next(); + Assert.assertEquals(actualPage2, pageSupplier.pages.get(1), + "The actual page should match the expected page."); + Assert.assertEquals(pager.hasNext(), true, "hasNext() should return true."); + } + + @Test + void testNextUntilEmpty() { + int pageSize = 3; + PageSupplier pageSupplier = + PaginationTestHelpers.newBasePageSupplier(3 * pageSize, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + TestPager pager = new TestPager(c, getDefaultTestOptions(pageSize)); + List actualItems = new ArrayList<>(); + int pageCount = 0; + while (pager.hasNext()) { + pageCount++; + List page = pager.next(); + actualItems.addAll(page); + // Assert each page is the same or smaller than the limit + // to make sure we aren't getting all the results in a single page + Assert.assertTrue(page.size() <= pageSize, + "The actual page size should be smaller or equal to the expected page size."); + } + Assert.assertEquals(actualItems, pageSupplier.allItems, + "The results should match all the pages."); + Assert.assertEquals(pageCount, pageSupplier.pages.size(), + "There should have been the correct number of pages."); + } + + @Test + void testNextUntilSmaller() { + int pageSize = 3; + PageSupplier pageSupplier = + PaginationTestHelpers.newBasePageSupplier(10, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + TestPager pager = new TestPager(c, getDefaultTestOptions(pageSize)); + List actualItems = new ArrayList<>(); + int pageCount = 0; + while (pager.hasNext()) { + pageCount++; + List page = pager.next(); + actualItems.addAll(page); + // Assert each page is the same or smaller than the limit + // to make sure we aren't getting all the results in a single page + Assert.assertTrue(page.size() <= pageSize, + "The actual page size should be smaller or equal to the expected page size."); + } + Assert.assertEquals(actualItems, pageSupplier.allItems, + "The results should match all the pages."); + Assert.assertEquals(pageCount, pageSupplier.pages.size(), + "There should have been the correct number of pages."); + } + + @Test + void testNextException() { + int pageSize = 2; + PageSupplier pageSupplier = + PaginationTestHelpers.newBasePageSupplier(pageSize - 1, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + TestPager pager = new TestPager(c, getDefaultTestOptions(pageSize)); + List actualPage = pager.next(); + // Assert first page + Assert.assertEquals(actualPage, pageSupplier.pages.get(0), + "The actual page should match the expected page."); + // hasNext false + Assert.assertEquals(pager.hasNext(), false, "hasNext() should return false."); + // next throws + Assert.assertThrows(NoSuchElementException.class, () -> { + pager.next(); + }); + } + + @Test + void testPagesAreImmutable() { + int pageSize = 1; + PageSupplier pageSupplier = + PaginationTestHelpers.newBasePageSupplier(pageSize, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + TestPager pager = new TestPager(c, getDefaultTestOptions(pageSize)); + List actualPage = pager.next(); + // Assert immutable + Assert.assertThrows(UnsupportedOperationException.class, () -> { + actualPage.add(17); + }); + } + + // test setNextPageOptions + @Test + void testSetNextPageOptions() { + int pageSize = 1; + PageSupplier pageSupplier = + PaginationTestHelpers.newBasePageSupplier(5 * pageSize, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + TestPager pager = new TestPager(c, getDefaultTestOptions(pageSize)); + Assert.assertNull(pager.nextPageOptionsRef.get().startKey(), + "startKey should initially be null."); + // Since we use a page size of 1, each next page options key, is the same as the element from + // the page + int page = 0; + while (pager.hasNext()) { + pager.next(); + if (pager.hasNext()) { + Assert.assertEquals(pager.nextPageOptionsRef.get().startKey(), page, + "the startKey should increment per page."); + } else { + // Options don't change for last page + Assert.assertEquals(pager.nextPageOptionsRef.get().startKey(), page - 1, + "The options should not be set for the final page."); + } + page++; + } + } + + @Test + void testNextResumesAfterError() { + int pageSize = 3; + PageSupplier pageSupplier = + PaginationTestHelpers.newBasePageSupplier(2 * pageSize, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + TestPager pager = new ThrowingTestPager(c, getDefaultTestOptions(pageSize)); + List actualPage1 = pager.next(); + // Assert pages + Assert.assertEquals(actualPage1, pageSupplier.pages.get(0), + "The actual page should match the expected page."); + // The startKey should point to row 2 (the last row we saw, note this is not doing n+1 paging) + Assert.assertEquals(pager.nextPageOptionsRef.get().startKey(), 2, + "The startKey should be 2 for the second page."); + Assert.assertThrows(RuntimeException.class, () -> pager.next()); + // Assert hasNext + Assert.assertEquals(pager.hasNext(), true, "hasNext() should return true."); + // The startKey should still point to the second page + Assert.assertEquals(pager.nextPageOptionsRef.get().startKey(), 2, + "The startKey should be 2 for the second page."); + List actualPage2 = pager.next(); + Assert.assertEquals(actualPage2, pageSupplier.pages.get(1), + "The actual page should match the expected page."); + } + + // asPager (getNext) + @Test + void testAsPagerGetNextFirstPage() { + int pageSize = 7; + PageSupplier pageSupplier = + PaginationTestHelpers.newBasePageSupplier(2 * pageSize, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + Pager pager = newPagination(c, getDefaultTestOptions(pageSize)).pager(); + List actualPage = pager.getNext(); + // Assert first page + Assert.assertEquals(actualPage, pageSupplier.pages.get(0), + "The actual page should match the expected page."); + } + + // asPager (getNext until consumed) + @Test + void testAsPagerGetNextUntilConsumed() { + int pageSize = 7; + PageSupplier pageSupplier = + PaginationTestHelpers.newBasePageSupplier(2 * pageSize, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + Pager pager = newPagination(c, getDefaultTestOptions(pageSize)).pager(); + List actualItems = new ArrayList<>(); + Iterator> expectedPages = pageSupplier.pages.iterator(); + int pageCount = 0; + while (pager.hasNext()) { + List page = pager.getNext(); + Assert.assertEquals(page, expectedPages.next()); + actualItems.addAll(page); + pageCount++; + } + // Assert items + Assert.assertEquals(actualItems, pageSupplier.allItems, + "The results should match all the pages."); + // Assert page count, note 3 because third page is empty + Assert.assertEquals(pageCount, 3, + "There should have been the correct number of pages."); + // Assert cannot be called again + Assert.assertThrows(IllegalStateException.class, + () -> pager.getNext() + ); + } + + // asPager (getAll) + @Test + void testAsPagerGetAll() { + int pageSize = 11; + PageSupplier pageSupplier = + PaginationTestHelpers.newBasePageSupplier(71, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + Pager pager = newPagination(c, getDefaultTestOptions(pageSize)).pager(); + List actualItems = pager.getAll(); + Assert.assertEquals(actualItems, pageSupplier.allItems, + "The results should match all the pages."); + // Assert consumed state prevents calling again + Assert.assertThrows(IllegalStateException.class, + () -> pager.getAll() + ); + } + + // asPager (getNext exception) + @Test + void testAsPagerGetNextResumesAfterError() { + // This is like testNextResumesAfterError but for Pager getNext + // We can't introspect the options here, but if we get the right results the options must be + // right since we tested that arleady in testNextResumesAfterError + int pageSize = 3; + PageSupplier pageSupplier = + PaginationTestHelpers.newBasePageSupplier(2 * pageSize, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + Pager pager = newThrowingPagination(c, getDefaultTestOptions(pageSize)).pager(); + List actualPage1 = pager.getNext(); + // Assert pages + Assert.assertEquals(actualPage1, pageSupplier.pages.get(0), + "The actual page should match the expected page."); + Assert.assertThrows(RuntimeException.class, () -> pager.getNext()); + // Assert hasNext + Assert.assertEquals(pager.hasNext(), true, "hasNext() should return true."); + List actualPage2 = pager.getNext(); + Assert.assertEquals(actualPage2, pageSupplier.pages.get(1), + "The actual page should match the expected page."); + } + + // asPager (getAll exception) + @Test + void testAsPagerGetAllRestartsAfterError() { + int pageSize = 1; + // Set up a supplier to do first page, [error], first page, second page, empty page + PageSupplier pageSupplier = newPageSupplierFromList( + List.of(List.of(1), // first page + List.of(1), // error, followed by first page replay + List.of(2), // second page + Collections.emptyList())); // final empty page + MockPagerClient c = new MockPagerClient(pageSupplier); + final AtomicInteger constructedOnce = new AtomicInteger(); + Pagination errorOnFirst = new Pagination(c, + getDefaultTestOptions(pageSize), (client, opts) -> { + // Note that Pager automatically makes an iterator for hasNext/getNext so that is call 0 + // Call 1 is the first getAll (that will throw) + // Call 2 should not throw + if (constructedOnce.getAndIncrement() > 1) { + return new TestPager(client, opts); + } else { + return new ThrowingTestPager(client, opts); + } + }); + Pager pager = errorOnFirst.pager(); + Assert.assertThrows(RuntimeException.class, () -> pager.getAll()); + Assert.assertEquals(pager.getAll(), List.of(1, 2), "The results should match all the pages."); + // Assert consumed state prevents calling again + Assert.assertThrows(IllegalStateException.class, + () -> pager.getAll() + ); + } + + // asPager (getNext then getAll) + @Test + void testAsPagerGetNextGetAllThrows() { + int pageSize = 7; + PageSupplier pageSupplier = + PaginationTestHelpers.newBasePageSupplier(2 * pageSize, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + Pager pager = newPagination(c, getDefaultTestOptions(pageSize)).pager(); + // Assert first page + Assert.assertEquals(pager.getNext(), pageSupplier.pages.get(0), + "The actual page should match the expected page."); + // Assert cannot call getAll once getNext has been called + Assert.assertThrows(IllegalStateException.class, + () -> pager.getAll() + ); + // Ensure we can call getNext() again + // Assert second page + Assert.assertEquals(pager.getNext(), pageSupplier.pages.get(1), + "The actual page should match the expected page."); + } + + // asPager (getAll then getNext) + @Test + void testAsPagerGetAllGetNextThrows() { + int pageSize = 7; + PageSupplier pageSupplier = + PaginationTestHelpers.newBasePageSupplier(2 * pageSize, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + Pager pager = new Pagination(c, + getDefaultTestOptions(pageSize), ThrowingTestPager::new).pager(); + // Make sure we stop the getAll in a non-consumed state + Assert.assertThrows(RuntimeException.class, + () -> pager.getAll() + ); + // Assert cannot call getNext once getAll has been called + Assert.assertThrows(IllegalStateException.class, + () -> pager.getNext() + ); + } + + // asIterable (pages) + @Test + void testPagesIterable() { + int pageSize = 23; + PageSupplier pageSupplier = + PaginationTestHelpers.newBasePageSupplier(3 * pageSize - 1, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + Iterator> expectedPages = pageSupplier.pages.iterator(); + for (List page : newPagination(c, getDefaultTestOptions(pageSize)).pages()) { + Assert.assertEquals(page, expectedPages.next()); + } + } + + // asIterable (rows) + @Test + void testRowsIterable() { + int pageSize = 23; + PageSupplier pageSupplier = + PaginationTestHelpers.newBasePageSupplier(3 * pageSize - 1, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + Iterator expectedRows = pageSupplier.allItems.iterator(); + for (Integer row : newPagination(c, getDefaultTestOptions(pageSize)).rows()) { + Assert.assertEquals(row, expectedRows.next()); + } + } + + // asStream (pages) + @Test + void testPagesStream() { + int pageSize = 23; + PageSupplier pageSupplier = + PaginationTestHelpers.newBasePageSupplier(4 * pageSize, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + Iterator> expectedPages = pageSupplier.pages.iterator(); + newPagination(c, getDefaultTestOptions(pageSize)).pages() + .forEach(i -> Assert.assertEquals(i, expectedPages.next())); + } + + // asStream (rows) + @Test + void testRowsStream() { + int pageSize = 17; + PageSupplier pageSupplier = + PaginationTestHelpers.newBasePageSupplier(4 * pageSize, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + Iterator expectedRows = pageSupplier.allItems.iterator(); + newPagination(c, getDefaultTestOptions(pageSize)).rows() + .forEach(i -> Assert.assertEquals(i, expectedRows.next())); + } + +} diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/BookmarkPageIteratorTest.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/BookmarkPageIteratorTest.java new file mode 100644 index 000000000..5de76065b --- /dev/null +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/BookmarkPageIteratorTest.java @@ -0,0 +1,205 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import static com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.getDefaultTestFindOptions; +import static com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.getRequiredTestFindOptionsBuilder; +import static com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.newBasePageSupplier; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import org.testng.Assert; +import org.testng.annotations.Test; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.MockPagerClient; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.PageSupplier; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestResult; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.PostFindOptions; +import com.ibm.cloud.cloudant.v1.model.PostFindOptions.Builder; +import com.ibm.cloud.sdk.core.http.ServiceCall; + +public class BookmarkPageIteratorTest { + + private Cloudant mockClient = new MockPagerClient(null); + + /** + * This test sub-class of BookmarkPager implicitly tests that various abstract methods are + * correctly called. + */ + class TestBookmarkPager extends BookmarkPageIterator { + + protected TestBookmarkPager(Cloudant client, PostFindOptions options) { + super(client, options, OptionsHandler.POST_FIND); + } + + Cloudant getClient() { + return this.client; + } + + /** + * Delegates to our next mock. If the BasePager didn't correctly call this the mocks wouldn't + * work. + */ + @Override + protected BiFunction> nextRequestFunction() { + return (c, o) -> { + return ((MockPagerClient) c).testCall(); + }; + } + + @Override + protected Function limitGetter() { + return PostFindOptions::limit; + } + + @Override + protected Function optionsToBuilderFunction() { + return PostFindOptions::newBuilder; + } + + @Override + protected Function builderToOptionsFunction() { + return Builder::build; + } + + @Override + protected Function> itemsGetter() { + return TestResult::getRows; + } + + @Override + Function bookmarkGetter() { + // Use the last row value as the bookmark + return result -> { + List rows = result.getRows(); + return String.valueOf(rows.get(rows.size() - 1)); + }; + } + + @Override + BiFunction bookmarkSetter() { + return Builder::bookmark; + } + + } + + // Test page size default + @Test + void testDefaultPageSize() { + TestBookmarkPager pager = + new TestBookmarkPager(mockClient, getRequiredTestFindOptionsBuilder().build()); + // Assert the limit provided as page size + Assert.assertEquals(pager.pageSize, 200, "The page size should be the default limit."); + } + + // Test page size limit + @Test + void testLimitPageSize() { + TestBookmarkPager pager = new TestBookmarkPager(mockClient, getDefaultTestFindOptions(42)); + // Assert the limit provided as page size + Assert.assertEquals(pager.pageSize, 42, "The page size should be the supplied limit."); + } + + // Test all items on page when no more pages + @Test + void testNextPageLessThanLimit() { + int pageSize = 21; + PageSupplier pageSupplier = newBasePageSupplier(pageSize - 1, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + TestBookmarkPager pager = new TestBookmarkPager(c, getDefaultTestFindOptions(pageSize)); + List actualPage = pager.next(); + // Assert first page + Assert.assertEquals(actualPage, pageSupplier.pages.get(0), + "The actual page should match the expected page."); + // Assert size + Assert.assertEquals(actualPage.size(), pageSize - 1, + "The actual page size should match the expected page size."); + // Assert hasNext false + Assert.assertFalse(pager.hasNext(), "hasNext() should return false."); + } + + // Test correct items on page when limit + @Test + void testNextPageEqualToLimit() { + int pageSize = 14; + PageSupplier pageSupplier = newBasePageSupplier(pageSize, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + TestBookmarkPager pager = new TestBookmarkPager(c, getDefaultTestFindOptions(pageSize)); + List actualPage = pager.next(); + // Assert first page + Assert.assertEquals(actualPage, pageSupplier.pages.get(0), + "The actual page should match the expected page."); + // Assert size is pageSize + Assert.assertEquals(actualPage.size(), pageSize, + "The actual page size should match the expected page size."); + // Assert hasNext true + Assert.assertTrue(pager.hasNext(), "hasNext() should return true."); + // Assert bookmark is correct, note the result rows start at zero, so pageSize - 1, not pageSize + Assert.assertEquals(pager.nextPageOptionsRef.get().bookmark(), String.valueOf(pageSize - 1), + "The bookmark should be one less than the page size."); + // Get next page + List actualSecondPage = pager.next(); + // Assert second page is empty + Assert.assertEquals(actualSecondPage.size(), 0, "The second page should be empty."); + // Assert hasNext false + Assert.assertFalse(pager.hasNext(), "hasNext() should return false."); + } + + // Test correct items on pages when more than limit + @Test + void testNextPageGreaterThanLimit() { + int pageSize = 7; + PageSupplier pageSupplier = newBasePageSupplier(pageSize + 2, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + TestBookmarkPager pager = new TestBookmarkPager(c, getDefaultTestFindOptions(pageSize)); + List actualPage = pager.next(); + // Assert first page + Assert.assertEquals(actualPage, pageSupplier.pages.get(0), + "The actual page should match the expected page."); + // Assert size is pageSize + Assert.assertEquals(actualPage.size(), pageSize, + "The actual page size should match the expected page size."); + // Assert hasNext true + Assert.assertTrue(pager.hasNext(), "hasNext() should return true."); + // Assert bookmark is correct, note the result rows start at zero, so pageSize - 1 + Assert.assertEquals(pager.nextPageOptionsRef.get().bookmark(), String.valueOf(pageSize - 1), + "The bookmark should be one less than the page size."); + // Get next page + List actualSecondPage = pager.next(); + // Assert first item on second page is correct + Assert.assertEquals(actualSecondPage.get(0), pageSize, + "The first item on the second page should be as expected."); + // Assert size is 2 + Assert.assertEquals(actualSecondPage.size(), 2, + "The actual page size should match the expected page size."); + // Assert second page + Assert.assertEquals(actualSecondPage, pageSupplier.pages.get(1), + "The actual page should match the expected page."); + // Assert hasNext false + Assert.assertFalse(pager.hasNext(), "hasNext() should return false."); + } + + // Test getting all items + @Test + void testGetAll() { + int pageSize = 3; + PageSupplier pageSupplier = newBasePageSupplier(pageSize * 12, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + List actualItems = new Pagination(c, + getDefaultTestFindOptions(pageSize), TestBookmarkPager::new).pager().getAll(); + Assert.assertEquals(actualItems, pageSupplier.allItems, + "The results should match all the pages."); + } +} diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/DesignDocsTest.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/DesignDocsTest.java new file mode 100644 index 000000000..94fda34ea --- /dev/null +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/DesignDocsTest.java @@ -0,0 +1,41 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import com.ibm.cloud.cloudant.features.MockCloudant; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.OptionsWrapper; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.PageSupplierFactory; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestAllDocsResult; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestDocsResultRow; +import com.ibm.cloud.cloudant.v1.model.AllDocsResult; +import com.ibm.cloud.cloudant.v1.model.DocsResultRow; +import com.ibm.cloud.cloudant.v1.model.PostDesignDocsOptions; + +public class DesignDocsTest extends + PaginationOperationTest { + + DesignDocsTest() { + super(new PageSupplierFactory(TestAllDocsResult::new, TestDocsResultRow::new, + true), OptionsWrapper.POST_DESIGN_DOCS.newProvider(), true); + } + + // New Pagination + @Override + protected Pagination makeNewPagination( + MockCloudant cloudant, PostDesignDocsOptions options) { + return Pagination.newPagination(cloudant, options); + } + +} diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/FindTest.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/FindTest.java new file mode 100644 index 000000000..4a309ae0e --- /dev/null +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/FindTest.java @@ -0,0 +1,41 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import com.ibm.cloud.cloudant.features.MockCloudant; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.OptionsWrapper; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.PageSupplierFactory; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestFindDocument; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestFindResult; +import com.ibm.cloud.cloudant.v1.model.Document; +import com.ibm.cloud.cloudant.v1.model.FindResult; +import com.ibm.cloud.cloudant.v1.model.PostFindOptions; + +public class FindTest extends + PaginationOperationTest { + + FindTest() { + super(new PageSupplierFactory(TestFindResult::new, TestFindDocument::new, + false), OptionsWrapper.POST_FIND.newProvider(), false); + } + + // New Pagination + @Override + protected Pagination makeNewPagination( + MockCloudant cloudant, PostFindOptions options) { + return Pagination.newPagination(cloudant, options); + } + +} diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/KeyPageIteratorTest.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/KeyPageIteratorTest.java new file mode 100644 index 000000000..da3b6b5cc --- /dev/null +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/KeyPageIteratorTest.java @@ -0,0 +1,300 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import org.testng.Assert; +import org.testng.annotations.Test; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.MockPagerClient; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.PageSupplier; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestResult; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.PostViewOptions; +import com.ibm.cloud.cloudant.v1.model.PostViewOptions.Builder; +import com.ibm.cloud.sdk.core.http.ServiceCall; + +import static com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.getDefaultTestOptions; +import static com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.getRequiredTestOptionsBuilder; +import static com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.newKeyPageSupplier; + +public class KeyPageIteratorTest { + + private Cloudant mockClient = new MockPagerClient(null); + + /** + * This test sub-class of KeyPager implicitly tests that various abstract methods are correctly + * called. + */ + class TestKeyPager extends KeyPageIterator { + + protected TestKeyPager(Cloudant client, PostViewOptions options) { + super(client, options, OptionsHandler.POST_VIEW); + } + + Cloudant getClient() { + return this.client; + } + + /** + * Delegates to our next mock. If the BasePager didn't correctly call this the mocks wouldn't + * work. + */ + @Override + protected BiFunction> nextRequestFunction() { + return (c, o) -> { + return ((MockPagerClient) c).testCall(); + }; + } + + @Override + protected Function limitGetter() { + return PostViewOptions::limit; + } + + @Override + protected BiFunction nextKeySetter() { + return Builder::startKey; + } + + @Override + protected Optional> nextKeyIdSetter() { + return Optional.of(Builder::startKeyDocId); + } + + @Override + protected Function nextKeyGetter() { + return Function.identity(); + } + + @Override + protected Function nextKeyIdGetter() { + return String::valueOf; + } + + @Override + protected Function optionsToBuilderFunction() { + return PostViewOptions::newBuilder; + } + + @Override + protected Function builderToOptionsFunction() { + return Builder::build; + } + + @Override + protected Function> itemsGetter() { + return TestResult::getRows; + } + + @Override + Optional checkBoundary(Integer penultimateItem, Integer lastItem) { + return Optional.empty(); + } + + } + + // Test page size default (+1) + @Test + void testDefaultPageSize() { + TestKeyPager pager = new TestKeyPager(mockClient, getRequiredTestOptionsBuilder().build()); + // Assert the default limit as page size + Assert.assertEquals(pager.pageSize, 201, + "The page size should be one more than the default limit."); + } + + // Test page size limit (+1) + @Test + void testLimitPageSize() { + TestKeyPager pager = new TestKeyPager(mockClient, getDefaultTestOptions(42)); + // Assert the limit provided as page size + Assert.assertEquals(pager.pageSize, 43, + "The page size should be one more than the supplied limit."); + } + + // Test all items on page when no more pages + @Test + void testnextPageLessThanLimit() { + int pageSize = 21; + PageSupplier pageSupplier = newKeyPageSupplier(pageSize, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + TestKeyPager pager = new TestKeyPager(c, getDefaultTestOptions(pageSize)); + List actualPage = pager.next(); + // Assert first page + Assert.assertEquals(actualPage, pageSupplier.pages.get(0), + "The actual page should match the expected page."); + // Assert size is pageSize + Assert.assertEquals(actualPage.size(), pageSize, + "The actual page size should match the expected page size."); + // Assert hasNext false because n+1 limit is 1 more than user page size + Assert.assertFalse(pager.hasNext(), "hasNext() should return false."); + } + + // Test correct items on page when n+1 + @Test + void testnextPageEqualToLimit() { + int pageSize = 14; + PageSupplier pageSupplier = newKeyPageSupplier(pageSize + 1, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + TestKeyPager pager = new TestKeyPager(c, getDefaultTestOptions(pageSize)); + List actualPage = pager.next(); + // Assert first page + Assert.assertEquals(actualPage, pageSupplier.pages.get(0), + "The actual page should match the expected page."); + // Assert size is pageSize + Assert.assertEquals(actualPage.size(), pageSize, + "The actual page size should match the expected page size."); + // Assert hasNext true + Assert.assertTrue(pager.hasNext(), "hasNext() should return true."); + // Assert start key is correct, note the result rows start at zero, so pageSize, not pageSize + + // 1 + Assert.assertEquals(pager.nextPageOptionsRef.get().startKey(), pageSize, + "The start key should be page size."); + // Get next page + List actualSecondPage = pager.next(); + // Assert first item on second page is correct (again we start at zero, so pageSize not pageSize + // + 1) + Assert.assertEquals(actualSecondPage.get(0), pageSize, + "The first item on the second page should be as expected."); + // Assert size is 1 + Assert.assertEquals(actualSecondPage.size(), 1, + "The actual page size should match the expected page size."); + // Assert second page + Assert.assertEquals(actualSecondPage, pageSupplier.pages.get(1), + "The actual page should match the expected page."); + // Assert hasNext false + Assert.assertFalse(pager.hasNext(), "hasNext() should return false."); + } + + // Test correct items on page when n+more + @Test + void testNextPageGreaterThanLimit() { + int pageSize = 7; + PageSupplier pageSupplier = newKeyPageSupplier(pageSize + 2, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + TestKeyPager pager = new TestKeyPager(c, getDefaultTestOptions(pageSize)); + List actualPage = pager.next(); + // Assert first page + Assert.assertEquals(actualPage, pageSupplier.pages.get(0), + "The actual page should match the expected page."); + // Assert size is pageSize + Assert.assertEquals(actualPage.size(), pageSize, + "The actual page size should match the expected page size."); + // Assert hasNext true + Assert.assertTrue(pager.hasNext(), "hasNext() should return true."); + // Assert start key is correct, note the result rows start at zero, so pageSize, not pageSize + + // 1 + Assert.assertEquals(pager.nextPageOptionsRef.get().startKey(), pageSize, + "The start key should be page size plus one."); + // Get next page + List actualSecondPage = pager.next(); + // Assert first item on second page is correct (again we start at zero, so pageSize not pageSize + // + 1) + Assert.assertEquals(actualSecondPage.get(0), pageSize, + "The first item on the second page should be as expected."); + // Assert size is 2 + Assert.assertEquals(actualSecondPage.size(), 2, + "The actual page size should match the expected page size."); + // Assert second page + Assert.assertEquals(actualSecondPage, pageSupplier.pages.get(1), + "The actual page should match the expected page."); + // Assert hasNext false + Assert.assertFalse(pager.hasNext(), "hasNext() should return false."); + } + + // Test getting all items + @Test + void testGetAll() { + int pageSize = 3; + PageSupplier pageSupplier = newKeyPageSupplier(pageSize * 12, pageSize); + MockPagerClient c = new MockPagerClient(pageSupplier); + List actualItems = new Pagination(c, + getDefaultTestOptions(pageSize), TestKeyPager::new).pager().getAll(); + Assert.assertEquals(actualItems, pageSupplier.allItems, + "The results should match all the pages."); + } + + @Test + void testNoBoundaryCheckByDefault() { + int pageSize = 1; + // Make pages with identical rows + List pageOne = new ArrayList<>(List.of(1, 1)); + List pageTwo = new ArrayList<>(List.of(1, 1)); + PageSupplier pageSupplier = + PaginationTestHelpers.newPageSupplierFromList(List.of(pageOne, pageTwo)); + MockPagerClient c = new MockPagerClient(pageSupplier); + TestKeyPager pager = new TestKeyPager(c, getDefaultTestOptions(pageSize)); + // Get and assert page + Assert.assertEquals(pager.next(), pageSupplier.pages.get(0), + "The actual page should match the expected page."); + // Assert hasNext true + Assert.assertTrue(pager.hasNext(), "hasNext() should return true."); + // Boundary check implementation should not throw + Assert.assertEquals(pager.next(), pageSupplier.pages.get(1), + "The actual page should match the expected page."); + } + + @Test + void testBoundaryFailureThrowsOnNext() { + int pageSize = 1; + // Make pages with identical rows + List pageOne = new ArrayList<>(List.of(1, 1)); + List pageTwo = new ArrayList<>(List.of(1, 1)); + PageSupplier pageSupplier = + PaginationTestHelpers.newPageSupplierFromList(List.of(pageOne, pageTwo)); + MockPagerClient c = new MockPagerClient(pageSupplier); + TestKeyPager pager = new TestKeyPager(c, getDefaultTestOptions(pageSize)) { + @Override + Optional checkBoundary(Integer penultimateItem, Integer lastItem) { + return Optional.of("Test boundary check failure"); + } + }; + // Get and assert page + Assert.assertEquals(pager.next(), pageSupplier.pages.get(0), + "The actual page should match the expected page."); + // Assert hasNext true + Assert.assertTrue(pager.hasNext(), "hasNext() should return true."); + // The optional isn't empty so it check boundary should throw + Assert.assertThrows(UnsupportedOperationException.class, () -> { + pager.next(); + }); + } + + @Test + void testNoBoundaryCheckWhenNoItemsLeft() { + int pageSize = 1; + // Make pages with identical rows + List pageOne = new ArrayList<>(List.of(1)); + PageSupplier pageSupplier = + PaginationTestHelpers.newPageSupplierFromList(List.of(pageOne)); + MockPagerClient c = new MockPagerClient(pageSupplier); + TestKeyPager pager = new TestKeyPager(c, getDefaultTestOptions(pageSize)) { + @Override + Optional checkBoundary(Integer penultimateItem, Integer lastItem) { + // Throw here to cause the test to fail if checkBoundary is called. + throw new RuntimeException("Test failure, checkBoundary should not be called."); + } + }; + // Get and assert page + Assert.assertEquals(pager.next(), pageSupplier.pages.get(0), + "The actual page should match the expected page."); + // Assert hasNext false + Assert.assertFalse(pager.hasNext(), "hasNext() should return false."); + } + +} diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/OptionsValidationTest.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/OptionsValidationTest.java new file mode 100644 index 000000000..8484721f2 --- /dev/null +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/OptionsValidationTest.java @@ -0,0 +1,118 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.List; +import org.testng.annotations.Test; +import org.testng.Assert; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.OptionsProvider; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.OptionsWrapper; +import com.ibm.cloud.cloudant.v1.model.PostSearchOptions; + +public class OptionsValidationTest { + + @Test(dataProvider = "allOptions", dataProviderClass = PaginationTestHelpers.class) + public void testOptionsValidationLimitLessThanMin(OptionsProvider provider) + throws Exception { + provider.setRequiredOpts(); + provider.set("limit", OptionsHandler.MIN_LIMIT - 1); + Assert.assertThrows("There should be a validation exception", IllegalArgumentException.class, + () -> { + provider.handler.validate(provider.build()); + }); + } + + @Test(dataProvider = "allOptions", dataProviderClass = PaginationTestHelpers.class) + public void testOptionsValidationLimitEqualToMin(OptionsProvider provider) + throws Exception { + provider.setRequiredOpts(); + provider.set("limit", OptionsHandler.MIN_LIMIT); + try { + provider.handler.validate(provider.build()); + } catch (IllegalArgumentException e) { + Assert.fail("There should be no validation exception.", e); + } + } + + @Test(dataProvider = "allOptions", dataProviderClass = PaginationTestHelpers.class) + public void testOptionsValidationLimitLessThanMax(OptionsProvider provider) + throws Exception { + provider.setRequiredOpts(); + provider.set("limit", OptionsHandler.MAX_LIMIT - 1); + try { + provider.handler.validate(provider.build()); + } catch (IllegalArgumentException e) { + Assert.fail("There should be no validation exception.", e); + } + } + + @Test(dataProvider = "allOptions", dataProviderClass = PaginationTestHelpers.class) + public void testOptionsValidationLimitEqualToMax(OptionsProvider provider) + throws Exception { + provider.setRequiredOpts(); + provider.set("limit", OptionsHandler.MAX_LIMIT); + try { + provider.handler.validate(provider.build()); + } catch (IllegalArgumentException e) { + Assert.fail("There should be no validation exception.", e); + } + } + + @Test(dataProvider = "allOptions", dataProviderClass = PaginationTestHelpers.class) + public void testOptionsValidationLimitGreaterThanMax(OptionsProvider provider) + throws Exception { + provider.setRequiredOpts(); + provider.set("limit", OptionsHandler.MAX_LIMIT + 1); + Assert.assertThrows("There should be a validation exception", IllegalArgumentException.class, + () -> { + provider.handler.validate(provider.build()); + }); + } + + @Test(dataProvider = "allOptions", dataProviderClass = PaginationTestHelpers.class) + public void testOptionsValidationLimitUnset(OptionsProvider provider) + throws Exception { + provider.setRequiredOpts(); + try { + provider.handler.validate(provider.build()); + } catch (IllegalArgumentException e) { + Assert.fail("There should be no validation exception.", e); + } + } + + @Test(dataProvider = "viewLikeOptions", dataProviderClass = PaginationTestHelpers.class) + public void testValidationExceptionForKeys(OptionsProvider provider) + throws Exception { + provider.setRequiredOpts(); + provider.set("keys", List.of("key1", "key2")); + Assert.assertThrows("There should be a validation exception", IllegalArgumentException.class, + () -> { + provider.handler.validate(provider.build()); + }); + } + + @Test(dataProvider = "searchFacetOptions", dataProviderClass = PaginationTestHelpers.class) + public void testValidationExceptionForFacetedSearch(String optionName, Object optionValue) + throws Exception { + OptionsProvider provider = + OptionsWrapper.POST_SEARCH.newProvider(); + provider.setRequiredOpts(); + provider.set(optionName, optionValue); + Assert.assertThrows("There should be a validation exception", IllegalArgumentException.class, + () -> { + provider.handler.validate(provider.build()); + }); + } + +} diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/PaginationOperationTest.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/PaginationOperationTest.java new file mode 100644 index 000000000..162b3911d --- /dev/null +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/PaginationOperationTest.java @@ -0,0 +1,352 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import com.ibm.cloud.cloudant.features.MockCloudant; +import com.ibm.cloud.cloudant.features.MockCloudant.MockError; +import com.ibm.cloud.cloudant.features.MockCloudant.MockInstruction; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.MockPagerCloudant; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.OptionsProvider; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.PageSupplier; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.PageSupplierFactory; + +public abstract class PaginationOperationTest { + + protected final PageSupplierFactory pageSupplierFactory; + protected final OptionsProvider provider; + protected final boolean plusOnePaging; + + private Function, PagingResult> pagerFn = p -> { + PagingResult result = new PagingResult<>(); + Pager pager = p.pager(); + while (pager.hasNext()) { + try { + List page = pager.getNext(); + result.handlePage(page); + } catch (Exception e) { + result.handleException(e); + } + } + return result; + }; + + private Function, PagingResult> pageStreamFn = p -> { + PagingResult result = new PagingResult<>(); + try { + p.pageStream().forEach(result::handlePage); + } catch (Exception e) { + result.handleException(e); + } + return result; + }; + + private Function, PagingResult> pagesFn = p -> { + PagingResult result = new PagingResult<>(); + try { + for (List page : p.pages()) { + result.handlePage(page); + } + } catch (Exception e) { + result.handleException(e); + } + return result; + }; + + private Function, PagingResult> rowStreamFn = p -> { + PagingResult result = new PagingResult<>(); + try { + p.rowStream().forEach(result::handleItem); + } catch (Exception e) { + result.handleException(e); + } + return result; + }; + + private Function, PagingResult> rowsFn = p -> { + PagingResult result = new PagingResult<>(); + try { + for (I row : p.rows()) { + result.handleItem(row); + } + } catch (Exception e) { + result.handleException(e); + } + return result; + }; + + PaginationOperationTest(PageSupplierFactory pageSupplierFactory, + OptionsProvider provider, boolean plusOnePaging) { + this.pageSupplierFactory = pageSupplierFactory; + this.provider = provider; + this.plusOnePaging = plusOnePaging; + } + + private static final class TestCase { + final int totalItems; + final int pageSize; + final int expectedPages; + + TestCase(int totalItems, int pageSize, boolean plusOnePaging) { + this.totalItems = totalItems; + this.pageSize = pageSize; + this.expectedPages = (totalItems == 0) ? 1 : getExpectedPages(plusOnePaging); + } + + private int getExpectedPages(boolean plusOnePaging) { + int fullPages = (totalItems / pageSize); + int partialPages = totalItems % pageSize == 0 ? 0 : 1; + int expectedPages = fullPages + partialPages; + // Need at least 1 empty page to know there are no more results + // if not ending on a partial page, except if the first page or + // using n+1 paging (because an exact user page is a partial real page). + if (partialPages == 0 && (!plusOnePaging || expectedPages == 0)) { + expectedPages += 1; // We will get at least 1 empty page + } + return expectedPages; + } + } + + private final List, TestCase>> pageAssertions = + List.of(PagingResult::assertPageCount, PagingResult::assertItemCount, + PagingResult::assertUniqueItemCount); + private final List, TestCase>> itemAssertions = + pageAssertions.subList(1, pageAssertions.size()); + + + private static final class PagingResult { + + boolean assertable = false; + Integer pageCount = 0; + List items = new ArrayList<>(); + List errors = new ArrayList<>(); + + private void setAssertable() { + if (!this.assertable) { + this.assertable = true; + } + } + + private void handleException(Exception e) { + setAssertable(); + errors.add(e); + } + + private void handleItem(I item) { + setAssertable(); + items.add(item); + } + + private void handlePage(Collection page) { + setAssertable(); + pageCount++; + items.addAll(page); + } + + private void assertItemCount(TestCase t) { + Assert.assertEquals(items.size(), t.totalItems, + "There should be the expected number of items."); + } + + private void assertPageCount(TestCase t) { + Assert.assertTrue(this.assertable, "PagingResult handled no pages."); + Assert.assertEquals(pageCount, t.expectedPages, + "There should be the expected number of pages."); + } + + private void assertUniqueItemCount(TestCase t) { + Set uniqueItems = new HashSet<>(); + uniqueItems.addAll(items); + Assert.assertEquals(uniqueItems.size(), t.totalItems, "The items should be unique."); + } + + } + + @DataProvider(name = "pageSets") + Object[][] getPageSets() { + int pageSize = 10; + return new Object[][] {makeTestCase(0, pageSize), // Empty page + makeTestCase(1, pageSize), // Partial page + makeTestCase(pageSize - 1, pageSize), // One less than a whole page + makeTestCase(pageSize, pageSize), // Exactly one page + makeTestCase(pageSize + 1, pageSize), // One more than a whole page + makeTestCase(3 * pageSize, pageSize), // Multiple pages, exact + makeTestCase(3 * pageSize + 1, pageSize), // Multiple pages, plus one + makeTestCase(4 * pageSize - 1, pageSize) // Multiple pages, partial finish + }; + } + + @DataProvider(name = "errorSuppliers") + Iterator getErrorSuppliers() { + return Arrays.stream(MockError.values()).flatMap(mockError -> { + return List.of(new Object[] {mockError, true}, new Object[] {mockError, false}).stream(); + }).iterator(); + } + + Object[] makeTestCase(int total, int pageSize) { + return new Object[] {new TestCase(total, pageSize, plusOnePaging)}; + } + + protected abstract Pagination makeNewPagination(MockCloudant c, O options); + + private Pagination makeTestPagination(int pageSize, Supplier> supplier) + throws Exception { + this.provider.setRequiredOpts(); + this.provider.set("limit", Integer.valueOf(pageSize).longValue()); + return makeNewPagination(new MockPagerCloudant(supplier), provider.build()); + } + + private void runPaginationTest(TestCase t, + Function, PagingResult> pagingFunction, + List, TestCase>> assertions) throws Exception { + Pagination pagination = makeTestPagination(t.pageSize, + this.pageSupplierFactory.newPageSupplier(t.totalItems, t.pageSize)); + PagingResult r = pagingFunction.apply(pagination); + for (BiConsumer, TestCase> assertion : assertions) { + assertion.accept(r, t); + } + } + + private void runPaginationErrorTest(Function, PagingResult> pagingFunction, + MockError error, boolean errorOnFirstPage) throws Exception { + int pageSize = Long.valueOf(OptionsHandler.MAX_LIMIT).intValue(); + int wholePages = 2; + int expectedItems = wholePages * pageSize; + List> instructions = new ArrayList<>(); + PageSupplier supplier = this.pageSupplierFactory.newPageSupplier(expectedItems, pageSize); + if (!errorOnFirstPage) { + instructions.add(supplier.get()); // Add a successful page + } + instructions.add(new MockInstruction<>(error)); // Add the error + // Add remaining instructions that might be needed + // For error on first page it is first page, second page, empty page (i.e. 3) + // For error on second page it is second page, empty page (i.e. 2) + int remainingInstructions = (errorOnFirstPage ? 1 : 0) + wholePages; + Stream.generate(supplier).limit(remainingInstructions).forEach(instructions::add); + Pagination pagination = + makeTestPagination(pageSize, new MockCloudant.QueuedSupplier(instructions)); + PagingResult r = pagingFunction.apply(pagination); + // Assert that there was a single exception + Assert.assertEquals(r.errors.size(), 1, "There should be a single exception."); + Assert.assertEquals(r.errors.get(0).getClass(), error.getExceptionClass(), + "The correct exception should be received."); + TestCase t; + if (pagerFn.equals(pagingFunction)) { + // If using pager then expect all pages/items + t = new TestCase(expectedItems, pageSize, this.plusOnePaging); + r.assertPageCount(new TestCase(expectedItems, pageSize, this.plusOnePaging)); + r.assertItemCount(new TestCase(expectedItems, pageSize, this.plusOnePaging)); + } else { + // Else (pages, rows, streams) expect only up to the error + int expectedPages = (errorOnFirstPage) ? 0 : 1; + expectedItems = expectedPages * pageSize; + if (!rowsFn.equals(pagingFunction) && !rowStreamFn.equals(pagingFunction)) { + // Only assert page count if we are handling pages + Assert.assertEquals(r.pageCount, expectedPages, + "There should be the correct number of pages."); + } + Assert.assertEquals(r.items.size(), expectedItems, + "There should be the correct number of items."); + } + + } + + // Check validation is wired + @Test + public void testValidationEnabled() throws Exception { + Assert.assertThrows("There should be a validation exception", IllegalArgumentException.class, + () -> { + runPaginationTest( + new TestCase(0, Long.valueOf(OptionsHandler.MIN_LIMIT - 1).intValue(), plusOnePaging), + p -> null, Collections.emptyList()); + }); + } + + // Check Pager + @Test(dataProvider = "pageSets") + public void testPager(TestCase t) throws Exception { + runPaginationTest(t, pagerFn, pageAssertions); + } + + // Check PageStream + @Test(dataProvider = "pageSets") + public void testPageStream(TestCase t) throws Exception { + runPaginationTest(t, pageStreamFn, pageAssertions); + } + + // Check Pages + @Test(dataProvider = "pageSets") + public void testPages(TestCase t) throws Exception { + runPaginationTest(t, pagesFn, pageAssertions); + } + + // Check RowStream + @Test(dataProvider = "pageSets") + public void testRowStream(TestCase t) throws Exception { + runPaginationTest(t, rowStreamFn, itemAssertions); + } + + // Check Rows + @Test(dataProvider = "pageSets") + public void testRows(TestCase t) throws Exception { + runPaginationTest(t, rowsFn, itemAssertions); + } + + // Check Pager errors + @Test(dataProvider = "errorSuppliers") + public void testPagerErrors(MockError error, boolean errorOnFirstPage) throws Exception { + runPaginationErrorTest(pagerFn, error, errorOnFirstPage); + } + + // Check PageStream errors + @Test(dataProvider = "errorSuppliers") + public void testPageStreamErrors(MockError error, boolean errorOnFirstPage) throws Exception { + runPaginationErrorTest(pageStreamFn, error, errorOnFirstPage); + } + + // Check Pages errors + @Test(dataProvider = "errorSuppliers") + public void testPagesErrors(MockError error, boolean errorOnFirstPage) throws Exception { + runPaginationErrorTest(pagesFn, error, errorOnFirstPage); + } + + // Check RowStream errors + @Test(dataProvider = "errorSuppliers") + public void testRowStreamErrors(MockError error, boolean errorOnFirstPage) throws Exception { + runPaginationErrorTest(rowStreamFn, error, errorOnFirstPage); + } + + // Check Rows errors + @Test(dataProvider = "errorSuppliers") + public void testRowsErrors(MockError error, boolean errorOnFirstPage) throws Exception { + runPaginationErrorTest(rowsFn, error, errorOnFirstPage); + } + + +} diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/PaginationTestHelpers.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/PaginationTestHelpers.java new file mode 100644 index 000000000..fee039864 --- /dev/null +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/PaginationTestHelpers.java @@ -0,0 +1,500 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.testng.annotations.DataProvider; +import com.ibm.cloud.cloudant.features.MockCloudant; +import com.ibm.cloud.cloudant.features.MockCloudant.MockInstruction; +import com.ibm.cloud.cloudant.features.MockCloudant.QueuedSupplier; +import com.ibm.cloud.cloudant.v1.model.AllDocsResult; +import com.ibm.cloud.cloudant.v1.model.DocsResultRow; +import com.ibm.cloud.cloudant.v1.model.Document; +import com.ibm.cloud.cloudant.v1.model.FindResult; +import com.ibm.cloud.cloudant.v1.model.PostAllDocsOptions; +import com.ibm.cloud.cloudant.v1.model.PostDesignDocsOptions; +import com.ibm.cloud.cloudant.v1.model.PostFindOptions; +import com.ibm.cloud.cloudant.v1.model.PostPartitionAllDocsOptions; +import com.ibm.cloud.cloudant.v1.model.PostPartitionFindOptions; +import com.ibm.cloud.cloudant.v1.model.PostPartitionSearchOptions; +import com.ibm.cloud.cloudant.v1.model.PostPartitionViewOptions; +import com.ibm.cloud.cloudant.v1.model.PostSearchOptions; +import com.ibm.cloud.cloudant.v1.model.PostViewOptions; +import com.ibm.cloud.cloudant.v1.model.SearchResult; +import com.ibm.cloud.cloudant.v1.model.SearchResultRow; +import com.ibm.cloud.cloudant.v1.model.ViewResult; +import com.ibm.cloud.cloudant.v1.model.ViewResultRow; +import com.ibm.cloud.sdk.core.http.ServiceCall; + +public class PaginationTestHelpers { + + static class MockPagerCloudant extends MockCloudant { + + MockPagerCloudant(Supplier> instructionSupplier) { + super(instructionSupplier); + } + + @Override + public com.ibm.cloud.sdk.core.http.ServiceCall postAllDocs( + PostAllDocsOptions postAllDocsOptions) { + return (ServiceCall) this.testCall(); + } + + @Override + public com.ibm.cloud.sdk.core.http.ServiceCall postDesignDocs( + PostDesignDocsOptions postDesignDocsOptions) { + return (ServiceCall) this.testCall(); + } + + @Override + public com.ibm.cloud.sdk.core.http.ServiceCall postFind( + PostFindOptions postFindOptions) { + return (ServiceCall) this.testCall(); + } + + @Override + public com.ibm.cloud.sdk.core.http.ServiceCall postPartitionAllDocs( + PostPartitionAllDocsOptions postPartitionAllDocsOptions) { + return (ServiceCall) this.testCall(); + } + + @Override + public com.ibm.cloud.sdk.core.http.ServiceCall postPartitionFind( + PostPartitionFindOptions postPartitionFindOptions) { + return (ServiceCall) this.testCall(); + } + + @Override + public com.ibm.cloud.sdk.core.http.ServiceCall postPartitionSearch( + PostPartitionSearchOptions postPartitionSearchOptions) { + return (ServiceCall) this.testCall(); + } + + @Override + public com.ibm.cloud.sdk.core.http.ServiceCall postPartitionView( + PostPartitionViewOptions postPartitionViewOptions) { + return (ServiceCall) this.testCall(); + } + + @Override + public com.ibm.cloud.sdk.core.http.ServiceCall postSearch( + PostSearchOptions postSearchOptions) { + return (ServiceCall) this.testCall(); + } + + @Override + public com.ibm.cloud.sdk.core.http.ServiceCall postView( + PostViewOptions postViewOptions) { + return (ServiceCall) this.testCall(); + } + + ServiceCall testCall() { + return new MockServiceCall(mocks.get()); + } + + } + + static class MockPagerClient extends MockPagerCloudant { + MockPagerClient(Supplier> instructionSupplier) { + super(instructionSupplier); + } + } + + static class TestResult { + private List rows; + + TestResult(List rows) { + this.rows = rows; + } + + List getRows() { + return this.rows; + } + } + + static class PageSupplierFactory { + + final boolean plusOnePaging; + final Function, R> itemsToPageResultFn; + final Function integerRowWrapFn; + + PageSupplierFactory(Function, R> itemsToPageResultFn, + Function integerRowWrapFn) { + this(itemsToPageResultFn, integerRowWrapFn, false); + } + + PageSupplierFactory(Function, R> itemsToPageResultFn, + Function integerRowWrapFn, boolean plusOnePaging) { + this.itemsToPageResultFn = itemsToPageResultFn; + this.integerRowWrapFn = integerRowWrapFn; + this.plusOnePaging = plusOnePaging; + } + + private List> getPages(int total, int pageSize) { + final List> pages = new ArrayList<>(); + List page = new ArrayList<>(); + for (int i = 0; i < total; i++) { + page.add(this.integerRowWrapFn.apply(i)); + if (i % pageSize == pageSize - 1) { + pages.add(page); + page = new ArrayList<>(); + } + } + // Add the final page, empty or otherwise + pages.add(page); + return pages; + } + + PageSupplier newPageSupplier(int total, int pageSize) { + List> pages = getPages(total, pageSize); + if (this.plusOnePaging) { + List> responsePages = repageForKeyBased(pages); + return new PageSupplier(this.itemsToPageResultFn, pages, responsePages); + } else { + return new PageSupplier(this.itemsToPageResultFn, pages); + } + + } + + private List> repageForKeyBased(List> pages) { + List> responsePages = new ArrayList<>(pages.size()); + int index = 0; + for (List page : pages) { + List responsePage = new ArrayList<>(page); + // Add the first element from the next page as n + 1 + try { + List nextPage = pages.get(index + 1); + responsePage.add(nextPage.get(0)); + } catch (IndexOutOfBoundsException e) { + // Suppress exception if pages/elements exhuasted + } + responsePages.add(responsePage); + index++; + } + return responsePages; + } + + } + + static class PageSupplier extends QueuedSupplier { + + final List allItems; + final List> pages; + + private PageSupplier(Function, R> itemsToPageFn, List> pages) { + this(itemsToPageFn, pages, pages); + } + + private PageSupplier(Function, R> itemsToPageFn, List> pages, + List> responsePages) { + super(responsePages.stream().map(itemsToPageFn).map(MockInstruction::new) + .collect(Collectors.toList())); + this.pages = pages; + this.allItems = this.pages.stream().flatMap(List::stream).collect(Collectors.toList()); + } + } + + static PageSupplier newBasePageSupplier(int total, int pageSize) { + return new PageSupplierFactory<>(TestResult::new, Function.identity()).newPageSupplier(total, + pageSize); + } + + static PageSupplier newKeyPageSupplier(int total, int pageSize) { + return new PageSupplierFactory(TestResult::new, Function.identity(), true) + .newPageSupplier(total, pageSize); + } + + static PageSupplier newViewPageSupplier(int total, int pageSize) { + return new PageSupplierFactory(TestViewResult::new, + TestViewResultRow::new, true).newPageSupplier(total, pageSize); + } + + static PageSupplier newPageSupplierFromList(List> pages) { + return new PageSupplier(TestResult::new, pages); + } + + static PostViewOptions getDefaultTestOptions(int limit) { + return getRequiredTestOptionsBuilder().limit(limit).build(); + } + + static PostViewOptions.Builder getRequiredTestOptionsBuilder() { + return new PostViewOptions.Builder().db("example-database").ddoc("test-ddoc").view("test-view"); + } + + static PostFindOptions getDefaultTestFindOptions(int limit) { + return getRequiredTestFindOptionsBuilder().limit(limit).build(); + } + + static PostFindOptions.Builder getRequiredTestFindOptionsBuilder() { + return new PostFindOptions.Builder().db("example-database") + .selector(Collections.singletonMap("testField", "testValue")); + } + + static final class OptionsWrapper { + + static final OptionsWrapper POST_ALL_DOCS = + new OptionsWrapper<>(OptionsHandler.POST_ALL_DOCS, PostAllDocsOptions.Builder::new); + static final OptionsWrapper POST_DESIGN_DOCS = + new OptionsWrapper<>(OptionsHandler.POST_DESIGN_DOCS, PostDesignDocsOptions.Builder::new); + static final OptionsWrapper POST_FIND = + new OptionsWrapper<>(OptionsHandler.POST_FIND, PostFindOptions.Builder::new); + static final OptionsWrapper POST_PARTITION_ALL_DOCS = + new OptionsWrapper<>(OptionsHandler.POST_PARTITION_ALL_DOCS, + PostPartitionAllDocsOptions.Builder::new); + static final OptionsWrapper POST_PARTITION_FIND = + new OptionsWrapper<>(OptionsHandler.POST_PARTITION_FIND, + PostPartitionFindOptions.Builder::new); + static final OptionsWrapper POST_PARTITION_SEARCH = + new OptionsWrapper<>(OptionsHandler.POST_PARTITION_SEARCH, + PostPartitionSearchOptions.Builder::new); + static final OptionsWrapper POST_PARTITION_VIEW = + new OptionsWrapper<>(OptionsHandler.POST_PARTITION_VIEW, + PostPartitionViewOptions.Builder::new); + static final OptionsWrapper POST_SEARCH = + new OptionsWrapper<>(OptionsHandler.POST_SEARCH, PostSearchOptions.Builder::new); + static final OptionsWrapper POST_VIEW = + new OptionsWrapper<>(OptionsHandler.POST_VIEW, PostViewOptions.Builder::new); + + private final OptionsHandler handler; + private final Supplier builderSupplier; + + OptionsWrapper(OptionsHandler handler, Supplier builderSupplier) { + this.handler = handler; + this.builderSupplier = builderSupplier; + } + + OptionsProvider newProvider() { + return new OptionsProvider(this.handler, this.builderSupplier.get()); + } + + } + + static final class OptionsProvider { + + final OptionsHandler handler; + private final B builder; + + OptionsProvider(OptionsHandler handler, B builder) { + this.handler = handler; + this.builder = builder; + } + + void setRequiredOpts() throws Exception { + // database is always required + this.set("db", "testdb"); + + // For partitioned operations we need + // partitionKey + if (OptionsHandler.POST_PARTITION_ALL_DOCS.equals(handler) + || OptionsHandler.POST_PARTITION_FIND.equals(handler) + || OptionsHandler.POST_PARTITION_SEARCH.equals(handler) + || OptionsHandler.POST_PARTITION_VIEW.equals(handler)) { + this.set("partitionKey", "testpart"); + } + + // For find operations we need + // selector + if (OptionsHandler.POST_FIND.equals(handler) + || OptionsHandler.POST_PARTITION_FIND.equals(handler)) { + Map selector = Collections.emptyMap(); + this.set("selector", selector); + } + + // For search operations we need + // ddoc + // index + // query + if (OptionsHandler.POST_PARTITION_SEARCH.equals(handler) + || OptionsHandler.POST_SEARCH.equals(handler)) { + this.set("ddoc", "testddoc"); + this.set("index", "testsearchindex"); + this.set("query", "*:*"); + } + + // For view operations we need + // ddoc + // view + if (OptionsHandler.POST_PARTITION_VIEW.equals(handler) + || OptionsHandler.POST_VIEW.equals(handler)) { + this.set("ddoc", "testddoc"); + this.set("view", "testview"); + } + } + + void set(String name, Object... values) throws Exception { + List> argTypes = Arrays.asList(values).stream().map(a -> { + Class c = a.getClass(); + if (List.class.isAssignableFrom(c)) { + c = List.class; + } + if (Long.class.equals(c)) { + c = long.class; + } + if (Map.class.isAssignableFrom(c)) { + c = Map.class; + } + return c; + }).collect(Collectors.toList()); + Class[] argTypesArray = argTypes.toArray(new Class[argTypes.size()]); + Method method = this.builder.getClass().getMethod(name, argTypesArray); + method.invoke(this.builder, values); + } + + O build() { + return this.handler.optionsFromBuilder(this.builder); + } + + } + + List> allDocsOptions = List.of(OptionsWrapper.POST_ALL_DOCS, + OptionsWrapper.POST_DESIGN_DOCS, OptionsWrapper.POST_PARTITION_ALL_DOCS); + + List> viewOptions = + List.of(OptionsWrapper.POST_PARTITION_VIEW, OptionsWrapper.POST_VIEW); + + List> viewLikeOptions = + Stream.of(allDocsOptions, viewOptions).flatMap(List::stream).collect(Collectors.toList()); + + List> findOptions = + List.of(OptionsWrapper.POST_FIND, OptionsWrapper.POST_PARTITION_FIND); + + List> searchOptions = + List.of(OptionsWrapper.POST_PARTITION_SEARCH, OptionsWrapper.POST_SEARCH); + + List> allOptions = Stream.of(findOptions, searchOptions, viewLikeOptions) + .flatMap(List::stream).collect(Collectors.toList()); + + public Iterator getIteratorFor(List> options) { + return options.stream().map(w -> { + return new Object[] {w.newProvider()}; + }).iterator(); + } + + @DataProvider(name = "allDocsOptions") + public Iterator allDocsOptions() { + return getIteratorFor(allDocsOptions); + } + + @DataProvider(name = "allOptions") + public Iterator allOptions() { + return getIteratorFor(allOptions); + } + + @DataProvider(name = "findOptions") + public Iterator findOptions() { + return getIteratorFor(findOptions); + } + + @DataProvider(name = "searchOptions") + public Iterator searchOptions() { + return getIteratorFor(searchOptions); + } + + @DataProvider(name = "searchFacetOptions") + public Iterator facets() { + List options = new ArrayList<>(5); + options.add(new Object[] {"counts", Collections.singletonList("aTestFieldToCount")}); + options.add(new Object[] {"groupField", "testField"}); + options.add(new Object[] {"groupLimit", 6L}); + options.add(new Object[] {"groupSort", Collections.singletonList("aTestFieldToGroupSort")}); + options.add(new Object[] {"ranges", Collections.singletonMap("aTestFieldForRanges", + Map.of("low", "[0 TO 5}", "high", "[5 TO 10]"))}); + return options.iterator(); + } + + @DataProvider(name = "viewLikeOptions") + public Iterator viewLikeOptions() { + return getIteratorFor(viewLikeOptions); + } + + @DataProvider(name = "viewOptions") + public Iterator viewOptions() { + return getIteratorFor(viewLikeOptions); + } + + static class TestDesignDocsResultRow extends DocsResultRow { + TestDesignDocsResultRow(Integer i) { + this.id = "_design/testdoc" + String.valueOf(i); + this.key = this.id; + } + } + + static class TestDocsResultRow extends DocsResultRow { + TestDocsResultRow(Integer i) { + this.id = "testdoc" + String.valueOf(i); + this.key = this.id; + } + } + + static class TestAllDocsResult extends AllDocsResult { + TestAllDocsResult(List rows) { + this.rows = rows; + } + } + + static class TestFindDocument extends Document { + + TestFindDocument() { + super(); + } + + TestFindDocument(Integer i) { + super(); + this.id = "testdoc" + String.valueOf(i); + } + } + + static class TestFindResult extends FindResult { + TestFindResult(List rows) { + this.docs = rows; + } + } + + static class TestSearchResultRow extends SearchResultRow { + TestSearchResultRow(Integer i) { + this.id = "testdoc" + String.valueOf(i); + } + } + + static class TestSearchResult extends SearchResult { + TestSearchResult(List rows) { + this.rows = rows; + } + } + + static class TestViewResultRow extends ViewResultRow { + TestViewResultRow(Integer i) { + this.id = "testdoc" + String.valueOf(i); + this.key = i; + } + } + + static class TestViewResult extends ViewResult { + TestViewResult(List rows) { + this.rows = rows; + } + } + +} diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/PartitionAllDocsTest.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/PartitionAllDocsTest.java new file mode 100644 index 000000000..386c10fa2 --- /dev/null +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/PartitionAllDocsTest.java @@ -0,0 +1,41 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import com.ibm.cloud.cloudant.features.MockCloudant; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.OptionsWrapper; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.PageSupplierFactory; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestAllDocsResult; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestDocsResultRow; +import com.ibm.cloud.cloudant.v1.model.AllDocsResult; +import com.ibm.cloud.cloudant.v1.model.DocsResultRow; +import com.ibm.cloud.cloudant.v1.model.PostPartitionAllDocsOptions; + +public class PartitionAllDocsTest extends + PaginationOperationTest { + + PartitionAllDocsTest() { + super(new PageSupplierFactory(TestAllDocsResult::new, TestDocsResultRow::new, + true), OptionsWrapper.POST_PARTITION_ALL_DOCS.newProvider(), true); + } + + // New Pagination + @Override + protected Pagination makeNewPagination( + MockCloudant cloudant, PostPartitionAllDocsOptions options) { + return Pagination.newPagination(cloudant, options); + } + +} diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/PartitionFindTest.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/PartitionFindTest.java new file mode 100644 index 000000000..3b59c5371 --- /dev/null +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/PartitionFindTest.java @@ -0,0 +1,41 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import com.ibm.cloud.cloudant.features.MockCloudant; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.OptionsWrapper; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.PageSupplierFactory; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestFindDocument; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestFindResult; +import com.ibm.cloud.cloudant.v1.model.Document; +import com.ibm.cloud.cloudant.v1.model.FindResult; +import com.ibm.cloud.cloudant.v1.model.PostPartitionFindOptions; + +public class PartitionFindTest extends + PaginationOperationTest { + + PartitionFindTest() { + super(new PageSupplierFactory(TestFindResult::new, TestFindDocument::new, + false), OptionsWrapper.POST_PARTITION_FIND.newProvider(), false); + } + + // New Pagination + @Override + protected Pagination makeNewPagination( + MockCloudant cloudant, PostPartitionFindOptions options) { + return Pagination.newPagination(cloudant, options); + } + +} diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/PartitionSearchTest.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/PartitionSearchTest.java new file mode 100644 index 000000000..aef53605e --- /dev/null +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/PartitionSearchTest.java @@ -0,0 +1,41 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import com.ibm.cloud.cloudant.features.MockCloudant; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.OptionsWrapper; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.PageSupplierFactory; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestSearchResult; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestSearchResultRow; +import com.ibm.cloud.cloudant.v1.model.PostPartitionSearchOptions; +import com.ibm.cloud.cloudant.v1.model.SearchResult; +import com.ibm.cloud.cloudant.v1.model.SearchResultRow; + +public class PartitionSearchTest extends + PaginationOperationTest { + + PartitionSearchTest() { + super(new PageSupplierFactory(TestSearchResult::new, TestSearchResultRow::new, + false), OptionsWrapper.POST_PARTITION_SEARCH.newProvider(), false); + } + + // New Pagination + @Override + protected Pagination makeNewPagination( + MockCloudant cloudant, PostPartitionSearchOptions options) { + return Pagination.newPagination(cloudant, options); + } + +} diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/PartitionViewTest.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/PartitionViewTest.java new file mode 100644 index 000000000..d00449e8d --- /dev/null +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/PartitionViewTest.java @@ -0,0 +1,41 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import com.ibm.cloud.cloudant.features.MockCloudant; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.OptionsWrapper; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.PageSupplierFactory; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestViewResult; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestViewResultRow; +import com.ibm.cloud.cloudant.v1.model.PostPartitionViewOptions; +import com.ibm.cloud.cloudant.v1.model.ViewResult; +import com.ibm.cloud.cloudant.v1.model.ViewResultRow; + +public class PartitionViewTest extends + PaginationOperationTest { + + PartitionViewTest() { + super(new PageSupplierFactory(TestViewResult::new, TestViewResultRow::new, + true), OptionsWrapper.POST_PARTITION_VIEW.newProvider(), true); + } + + // New Pagination + @Override + protected Pagination makeNewPagination( + MockCloudant cloudant, PostPartitionViewOptions options) { + return Pagination.newPagination(cloudant, options); + } + +} diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/SearchTest.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/SearchTest.java new file mode 100644 index 000000000..c45df7bfc --- /dev/null +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/SearchTest.java @@ -0,0 +1,41 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import com.ibm.cloud.cloudant.features.MockCloudant; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.OptionsWrapper; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.PageSupplierFactory; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestSearchResult; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestSearchResultRow; +import com.ibm.cloud.cloudant.v1.model.PostSearchOptions; +import com.ibm.cloud.cloudant.v1.model.SearchResult; +import com.ibm.cloud.cloudant.v1.model.SearchResultRow; + +public class SearchTest extends + PaginationOperationTest { + + SearchTest() { + super(new PageSupplierFactory(TestSearchResult::new, TestSearchResultRow::new, + false), OptionsWrapper.POST_SEARCH.newProvider(), false); + } + + // New Pagination + @Override + protected Pagination makeNewPagination( + MockCloudant cloudant, PostSearchOptions options) { + return Pagination.newPagination(cloudant, options); + } + +} diff --git a/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/ViewTest.java b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/ViewTest.java new file mode 100644 index 000000000..a9c078a36 --- /dev/null +++ b/modules/cloudant/src/test/java/com/ibm/cloud/cloudant/features/pagination/ViewTest.java @@ -0,0 +1,41 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 com.ibm.cloud.cloudant.features.pagination; + +import com.ibm.cloud.cloudant.features.MockCloudant; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.OptionsWrapper; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.PageSupplierFactory; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestViewResult; +import com.ibm.cloud.cloudant.features.pagination.PaginationTestHelpers.TestViewResultRow; +import com.ibm.cloud.cloudant.v1.model.PostViewOptions; +import com.ibm.cloud.cloudant.v1.model.ViewResult; +import com.ibm.cloud.cloudant.v1.model.ViewResultRow; + +public class ViewTest extends + PaginationOperationTest { + + ViewTest() { + super(new PageSupplierFactory(TestViewResult::new, TestViewResultRow::new, + true), OptionsWrapper.POST_VIEW.newProvider(), true); + } + + // New Pagination + @Override + protected Pagination makeNewPagination( + MockCloudant cloudant, PostViewOptions options) { + return Pagination.newPagination(cloudant, options); + } + +} diff --git a/modules/examples/src/main/java/features/pagination/AllDocsPagination.java b/modules/examples/src/main/java/features/pagination/AllDocsPagination.java new file mode 100644 index 000000000..eb928b5e6 --- /dev/null +++ b/modules/examples/src/main/java/features/pagination/AllDocsPagination.java @@ -0,0 +1,95 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 features.pagination; + +import java.util.List; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.DocsResultRow; +import com.ibm.cloud.cloudant.v1.model.PostAllDocsOptions; +import com.ibm.cloud.cloudant.features.pagination.Pager; +import com.ibm.cloud.cloudant.features.pagination.Pagination; + +public class AllDocsPagination { + + public static void main(String[] args) { + + // Initialize service + Cloudant service = Cloudant.newInstance(); + + // Setup options + PostAllDocsOptions options = new PostAllDocsOptions.Builder() + .db("orders") // example database name + .limit(50) // limit option sets the page size + .startKey("abc") // start from example doc ID abc + .build(); + + // Create pagination + Pagination pagination = Pagination.newPagination(service, options); + // pagination can be reused without side-effects as a factory for iterables, streams or pagers + // options are fixed at pagination creation time + + // Option: iterate pages + // Ideal for using an enhanced for loop with each page. + // The Iterable returned from pages() is reusable in that + // calling iterator() returns a new iterator each time. + // The returned iterators, however, are single use. + for (List page : pagination.pages()) { + // Do something with page + } + + // Option: stream pages + // Ideal for lazy functional processing of pages and total page limits + pagination.pageStream() // a new stream of the pages + .limit(20) // use Java stream limit to get only the first 20 pages (different from 50 limit used for page size) + .forEach(page -> { // stream operations e.g. terminal forEach + // Do something with page + }); + + // Option: iterate rows + // Ideal for using an enhanced for loop with each row. + // The Iterable returned from rows() is reusable in that + // calling iterator() returns a new iterator each time. + // The returned iterators, however, are single use. + for (DocsResultRow row : pagination.rows()) { + // Do something with row + } + + // Option: stream rows + // Ideal for lazy functional processing of rows and total row limits + pagination.rowStream() // a new stream of the result rows + .limit(1000) // use Java stream limit to cap at 1000 rows (20 page requests of 50 rows each in this example) + .forEach(row -> { // stream operations e.g. terminal forEach + // Do something with row + }); + + // Option: use pager next page + // For retrieving one page at a time with a method call. + Pager pagePager = pagination.pager(); + if (pagePager.hasNext()) { + List page = pagePager.getNext(); + // Do something with page + } + + // Option: use pager all results + // For retrieving all result rows in a single list + // Note: all result rows may be very large! + // Preferably use streams/iterables instead of getAll for memory efficiency with large result sets. + Pager allPager = pagination.pager(); + List allRows = allPager.getAll(); + for (DocsResultRow row : allRows) { + // Do something with row + } + + } + +} diff --git a/modules/examples/src/main/java/features/pagination/DesignDocsPagination.java b/modules/examples/src/main/java/features/pagination/DesignDocsPagination.java new file mode 100644 index 000000000..b8e79fa88 --- /dev/null +++ b/modules/examples/src/main/java/features/pagination/DesignDocsPagination.java @@ -0,0 +1,94 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 features.pagination; + +import java.util.List; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.DocsResultRow; +import com.ibm.cloud.cloudant.v1.model.PostDesignDocsOptions; +import com.ibm.cloud.cloudant.features.pagination.Pager; +import com.ibm.cloud.cloudant.features.pagination.Pagination; + +public class DesignDocsPagination { + + public static void main(String[] args) { + + // Initialize service + Cloudant service = Cloudant.newInstance(); + + // Setup options + PostDesignDocsOptions options = new PostDesignDocsOptions.Builder() + .db("shoppers") // example database name + .limit(50) // limit option sets the page size + .build(); + + // Create pagination + Pagination pagination = Pagination.newPagination(service, options); + // pagination can be reused without side-effects as a factory for iterables, streams or pagers + // options are fixed at pagination creation time + + // Option: iterate pages + // Ideal for using an enhanced for loop with each page. + // The Iterable returned from pages() is reusable in that + // calling iterator() returns a new iterator each time. + // The returned iterators, however, are single use. + for (List page : pagination.pages()) { + // Do something with page + } + + // Option: stream pages + // Ideal for lazy functional processing of pages and total page limits + pagination.pageStream() // a new stream of the pages + .limit(20) // use Java stream limit to get only the first 20 pages (different from 50 limit used for page size) + .forEach(page -> { // stream operations e.g. terminal forEach + // Do something with page + }); + + // Option: iterate rows + // Ideal for using an enhanced for loop with each row. + // The Iterable returned from rows() is reusable in that + // calling iterator() returns a new iterator each time. + // The returned iterators, however, are single use. + for (DocsResultRow row : pagination.rows()) { + // Do something with row + } + + // Option: stream rows + // Ideal for lazy functional processing of rows and total row limits + pagination.rowStream() // a new stream of the rows + .limit(1000) // use Java stream limit to cap at 1000 rows (20 page requests of 50 rows each in this example) + .forEach(row -> { // stream operations e.g. terminal forEach + // Do something with row + }); + + // Option: use pager next page + // For retrieving one page at a time with a method call. + Pager pagePager = pagination.pager(); + if (pagePager.hasNext()) { + List page = pagePager.getNext(); + // Do something with page + } + + // Option: use pager all results + // For retrieving all result rows in a single list + // Note: all result rows may be very large! + // Preferably use streams/iterables instead of getAll for memory efficiency with large result sets. + Pager allPager = pagination.pager(); + List allRows = allPager.getAll(); + for (DocsResultRow row : allRows) { + // Do something with row + } + + } + +} diff --git a/modules/examples/src/main/java/features/pagination/FindPagination.java b/modules/examples/src/main/java/features/pagination/FindPagination.java new file mode 100644 index 000000000..9e1e5e863 --- /dev/null +++ b/modules/examples/src/main/java/features/pagination/FindPagination.java @@ -0,0 +1,98 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 features.pagination; + +import java.util.Collections; +import java.util.List; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.Document; +import com.ibm.cloud.cloudant.v1.model.PostFindOptions; +import com.ibm.cloud.cloudant.features.pagination.Pager; +import com.ibm.cloud.cloudant.features.pagination.Pagination; + +public class FindPagination { + + public static void main(String[] args) { + + // Initialize service + Cloudant service = Cloudant.newInstance(); + + // Setup options + PostFindOptions options = new PostFindOptions.Builder() + .db("shoppers") // Database name + .limit(50) // limit option sets the page size + .fields(List.of("_id", "type", "name", "email")) // return these fields + .selector(Collections.singletonMap("email_verified", "true")) // select docs with verified emails + .sort(Collections.singletonList(Collections.singletonMap("email", "desc"))) // sort descending by email + .build(); + + // Create pagination + Pagination pagination = Pagination.newPagination(service, options); + // pagination can be reused without side-effects as a factory for iterables, streams or pagers + // options are fixed at pagination creation time + + // Option: iterate pages + // Ideal for using an enhanced for loop with each page. + // The Iterable returned from pages() is reusable in that + // calling iterator() returns a new iterator each time. + // The returned iterators, however, are single use. + for (List page : pagination.pages()) { + // Do something with page + } + + // Option: stream pages + // Ideal for lazy functional processing of pages and total page limits + pagination.pageStream() // a new stream of the pages + .limit(20) // use Java stream limit to get only the first 20 pages (different from 50 limit used for page size) + .forEach(page -> { // stream operations e.g. terminal forEach + // Do something with page + }); + + // Option: iterate rows + // Ideal for using an enhanced for loop with each row. + // The Iterable returned from rows() is reusable in that + // calling iterator() returns a new iterator each time. + // The returned iterators, however, are single use. + for (Document row : pagination.rows()) { + // Do something with row + } + + // Option: stream rows + // Ideal for lazy functional processing of rows and total row limits + pagination.rowStream() // a new stream of the rows + .limit(1000) // use Java stream limit to cap at 1000 rows (20 page requests of 50 rows each in this example) + .forEach(row -> { // stream operations e.g. terminal forEach + // Do something with row + }); + + // Option: use pager next page + // For retrieving one page at a time with a method call. + Pager pagePager = pagination.pager(); + if (pagePager.hasNext()) { + List page = pagePager.getNext(); + // Do something with page + } + + // Option: use pager all results + // For retrieving all result rows in a single list + // Note: all result rows may be very large! + // Preferably use streams/iterables instead of getAll for memory efficiency with large result sets. + Pager allPager = pagination.pager(); + List allRows = allPager.getAll(); + for (Document row : allRows) { + // Do something with row + } + + } + +} diff --git a/modules/examples/src/main/java/features/pagination/PartitionAllDocsPagination.java b/modules/examples/src/main/java/features/pagination/PartitionAllDocsPagination.java new file mode 100644 index 000000000..39059bcb6 --- /dev/null +++ b/modules/examples/src/main/java/features/pagination/PartitionAllDocsPagination.java @@ -0,0 +1,95 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 features.pagination; + +import java.util.List; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.DocsResultRow; +import com.ibm.cloud.cloudant.v1.model.PostPartitionAllDocsOptions; +import com.ibm.cloud.cloudant.features.pagination.Pager; +import com.ibm.cloud.cloudant.features.pagination.Pagination; + +public class PartitionAllDocsPagination { + + public static void main(String[] args) { + + // Initialize service + Cloudant service = Cloudant.newInstance(); + + // Setup options + PostPartitionAllDocsOptions options = new PostPartitionAllDocsOptions.Builder() + .db("events") // example database name + .limit(50) // limit option sets the page size + .partitionKey("ns1HJS13AMkK") // query only this partition + .build(); + + // Create pagination + Pagination pagination = Pagination.newPagination(service, options); + // pagination can be reused without side-effects as a factory for iterables, streams or pagers + // options are fixed at pagination creation time + + // Option: iterate pages + // Ideal for using an enhanced for loop with each page. + // The Iterable returned from pages() is reusable in that + // calling iterator() returns a new iterator each time. + // The returned iterators, however, are single use. + for (List page : pagination.pages()) { + // Do something with page + } + + // Option: stream pages + // Ideal for lazy functional processing of pages and total page limits + pagination.pageStream() // a new stream of the pages + .limit(20) // use Java stream limit to get only the first 20 pages (different from 50 limit used for page size) + .forEach(page -> { // stream operations e.g. terminal forEach + // Do something with page + }); + + // Option: iterate rows + // Ideal for using an enhanced for loop with each row. + // The Iterable returned from rows() is reusable in that + // calling iterator() returns a new iterator each time. + // The returned iterators, however, are single use. + for (DocsResultRow row : pagination.rows()) { + // Do something with row + } + + // Option: stream rows + // Ideal for lazy functional processing of rows and total row limits + pagination.rowStream() // a new stream of the rows + .limit(1000) // use Java stream limit to cap at 1000 rows (20 page requests of 50 rows each in this example) + .forEach(row -> { // stream operations e.g. terminal forEach + // Do something with row + }); + + // Option: use pager next page + // For retrieving one page at a time with a method call. + Pager pagePager = pagination.pager(); + if (pagePager.hasNext()) { + List page = pagePager.getNext(); + // Do something with page + } + + // Option: use pager all results + // For retrieving all result rows in a single list + // Note: all result rows may be very large! + // Preferably use streams/iterables instead of getAll for memory efficiency with large result sets. + Pager allPager = pagination.pager(); + List allRows = allPager.getAll(); + for (DocsResultRow row : allRows) { + // Do something with row + } + + } + +} diff --git a/modules/examples/src/main/java/features/pagination/PartitionFindPagination.java b/modules/examples/src/main/java/features/pagination/PartitionFindPagination.java new file mode 100644 index 000000000..5d2cfc890 --- /dev/null +++ b/modules/examples/src/main/java/features/pagination/PartitionFindPagination.java @@ -0,0 +1,98 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 features.pagination; + +import java.util.Collections; +import java.util.List; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.Document; +import com.ibm.cloud.cloudant.v1.model.PostPartitionFindOptions; +import com.ibm.cloud.cloudant.features.pagination.Pager; +import com.ibm.cloud.cloudant.features.pagination.Pagination; + +public class PartitionFindPagination { + + public static void main(String[] args) { + + // Initialize service + Cloudant service = Cloudant.newInstance(); + + // Setup options + PostPartitionFindOptions options = new PostPartitionFindOptions.Builder() + .db("events") // example database name + .limit(50) // limit option sets the page size + .partitionKey("ns1HJS13AMkK") // query only this partition + .fields(List.of("productId", "eventType", "date")) // return these fields + .selector(Collections.singletonMap("userId", "abc123")) // select documents with "userId" field equal to "abc123" + .build(); + + // Create pagination + Pagination pagination = Pagination.newPagination(service, options); + // pagination can be reused without side-effects as a factory for iterables, streams or pagers + // options are fixed at pagination creation time + + // Option: iterate pages + // Ideal for using an enhanced for loop with each page. + // The Iterable returned from pages() is reusable in that + // calling iterator() returns a new iterator each time. + // The returned iterators, however, are single use. + for (List page : pagination.pages()) { + // Do something with page + } + + // Option: stream pages + // Ideal for lazy functional processing of pages and total page limits + pagination.pageStream() // a new stream of the pages + .limit(20) // use Java stream limit to get only the first 20 pages (different from 50 limit used for page size) + .forEach(page -> { // stream operations e.g. terminal forEach + // Do something with page + }); + + // Option: iterate rows + // Ideal for using an enhanced for loop with each row. + // The Iterable returned from rows() is reusable in that + // calling iterator() returns a new iterator each time. + // The returned iterators, however, are single use. + for (Document row : pagination.rows()) { + // Do something with row + } + + // Option: stream rows + // Ideal for lazy functional processing of rows and total row limits + pagination.rowStream() // a new stream of the rows + .limit(1000) // use Java stream limit to cap at 1000 rows (20 page requests of 50 rows each in this example) + .forEach(row -> { // stream operations e.g. terminal forEach + // Do something with row + }); + + // Option: use pager next page + // For retrieving one page at a time with a method call. + Pager pagePager = pagination.pager(); + if (pagePager.hasNext()) { + List page = pagePager.getNext(); + // Do something with page + } + + // Option: use pager all results + // For retrieving all result rows in a single list + // Note: all result rows may be very large! + // Preferably use streams/iterables instead of getAll for memory efficiency with large result sets. + Pager allPager = pagination.pager(); + List allRows = allPager.getAll(); + for (Document row : allRows) { + // Do something with row + } + + } + +} diff --git a/modules/examples/src/main/java/features/pagination/PartitionSearchPagination.java b/modules/examples/src/main/java/features/pagination/PartitionSearchPagination.java new file mode 100644 index 000000000..83c040758 --- /dev/null +++ b/modules/examples/src/main/java/features/pagination/PartitionSearchPagination.java @@ -0,0 +1,98 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 features.pagination; + +import java.util.List; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.SearchResultRow; +import com.ibm.cloud.cloudant.v1.model.PostPartitionSearchOptions; +import com.ibm.cloud.cloudant.features.pagination.Pager; +import com.ibm.cloud.cloudant.features.pagination.Pagination; + +public class PartitionSearchPagination { + + public static void main(String[] args) { + + // Initialize service + Cloudant service = Cloudant.newInstance(); + + // Setup options + PostPartitionSearchOptions options = new PostPartitionSearchOptions.Builder() + .db("events") // example database name + .limit(50) // limit option sets the page size + .partitionKey("ns1HJS13AMkK") // query only this partition + .ddoc("checkout") // use the checkout design document + .index("findByDate") // search in this index + .query("date:[2019-01-01T12:00:00.000Z TO 2019-01-31T12:00:00.000Z]") // Lucene search query + .build(); + + // Create pagination + Pagination pagination = Pagination.newPagination(service, options); + // pagination can be reused without side-effects as a factory for iterables, streams or pagers + // options are fixed at pagination creation time + + // Option: iterate pages + // Ideal for using an enhanced for loop with each page. + // The Iterable returned from pages() is reusable in that + // calling iterator() returns a new iterator each time. + // The returned iterators, however, are single use. + for (List page : pagination.pages()) { + // Do something with page + } + + // Option: stream pages + // Ideal for lazy functional processing of pages and total page limits + pagination.pageStream() // a new stream of the pages + .limit(20) // use Java stream limit to get only the first 20 pages (different from 50 limit used for page size) + .forEach(page -> { // stream operations e.g. terminal forEach + // Do something with page + }); + + // Option: iterate rows + // Ideal for using an enhanced for loop with each row. + // The Iterable returned from rows() is reusable in that + // calling iterator() returns a new iterator each time. + // The returned iterators, however, are single use. + for (SearchResultRow row : pagination.rows()) { + // Do something with row + } + + // Option: stream rows + // Ideal for lazy functional processing of rows and total row limits + pagination.rowStream() // a new stream of the rows + .limit(1000) // use Java stream limit to cap at 1000 rows (20 page requests of 50 rows each in this example) + .forEach(row -> { // stream operations e.g. terminal forEach + // Do something with row + }); + + // Option: use pager next page + // For retrieving one page at a time with a method call. + Pager pagePager = pagination.pager(); + if (pagePager.hasNext()) { + List page = pagePager.getNext(); + // Do something with page + } + + // Option: use pager all results + // For retrieving all result rows in a single list + // Note: all result rows may be very large! + // Preferably use streams/iterables instead of getAll for memory efficiency with large result sets. + Pager allPager = pagination.pager(); + List allRows = allPager.getAll(); + for (SearchResultRow row : allRows) { + // Do something with row + } + + } + +} diff --git a/modules/examples/src/main/java/features/pagination/PartitionViewPagination.java b/modules/examples/src/main/java/features/pagination/PartitionViewPagination.java new file mode 100644 index 000000000..92a040f18 --- /dev/null +++ b/modules/examples/src/main/java/features/pagination/PartitionViewPagination.java @@ -0,0 +1,97 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 features.pagination; + +import java.util.List; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.ViewResultRow; +import com.ibm.cloud.cloudant.v1.model.PostPartitionViewOptions; +import com.ibm.cloud.cloudant.features.pagination.Pager; +import com.ibm.cloud.cloudant.features.pagination.Pagination; + +public class PartitionViewPagination { + + public static void main(String[] args) { + + // Initialize service + Cloudant service = Cloudant.newInstance(); + + // Setup options + PostPartitionViewOptions options = new PostPartitionViewOptions.Builder() + .db("events") // example database name + .limit(50) // limit option sets the page size + .partitionKey("ns1HJS13AMkK") // query only this partition + .ddoc("checkout") // use the checkout design document + .view("byProductId") // the view to use + .build(); + + // Create pagination + Pagination pagination = Pagination.newPagination(service, options); + // pagination can be reused without side-effects as a factory for iterables, streams or pagers + // options are fixed at pagination creation time + + // Option: iterate pages + // Ideal for using an enhanced for loop with each page. + // The Iterable returned from pages() is reusable in that + // calling iterator() returns a new iterator each time. + // The returned iterators, however, are single use. + for (List page : pagination.pages()) { + // Do something with page + } + + // Option: stream pages + // Ideal for lazy functional processing of pages and total page limits + pagination.pageStream() // a new stream of the pages + .limit(20) // use Java stream limit to get only the first 20 pages (different from 50 limit used for page size) + .forEach(page -> { // stream operations e.g. terminal forEach + // Do something with page + }); + + // Option: iterate rows + // Ideal for using an enhanced for loop with each row. + // The Iterable returned from rows() is reusable in that + // calling iterator() returns a new iterator each time. + // The returned iterators, however, are single use. + for (ViewResultRow row : pagination.rows()) { + // Do something with row + } + + // Option: stream rows + // Ideal for lazy functional processing of rows and total row limits + pagination.rowStream() // a new stream of the rows + .limit(1000) // use Java stream limit to cap at 1000 rows (20 page requests of 50 rows each in this example) + .forEach(row -> { // stream operations e.g. terminal forEach + // Do something with row + }); + + // Option: use pager next page + // For retrieving one page at a time with a method call. + Pager pagePager = pagination.pager(); + if (pagePager.hasNext()) { + List page = pagePager.getNext(); + // Do something with page + } + + // Option: use pager all results + // For retrieving all result rows in a single list + // Note: all result rows may be very large! + // Preferably use streams/iterables instead of getAll for memory efficiency with large result sets. + Pager allPager = pagination.pager(); + List allRows = allPager.getAll(); + for (ViewResultRow row : allRows) { + // Do something with row + } + + } + +} diff --git a/modules/examples/src/main/java/features/pagination/SearchPagination.java b/modules/examples/src/main/java/features/pagination/SearchPagination.java new file mode 100644 index 000000000..d905984b7 --- /dev/null +++ b/modules/examples/src/main/java/features/pagination/SearchPagination.java @@ -0,0 +1,97 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 features.pagination; + +import java.util.List; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.SearchResultRow; +import com.ibm.cloud.cloudant.v1.model.PostSearchOptions; +import com.ibm.cloud.cloudant.features.pagination.Pager; +import com.ibm.cloud.cloudant.features.pagination.Pagination; + +public class SearchPagination { + + public static void main(String[] args) { + + // Initialize service + Cloudant service = Cloudant.newInstance(); + + // Setup options + PostSearchOptions options = new PostSearchOptions.Builder() + .db("shoppers") // example database name + .limit(50) // limit option sets the page size + .ddoc("allUsers") // use the allUsers design document + .index("activeUsers") // search in this index + .query("name:Jane* AND active:True") // Lucene search query + .build(); + + // Create pagination + Pagination pagination = Pagination.newPagination(service, options); + // pagination can be reused without side-effects as a factory for iterables, streams or pagers + // options are fixed at pagination creation time + + // Option: iterate pages + // Ideal for using an enhanced for loop with each page. + // The Iterable returned from pages() is reusable in that + // calling iterator() returns a new iterator each time. + // The returned iterators, however, are single use. + for (List page : pagination.pages()) { + // Do something with page + } + + // Option: stream pages + // Ideal for lazy functional processing of pages and total page limits + pagination.pageStream() // a new stream of the pages + .limit(20) // use Java stream limit to get only the first 20 pages (different from 50 limit used for page size) + .forEach(page -> { // stream operations e.g. terminal forEach + // Do something with page + }); + + // Option: iterate rows + // Ideal for using an enhanced for loop with each row. + // The Iterable returned from rows() is reusable in that + // calling iterator() returns a new iterator each time. + // The returned iterators, however, are single use. + for (SearchResultRow row : pagination.rows()) { + // Do something with row + } + + // Option: stream rows + // Ideal for lazy functional processing of rows and total row limits + pagination.rowStream() // a new stream of the rows + .limit(1000) // use Java stream limit to cap at 1000 rows (20 page requests of 50 rows each in this example) + .forEach(row -> { // stream operations e.g. terminal forEach + // Do something with row + }); + + // Option: use pager next page + // For retrieving one page at a time with a method call. + Pager pagePager = pagination.pager(); + if (pagePager.hasNext()) { + List page = pagePager.getNext(); + // Do something with page + } + + // Option: use pager all results + // For retrieving all result rows in a single list + // Note: all result rows may be very large! + // Preferably use streams/iterables instead of getAll for memory efficiency with large result sets. + Pager allPager = pagination.pager(); + List allRows = allPager.getAll(); + for (SearchResultRow row : allRows) { + // Do something with row + } + + } + +} diff --git a/modules/examples/src/main/java/features/pagination/ViewPagination.java b/modules/examples/src/main/java/features/pagination/ViewPagination.java new file mode 100644 index 000000000..e231d657d --- /dev/null +++ b/modules/examples/src/main/java/features/pagination/ViewPagination.java @@ -0,0 +1,96 @@ +/** + * © Copyright IBM Corporation 2025. All Rights Reserved. + * + * 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 + * + * http://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 features.pagination; + +import java.util.List; +import com.ibm.cloud.cloudant.v1.Cloudant; +import com.ibm.cloud.cloudant.v1.model.ViewResultRow; +import com.ibm.cloud.cloudant.v1.model.PostViewOptions; +import com.ibm.cloud.cloudant.features.pagination.Pager; +import com.ibm.cloud.cloudant.features.pagination.Pagination; + +public class ViewPagination { + + public static void main(String[] args) { + + // Initialize service + Cloudant service = Cloudant.newInstance(); + + // Setup options + PostViewOptions options = new PostViewOptions.Builder() + .db("shoppers") // example database name + .limit(50) // limit option sets the page size + .ddoc("allUsers") // use the allUsers design document + .view("getVerifiedEmails") // the view to use + .build(); + + // Create pagination + Pagination pagination = Pagination.newPagination(service, options); + // pagination can be reused without side-effects as a factory for iterables, streams or pagers + // options are fixed at pagination creation time + + // Option: iterate pages + // Ideal for using an enhanced for loop with each page. + // The Iterable returned from pages() is reusable in that + // calling iterator() returns a new iterator each time. + // The returned iterators, however, are single use. + for (List page : pagination.pages()) { + // Do something with page + } + + // Option: stream pages + // Ideal for lazy functional processing of pages and total page limits + pagination.pageStream() // a new stream of the pages + .limit(20) // use Java stream limit to get only the first 20 pages (different from 50 limit used for page size) + .forEach(page -> { // stream operations e.g. terminal forEach + // Do something with page + }); + + // Option: iterate rows + // Ideal for using an enhanced for loop with each row. + // The Iterable returned from rows() is reusable in that + // calling iterator() returns a new iterator each time. + // The returned iterators, however, are single use. + for (ViewResultRow row : pagination.rows()) { + // Do something with row + } + + // Option: stream rows + // Ideal for lazy functional processing of rows and total row limits + pagination.rowStream() // a new stream of the rows + .limit(1000) // use Java stream limit to cap at 1000 rows (20 page requests of 50 rows each in this example) + .forEach(row -> { // stream operations e.g. terminal forEach + // Do something with row + }); + + // Option: use pager next page + // For retrieving one page at a time with a method call. + Pager pagePager = pagination.pager(); + if (pagePager.hasNext()) { + List page = pagePager.getNext(); + // Do something with page + } + + // Option: use pager all results + // For retrieving all result rows in a single list + // Note: all result rows may be very large! + // Preferably use streams/iterables instead of getAll for memory efficiency with large result sets. + Pager allPager = pagination.pager(); + List allRows = allPager.getAll(); + for (ViewResultRow row : allRows) { + // Do something with row + } + + } + +} diff --git a/pom.xml b/pom.xml index 9ada9ab54..3e3c6d531 100644 --- a/pom.xml +++ b/pom.xml @@ -298,6 +298,19 @@ 1.8 1.8 + + + test-compile + process-test-sources + + testCompile + + + 11 + 11 + + + org.apache.maven.plugins