Skip to content

Commit 9fe1db8

Browse files
authored
Add null check for paginator hasNextPage (#5139)
* Add null check for paginator hasNextPage * Add codegen tests for more_results * Add hasNextPage null check for customized paginators * Add unit test for null more_results field
1 parent 39e3a5b commit 9fe1db8

File tree

11 files changed

+420
-3
lines changed

11 files changed

+420
-3
lines changed

codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/PaginatorsClassSpec.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ protected MemberModel memberModelForResponseMember(String input) {
169169
protected CodeBlock hasNextPageMethodBody() {
170170
if (paginatorDefinition.getMoreResults() != null) {
171171
return CodeBlock.builder()
172-
.add("return $N.$L.booleanValue()",
172+
.add("return $1N.$2L != null && $1N.$2L.booleanValue()",
173173
PREVIOUS_PAGE_METHOD_ARGUMENT,
174174
fluentGetterMethodForResponseMember(paginatorDefinition.getMoreResults()))
175175
.build();

codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/customizations/SameTokenAsyncResponseClassSpec.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ protected TypeSpec.Builder nextPageFetcherClass() {
7676
protected CodeBlock hasNextPageMethodBody() {
7777
if (paginatorDefinition.getMoreResults() != null) {
7878
return CodeBlock.builder()
79-
.add("return $N.$L.booleanValue()",
79+
.add("return $1N.$2L != null && $1N.$2L.booleanValue()",
8080
PREVIOUS_PAGE_METHOD_ARGUMENT,
8181
fluentGetterMethodForResponseMember(paginatorDefinition.getMoreResults()))
8282
.build();

codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/customizations/SameTokenSyncResponseClassSpec.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ protected TypeSpec.Builder nextPageFetcherClass() {
7474
protected CodeBlock hasNextPageMethodBody() {
7575
if (paginatorDefinition.getMoreResults() != null) {
7676
return CodeBlock.builder()
77-
.add("return $N.$L.booleanValue()",
77+
.add("return $1N.$2L != null && $1N.$2L.booleanValue()",
7878
PREVIOUS_PAGE_METHOD_ARGUMENT,
7979
fluentGetterMethodForResponseMember(paginatorDefinition.getMoreResults()))
8080
.build();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package software.amazon.awssdk.services.jsonprotocoltests.paginators;
2+
3+
import java.util.Collections;
4+
import java.util.Iterator;
5+
import java.util.function.Function;
6+
import software.amazon.awssdk.annotations.Generated;
7+
import software.amazon.awssdk.core.pagination.sync.PaginatedItemsIterable;
8+
import software.amazon.awssdk.core.pagination.sync.PaginatedResponsesIterator;
9+
import software.amazon.awssdk.core.pagination.sync.SdkIterable;
10+
import software.amazon.awssdk.core.pagination.sync.SyncPageFetcher;
11+
import software.amazon.awssdk.services.jsonprotocoltests.JsonProtocolTestsClient;
12+
import software.amazon.awssdk.services.jsonprotocoltests.internal.UserAgentUtils;
13+
import software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyAndMoreResultsRequest;
14+
import software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyAndMoreResultsResponse;
15+
import software.amazon.awssdk.services.jsonprotocoltests.model.SimpleStruct;
16+
17+
/**
18+
* <p>
19+
* Represents the output for the
20+
* {@link software.amazon.awssdk.services.jsonprotocoltests.JsonProtocolTestsClient#paginatedOperationWithResultKeyAndMoreResultsPaginator(software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyAndMoreResultsRequest)}
21+
* operation which is a paginated operation. This class is an iterable of
22+
* {@link software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyAndMoreResultsResponse}
23+
* that can be used to iterate through all the response pages of the operation.
24+
* </p>
25+
* <p>
26+
* When the operation is called, an instance of this class is returned. At this point, no service calls are made yet and
27+
* so there is no guarantee that the request is valid. As you iterate through the iterable, SDK will start lazily
28+
* loading response pages by making service calls until there are no pages left or your iteration stops. If there are
29+
* errors in your request, you will see the failures only after you start iterating through the iterable.
30+
* </p>
31+
*
32+
* <p>
33+
* The following are few ways to iterate through the response pages:
34+
* </p>
35+
* 1) Using a Stream
36+
*
37+
* <pre>
38+
* {@code
39+
* software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithResultKeyAndMoreResultsIterable responses = client.paginatedOperationWithResultKeyAndMoreResultsPaginator(request);
40+
* responses.stream().forEach(....);
41+
* }
42+
* </pre>
43+
*
44+
* 2) Using For loop
45+
*
46+
* <pre>
47+
* {
48+
* &#064;code
49+
* software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithResultKeyAndMoreResultsIterable responses = client
50+
* .paginatedOperationWithResultKeyAndMoreResultsPaginator(request);
51+
* for (software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyAndMoreResultsResponse response : responses) {
52+
* // do something;
53+
* }
54+
* }
55+
* </pre>
56+
*
57+
* 3) Use iterator directly
58+
*
59+
* <pre>
60+
* {@code
61+
* software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithResultKeyAndMoreResultsIterable responses = client.paginatedOperationWithResultKeyAndMoreResultsPaginator(request);
62+
* responses.iterator().forEachRemaining(....);
63+
* }
64+
* </pre>
65+
* <p>
66+
* <b>Please notice that the configuration of MaxResults won't limit the number of results you get with the paginator.
67+
* It only limits the number of results in each page.</b>
68+
* </p>
69+
* <p>
70+
* <b>Note: If you prefer to have control on service calls, use the
71+
* {@link #paginatedOperationWithResultKeyAndMoreResults(software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyAndMoreResultsRequest)}
72+
* operation.</b>
73+
* </p>
74+
*/
75+
@Generated("software.amazon.awssdk:codegen")
76+
public class PaginatedOperationWithResultKeyAndMoreResultsIterable implements
77+
SdkIterable<PaginatedOperationWithResultKeyAndMoreResultsResponse> {
78+
private final JsonProtocolTestsClient client;
79+
80+
private final PaginatedOperationWithResultKeyAndMoreResultsRequest firstRequest;
81+
82+
private final SyncPageFetcher nextPageFetcher;
83+
84+
public PaginatedOperationWithResultKeyAndMoreResultsIterable(JsonProtocolTestsClient client,
85+
PaginatedOperationWithResultKeyAndMoreResultsRequest firstRequest) {
86+
this.client = client;
87+
this.firstRequest = UserAgentUtils.applyPaginatorUserAgent(firstRequest);
88+
this.nextPageFetcher = new PaginatedOperationWithResultKeyAndMoreResultsResponseFetcher();
89+
}
90+
91+
@Override
92+
public Iterator<PaginatedOperationWithResultKeyAndMoreResultsResponse> iterator() {
93+
return PaginatedResponsesIterator.builder().nextPageFetcher(nextPageFetcher).build();
94+
}
95+
96+
/**
97+
* Returns an iterable to iterate through the paginated
98+
* {@link PaginatedOperationWithResultKeyAndMoreResultsResponse#items()} member. The returned iterable is used to
99+
* iterate through the results across all response pages and not a single page.
100+
*
101+
* This method is useful if you are interested in iterating over the paginated member in the response pages instead
102+
* of the top level pages. Similar to iteration over pages, this method internally makes service calls to get the
103+
* next list of results until the iteration stops or there are no more results.
104+
*/
105+
public final SdkIterable<SimpleStruct> items() {
106+
Function<PaginatedOperationWithResultKeyAndMoreResultsResponse, Iterator<SimpleStruct>> getIterator = response -> {
107+
if (response != null && response.items() != null) {
108+
return response.items().iterator();
109+
}
110+
return Collections.emptyIterator();
111+
};
112+
return PaginatedItemsIterable.<PaginatedOperationWithResultKeyAndMoreResultsResponse, SimpleStruct> builder()
113+
.pagesIterable(this).itemIteratorFunction(getIterator).build();
114+
}
115+
116+
private class PaginatedOperationWithResultKeyAndMoreResultsResponseFetcher implements
117+
SyncPageFetcher<PaginatedOperationWithResultKeyAndMoreResultsResponse> {
118+
@Override
119+
public boolean hasNextPage(PaginatedOperationWithResultKeyAndMoreResultsResponse previousPage) {
120+
return previousPage.truncated() != null && previousPage.truncated().booleanValue();
121+
}
122+
123+
@Override
124+
public PaginatedOperationWithResultKeyAndMoreResultsResponse nextPage(
125+
PaginatedOperationWithResultKeyAndMoreResultsResponse previousPage) {
126+
if (previousPage == null) {
127+
return client.paginatedOperationWithResultKeyAndMoreResults(firstRequest);
128+
}
129+
return client.paginatedOperationWithResultKeyAndMoreResults(firstRequest.toBuilder()
130+
.nextToken(previousPage.nextToken()).build());
131+
}
132+
}
133+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package software.amazon.awssdk.services.jsonprotocoltests.paginators;
2+
3+
import java.util.Collections;
4+
import java.util.Iterator;
5+
import java.util.concurrent.CompletableFuture;
6+
import java.util.function.Function;
7+
import org.reactivestreams.Subscriber;
8+
import software.amazon.awssdk.annotations.Generated;
9+
import software.amazon.awssdk.core.async.SdkPublisher;
10+
import software.amazon.awssdk.core.pagination.async.AsyncPageFetcher;
11+
import software.amazon.awssdk.core.pagination.async.PaginatedItemsPublisher;
12+
import software.amazon.awssdk.core.pagination.async.ResponsesSubscription;
13+
import software.amazon.awssdk.services.jsonprotocoltests.JsonProtocolTestsAsyncClient;
14+
import software.amazon.awssdk.services.jsonprotocoltests.internal.UserAgentUtils;
15+
import software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyAndMoreResultsRequest;
16+
import software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyAndMoreResultsResponse;
17+
import software.amazon.awssdk.services.jsonprotocoltests.model.SimpleStruct;
18+
19+
/**
20+
* <p>
21+
* Represents the output for the
22+
* {@link software.amazon.awssdk.services.jsonprotocoltests.JsonProtocolTestsAsyncClient#paginatedOperationWithResultKeyAndMoreResultsPaginator(software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyAndMoreResultsRequest)}
23+
* operation which is a paginated operation. This class is a type of {@link org.reactivestreams.Publisher} which can be
24+
* used to provide a sequence of
25+
* {@link software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyAndMoreResultsResponse}
26+
* response pages as per demand from the subscriber.
27+
* </p>
28+
* <p>
29+
* When the operation is called, an instance of this class is returned. At this point, no service calls are made yet and
30+
* so there is no guarantee that the request is valid. If there are errors in your request, you will see the failures
31+
* only after you start streaming the data. The subscribe method should be called as a request to start streaming data.
32+
* For more info, see {@link org.reactivestreams.Publisher#subscribe(org.reactivestreams.Subscriber)}. Each call to the
33+
* subscribe method will result in a new {@link org.reactivestreams.Subscription} i.e., a new contract to stream data
34+
* from the starting request.
35+
* </p>
36+
*
37+
* <p>
38+
* The following are few ways to use the response class:
39+
* </p>
40+
* 1) Using the subscribe helper method
41+
*
42+
* <pre>
43+
* {@code
44+
* software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithResultKeyAndMoreResultsPublisher publisher = client.paginatedOperationWithResultKeyAndMoreResultsPaginator(request);
45+
* CompletableFuture<Void> future = publisher.subscribe(res -> { // Do something with the response });
46+
* future.get();
47+
* }
48+
* </pre>
49+
*
50+
* 2) Using a custom subscriber
51+
*
52+
* <pre>
53+
* {@code
54+
* software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithResultKeyAndMoreResultsPublisher publisher = client.paginatedOperationWithResultKeyAndMoreResultsPaginator(request);
55+
* publisher.subscribe(new Subscriber<software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyAndMoreResultsResponse>() {
56+
*
57+
* public void onSubscribe(org.reactivestreams.Subscriber subscription) { //... };
58+
*
59+
*
60+
* public void onNext(software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyAndMoreResultsResponse response) { //... };
61+
* });}
62+
* </pre>
63+
*
64+
* As the response is a publisher, it can work well with third party reactive streams implementations like RxJava2.
65+
* <p>
66+
* <b>Please notice that the configuration of MaxResults won't limit the number of results you get with the paginator.
67+
* It only limits the number of results in each page.</b>
68+
* </p>
69+
* <p>
70+
* <b>Note: If you prefer to have control on service calls, use the
71+
* {@link #paginatedOperationWithResultKeyAndMoreResults(software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyAndMoreResultsRequest)}
72+
* operation.</b>
73+
* </p>
74+
*/
75+
@Generated("software.amazon.awssdk:codegen")
76+
public class PaginatedOperationWithResultKeyAndMoreResultsPublisher implements
77+
SdkPublisher<PaginatedOperationWithResultKeyAndMoreResultsResponse> {
78+
private final JsonProtocolTestsAsyncClient client;
79+
80+
private final PaginatedOperationWithResultKeyAndMoreResultsRequest firstRequest;
81+
82+
private final AsyncPageFetcher nextPageFetcher;
83+
84+
private boolean isLastPage;
85+
86+
public PaginatedOperationWithResultKeyAndMoreResultsPublisher(JsonProtocolTestsAsyncClient client,
87+
PaginatedOperationWithResultKeyAndMoreResultsRequest firstRequest) {
88+
this(client, firstRequest, false);
89+
}
90+
91+
private PaginatedOperationWithResultKeyAndMoreResultsPublisher(JsonProtocolTestsAsyncClient client,
92+
PaginatedOperationWithResultKeyAndMoreResultsRequest firstRequest, boolean isLastPage) {
93+
this.client = client;
94+
this.firstRequest = UserAgentUtils.applyPaginatorUserAgent(firstRequest);
95+
this.isLastPage = isLastPage;
96+
this.nextPageFetcher = new PaginatedOperationWithResultKeyAndMoreResultsResponseFetcher();
97+
}
98+
99+
@Override
100+
public void subscribe(Subscriber<? super PaginatedOperationWithResultKeyAndMoreResultsResponse> subscriber) {
101+
subscriber.onSubscribe(ResponsesSubscription.builder().subscriber(subscriber).nextPageFetcher(nextPageFetcher).build());
102+
}
103+
104+
/**
105+
* Returns a publisher that can be used to get a stream of data. You need to subscribe to the publisher to request
106+
* the stream of data. The publisher has a helper forEach method that takes in a {@link java.util.function.Consumer}
107+
* and then applies that consumer to each response returned by the service.
108+
*/
109+
public final SdkPublisher<SimpleStruct> items() {
110+
Function<PaginatedOperationWithResultKeyAndMoreResultsResponse, Iterator<SimpleStruct>> getIterator = response -> {
111+
if (response != null && response.items() != null) {
112+
return response.items().iterator();
113+
}
114+
return Collections.emptyIterator();
115+
};
116+
return PaginatedItemsPublisher.builder()
117+
.nextPageFetcher(new PaginatedOperationWithResultKeyAndMoreResultsResponseFetcher())
118+
.iteratorFunction(getIterator).isLastPage(isLastPage).build();
119+
}
120+
121+
private class PaginatedOperationWithResultKeyAndMoreResultsResponseFetcher implements
122+
AsyncPageFetcher<PaginatedOperationWithResultKeyAndMoreResultsResponse> {
123+
@Override
124+
public boolean hasNextPage(final PaginatedOperationWithResultKeyAndMoreResultsResponse previousPage) {
125+
return previousPage.truncated() != null && previousPage.truncated().booleanValue();
126+
}
127+
128+
@Override
129+
public CompletableFuture<PaginatedOperationWithResultKeyAndMoreResultsResponse> nextPage(
130+
final PaginatedOperationWithResultKeyAndMoreResultsResponse previousPage) {
131+
if (previousPage == null) {
132+
return client.paginatedOperationWithResultKeyAndMoreResults(firstRequest);
133+
}
134+
return client.paginatedOperationWithResultKeyAndMoreResults(firstRequest.toBuilder()
135+
.nextToken(previousPage.nextToken()).build());
136+
}
137+
}
138+
}

codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/paginators.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
"limit_key": "MaxResults",
77
"result_key": "Items"
88
},
9+
"PaginatedOperationWithResultKeyAndMoreResults": {
10+
"input_token": "NextToken",
11+
"output_token": "NextToken",
12+
"more_results": "Truncated",
13+
"limit_key": "MaxResults",
14+
"result_key": "Items"
15+
},
916
"PaginatedOperationWithoutResultKey": {
1017
"input_token": "NextToken",
1118
"output_token": "NextToken",

codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/service-2.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,20 @@
7272
},
7373
"documentation": "Some paginated operation with result_key in paginators.json file"
7474
},
75+
"PaginatedOperationWithResultKeyAndMoreResults": {
76+
"name": "PaginatedOperationWithResultKeyAndMoreResults",
77+
"http": {
78+
"method": "POST",
79+
"requestUri": "/"
80+
},
81+
"input": {
82+
"shape": "PaginatedOperationWithResultKeyAndMoreResultsRequest"
83+
},
84+
"output": {
85+
"shape": "PaginatedOperationWithResultKeyAndMoreResultsResponse"
86+
},
87+
"documentation": "Some paginated operation with more_results in paginators.json file"
88+
},
7589
"PaginatedOperationWithoutResultKey": {
7690
"name": "PaginatedOperationWithoutResultKey",
7791
"http": {
@@ -336,6 +350,40 @@
336350
},
337351
"documentation": "<p>Response type of a single page</p>"
338352
},
353+
"PaginatedOperationWithResultKeyAndMoreResultsRequest": {
354+
"type": "structure",
355+
"required": [
356+
"NextToken"
357+
],
358+
"members": {
359+
"NextToken": {
360+
"shape": "String",
361+
"documentation": "<p>Token for the next set of results</p>"
362+
},
363+
"MaxResults": {
364+
"shape": "String",
365+
"documentation": "<p>Maximum number of results in a single page</p>"
366+
}
367+
}
368+
},
369+
"PaginatedOperationWithResultKeyAndMoreResultsResponse": {
370+
"type": "structure",
371+
"members": {
372+
"NextToken": {
373+
"shape": "String",
374+
"documentation": "<p>Token for the next set of results</p>"
375+
},
376+
"Items": {
377+
"shape": "ListOfSimpleStructs",
378+
"documentation": "<p>Maximum number of results in a single page</p>"
379+
},
380+
"Truncated":{
381+
"shape":"Boolean",
382+
"documentation":"<p>A flag that indicates whether there are more results in the list. When this value is true, the list in this response is truncated.</p>"
383+
}
384+
},
385+
"documentation": "<p>Response type of multiple pages</p>"
386+
},
339387
"PaginatedOperationWithoutResultKeyRequest": {
340388
"type": "structure",
341389
"required": [

test/codegen-generated-classes-test/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,11 @@
263263
<version>${awsjavasdk.version}</version>
264264
<scope>test</scope>
265265
</dependency>
266+
<dependency>
267+
<groupId>org.mockito</groupId>
268+
<artifactId>mockito-junit-jupiter</artifactId>
269+
<scope>test</scope>
270+
</dependency>
266271
</dependencies>
267272

268273
<build>

0 commit comments

Comments
 (0)