From 3b12e2432aa3d6cff4d5e35bb85849fbae5ec4f9 Mon Sep 17 00:00:00 2001 From: Varun Nandi Date: Wed, 6 Dec 2017 12:53:14 -0800 Subject: [PATCH] Support Async pagination --- .../amazon/awssdk/spotbugs-suppressions.xml | 7 + .../docs/AsyncOperationDocProvider.java | 48 +- .../codegen/docs/DocumentationBuilder.java | 2 + .../awssdk/codegen/docs/PaginationDocs.java | 127 ++++- .../docs/SyncOperationDocProvider.java | 11 +- .../tasks/PaginatorsGeneratorTasks.java | 23 +- .../awssdk/codegen/poet/PoetExtensions.java | 15 +- .../codegen/poet/client/AsyncClientClass.java | 9 + .../poet/client/AsyncClientInterface.java | 59 +++ .../codegen/poet/client/SyncClientClass.java | 2 +- .../poet/client/SyncClientInterface.java | 4 +- .../paginators/AsyncResponseClassSpec.java | 257 ++++++++++ .../PaginatorResponseClassSpec.java | 459 ------------------ .../poet/paginators/PaginatorsClassSpec.java | 365 ++++++++++++++ .../paginators/SyncResponseClassSpec.java | 226 +++++++++ .../awssdk/codegen/utils/PaginatorUtils.java | 6 +- ...va => PaginatedResponseClassSpecTest.java} | 30 +- .../poet/client/c2j/json/customization.config | 3 +- .../poet/client/c2j/json/service-2.json | 3 - .../poet/client/test-async-client-class.java | 254 ++++++++-- .../test-json-async-client-interface.java | 274 ++++++++++- .../poet/client/test-json-client-class.java | 160 +++--- .../client/test-json-client-interface.java | 185 +++++-- ...inatedOperationWithResultKeyIterable.java} | 54 ++- ...inatedOperationWithResultKeyPublisher.java | 150 ++++++ ...tedOperationWithoutResultKeyIterable.java} | 44 +- ...tedOperationWithoutResultKeyPublisher.java | 132 +++++ .../poet/paginators/customization.config | 3 +- .../codegen/poet/paginators/service-2.json | 3 - .../pagination/PaginatedItemsIterable.java | 13 +- .../PaginatedResponsesIterator.java | 4 +- ...tPageFetcher.java => SyncPageFetcher.java} | 2 +- .../pagination/async/AsyncPageFetcher.java | 44 ++ .../pagination/async/EmptySubscription.java | 63 +++ .../pagination/async/ItemsSubscription.java | 103 ++++ .../async/PaginatedItemsPublisher.java | 50 ++ .../async/PaginationSubscription.java | 104 ++++ .../async/ResponsesSubscription.java | 62 +++ .../core/pagination/async/SdkPublisher.java | 33 ++ .../async/SequentialSubscriber.java | 63 +++ .../PaginatedItemsIterableTest.java | 12 +- pom.xml | 5 + .../ScanPaginatorIntegrationTest.java | 84 +++- services/pom.xml | 5 + ...istObjectsV2PaginatorsIntegrationTest.java | 307 ++++++++++++ 45 files changed, 3113 insertions(+), 756 deletions(-) create mode 100644 codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/AsyncResponseClassSpec.java delete mode 100644 codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/PaginatorResponseClassSpec.java create mode 100644 codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/PaginatorsClassSpec.java create mode 100644 codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/SyncResponseClassSpec.java rename codegen/src/test/java/software/amazon/awssdk/codegen/poet/paginators/{PaginatorResponseClassSpecTest.java => PaginatedResponseClassSpecTest.java} (71%) rename codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/{PaginatedOperationWithResultKeyPaginator.java => PaginatedOperationWithResultKeyIterable.java} (65%) create mode 100644 codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithResultKeyPublisher.java rename codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/{PaginatedOperationWithoutResultKeyPaginator.java => PaginatedOperationWithoutResultKeyIterable.java} (62%) create mode 100644 codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithoutResultKeyPublisher.java rename core/src/main/java/software/amazon/awssdk/core/pagination/{NextPageFetcher.java => SyncPageFetcher.java} (96%) create mode 100644 core/src/main/java/software/amazon/awssdk/core/pagination/async/AsyncPageFetcher.java create mode 100644 core/src/main/java/software/amazon/awssdk/core/pagination/async/EmptySubscription.java create mode 100644 core/src/main/java/software/amazon/awssdk/core/pagination/async/ItemsSubscription.java create mode 100644 core/src/main/java/software/amazon/awssdk/core/pagination/async/PaginatedItemsPublisher.java create mode 100644 core/src/main/java/software/amazon/awssdk/core/pagination/async/PaginationSubscription.java create mode 100644 core/src/main/java/software/amazon/awssdk/core/pagination/async/ResponsesSubscription.java create mode 100644 core/src/main/java/software/amazon/awssdk/core/pagination/async/SdkPublisher.java create mode 100644 core/src/main/java/software/amazon/awssdk/core/pagination/async/SequentialSubscriber.java create mode 100644 services/s3/src/it/java/software/amazon/awssdk/services/s3/ListObjectsV2PaginatorsIntegrationTest.java diff --git a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml index e7a1624f7e82..7c6c2f6ca2c7 100644 --- a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml +++ b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml @@ -60,4 +60,11 @@ wrap it in an sdk-specific exception. --> + + + + + + + diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/docs/AsyncOperationDocProvider.java b/codegen/src/main/java/software/amazon/awssdk/codegen/docs/AsyncOperationDocProvider.java index 05da2f72d256..80be3fd23f31 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/docs/AsyncOperationDocProvider.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/docs/AsyncOperationDocProvider.java @@ -16,9 +16,9 @@ package software.amazon.awssdk.codegen.docs; import java.util.Map; +import software.amazon.awssdk.codegen.internal.ImmutableMapParameter; import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; import software.amazon.awssdk.codegen.model.intermediate.OperationModel; -import software.amazon.awssdk.core.util.ImmutableMapParameter; /** * Implementations of {@link OperationDocProvider} for async client methods. This implementation is for the typical @@ -86,10 +86,14 @@ protected void applyThrows(DocumentationBuilder docBuilder) { * @return Factories to use for the {@link ClientType#ASYNC} method type. */ static Map asyncFactories() { - return ImmutableMapParameter.of(SimpleMethodOverload.NORMAL, AsyncOperationDocProvider::new, - SimpleMethodOverload.NO_ARG, AsyncNoArg::new, - SimpleMethodOverload.FILE, AsyncFile::new, - SimpleMethodOverload.CONSUMER_BUILDER, AsyncConsumerBuilder::new); + return new ImmutableMapParameter.Builder() + .put(SimpleMethodOverload.NORMAL, AsyncOperationDocProvider::new) + .put(SimpleMethodOverload.NO_ARG, AsyncNoArg::new) + .put(SimpleMethodOverload.FILE, AsyncFile::new) + .put(SimpleMethodOverload.CONSUMER_BUILDER, AsyncConsumerBuilder::new) + .put(SimpleMethodOverload.PAGINATED, AsyncPaginated::new) + .put(SimpleMethodOverload.NO_ARG_PAGINATED, AsyncPaginatedNoArg::new) + .build(); } /** @@ -150,4 +154,38 @@ protected void applyParams(DocumentationBuilder docBuilder) { opModel.getInputShape().getC2jName()); } } + + /** + * Provider for traditional paginated method that takes in a request object and returns a response object. + */ + private static class AsyncPaginated extends AsyncOperationDocProvider { + + private AsyncPaginated(IntermediateModel model, OperationModel opModel) { + super(model, opModel); + } + + @Override + protected String appendToDescription() { + return paginationDocs.getDocsForAsyncOperation(); + } + + @Override + protected void applyReturns(DocumentationBuilder docBuilder) { + docBuilder.returns("A custom publisher that can be subscribed to request a stream of response pages."); + } + } + + /** + * Provider for paginated simple method that takes no arguments and creates an empty request object. + */ + private static class AsyncPaginatedNoArg extends AsyncPaginated { + + private AsyncPaginatedNoArg(IntermediateModel model, OperationModel opModel) { + super(model, opModel); + } + + @Override + protected void applyParams(DocumentationBuilder docBuilder) { + } + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/docs/DocumentationBuilder.java b/codegen/src/main/java/software/amazon/awssdk/codegen/docs/DocumentationBuilder.java index a68f391d6444..fcf62d4d7c5c 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/docs/DocumentationBuilder.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/docs/DocumentationBuilder.java @@ -30,6 +30,8 @@ */ public final class DocumentationBuilder { + // TODO This prefix is not suitable for paginated operations. Either remove it for paginated operations + // or change the statement to something generic private static final String ASYNC_THROWS_PREFIX = "The CompletableFuture returned by this method can be completed " + "exceptionally with the following exceptions."; diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/docs/PaginationDocs.java b/codegen/src/main/java/software/amazon/awssdk/codegen/docs/PaginationDocs.java index 6413e2fabcd9..86a3d49f2166 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/docs/PaginationDocs.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/docs/PaginationDocs.java @@ -17,13 +17,20 @@ import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.TypeName; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; import software.amazon.awssdk.codegen.model.intermediate.OperationModel; import software.amazon.awssdk.codegen.poet.PoetExtensions; import software.amazon.awssdk.codegen.utils.PaginatorUtils; +import software.amazon.awssdk.core.pagination.async.SequentialSubscriber; public class PaginationDocs { + private static final String SUBSCRIBE_METHOD_NAME = "subscribe"; + private final OperationModel operationModel; private final PoetExtensions poetExtensions; @@ -62,7 +69,7 @@ public String getDocsForSyncResponseClass(ClassName clientInterface) { .add("

Represents the output for the {@link $T#$L($T)} operation which is a paginated operation." + " This class is an iterable of {@link $T} that can be used to iterate through all the " + "response pages of the operation.

", - clientInterface, getSyncPaginatedMethodName(), requestType(), syncResponsePageType()) + clientInterface, getPaginatedMethodName(), requestType(), syncResponsePageType()) .add("

When the operation is called, an instance of this class is returned. At this point, " + "no service calls are made yet and so there is no guarantee that the request is valid. " + "As you iterate through the iterable, SDK will start lazily loading response pages by making " @@ -73,10 +80,54 @@ clientInterface, getSyncPaginatedMethodName(), requestType(), syncResponsePageTy .toString(); } + /** + * Constructs additional documentation on the async client operation that is appended to the service documentation. + */ + public String getDocsForAsyncOperation() { + return CodeBlock.builder() + .add("

This is a variant of {@link #$L($T)} operation. " + + "The return type is a custom publisher that can be subscribed to request a stream of response " + + "pages. SDK will internally handle making service calls for you.\n

", + operationModel.getMethodName(), requestType()) + .add("

When the operation is called, an instance of this class is returned. At this point, " + + "no service calls are made yet and so there is no guarantee that the request is valid. " + + "If there are errors in your request, you will see the failures only after you start streaming " + + "the data. The subscribe method should be called as a request to start streaming data. " + + "For more info, see {@link $T#$L($T)}. Each call to the subscribe method will result in a new " + + "{@link $T} i.e., a new contract to stream data from the starting request.

", + getPublisherType(), SUBSCRIBE_METHOD_NAME, getSubscriberType(), getSubscriptionType()) + .add(getAsyncCodeSnippets()) + .build() + .toString(); + } + + /** + * Constructs javadocs for the generated response classes of a paginated operation in Async client. + * @param clientInterface A java poet {@link ClassName} type of the Async client interface + */ + public String getDocsForAsyncResponseClass(ClassName clientInterface) { + return CodeBlock.builder() + .add("

Represents the output for the {@link $T#$L($T)} operation which is a paginated operation." + + " This class is a type of {@link $T} which can be used to provide a sequence of {@link $T} " + + "response pages as per demand from the subscriber.

", + clientInterface, getPaginatedMethodName(), requestType(), getPublisherType(), + syncResponsePageType()) + .add("

When the operation is called, an instance of this class is returned. At this point, " + + "no service calls are made yet and so there is no guarantee that the request is valid. " + + "If there are errors in your request, you will see the failures only after you start streaming " + + "the data. The subscribe method should be called as a request to start streaming data. " + + "For more info, see {@link $T#$L($T)}. Each call to the subscribe method will result in a new " + + "{@link $T} i.e., a new contract to stream data from the starting request.

", + getPublisherType(), SUBSCRIBE_METHOD_NAME, getSubscriberType(), getSubscriptionType()) + .add(getAsyncCodeSnippets()) + .build() + .toString(); + } + private String getSyncCodeSnippets() { CodeBlock callOperationOnClient = CodeBlock.builder() .addStatement("$T responses = client.$L(request)", syncPaginatedResponseType(), - getSyncPaginatedMethodName()) + getPaginatedMethodName()) .build(); return CodeBlock.builder() @@ -103,20 +154,56 @@ private String getSyncCodeSnippets() { .toString(); } + private String getAsyncCodeSnippets() { + CodeBlock callOperationOnClient = CodeBlock.builder() + .addStatement("$T publisher = client.$L(request)", + asyncPaginatedResponseType(), + getPaginatedMethodName()) + .build(); + + return CodeBlock.builder() + .add("\n\n

The following are few ways to use the response class:

") + .add("1) Using the forEach helper method", + TypeName.get(SequentialSubscriber.class)) + .add(buildCode(CodeBlock.builder() + .add(callOperationOnClient) + .add(CodeBlock.builder() + .addStatement("CompletableFuture future = publisher" + + ".forEach(res -> " + + "{ // Do something with the response })") + .addStatement("future.get()") + .build()) + .build())) + .add("\n\n2) Using a custom subscriber") + .add(buildCode(CodeBlock.builder() + .add(callOperationOnClient) + .add("publisher.subscribe(new Subscriber<$T>() {\n\n", syncResponsePageType()) + .addStatement("public void onSubscribe($T subscription) { //... }", + getSubscriberType()) + .add("\n\n") + .addStatement("public void onNext($T response) { //... }", syncResponsePageType()) + .add("});") + .build())) + .add("As the response is a publisher, it can work well with third party reactive streams implementations " + + "like RxJava2.") + .add(noteAboutSyncNonPaginatedMethod()) + .build() + .toString(); + } + private CodeBlock buildCode(CodeBlock codeSnippet) { return CodeBlock.builder() .add("
{@code\n")
                         .add(codeSnippet)
                         .add("}
") .build(); - } /** * @return Method name for the sync paginated operation */ - private String getSyncPaginatedMethodName() { - return PaginatorUtils.getSyncMethodName(operationModel.getMethodName()); + private String getPaginatedMethodName() { + return PaginatorUtils.getPaginatedMethodName(operationModel.getMethodName()); } /** @@ -140,14 +227,42 @@ private ClassName syncResponsePageType() { /** * @return A Poet {@link ClassName} for the return type of sync paginated operation. */ - public ClassName syncPaginatedResponseType() { + private ClassName syncPaginatedResponseType() { return poetExtensions.getResponseClassForPaginatedSyncOperation(operationModel.getOperationName()); } + /** + * @return A Poet {@link ClassName} for the return type of Async paginated operation. + */ + private ClassName asyncPaginatedResponseType() { + return poetExtensions.getResponseClassForPaginatedAsyncOperation(operationModel.getOperationName()); + } + private CodeBlock noteAboutSyncNonPaginatedMethod() { return CodeBlock.builder() .add("\n

Note: If you prefer to have control on service calls, use the {@link #$L($T)} operation." + "

", operationModel.getMethodName(), requestType()) .build(); } + + /** + * @return A Poet {@link ClassName} for the reactive streams {@link Publisher}. + */ + private ClassName getPublisherType() { + return ClassName.get(Publisher.class); + } + + /** + * @return A Poet {@link ClassName} for the reactive streams {@link Subscriber}. + */ + private ClassName getSubscriberType() { + return ClassName.get(Subscriber.class); + } + + /** + * @return A Poet {@link ClassName} for the reactive streams {@link Subscription}. + */ + private ClassName getSubscriptionType() { + return ClassName.get(Subscription.class); + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/docs/SyncOperationDocProvider.java b/codegen/src/main/java/software/amazon/awssdk/codegen/docs/SyncOperationDocProvider.java index c329bedf3258..7bd82ecdb44f 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/docs/SyncOperationDocProvider.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/docs/SyncOperationDocProvider.java @@ -204,7 +204,7 @@ protected void applyParams(DocumentationBuilder docBuilder) { } /** - * Provider for standard paginated method that that takes in a request object and returns a response object. + * Provider for standard paginated method that takes in a request object and returns a response object. */ private static class SyncPaginated extends SyncOperationDocProvider { @@ -214,7 +214,12 @@ private SyncPaginated(IntermediateModel model, OperationModel opModel) { @Override protected String appendToDescription() { - return opModel.isPaginated() ? paginationDocs.getDocsForSyncOperation() : null; + return paginationDocs.getDocsForSyncOperation(); + } + + @Override + protected void applyReturns(DocumentationBuilder docBuilder) { + docBuilder.returns("A custom iterable that can be used to iterate through all the response pages."); } } @@ -230,7 +235,7 @@ private SyncPaginatedNoArg(IntermediateModel model, OperationModel opModel) { @Override protected void applyParams(DocumentationBuilder docBuilder) { // Link to non-simple method for discoverability - docBuilder.see("#%s(%s)", PaginatorUtils.getSyncMethodName(opModel.getMethodName()), + docBuilder.see("#%s(%s)", PaginatorUtils.getPaginatedMethodName(opModel.getMethodName()), opModel.getInput().getVariableType()); } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/PaginatorsGeneratorTasks.java b/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/PaginatorsGeneratorTasks.java index f353b70bd619..12a3314109d1 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/PaginatorsGeneratorTasks.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/PaginatorsGeneratorTasks.java @@ -18,15 +18,18 @@ import static software.amazon.awssdk.utils.FunctionalUtils.safeFunction; import java.io.IOException; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; import software.amazon.awssdk.codegen.emitters.GeneratorTask; import software.amazon.awssdk.codegen.emitters.GeneratorTaskParams; import software.amazon.awssdk.codegen.emitters.PoetGeneratorTask; import software.amazon.awssdk.codegen.model.service.PaginatorDefinition; import software.amazon.awssdk.codegen.poet.ClassSpec; -import software.amazon.awssdk.codegen.poet.paginators.PaginatorResponseClassSpec; +import software.amazon.awssdk.codegen.poet.paginators.AsyncResponseClassSpec; +import software.amazon.awssdk.codegen.poet.paginators.SyncResponseClassSpec; public class PaginatorsGeneratorTasks extends BaseGeneratorTasks { @@ -44,15 +47,25 @@ protected boolean hasTasks() { @Override protected List createTasks() throws Exception { - info("Emitting paginator classes"); return model.getPaginators().entrySet().stream() .filter(entry -> entry.getValue().isValid()) - .map(safeFunction(this::createTask)) + .flatMap(safeFunction(this::createSyncAndAsyncTasks)) .collect(Collectors.toList()); } - private GeneratorTask createTask(Map.Entry entry) throws IOException { - ClassSpec classSpec = new PaginatorResponseClassSpec(model, entry.getKey(), entry.getValue()); + private Stream createSyncAndAsyncTasks(Map.Entry entry) throws IOException { + return Arrays.asList(createSyncTask(entry), createAsyncTask(entry)) + .stream(); + } + + private GeneratorTask createSyncTask(Map.Entry entry) throws IOException { + ClassSpec classSpec = new SyncResponseClassSpec(model, entry.getKey(), entry.getValue()); + + return new PoetGeneratorTask(paginatorsClassDir, model.getFileHeader(), classSpec); + } + + private GeneratorTask createAsyncTask(Map.Entry entry) throws IOException { + ClassSpec classSpec = new AsyncResponseClassSpec(model, entry.getKey(), entry.getValue()); return new PoetGeneratorTask(paginatorsClassDir, model.getFileHeader(), classSpec); } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/PoetExtensions.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/PoetExtensions.java index 53987863a81c..cdefdf41ed9f 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/PoetExtensions.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/PoetExtensions.java @@ -67,11 +67,22 @@ public ClassName getClientClass(String className) { * @return A Poet {@link ClassName} for the response type of a paginated operation in the base service package. * * Example: If operationName is "ListTables", then the response type of the paginated operation - * will be "ListTablesPaginator" class in the base service package. + * will be "ListTablesIterable" class. */ @ReviewBeforeRelease("Naming of response shape for paginated APIs") public ClassName getResponseClassForPaginatedSyncOperation(String operationName) { - return ClassName.get(model.getMetadata().getFullPaginatorsPackageName(), operationName + "Paginator"); + return ClassName.get(model.getMetadata().getFullPaginatorsPackageName(), operationName + "Iterable"); } + /** + * @param operationName Name of the operation + * @return A Poet {@link ClassName} for the response type of a async paginated operation in the base service package. + * + * Example: If operationName is "ListTables", then the async response type of the paginated operation + * will be "ListTablesPublisher" class. + */ + @ReviewBeforeRelease("Naming of response shape for paginated APIs") + public ClassName getResponseClassForPaginatedAsyncOperation(String operationName) { + return ClassName.get(model.getMetadata().getFullPaginatorsPackageName(), operationName + "Publisher"); + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientClass.java index 4cd3ca9ee31d..d3dd9254f395 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientClass.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientClass.java @@ -60,6 +60,7 @@ public TypeSpec poetSpec() { interfaceClass) .addMethod(nameMethod()) .addMethods(operations()) + .addMethods(paginatedTraditionalMethods()) .addMethod(closeMethod()) .addMethods(protocolSpec.additionalMethods()) .addMethod(protocolSpec.initProtocolFactory(model)); @@ -128,6 +129,14 @@ protected MethodSpec.Builder operationBody(MethodSpec.Builder builder, Operation } + @Override + protected MethodSpec.Builder paginatedMethodBody(MethodSpec.Builder builder, OperationModel opModel) { + return builder.addModifiers(Modifier.PUBLIC) + .addStatement("return new $T(this, $L)", + poetExtensions.getResponseClassForPaginatedAsyncOperation(opModel.getOperationName()), + opModel.getInput().getVariableName()); + } + @Override public ClassName className() { return className; diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientInterface.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientInterface.java index 2811dd9fecc8..da632848b545 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientInterface.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientInterface.java @@ -36,7 +36,9 @@ import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; import software.amazon.awssdk.codegen.model.intermediate.OperationModel; import software.amazon.awssdk.codegen.poet.ClassSpec; +import software.amazon.awssdk.codegen.poet.PoetExtensions; import software.amazon.awssdk.codegen.poet.PoetUtils; +import software.amazon.awssdk.codegen.utils.PaginatorUtils; import software.amazon.awssdk.core.SdkClient; import software.amazon.awssdk.core.async.AsyncRequestProvider; import software.amazon.awssdk.core.async.AsyncResponseHandler; @@ -52,6 +54,7 @@ public class AsyncClientInterface implements ClassSpec { protected final ClassName className; protected final String clientPackageName; private final String modelPackage; + private final PoetExtensions poetExtensions; public AsyncClientInterface(IntermediateModel model) { this.modelPackage = model.getMetadata().getFullModelPackageName(); @@ -59,6 +62,7 @@ public AsyncClientInterface(IntermediateModel model) { this.model = model; this.className = ClassName.get(model.getMetadata().getFullClientPackageName(), model.getMetadata().getAsyncInterface()); + this.poetExtensions = new PoetExtensions(model); } @Override @@ -132,9 +136,64 @@ private Iterable operationsAndSimpleMethods() { .flatMap(List::stream) .map(MethodSpec.Builder::build) .collect(toList())); + + methods.addAll(paginatedTraditionalMethods()); + methods.addAll(paginatedSimpleMethods()); + return methods.stream().sorted(Comparator.comparing(m -> m.name)).collect(toList()); } + protected List paginatedTraditionalMethods() { + return model.getOperations().values().stream() + .filter(operationModel -> operationModel.isPaginated()) + .map(this::paginatedTraditionalMethod) + .collect(toList()); + } + + private MethodSpec paginatedTraditionalMethod(OperationModel opModel) { + final String methodName = PaginatorUtils.getPaginatedMethodName(opModel.getMethodName()); + final ClassName requestType = ClassName.get(modelPackage, opModel.getInput().getVariableType()); + final ClassName responsePojoType = poetExtensions.getResponseClassForPaginatedAsyncOperation(opModel.getOperationName()); + + MethodSpec.Builder builder = MethodSpec.methodBuilder(methodName) + .returns(responsePojoType) + .addParameter(requestType, opModel.getInput().getVariableName()) + .addJavadoc(opModel.getDocs(model, + ClientType.ASYNC, + SimpleMethodOverload.PAGINATED)); + + return paginatedMethodBody(builder, opModel).build(); + } + + protected MethodSpec.Builder paginatedMethodBody(MethodSpec.Builder builder, OperationModel operationModel) { + return builder.addModifiers(Modifier.DEFAULT, Modifier.PUBLIC) + .addStatement("throw new $T()", UnsupportedOperationException.class); + } + + private List paginatedSimpleMethods() { + return model.getOperations().values().stream() + .filter(operationModel -> operationModel.isPaginated()) + .filter(operationModel -> operationModel.getInputShape().isSimpleMethod()) + .map(this::paginatedSimpleMethod) + .collect(toList()); + } + + private MethodSpec paginatedSimpleMethod(OperationModel opModel) { + final String methodName = PaginatorUtils.getPaginatedMethodName(opModel.getMethodName()); + final ClassName requestType = ClassName.get(modelPackage, opModel.getInput().getVariableType()); + final ClassName responsePojoType = poetExtensions.getResponseClassForPaginatedAsyncOperation(opModel.getOperationName()); + + MethodSpec.Builder builder = MethodSpec.methodBuilder(methodName) + .addModifiers(Modifier.DEFAULT, Modifier.PUBLIC) + .returns(responsePojoType) + .addStatement("return $L($T.builder().build())", methodName, requestType) + .addJavadoc(opModel.getDocs(model, + ClientType.ASYNC, + SimpleMethodOverload.NO_ARG_PAGINATED)); + + return builder.build(); + } + /** * @param opModel Operation to generate simple methods for. * @return All simple method overloads for a given operation. diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientClass.java index 88f7eff90cba..3f3d36279825 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientClass.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientClass.java @@ -164,7 +164,7 @@ private List paginatedMethods(OperationModel opModel) { paginatedMethodSpecs.add(SyncClientInterface.operationMethodSignature(model, opModel, SimpleMethodOverload.PAGINATED, - PaginatorUtils.getSyncMethodName( + PaginatorUtils.getPaginatedMethodName( opModel.getMethodName())) .addAnnotation(Override.class) .returns(poetExtensions.getResponseClassForPaginatedSyncOperation( diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientInterface.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientInterface.java index 4fdc8f751fac..9ff6302691ed 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientInterface.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientInterface.java @@ -247,7 +247,7 @@ private List paginatedMethods(OperationModel opModel) { paginatedMethodSpecs.add(operationMethodSignature(model, opModel, SimpleMethodOverload.PAGINATED, - PaginatorUtils.getSyncMethodName(opModel.getMethodName())) + PaginatorUtils.getPaginatedMethodName(opModel.getMethodName())) .returns(poetExtensions.getResponseClassForPaginatedSyncOperation( opModel.getOperationName())) .addModifiers(Modifier.DEFAULT) @@ -259,7 +259,7 @@ private List paginatedMethods(OperationModel opModel) { } private MethodSpec paginatedSimpleMethod(OperationModel opModel) { - String paginatedMethodName = PaginatorUtils.getSyncMethodName(opModel.getMethodName()); + String paginatedMethodName = PaginatorUtils.getPaginatedMethodName(opModel.getMethodName()); ClassName requestType = ClassName.get(model.getMetadata().getFullModelPackageName(), opModel.getInput().getVariableType()); diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/AsyncResponseClassSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/AsyncResponseClassSpec.java new file mode 100644 index 000000000000..786e8451ddbe --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/AsyncResponseClassSpec.java @@ -0,0 +1,257 @@ +/* + * Copyright 2010-2018 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.codegen.poet.paginators; + +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import com.squareup.javapoet.WildcardTypeName; +import java.util.Collections; +import java.util.Iterator; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.lang.model.element.Modifier; +import org.reactivestreams.Subscriber; +import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; +import software.amazon.awssdk.codegen.model.intermediate.MemberModel; +import software.amazon.awssdk.codegen.model.service.PaginatorDefinition; +import software.amazon.awssdk.codegen.poet.ClassSpec; +import software.amazon.awssdk.codegen.poet.PoetUtils; +import software.amazon.awssdk.core.pagination.async.AsyncPageFetcher; +import software.amazon.awssdk.core.pagination.async.EmptySubscription; +import software.amazon.awssdk.core.pagination.async.PaginatedItemsPublisher; +import software.amazon.awssdk.core.pagination.async.ResponsesSubscription; +import software.amazon.awssdk.core.pagination.async.SdkPublisher; + +/** + * Java poet {@link ClassSpec} to generate the response class for async paginated operations. + */ +public class AsyncResponseClassSpec extends PaginatorsClassSpec { + + private static final String SUBSCRIBER = "subscriber"; + private static final String SUBSCRIBE_METHOD = "subscribe"; + private static final String LAST_PAGE_FIELD = "isLastPage"; + private static final String LAST_PAGE_METHOD = "withLastPage"; + + public AsyncResponseClassSpec(IntermediateModel model, String c2jOperationName, PaginatorDefinition paginatorDefinition) { + super(model, c2jOperationName, paginatorDefinition); + } + + @Override + public TypeSpec poetSpec() { + TypeSpec.Builder specBuilder = TypeSpec.classBuilder(className()) + .addModifiers(Modifier.PUBLIC) + .addAnnotation(PoetUtils.GENERATED) + .addSuperinterface(getAsyncResponseInterface()) + .addFields(Stream.of(asyncClientInterfaceField(), + requestClassField(), + asyncPageFetcherField(), + lastPageField()) + .collect(Collectors.toList())) + .addMethod(publicConstructor()) + .addMethod(privateConstructor()) + .addMethod(subscribeMethod()) + .addMethods(getMethodSpecsForResultKeyList()) + .addMethod(resumeMethod()) + .addJavadoc(paginationDocs.getDocsForAsyncResponseClass( + getAsyncClientInterfaceName())) + .addType(nextPageFetcherClass()); + + return specBuilder.build(); + } + + @Override + public ClassName className() { + return poetExtensions.getResponseClassForPaginatedAsyncOperation(c2jOperationName); + } + + /** + * Returns the interface that is implemented by the Paginated Async Response class. + */ + private TypeName getAsyncResponseInterface() { + return ParameterizedTypeName.get(ClassName.get(SdkPublisher.class), responseType()); + } + + /** + * @return A Poet {@link ClassName} for the async client interface + */ + private ClassName getAsyncClientInterfaceName() { + return poetExtensions.getClientClass(model.getMetadata().getAsyncInterface()); + } + + private FieldSpec asyncClientInterfaceField() { + return FieldSpec.builder(getAsyncClientInterfaceName(), CLIENT_MEMBER, Modifier.PRIVATE, Modifier.FINAL).build(); + } + + private FieldSpec asyncPageFetcherField() { + return FieldSpec.builder(AsyncPageFetcher.class, NEXT_PAGE_FETCHER_MEMBER, Modifier.PRIVATE, Modifier.FINAL).build(); + } + + private FieldSpec lastPageField() { + return FieldSpec.builder(boolean.class, LAST_PAGE_FIELD, Modifier.PRIVATE).build(); + } + + private MethodSpec publicConstructor() { + return MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(getAsyncClientInterfaceName(), CLIENT_MEMBER, Modifier.FINAL) + .addParameter(requestType(), REQUEST_MEMBER, Modifier.FINAL) + .addStatement("this($L, $L, false)", CLIENT_MEMBER, REQUEST_MEMBER) + .build(); + } + + private MethodSpec privateConstructor() { + return MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(getAsyncClientInterfaceName(), CLIENT_MEMBER, Modifier.FINAL) + .addParameter(requestType(), REQUEST_MEMBER, Modifier.FINAL) + .addParameter(boolean.class, LAST_PAGE_FIELD, Modifier.FINAL) + .addStatement("this.$L = $L", CLIENT_MEMBER, CLIENT_MEMBER) + .addStatement("this.$L = $L", REQUEST_MEMBER, REQUEST_MEMBER) + .addStatement("this.$L = $L", LAST_PAGE_FIELD, LAST_PAGE_FIELD) + .addStatement("this.$L = new $L()", NEXT_PAGE_FETCHER_MEMBER, nextPageFetcherClassName()) + .build(); + } + + /** + * A {@link MethodSpec} for the subscribe() method which is inherited from the interface. + */ + private MethodSpec subscribeMethod() { + return MethodSpec.methodBuilder(SUBSCRIBE_METHOD) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(ParameterizedTypeName.get(ClassName.get(Subscriber.class), + WildcardTypeName.supertypeOf(responseType())), + SUBSCRIBER) + .addStatement("$L.onSubscribe(new $T($L, $L))", SUBSCRIBER, ResponsesSubscription.class, + SUBSCRIBER, NEXT_PAGE_FETCHER_MEMBER) + .build(); + } + + /** + * Returns iterable of {@link MethodSpec} to generate helper methods for all members + * in {@link PaginatorDefinition#getResultKey()}. + * + * The helper methods return a publisher that can be used to stream over the collection of result keys. + * These methods will only be generated if {@link PaginatorDefinition#getResultKey()} is not null and a non-empty list. + */ + private Iterable getMethodSpecsForResultKeyList() { + if (paginatorDefinition.getResultKey() != null) { + return paginatorDefinition.getResultKey().stream() + .map(this::getMethodsSpecForSingleResultKey) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + } + + /* + * Generate a method spec for single element in {@link PaginatorDefinition#getResultKey()} list. + * + * If the element is "Folders" and its type is "List", generated code looks like: + * + * public SdkPublisher folders() { + * Function> getIterator = response -> { + * if (response != null && response.folders() != null) { + * return response.folders().iterator(); + * } + * return Collections.emptyIterator(); + * }; + * return new PaginatedItemsPublisher(new DescribeFolderContentsResponseFetcher(), getIterator); + * } + */ + private MethodSpec getMethodsSpecForSingleResultKey(String resultKey) { + TypeName resultKeyType = getTypeForResultKey(resultKey); + MemberModel resultKeyModel = memberModelForResponseMember(resultKey); + + return MethodSpec.methodBuilder(resultKeyModel.getFluentGetterMethodName()) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .returns(ParameterizedTypeName.get(ClassName.get(SdkPublisher.class), resultKeyType)) + .addCode("$T getIterator = ", + ParameterizedTypeName.get(ClassName.get(Function.class), + responseType(), + ParameterizedTypeName.get(ClassName.get(Iterator.class), + resultKeyType))) + .addCode(getIteratorLambdaBlock(resultKey, resultKeyModel)) + .addCode("\n") + .addStatement("return new $T(new $L(), getIterator, $L)", + PaginatedItemsPublisher.class, + nextPageFetcherClassName(), + LAST_PAGE_FIELD) + .addJavadoc(CodeBlock.builder() + .add("Returns a publisher that can be used to get a stream of data. You need to " + + "subscribe to the publisher to request the stream of data. The publisher " + + "has a helper forEach method that takes in a {@link $T} and then applies " + + "that consumer to each response returned by the service.", + TypeName.get(Consumer.class)) + .build()) + .build(); + } + + /**aW + * Generates a inner class that implements {@link AsyncPageFetcher}. This is a helper class that can be used + * to find if there are more pages in the response and to get the next page if exists. + */ + private TypeSpec nextPageFetcherClass() { + return TypeSpec.classBuilder(nextPageFetcherClassName()) + .addModifiers(Modifier.PRIVATE) + .addSuperinterface(ParameterizedTypeName.get(ClassName.get(AsyncPageFetcher.class), responseType())) + .addMethod(MethodSpec.methodBuilder(HAS_NEXT_PAGE_METHOD) + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .addParameter(responseType(), PREVIOUS_PAGE_METHOD_ARGUMENT, Modifier.FINAL) + .returns(boolean.class) + .addStatement(hasNextPageMethodBody()) + .build()) + .addMethod(MethodSpec.methodBuilder(NEXT_PAGE_METHOD) + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .addParameter(responseType(), PREVIOUS_PAGE_METHOD_ARGUMENT, Modifier.FINAL) + .returns(ParameterizedTypeName.get(ClassName.get(CompletableFuture.class), + responseType())) + .addCode(nextPageMethodBody()) + .build()) + .build(); + } + + private MethodSpec resumeMethod() { + return resumeMethodBuilder().addCode(CodeBlock.builder() + .addStatement("return $L", anonymousClassWithEmptySubscription()) + .build()) + .build(); + } + + private TypeSpec anonymousClassWithEmptySubscription() { + return TypeSpec.anonymousClassBuilder("$L, $L, true", CLIENT_MEMBER, REQUEST_MEMBER) + .addSuperinterface(className()) + .addMethod(MethodSpec.methodBuilder(SUBSCRIBE_METHOD) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(ParameterizedTypeName.get(ClassName.get(Subscriber.class), + WildcardTypeName.supertypeOf(responseType())), + SUBSCRIBER) + .addStatement("$L.onSubscribe(new $T($L))", SUBSCRIBER, + TypeName.get(EmptySubscription.class), SUBSCRIBER) + .build()) + .build(); + } +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/PaginatorResponseClassSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/PaginatorResponseClassSpec.java deleted file mode 100644 index 84a69d0237f9..000000000000 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/PaginatorResponseClassSpec.java +++ /dev/null @@ -1,459 +0,0 @@ -/* - * Copyright 2010-2018 Amazon.com, Inc. or its affiliates. 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. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.codegen.poet.paginators; - -import com.squareup.javapoet.ClassName; -import com.squareup.javapoet.CodeBlock; -import com.squareup.javapoet.FieldSpec; -import com.squareup.javapoet.MethodSpec; -import com.squareup.javapoet.ParameterizedTypeName; -import com.squareup.javapoet.TypeName; -import com.squareup.javapoet.TypeSpec; -import java.security.InvalidParameterException; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.lang.model.element.Modifier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.codegen.docs.PaginationDocs; -import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; -import software.amazon.awssdk.codegen.model.intermediate.MemberModel; -import software.amazon.awssdk.codegen.model.intermediate.OperationModel; -import software.amazon.awssdk.codegen.model.intermediate.ShapeModel; -import software.amazon.awssdk.codegen.model.service.PaginatorDefinition; -import software.amazon.awssdk.codegen.poet.ClassSpec; -import software.amazon.awssdk.codegen.poet.PoetExtensions; -import software.amazon.awssdk.codegen.poet.PoetUtils; -import software.amazon.awssdk.codegen.poet.model.TypeProvider; -import software.amazon.awssdk.core.pagination.NextPageFetcher; -import software.amazon.awssdk.core.pagination.PaginatedItemsIterable; -import software.amazon.awssdk.core.pagination.PaginatedResponsesIterator; -import software.amazon.awssdk.core.pagination.SdkIterable; - -/** - * Java poet {@link ClassSpec} to generate the response class for sync paginated operations. - */ -public class PaginatorResponseClassSpec implements ClassSpec { - - private static final Logger log = LoggerFactory.getLogger(PaginatorResponseClassSpec.class); - - private static final String CLIENT_MEMBER = "client"; - private static final String REQUEST_MEMBER = "firstRequest"; - private static final String NEXT_PAGE_FETCHER_MEMBER = "nextPageFetcher"; - private static final String HAS_NEXT_PAGE_METHOD = "hasNextPage"; - private static final String NEXT_PAGE_METHOD = "nextPage"; - private static final String PREVIOUS_PAGE_METHOD_ARGUMENT = "previousPage"; - - private final IntermediateModel model; - private final PoetExtensions poetExtensions; - private final TypeProvider typeProvider; - private final String c2jOperationName; - private final PaginatorDefinition paginatorDefinition; - private final OperationModel operationModel; - private final PaginationDocs paginationDocs; - - public PaginatorResponseClassSpec(IntermediateModel intermediateModel, - String c2jOperationName, - PaginatorDefinition paginatorDefinition) { - this.model = intermediateModel; - this.poetExtensions = new PoetExtensions(intermediateModel); - this.typeProvider = new TypeProvider(intermediateModel); - this.c2jOperationName = c2jOperationName; - this.paginatorDefinition = paginatorDefinition; - this.operationModel = model.getOperation(c2jOperationName); - this.paginationDocs = new PaginationDocs(intermediateModel, operationModel); - } - - @Override - public TypeSpec poetSpec() { - TypeSpec.Builder specBuilder = TypeSpec.classBuilder(className()) - .addModifiers(Modifier.PUBLIC, Modifier.FINAL) - .addAnnotation(PoetUtils.GENERATED) - .addSuperinterface(getPaginatedResponseInterface()) - .addFields(Stream.of(syncClientInterfaceField(), - requestClassField(), - nextPageSupplierField()) - .collect(Collectors.toList())) - .addMethod(constructor()) - .addMethod(iteratorMethod()) - .addMethods(getMethodSpecsForResultKeyList()) - .addJavadoc(paginationDocs.getDocsForSyncResponseClass( - getClientInterfaceName())) - .addType(nextPageFetcherClass()); - - return specBuilder.build(); - } - - @Override - public ClassName className() { - return poetExtensions.getResponseClassForPaginatedSyncOperation(c2jOperationName); - } - - /** - * Returns the interface that is implemented by the Paginated Response class. - */ - private TypeName getPaginatedResponseInterface() { - return ParameterizedTypeName.get(ClassName.get(SdkIterable.class), responseType()); - } - - /** - * @return A Poet {@link ClassName} for the sync operation request type. - * - * Example: For ListTables operation, it will be "ListTablesRequest" class. - */ - private ClassName requestType() { - return poetExtensions.getModelClass(operationModel.getInput().getVariableType()); - } - - /** - * @return A Poet {@link ClassName} for the sync operation response type. - * - * Example: For ListTables operation, it will be "ListTablesResponse" class. - */ - private ClassName responseType() { - return poetExtensions.getModelClass(operationModel.getReturnType().getReturnType()); - } - - /** - * @return A Poet {@link ClassName} for the sync client interface - */ - private ClassName getClientInterfaceName() { - return poetExtensions.getClientClass(model.getMetadata().getSyncInterface()); - } - - private FieldSpec syncClientInterfaceField() { - return FieldSpec.builder(getClientInterfaceName(), CLIENT_MEMBER, Modifier.PRIVATE, Modifier.FINAL).build(); - } - - private FieldSpec requestClassField() { - return FieldSpec.builder(requestType(), REQUEST_MEMBER, Modifier.PRIVATE, Modifier.FINAL).build(); - } - - private FieldSpec nextPageSupplierField() { - return FieldSpec.builder(NextPageFetcher.class, NEXT_PAGE_FETCHER_MEMBER, Modifier.PRIVATE, Modifier.FINAL).build(); - } - - private String nextPageFetcherClassName() { - return operationModel.getReturnType().getReturnType() + "Fetcher"; - } - - private MethodSpec constructor() { - return MethodSpec.constructorBuilder() - .addModifiers(Modifier.PUBLIC) - .addParameter(getClientInterfaceName(), CLIENT_MEMBER, Modifier.FINAL) - .addParameter(requestType(), REQUEST_MEMBER, Modifier.FINAL) - .addStatement("this.$L = $L", CLIENT_MEMBER, CLIENT_MEMBER) - .addStatement("this.$L = $L", REQUEST_MEMBER, REQUEST_MEMBER) - .addStatement("this.$L = new $L()", NEXT_PAGE_FETCHER_MEMBER, nextPageFetcherClassName()) - .build(); - } - - /** - * A {@link MethodSpec} for the overridden iterator() method which is inherited - * from the interface. - */ - private MethodSpec iteratorMethod() { - return MethodSpec.methodBuilder("iterator") - .addAnnotation(Override.class) - .addModifiers(Modifier.PUBLIC) - .returns(ParameterizedTypeName.get(ClassName.get(Iterator.class), responseType())) - .addStatement("return new $T($L)", PaginatedResponsesIterator.class, NEXT_PAGE_FETCHER_MEMBER) - .build(); - } - - /** - * Returns iterable of {@link MethodSpec} to generate helper methods for all members - * in {@link PaginatorDefinition#getResultKey()}. All the generated methods return an SdkIterable. - * - * The helper methods to iterate on paginated member will be generated only - * if {@link PaginatorDefinition#getResultKey()} is not null and a non-empty list. - */ - private Iterable getMethodSpecsForResultKeyList() { - if (paginatorDefinition.getResultKey() != null) { - return paginatorDefinition.getResultKey().stream() - .map(this::getMethodsSpecForSingleResultKey) - .collect(Collectors.toList()); - } - return Collections.emptyList(); - } - - /* - * Generate a method spec for single element in {@link PaginatorDefinition#getResultKey()} list. - * - * If the element is "Folders" and its type is "List", generated code looks like: - * - * public SdkIterable folders() { - * Function> getPaginatedMemberIterator = - * response -> response != null ? response.folders().iterator() : null; - * - * return new PaginatedItemsIterable(this, getPaginatedMemberIterator); - * } - */ - private MethodSpec getMethodsSpecForSingleResultKey(String resultKey) { - TypeName resultKeyType = getTypeForResultKey(resultKey); - MemberModel resultKeyModel = memberModelForResponseMember(resultKey); - - return MethodSpec.methodBuilder(resultKeyModel.getFluentGetterMethodName()) - .addModifiers(Modifier.PUBLIC) - .returns(ParameterizedTypeName.get(ClassName.get(SdkIterable.class), resultKeyType)) - .addCode("$T getIterator = ", - ParameterizedTypeName.get(ClassName.get(Function.class), - responseType(), - ParameterizedTypeName.get(ClassName.get(Iterator.class), - resultKeyType))) - .addCode(getPaginatedMemberIteratorLambdaBlock(resultKey, resultKeyModel)) - .addCode("\n") - .addStatement("return new $T(this, getIterator)", PaginatedItemsIterable.class) - .addJavadoc(CodeBlock.builder() - .add("Returns an iterable to iterate through the paginated {@link $T#$L()} member. " - + "The returned iterable is used to iterate through the results across all " - + "response pages and not a single page.\n", - responseType(), resultKeyModel.getFluentGetterMethodName()) - .add("\n") - .add("This method is useful if you are interested in iterating over the paginated " - + "member in the response pages instead of the top level pages. " - + "Similar to iteration over pages, this method internally makes service " - + "calls to get the next list of results until the iteration stops or " - + "there are no more results.") - .build()) - .build(); - } - - private CodeBlock getPaginatedMemberIteratorLambdaBlock(String resultKey, MemberModel resultKeyModel) { - final String response = "response"; - final String fluentGetter = fluentGetterMethodForResponseMember(resultKey); - - CodeBlock iteratorBlock = null; - - if (resultKeyModel.isList()) { - iteratorBlock = CodeBlock.builder().add("$L.$L.iterator()", response, fluentGetter).build(); - - } else if (resultKeyModel.isMap()) { - iteratorBlock = CodeBlock.builder().add("$L.$L.entrySet().iterator()", response, fluentGetter).build(); - } - - return CodeBlock.builder() - .addStatement("$L -> $L != null ? $L : null", response, response, iteratorBlock) - .build(); - } - - /** - * Returns a list of fluent setter method names for members in {@link PaginatorDefinition#getInputToken()} list. - * The size of list returned by this method is equal to the size of {@link PaginatorDefinition#getInputToken()} list. - */ - private List fluentSetterMethodNamesForInputToken() { - return paginatorDefinition.getInputToken().stream() - .map(this::fluentSetterNameForSingleInputToken) - .collect(Collectors.toList()); - } - - /** - * Returns the fluent setter method name for a single member in the request. - * - * The values in {@link PaginatorDefinition#getInputToken()} are not nested unlike - * {@link PaginatorDefinition#getOutputToken()}. - */ - private String fluentSetterNameForSingleInputToken(String inputToken) { - return operationModel.getInputShape() - .findMemberModelByC2jName(inputToken) - .getFluentSetterMethodName(); - } - - /** - * Returns a list of fluent getter methods for members in {@link PaginatorDefinition#getOutputToken()} list. - * The size of list returned by this method is equal to the size of {@link PaginatorDefinition#getOutputToken()} list. - */ - private List fluentGetterMethodsForOutputToken() { - return paginatorDefinition.getOutputToken().stream() - .map(this::fluentGetterMethodForResponseMember) - .collect(Collectors.toList()); - } - - /** - * Returns the fluent getter method for a single member in the response. - * The returned String includes the '()' after each method name. - * - * The input member can be a nested String. An example would be StreamDescription.LastEvaluatedShardId - * which represents LastEvaluatedShardId member in StreamDescription class. The return value for it - * would be "streamDescription().lastEvaluatedShardId()" - * - * @param member A top level or nested member in response of {@link #c2jOperationName}. - */ - private String fluentGetterMethodForResponseMember(String member) { - final String[] hierarchy = member.split("\\."); - - if (hierarchy.length < 1) { - throw new IllegalArgumentException(String.format("Error when splitting member %s for operation %s", - member, c2jOperationName)); - } - - ShapeModel parentShape = operationModel.getOutputShape(); - final StringBuilder getterMethod = new StringBuilder(); - - for (String str : hierarchy) { - getterMethod.append(".") - .append(parentShape.findMemberModelByC2jName(str).getFluentGetterMethodName()) - .append("()"); - - parentShape = parentShape.findMemberModelByC2jName(str).getShape(); - } - - return getterMethod.substring(1); - } - - /** - * @param input A top level or nested member in response of {@link #c2jOperationName}. - * - * @return The {@link MemberModel} of the {@link PaginatorDefinition#getResultKey()}. If input value is nested, - * then member model of the last child shape is returned. - * - * For example, if input is StreamDescription.Shards, then the return value is "Shard" which is the member model for - * the Shards. - */ - private MemberModel memberModelForResponseMember(String input) { - final String[] hierarchy = input.split("\\."); - - if (hierarchy.length < 1) { - throw new IllegalArgumentException(String.format("Error when splitting value %s for operation %s", - input, c2jOperationName)); - } - - ShapeModel shape = operationModel.getOutputShape(); - - for (int i = 0; i < hierarchy.length - 1; i++) { - shape = shape.findMemberModelByC2jName(hierarchy[i]).getShape(); - } - - return shape.getMemberByC2jName(hierarchy[hierarchy.length - 1]); - } - - /* - * Returns the {@link TypeName} for a value in the {@link PaginatorDefinition#getResultKey()} list. - * - * Examples: - * If paginated item is represented as List, then member type is String. - * If paginated item is represented as List, then member type is Foo. - * If paginated item is represented as Map>, - * then member type is Map.Entry>. - */ - private TypeName getTypeForResultKey(String singleResultKey) { - MemberModel resultKeyModel = memberModelForResponseMember(singleResultKey); - - if (resultKeyModel == null) { - throw new InvalidParameterException("MemberModel is not found for result key: " + singleResultKey); - } - - if (resultKeyModel.isList()) { - return typeProvider.fieldType(resultKeyModel.getListModel().getListMemberModel()); - } else if (resultKeyModel.isMap()) { - return typeProvider.mapEntryWithConcreteTypes(resultKeyModel.getMapModel()); - } else { - throw new IllegalArgumentException(String.format("Key %s in paginated operation %s should be either a list or a map", - singleResultKey, c2jOperationName)); - } - } - - /** - * Generates a inner class that implements {@link NextPageFetcher}. An instance of this class - * is passed to {@link PaginatedResponsesIterator} to be used while iterating through pages. - */ - private TypeSpec nextPageFetcherClass() { - return TypeSpec.classBuilder(nextPageFetcherClassName()) - .addModifiers(Modifier.PRIVATE) - .addSuperinterface(ParameterizedTypeName.get(ClassName.get(NextPageFetcher.class), responseType())) - .addMethod(MethodSpec.methodBuilder(HAS_NEXT_PAGE_METHOD) - .addModifiers(Modifier.PUBLIC) - .addAnnotation(Override.class) - .addParameter(responseType(), PREVIOUS_PAGE_METHOD_ARGUMENT) - .returns(boolean.class) - .addStatement(hasNextPageMethodBody()) - .build()) - .addMethod(MethodSpec.methodBuilder(NEXT_PAGE_METHOD) - .addModifiers(Modifier.PUBLIC) - .addAnnotation(Override.class) - .addParameter(responseType(), PREVIOUS_PAGE_METHOD_ARGUMENT) - .returns(responseType()) - .addCode(nextPageMethodBody()) - .build()) - .build(); - } - - private String hasNextPageMethodBody() { - String body; - - if (paginatorDefinition.getMoreResults() != null) { - body = String.format("return %s.%s.booleanValue()", - PREVIOUS_PAGE_METHOD_ARGUMENT, - fluentGetterMethodForResponseMember(paginatorDefinition.getMoreResults())); - } else { - // If there is no more_results token, then output_token will be a single value - body = String.format("return %s.%s != null", - PREVIOUS_PAGE_METHOD_ARGUMENT, - fluentGetterMethodsForOutputToken().get(0)); - } - - return body; - } - - /* - * Returns {@link CodeBlock} for the NEXT_PAGE_METHOD. - * - * A sample from dynamoDB listTables paginator: - * - * if (oldPage == null) { - * return client.listTables(firstRequest); - * } else { - * return client.listTables(firstRequest.toBuilder().exclusiveStartTableName(response.lastEvaluatedTableName()) - * .build()); - * } - */ - private CodeBlock nextPageMethodBody() { - return CodeBlock.builder() - .beginControlFlow("if ($L == null)", PREVIOUS_PAGE_METHOD_ARGUMENT) - .addStatement("return $L.$L($L)", CLIENT_MEMBER, operationModel.getMethodName(), REQUEST_MEMBER) - .endControlFlow() - .addStatement(codeToGetNextPageIfOldResponseIsNotNull()) - .build(); - } - - /** - * Generates the code to get next page by using values from old page. - * - * Sample generated code: - * return client.listTables(firstRequest.toBuilder().exclusiveStartTableName(response.lastEvaluatedTableName()).build()); - */ - private String codeToGetNextPageIfOldResponseIsNotNull() { - StringBuilder sb = new StringBuilder(); - - sb.append(String.format("return %s.%s(%s.toBuilder()", CLIENT_MEMBER, operationModel.getMethodName(), REQUEST_MEMBER)); - - List requestSetterNames = fluentSetterMethodNamesForInputToken(); - List responseGetterMethods = fluentGetterMethodsForOutputToken(); - - for (int i = 0; i < paginatorDefinition.getInputToken().size(); i++) { - sb.append(String.format(".%s(%s.%s)", requestSetterNames.get(i), PREVIOUS_PAGE_METHOD_ARGUMENT, - responseGetterMethods.get(i))); - } - - sb.append(".build())"); - - return sb.toString(); - } -} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/PaginatorsClassSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/PaginatorsClassSpec.java new file mode 100644 index 000000000000..2e6f5cb4751d --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/PaginatorsClassSpec.java @@ -0,0 +1,365 @@ +/* + * Copyright 2010-2018 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.codegen.poet.paginators; + +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeName; +import java.security.InvalidParameterException; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import javax.lang.model.element.Modifier; +import software.amazon.awssdk.codegen.docs.PaginationDocs; +import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; +import software.amazon.awssdk.codegen.model.intermediate.MemberModel; +import software.amazon.awssdk.codegen.model.intermediate.OperationModel; +import software.amazon.awssdk.codegen.model.intermediate.ShapeModel; +import software.amazon.awssdk.codegen.model.service.PaginatorDefinition; +import software.amazon.awssdk.codegen.poet.ClassSpec; +import software.amazon.awssdk.codegen.poet.PoetExtensions; +import software.amazon.awssdk.codegen.poet.model.TypeProvider; + +public abstract class PaginatorsClassSpec implements ClassSpec { + + protected static final String CLIENT_MEMBER = "client"; + protected static final String REQUEST_MEMBER = "firstRequest"; + protected static final String NEXT_PAGE_FETCHER_MEMBER = "nextPageFetcher"; + protected static final String HAS_NEXT_PAGE_METHOD = "hasNextPage"; + protected static final String NEXT_PAGE_METHOD = "nextPage"; + protected static final String RESUME_METHOD = "resume"; + protected static final String PREVIOUS_PAGE_METHOD_ARGUMENT = "previousPage"; + protected static final String RESPONSE_LITERAL = "response"; + protected static final String LAST_SUCCESSFUL_PAGE_LITERAL = "lastSuccessfulPage"; + + protected final IntermediateModel model; + protected final String c2jOperationName; + protected final PaginatorDefinition paginatorDefinition; + protected final PoetExtensions poetExtensions; + protected final TypeProvider typeProvider; + protected final OperationModel operationModel; + protected final PaginationDocs paginationDocs; + + public PaginatorsClassSpec(IntermediateModel model, String c2jOperationName, PaginatorDefinition paginatorDefinition) { + this.model = model; + this.c2jOperationName = c2jOperationName; + this.paginatorDefinition = paginatorDefinition; + this.poetExtensions = new PoetExtensions(model); + this.typeProvider = new TypeProvider(model); + this.operationModel = model.getOperation(c2jOperationName); + this.paginationDocs = new PaginationDocs(model, operationModel); + } + + /** + * @return A Poet {@link ClassName} for the operation request type. + * + * Example: For ListTables operation, it will be "ListTablesRequest" class. + */ + protected ClassName requestType() { + return poetExtensions.getModelClass(operationModel.getInput().getVariableType()); + } + + /** + * @return A Poet {@link ClassName} for the sync operation response type. + * + * Example: For ListTables operation, it will be "ListTablesResponse" class. + */ + protected ClassName responseType() { + return poetExtensions.getModelClass(operationModel.getReturnType().getReturnType()); + } + + // Generates + // private final ListTablesRequest firstRequest; + protected FieldSpec requestClassField() { + return FieldSpec.builder(requestType(), REQUEST_MEMBER, Modifier.PRIVATE, Modifier.FINAL).build(); + } + + protected String nextPageFetcherClassName() { + return operationModel.getReturnType().getReturnType() + "Fetcher"; + } + + protected MethodSpec.Builder resumeMethodBuilder() { + return MethodSpec.methodBuilder(RESUME_METHOD) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addParameter(responseType(), LAST_SUCCESSFUL_PAGE_LITERAL, Modifier.FINAL) + .returns(className()) + .addCode(CodeBlock.builder() + .beginControlFlow("if ($L.$L($L))", NEXT_PAGE_FETCHER_MEMBER, + HAS_NEXT_PAGE_METHOD, LAST_SUCCESSFUL_PAGE_LITERAL) + .addStatement("return new $T($L, $L)", className(), CLIENT_MEMBER, + constructRequestFromLastPage(LAST_SUCCESSFUL_PAGE_LITERAL)) + .endControlFlow() + .build()) + .addJavadoc(CodeBlock.builder() + .add("

A helper method to resume the pages in case of unexpected failures. " + + "The method takes the last successful response page as input and returns an " + + "instance of {@link $T} that can be used to retrieve the consecutive pages " + + "that follows the input page.

", className()) + .build()); + } + + /* + * Returns the {@link TypeName} for a value in the {@link PaginatorDefinition#getResultKey()} list. + * + * Examples: + * If paginated item is represented as List, then member type is String. + * If paginated item is represented as List, then member type is Foo. + * If paginated item is represented as Map>, + * then member type is Map.Entry>. + */ + protected TypeName getTypeForResultKey(String singleResultKey) { + MemberModel resultKeyModel = memberModelForResponseMember(singleResultKey); + + if (resultKeyModel == null) { + throw new InvalidParameterException("MemberModel is not found for result key: " + singleResultKey); + } + + if (resultKeyModel.isList()) { + return typeProvider.fieldType(resultKeyModel.getListModel().getListMemberModel()); + } else if (resultKeyModel.isMap()) { + return typeProvider.mapEntryWithConcreteTypes(resultKeyModel.getMapModel()); + } else { + throw new IllegalArgumentException(String.format("Key %s in paginated operation %s should be either a list or a map", + singleResultKey, c2jOperationName)); + } + } + + /** + * @param input A top level or nested member in response of {@link #c2jOperationName}. + * + * @return The {@link MemberModel} of the {@link PaginatorDefinition#getResultKey()}. If input value is nested, + * then member model of the last child shape is returned. + * + * For example, if input is StreamDescription.Shards, then the return value is "Shard" which is the member model for + * the Shards. + */ + protected MemberModel memberModelForResponseMember(String input) { + final String[] hierarchy = input.split("\\."); + + if (hierarchy.length < 1) { + throw new IllegalArgumentException(String.format("Error when splitting value %s for operation %s", + input, c2jOperationName)); + } + + ShapeModel shape = operationModel.getOutputShape(); + + for (int i = 0; i < hierarchy.length - 1; i++) { + shape = shape.findMemberModelByC2jName(hierarchy[i]).getShape(); + } + + return shape.getMemberByC2jName(hierarchy[hierarchy.length - 1]); + } + + protected String hasNextPageMethodBody() { + String body; + + if (paginatorDefinition.getMoreResults() != null) { + body = String.format("return %s.%s.booleanValue()", + PREVIOUS_PAGE_METHOD_ARGUMENT, + fluentGetterMethodForResponseMember(paginatorDefinition.getMoreResults())); + } else { + // If there is no more_results token, then output_token will be a single value + body = String.format("return %s.%s != null", + PREVIOUS_PAGE_METHOD_ARGUMENT, + fluentGetterMethodsForOutputToken().get(0)); + } + + return body; + } + + /* + * Returns {@link CodeBlock} for the NEXT_PAGE_METHOD. + * + * A sample from dynamoDB listTables paginator: + * + * if (oldPage == null) { + * return client.listTables(firstRequest); + * } else { + * return client.listTables(firstRequest.toBuilder().exclusiveStartTableName(response.lastEvaluatedTableName()) + * .build()); + * } + */ + protected CodeBlock nextPageMethodBody() { + return CodeBlock.builder() + .beginControlFlow("if ($L == null)", PREVIOUS_PAGE_METHOD_ARGUMENT) + .addStatement("return $L.$L($L)", CLIENT_MEMBER, operationModel.getMethodName(), REQUEST_MEMBER) + .endControlFlow() + .addStatement(codeToGetNextPageIfOldResponseIsNotNull()) + .build(); + } + + /** + * Generates the code to get next page by using values from old page. + * + * Sample generated code: + * return client.listTables(firstRequest.toBuilder().exclusiveStartTableName(response.lastEvaluatedTableName()).build()); + */ + private String codeToGetNextPageIfOldResponseIsNotNull() { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("return %s.%s(%s)", CLIENT_MEMBER, + operationModel.getMethodName(), + constructRequestFromLastPage(PREVIOUS_PAGE_METHOD_ARGUMENT))); + return sb.toString(); + } + + /** + * Generates the code to construct a request object from the last successful page + * by setting the fields required to get the next page. + * + * Sample code: if responsePage string is "response" + * firstRequest.toBuilder().exclusiveStartTableName(response.lastEvaluatedTableName()).build() + */ + protected String constructRequestFromLastPage(String responsePage) { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("%s.toBuilder()", REQUEST_MEMBER)); + + List requestSetterNames = fluentSetterMethodNamesForInputToken(); + List responseGetterMethods = fluentGetterMethodsForOutputToken(); + + for (int i = 0; i < paginatorDefinition.getInputToken().size(); i++) { + sb.append(String.format(".%s(%s.%s)", requestSetterNames.get(i), responsePage, + responseGetterMethods.get(i))); + } + + sb.append(".build()"); + return sb.toString(); + } + + /** + * Returns a list of fluent setter method names for members in {@link PaginatorDefinition#getInputToken()} list. + * The size of list returned by this method is equal to the size of {@link PaginatorDefinition#getInputToken()} list. + */ + private List fluentSetterMethodNamesForInputToken() { + return paginatorDefinition.getInputToken().stream() + .map(this::fluentSetterNameForSingleInputToken) + .collect(Collectors.toList()); + } + + /** + * Returns the fluent setter method name for a single member in the request. + * + * The values in {@link PaginatorDefinition#getInputToken()} are not nested unlike + * {@link PaginatorDefinition#getOutputToken()}. + */ + private String fluentSetterNameForSingleInputToken(String inputToken) { + return operationModel.getInputShape() + .findMemberModelByC2jName(inputToken) + .getFluentSetterMethodName(); + } + + /** + * Returns a list of fluent getter methods for members in {@link PaginatorDefinition#getOutputToken()} list. + * The size of list returned by this method is equal to the size of {@link PaginatorDefinition#getOutputToken()} list. + */ + private List fluentGetterMethodsForOutputToken() { + return paginatorDefinition.getOutputToken().stream() + .map(this::fluentGetterMethodForResponseMember) + .collect(Collectors.toList()); + } + + /** + * Returns the fluent getter method for a single member in the response. + * The returned String includes the '()' after each method name. + * + * The input member can be a nested String. An example would be StreamDescription.LastEvaluatedShardId + * which represents LastEvaluatedShardId member in StreamDescription class. The return value for it + * would be "streamDescription().lastEvaluatedShardId()" + * + * @param member A top level or nested member in response of {@link #c2jOperationName}. + */ + private String fluentGetterMethodForResponseMember(String member) { + final String[] hierarchy = member.split("\\."); + + if (hierarchy.length < 1) { + throw new IllegalArgumentException(String.format("Error when splitting member %s for operation %s", + member, c2jOperationName)); + } + + ShapeModel parentShape = operationModel.getOutputShape(); + final StringBuilder getterMethod = new StringBuilder(); + + for (String str : hierarchy) { + getterMethod.append(".") + .append(parentShape.findMemberModelByC2jName(str).getFluentGetterMethodName()) + .append("()"); + + parentShape = parentShape.findMemberModelByC2jName(str).getShape(); + } + + return getterMethod.substring(1); + } + + protected CodeBlock getIteratorLambdaBlock(String resultKey, MemberModel resultKeyModel) { + final String conditionalStatement = getConditionalStatementforIteratorLambda(resultKey); + final String fluentGetter = fluentGetterMethodForResponseMember(resultKey); + + CodeBlock iteratorBlock = null; + if (resultKeyModel.isList()) { + iteratorBlock = CodeBlock.builder().addStatement("return $L.$L.iterator()", RESPONSE_LITERAL, fluentGetter).build(); + + } else if (resultKeyModel.isMap()) { + iteratorBlock = CodeBlock.builder().addStatement("return $L.$L.entrySet().iterator()", + RESPONSE_LITERAL, + fluentGetter).build(); + } + + CodeBlock conditionalBlock = CodeBlock.builder() + .beginControlFlow("if ($L)", conditionalStatement) + .add(iteratorBlock) + .endControlFlow() + .addStatement("return $T.emptyIterator()", TypeName.get(Collections.class)) + .build(); + + return CodeBlock.builder() + .add("$L -> { $L };", RESPONSE_LITERAL, conditionalBlock) + .build(); + } + + /** + * Returns a conditional statement string that verifies the fluent methods to return result key are not null. + * + * If resultKey is StreamDescription.LastEvaluatedShardId, output of this method would be + * "response != null && response.streamDescription() != null && response.streamDescription().lastEvaluatedShardId() != null" + * + * @param resultKey A top level or nested member in response of {@link #c2jOperationName}. + */ + private String getConditionalStatementforIteratorLambda(String resultKey) { + final String[] hierarchy = resultKey.split("\\."); + + if (hierarchy.length < 1) { + throw new IllegalArgumentException(String.format("Error when splitting member %s for operation %s", + resultKey, c2jOperationName)); + } + + String currentFluentMethod = RESPONSE_LITERAL; + ShapeModel parentShape = operationModel.getOutputShape(); + + final StringBuilder conditionStatement = new StringBuilder(String.format("%s != null", currentFluentMethod)); + + for (String str : hierarchy) { + currentFluentMethod = String.format("%s.%s()", currentFluentMethod, parentShape.findMemberModelByC2jName(str) + .getFluentGetterMethodName()); + conditionStatement.append(" && "); + conditionStatement.append(String.format("%s != null", currentFluentMethod)); + + parentShape = parentShape.findMemberModelByC2jName(str).getShape(); + } + + return conditionStatement.toString(); + } +} \ No newline at end of file diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/SyncResponseClassSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/SyncResponseClassSpec.java new file mode 100644 index 000000000000..a0d450d4e41e --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/SyncResponseClassSpec.java @@ -0,0 +1,226 @@ +/* + * Copyright 2010-2018 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.codegen.poet.paginators; + +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.Collections; +import java.util.Iterator; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.lang.model.element.Modifier; +import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; +import software.amazon.awssdk.codegen.model.intermediate.MemberModel; +import software.amazon.awssdk.codegen.model.service.PaginatorDefinition; +import software.amazon.awssdk.codegen.poet.ClassSpec; +import software.amazon.awssdk.codegen.poet.PoetUtils; +import software.amazon.awssdk.core.pagination.PaginatedItemsIterable; +import software.amazon.awssdk.core.pagination.PaginatedResponsesIterator; +import software.amazon.awssdk.core.pagination.SdkIterable; +import software.amazon.awssdk.core.pagination.SyncPageFetcher; + +/** + * Java poet {@link ClassSpec} to generate the response class for sync paginated operations. + */ +public class SyncResponseClassSpec extends PaginatorsClassSpec { + + private static final String ITERATOR_METHOD = "iterator"; + + public SyncResponseClassSpec(IntermediateModel model, String c2jOperationName, PaginatorDefinition paginatorDefinition) { + super(model, c2jOperationName, paginatorDefinition); + } + + @Override + public TypeSpec poetSpec() { + TypeSpec.Builder specBuilder = TypeSpec.classBuilder(className()) + .addModifiers(Modifier.PUBLIC) + .addAnnotation(PoetUtils.GENERATED) + .addSuperinterface(getSyncResponseInterface()) + .addFields(Stream.of(syncClientInterfaceField(), + requestClassField(), + syncPageFetcherField()) + .collect(Collectors.toList())) + .addMethod(constructor()) + .addMethod(iteratorMethod()) + .addMethods(getMethodSpecsForResultKeyList()) + .addMethod(resumeMethod()) + .addJavadoc(paginationDocs.getDocsForSyncResponseClass( + getClientInterfaceName())) + .addType(nextPageFetcherClass()); + + return specBuilder.build(); + } + + @Override + public ClassName className() { + return poetExtensions.getResponseClassForPaginatedSyncOperation(c2jOperationName); + } + + /** + * Returns the interface that is implemented by the Paginated Sync Response class. + */ + private TypeName getSyncResponseInterface() { + return ParameterizedTypeName.get(ClassName.get(SdkIterable.class), responseType()); + } + + /** + * @return A Poet {@link ClassName} for the sync client interface + */ + private ClassName getClientInterfaceName() { + return poetExtensions.getClientClass(model.getMetadata().getSyncInterface()); + } + + private FieldSpec syncClientInterfaceField() { + return FieldSpec.builder(getClientInterfaceName(), CLIENT_MEMBER, Modifier.PRIVATE, Modifier.FINAL).build(); + } + + private FieldSpec syncPageFetcherField() { + return FieldSpec.builder(SyncPageFetcher.class, NEXT_PAGE_FETCHER_MEMBER, Modifier.PRIVATE, Modifier.FINAL).build(); + } + + private MethodSpec constructor() { + return MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(getClientInterfaceName(), CLIENT_MEMBER, Modifier.FINAL) + .addParameter(requestType(), REQUEST_MEMBER, Modifier.FINAL) + .addStatement("this.$L = $L", CLIENT_MEMBER, CLIENT_MEMBER) + .addStatement("this.$L = $L", REQUEST_MEMBER, REQUEST_MEMBER) + .addStatement("this.$L = new $L()", NEXT_PAGE_FETCHER_MEMBER, nextPageFetcherClassName()) + .build(); + } + + /** + * A {@link MethodSpec} for the overridden iterator() method which is inherited + * from the interface. + */ + private MethodSpec iteratorMethod() { + return MethodSpec.methodBuilder(ITERATOR_METHOD) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(ParameterizedTypeName.get(ClassName.get(Iterator.class), responseType())) + .addStatement("return new $T($L)", PaginatedResponsesIterator.class, NEXT_PAGE_FETCHER_MEMBER) + .build(); + } + + /** + * Returns iterable of {@link MethodSpec} to generate helper methods for all members + * in {@link PaginatorDefinition#getResultKey()}. All the generated methods return an SdkIterable. + * + * The helper methods to iterate on paginated member will be generated only + * if {@link PaginatorDefinition#getResultKey()} is not null and a non-empty list. + */ + private Iterable getMethodSpecsForResultKeyList() { + if (paginatorDefinition.getResultKey() != null) { + return paginatorDefinition.getResultKey().stream() + .map(this::getMethodsSpecForSingleResultKey) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + } + + /* + * Generate a method spec for single element in {@link PaginatorDefinition#getResultKey()} list. + * + * If the element is "Folders" and its type is "List", generated code looks like: + * + * public SdkIterable folders() { + * Function> getIterator = response -> { + * if (response != null && response.folders() != null) { + * return response.folders().iterator(); + * } + * return Collections.emptyIterator(); + * }; + * return new PaginatedItemsIterable(this, getIterator); + * } + */ + private MethodSpec getMethodsSpecForSingleResultKey(String resultKey) { + TypeName resultKeyType = getTypeForResultKey(resultKey); + MemberModel resultKeyModel = memberModelForResponseMember(resultKey); + + return MethodSpec.methodBuilder(resultKeyModel.getFluentGetterMethodName()) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .returns(ParameterizedTypeName.get(ClassName.get(SdkIterable.class), resultKeyType)) + .addCode("$T getIterator = ", + ParameterizedTypeName.get(ClassName.get(Function.class), + responseType(), + ParameterizedTypeName.get(ClassName.get(Iterator.class), + resultKeyType))) + .addCode(getIteratorLambdaBlock(resultKey, resultKeyModel)) + .addCode("\n") + .addStatement("return new $T(this, getIterator)", PaginatedItemsIterable.class) + .addJavadoc(CodeBlock.builder() + .add("Returns an iterable to iterate through the paginated {@link $T#$L()} member." + + " The returned iterable is used to iterate through the results across all " + + "response pages and not a single page.\n", + responseType(), resultKeyModel.getFluentGetterMethodName()) + .add("\n") + .add("This method is useful if you are interested in iterating over the paginated " + + "member in the response pages instead of the top level pages. " + + "Similar to iteration over pages, this method internally makes service " + + "calls to get the next list of results until the iteration stops or " + + "there are no more results.") + .build()) + .build(); + } + + /** + * Generates a inner class that implements {@link SyncPageFetcher}. An instance of this class + * is passed to {@link PaginatedResponsesIterator} to be used while iterating through pages. + */ + private TypeSpec nextPageFetcherClass() { + return TypeSpec.classBuilder(nextPageFetcherClassName()) + .addModifiers(Modifier.PRIVATE) + .addSuperinterface(ParameterizedTypeName.get(ClassName.get(SyncPageFetcher.class), responseType())) + .addMethod(MethodSpec.methodBuilder(HAS_NEXT_PAGE_METHOD) + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .addParameter(responseType(), PREVIOUS_PAGE_METHOD_ARGUMENT) + .returns(boolean.class) + .addStatement(hasNextPageMethodBody()) + .build()) + .addMethod(MethodSpec.methodBuilder(NEXT_PAGE_METHOD) + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .addParameter(responseType(), PREVIOUS_PAGE_METHOD_ARGUMENT) + .returns(responseType()) + .addCode(nextPageMethodBody()) + .build()) + .build(); + } + + private MethodSpec resumeMethod() { + return resumeMethodBuilder().addStatement("return $L", anonymousClassWithEmptyIterator()) + .build(); + } + + private TypeSpec anonymousClassWithEmptyIterator() { + return TypeSpec.anonymousClassBuilder("$L, $L", CLIENT_MEMBER, REQUEST_MEMBER) + .addSuperinterface(className()) + .addMethod(MethodSpec.methodBuilder(ITERATOR_METHOD) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(ParameterizedTypeName.get(ClassName.get(Iterator.class), responseType())) + .addStatement("return $T.emptyIterator()", TypeName.get(Collections.class)) + .build()) + .build(); + } +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/utils/PaginatorUtils.java b/codegen/src/main/java/software/amazon/awssdk/codegen/utils/PaginatorUtils.java index 2e6648352a51..4e43e22d531e 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/utils/PaginatorUtils.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/utils/PaginatorUtils.java @@ -24,10 +24,10 @@ private PaginatorUtils() { /** * @param methodName Name of a method in sync client - * @return the name of the pagination enabled sync operation + * @return the name of the auto-pagination enabled operation */ @ReviewBeforeRelease("Naming of paginated APIs") - public static String getSyncMethodName(String methodName) { - return methodName + "Iterable"; + public static String getPaginatedMethodName(String methodName) { + return methodName + "Paginator"; } } diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/paginators/PaginatorResponseClassSpecTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/paginators/PaginatedResponseClassSpecTest.java similarity index 71% rename from codegen/src/test/java/software/amazon/awssdk/codegen/poet/paginators/PaginatorResponseClassSpecTest.java rename to codegen/src/test/java/software/amazon/awssdk/codegen/poet/paginators/PaginatedResponseClassSpecTest.java index c03f8327e59d..974a1cc7bb5b 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/paginators/PaginatorResponseClassSpecTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/paginators/PaginatedResponseClassSpecTest.java @@ -31,17 +31,17 @@ import software.amazon.awssdk.codegen.poet.ClassSpec; import software.amazon.awssdk.codegen.utils.ModelLoaderUtils; -public class PaginatorResponseClassSpecTest { +public class PaginatedResponseClassSpecTest { private static IntermediateModel intermediateModel; private static Paginators paginators; @BeforeClass public static void setUp() throws IOException { - File serviceModelFile = new File(PaginatorResponseClassSpecTest.class.getResource("service-2.json").getFile()); - File customizationConfigFile = new File(PaginatorResponseClassSpecTest.class.getResource("customization.config") + File serviceModelFile = new File(PaginatedResponseClassSpecTest.class.getResource("service-2.json").getFile()); + File customizationConfigFile = new File(PaginatedResponseClassSpecTest.class.getResource("customization.config") .getFile()); - File paginatorsModel = new File(PaginatorResponseClassSpecTest.class.getResource("paginators.json") + File paginatorsModel = new File(PaginatedResponseClassSpecTest.class.getResource("paginators.json") .getFile()); paginators = getPaginatorsModel(paginatorsModel); @@ -60,15 +60,29 @@ private static Paginators getPaginatorsModel(File file) { } @Test - public void testGeneratedResponseClassesForPaginatedOperations() { + public void testGeneratedResponseForSyncOperations() { paginators.getPaginators().entrySet() .stream() .filter(entry -> entry.getValue().isValid()) .forEach(entry -> { - ClassSpec classSpec = new PaginatorResponseClassSpec(intermediateModel, - entry.getKey(), - entry.getValue()); + ClassSpec classSpec = new SyncResponseClassSpec(intermediateModel, + entry.getKey(), + entry.getValue()); + assertThat(classSpec, generatesTo(classSpec.className().simpleName() + ".java")); + }); + } + + @Test + public void testGeneratedResponseForAsyncOperations() { + paginators.getPaginators().entrySet() + .stream() + .filter(entry -> entry.getValue().isValid()) + .forEach(entry -> + { + ClassSpec classSpec = new AsyncResponseClassSpec(intermediateModel, + entry.getKey(), + entry.getValue()); assertThat(classSpec, generatesTo(classSpec.className().simpleName() + ".java")); }); } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/json/customization.config b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/json/customization.config index 1d9307c09d0f..1e2ac52e4f2f 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/json/customization.config +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/json/customization.config @@ -4,5 +4,6 @@ }, "presignersFqcn": "software.amazon.awssdk.services.acm.presign.AcmClientPresigners", "serviceSpecificHttpConfig": "MyServiceHttpConfig.CONFIG", - "serviceSpecificClientConfigClass": "AdvancedConfiguration" + "serviceSpecificClientConfigClass": "AdvancedConfiguration", + "verifiedSimpleMethods" : ["paginatedOperationWithResultKey"] } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/json/service-2.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/json/service-2.json index 673a6ad2a685..0f58e416c3c1 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/json/service-2.json +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/json/service-2.json @@ -237,9 +237,6 @@ }, "PaginatedOperationWithResultKeyRequest": { "type": "structure", - "required": [ - "NextToken" - ], "members": { "NextToken": { "shape": "subMember", diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-async-client-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-async-client-class.java index f05b9bcccd88..be0e7eb9661e 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-async-client-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-async-client-class.java @@ -31,6 +31,8 @@ import software.amazon.awssdk.services.json.model.StreamingInputOperationResponse; import software.amazon.awssdk.services.json.model.StreamingOutputOperationRequest; import software.amazon.awssdk.services.json.model.StreamingOutputOperationResponse; +import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPublisher; +import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPublisher; import software.amazon.awssdk.services.json.transform.APostOperationRequestMarshaller; import software.amazon.awssdk.services.json.transform.APostOperationResponseUnmarshaller; import software.amazon.awssdk.services.json.transform.APostOperationWithOutputRequestMarshaller; @@ -95,14 +97,14 @@ public final String serviceName() { public CompletableFuture aPostOperation(APostOperationRequest aPostOperationRequest) { HttpResponseHandler responseHandler = protocolFactory.createResponseHandler( - new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), - new APostOperationResponseUnmarshaller()); + new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), + new APostOperationResponseUnmarshaller()); HttpResponseHandler errorResponseHandler = createErrorResponseHandler(); return clientHandler.execute(new ClientExecutionParams() - .withMarshaller(new APostOperationRequestMarshaller(protocolFactory)).withResponseHandler(responseHandler) - .withErrorResponseHandler(errorResponseHandler).withInput(aPostOperationRequest)); + .withMarshaller(new APostOperationRequestMarshaller(protocolFactory)).withResponseHandler(responseHandler) + .withErrorResponseHandler(errorResponseHandler).withInput(aPostOperationRequest)); } /** @@ -130,19 +132,19 @@ public CompletableFuture aPostOperation(APostOperationRe */ @Override public CompletableFuture aPostOperationWithOutput( - APostOperationWithOutputRequest aPostOperationWithOutputRequest) { + APostOperationWithOutputRequest aPostOperationWithOutputRequest) { HttpResponseHandler responseHandler = protocolFactory.createResponseHandler( - new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), - new APostOperationWithOutputResponseUnmarshaller()); + new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), + new APostOperationWithOutputResponseUnmarshaller()); HttpResponseHandler errorResponseHandler = createErrorResponseHandler(); return clientHandler - .execute(new ClientExecutionParams() - .withMarshaller(new APostOperationWithOutputRequestMarshaller(protocolFactory)) - .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) - .withInput(aPostOperationWithOutputRequest)); + .execute(new ClientExecutionParams() + .withMarshaller(new APostOperationWithOutputRequestMarshaller(protocolFactory)) + .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) + .withInput(aPostOperationWithOutputRequest)); } /** @@ -170,19 +172,19 @@ public CompletableFuture aPostOperationWithOut */ @Override public CompletableFuture getWithoutRequiredMembers( - GetWithoutRequiredMembersRequest getWithoutRequiredMembersRequest) { + GetWithoutRequiredMembersRequest getWithoutRequiredMembersRequest) { HttpResponseHandler responseHandler = protocolFactory.createResponseHandler( - new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), - new GetWithoutRequiredMembersResponseUnmarshaller()); + new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), + new GetWithoutRequiredMembersResponseUnmarshaller()); HttpResponseHandler errorResponseHandler = createErrorResponseHandler(); return clientHandler - .execute(new ClientExecutionParams() - .withMarshaller(new GetWithoutRequiredMembersRequestMarshaller(protocolFactory)) - .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) - .withInput(getWithoutRequiredMembersRequest)); + .execute(new ClientExecutionParams() + .withMarshaller(new GetWithoutRequiredMembersRequestMarshaller(protocolFactory)) + .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) + .withInput(getWithoutRequiredMembersRequest)); } /** @@ -207,19 +209,19 @@ public CompletableFuture getWithoutRequiredMe */ @Override public CompletableFuture paginatedOperationWithResultKey( - PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) { + PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) { HttpResponseHandler responseHandler = protocolFactory.createResponseHandler( - new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), - new PaginatedOperationWithResultKeyResponseUnmarshaller()); + new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), + new PaginatedOperationWithResultKeyResponseUnmarshaller()); HttpResponseHandler errorResponseHandler = createErrorResponseHandler(); return clientHandler - .execute(new ClientExecutionParams() - .withMarshaller(new PaginatedOperationWithResultKeyRequestMarshaller(protocolFactory)) - .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) - .withInput(paginatedOperationWithResultKeyRequest)); + .execute(new ClientExecutionParams() + .withMarshaller(new PaginatedOperationWithResultKeyRequestMarshaller(protocolFactory)) + .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) + .withInput(paginatedOperationWithResultKeyRequest)); } /** @@ -244,19 +246,19 @@ public CompletableFuture paginatedOpera */ @Override public CompletableFuture paginatedOperationWithoutResultKey( - PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) { + PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) { HttpResponseHandler responseHandler = protocolFactory.createResponseHandler( - new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), - new PaginatedOperationWithoutResultKeyResponseUnmarshaller()); + new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), + new PaginatedOperationWithoutResultKeyResponseUnmarshaller()); HttpResponseHandler errorResponseHandler = createErrorResponseHandler(); return clientHandler - .execute(new ClientExecutionParams() - .withMarshaller(new PaginatedOperationWithoutResultKeyRequestMarshaller(protocolFactory)) - .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) - .withInput(paginatedOperationWithoutResultKeyRequest)); + .execute(new ClientExecutionParams() + .withMarshaller(new PaginatedOperationWithoutResultKeyRequestMarshaller(protocolFactory)) + .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) + .withInput(paginatedOperationWithoutResultKeyRequest)); } /** @@ -286,18 +288,18 @@ public CompletableFuture paginatedOp */ @Override public CompletableFuture streamingInputOperation( - StreamingInputOperationRequest streamingInputOperationRequest, AsyncRequestProvider requestProvider) { + StreamingInputOperationRequest streamingInputOperationRequest, AsyncRequestProvider requestProvider) { HttpResponseHandler responseHandler = protocolFactory.createResponseHandler( - new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), - new StreamingInputOperationResponseUnmarshaller()); + new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), + new StreamingInputOperationResponseUnmarshaller()); HttpResponseHandler errorResponseHandler = createErrorResponseHandler(); return clientHandler.execute(new ClientExecutionParams() - .withMarshaller(new StreamingInputOperationRequestMarshaller(protocolFactory)) - .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) - .withAsyncRequestProvider(requestProvider).withInput(streamingInputOperationRequest)); + .withMarshaller(new StreamingInputOperationRequestMarshaller(protocolFactory)) + .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) + .withAsyncRequestProvider(requestProvider).withInput(streamingInputOperationRequest)); } /** @@ -326,20 +328,166 @@ public CompletableFuture streamingInputOperatio */ @Override public CompletableFuture streamingOutputOperation( - StreamingOutputOperationRequest streamingOutputOperationRequest, - AsyncResponseHandler asyncResponseHandler) { + StreamingOutputOperationRequest streamingOutputOperationRequest, + AsyncResponseHandler asyncResponseHandler) { HttpResponseHandler responseHandler = protocolFactory.createResponseHandler( - new JsonOperationMetadata().withPayloadJson(false).withHasStreamingSuccessResponse(true), - new StreamingOutputOperationResponseUnmarshaller()); + new JsonOperationMetadata().withPayloadJson(false).withHasStreamingSuccessResponse(true), + new StreamingOutputOperationResponseUnmarshaller()); HttpResponseHandler errorResponseHandler = createErrorResponseHandler(); return clientHandler.execute( - new ClientExecutionParams() - .withMarshaller(new StreamingOutputOperationRequestMarshaller(protocolFactory)) - .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) - .withInput(streamingOutputOperationRequest), asyncResponseHandler); + new ClientExecutionParams() + .withMarshaller(new StreamingOutputOperationRequestMarshaller(protocolFactory)) + .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) + .withInput(streamingOutputOperationRequest), asyncResponseHandler); + } + + /** + * Some paginated operation with result_key in paginators.json file
+ *

+ * This is a variant of + * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} + * operation. The return type is a custom publisher that can be subscribed to request a stream of response pages. + * SDK will internally handle making service calls for you. + *

+ *

+ * When the operation is called, an instance of this class is returned. At this point, no service calls are made yet + * and so there is no guarantee that the request is valid. If there are errors in your request, you will see the + * failures only after you start streaming the data. The subscribe method should be called as a request to start + * streaming data. For more info, see + * {@link org.reactivestreams.Publisher#subscribe(org.reactivestreams.Subscriber)}. Each call to the subscribe + * method will result in a new {@link org.reactivestreams.Subscription} i.e., a new contract to stream data from the + * starting request. + *

+ * + *

+ * The following are few ways to use the response class: + *

+ * 1) Using the forEach helper method + * + *
+     * {@code
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPublisher publisher = client.paginatedOperationWithResultKeyPaginator(request);
+     * CompletableFuture future = publisher.forEach(res -> { // Do something with the response });
+     * future.get();
+     * }
+     * 
+ * + * 2) Using a custom subscriber + * + *
+     * {@code
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPublisher publisher = client.paginatedOperationWithResultKeyPaginator(request);
+     * publisher.subscribe(new Subscriber() {
+     *
+     * public void onSubscribe(org.reactivestreams.Subscriber subscription) { //... };
+     *
+     *
+     * public void onNext(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyResponse response) { //... };
+     * });}
+     * 
+ * + * As the response is a publisher, it can work well with third party reactive streams implementations like RxJava2. + *

+ * Note: If you prefer to have control on service calls, use the + * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} + * operation. + *

+ * + * @param paginatedOperationWithResultKeyRequest + * @return A custom publisher that can be subscribed to request a stream of response pages.
+ * The CompletableFuture returned by this method can be completed exceptionally with the following + * exceptions. + *
    + *
  • SdkException Base class for all exceptions that can be thrown by the SDK (both service and client). + * Can be used for catch all scenarios.
  • + *
  • SdkClientException If any client side error occurs such as an IO related failure, failure to get + * credentials, etc.
  • + *
  • JsonException Base class for all service exceptions. Unknown exceptions will be thrown as an instance + * of this type.
  • + *
+ * @sample JsonAsyncClient.PaginatedOperationWithResultKey + * @see AWS API Documentation + */ + public PaginatedOperationWithResultKeyPublisher paginatedOperationWithResultKeyPaginator( + PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) { + return new PaginatedOperationWithResultKeyPublisher(this, paginatedOperationWithResultKeyRequest); + } + + /** + * Some paginated operation without result_key in paginators.json file
+ *

+ * This is a variant of + * {@link #paginatedOperationWithoutResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyRequest)} + * operation. The return type is a custom publisher that can be subscribed to request a stream of response pages. + * SDK will internally handle making service calls for you. + *

+ *

+ * When the operation is called, an instance of this class is returned. At this point, no service calls are made yet + * and so there is no guarantee that the request is valid. If there are errors in your request, you will see the + * failures only after you start streaming the data. The subscribe method should be called as a request to start + * streaming data. For more info, see + * {@link org.reactivestreams.Publisher#subscribe(org.reactivestreams.Subscriber)}. Each call to the subscribe + * method will result in a new {@link org.reactivestreams.Subscription} i.e., a new contract to stream data from the + * starting request. + *

+ * + *

+ * The following are few ways to use the response class: + *

+ * 1) Using the forEach helper method + * + *
+     * {@code
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPublisher publisher = client.paginatedOperationWithoutResultKeyPaginator(request);
+     * CompletableFuture future = publisher.forEach(res -> { // Do something with the response });
+     * future.get();
+     * }
+     * 
+ * + * 2) Using a custom subscriber + * + *
+     * {@code
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPublisher publisher = client.paginatedOperationWithoutResultKeyPaginator(request);
+     * publisher.subscribe(new Subscriber() {
+     *
+     * public void onSubscribe(org.reactivestreams.Subscriber subscription) { //... };
+     *
+     *
+     * public void onNext(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyResponse response) { //... };
+     * });}
+     * 
+ * + * As the response is a publisher, it can work well with third party reactive streams implementations like RxJava2. + *

+ * Note: If you prefer to have control on service calls, use the + * {@link #paginatedOperationWithoutResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyRequest)} + * operation. + *

+ * + * @param paginatedOperationWithoutResultKeyRequest + * @return A custom publisher that can be subscribed to request a stream of response pages.
+ * The CompletableFuture returned by this method can be completed exceptionally with the following + * exceptions. + *
    + *
  • SdkException Base class for all exceptions that can be thrown by the SDK (both service and client). + * Can be used for catch all scenarios.
  • + *
  • SdkClientException If any client side error occurs such as an IO related failure, failure to get + * credentials, etc.
  • + *
  • JsonException Base class for all service exceptions. Unknown exceptions will be thrown as an instance + * of this type.
  • + *
+ * @sample JsonAsyncClient.PaginatedOperationWithoutResultKey + * @see AWS API Documentation + */ + public PaginatedOperationWithoutResultKeyPublisher paginatedOperationWithoutResultKeyPaginator( + PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) { + return new PaginatedOperationWithoutResultKeyPublisher(this, paginatedOperationWithoutResultKeyRequest); } @Override @@ -349,13 +497,13 @@ public void close() { private software.amazon.awssdk.core.protocol.json.SdkJsonProtocolFactory init() { return new SdkJsonProtocolFactory(new JsonClientMetadata() - .withProtocolVersion("1.1") - .withSupportsCbor(false) - .withSupportsIon(false) - .withBaseServiceExceptionClass(software.amazon.awssdk.services.json.model.JsonException.class) - .withContentTypeOverride("") - .addErrorMetadata( - new JsonErrorShapeMetadata().withErrorCode("InvalidInput").withModeledClass(InvalidInputException.class))); + .withProtocolVersion("1.1") + .withSupportsCbor(false) + .withSupportsIon(false) + .withBaseServiceExceptionClass(software.amazon.awssdk.services.json.model.JsonException.class) + .withContentTypeOverride("") + .addErrorMetadata( + new JsonErrorShapeMetadata().withErrorCode("InvalidInput").withModeledClass(InvalidInputException.class))); } private HttpResponseHandler createErrorResponseHandler() { diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-async-client-interface.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-async-client-interface.java index 4214500d0fab..97a265cad9d6 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-async-client-interface.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-async-client-interface.java @@ -21,6 +21,8 @@ import software.amazon.awssdk.services.json.model.StreamingInputOperationResponse; import software.amazon.awssdk.services.json.model.StreamingOutputOperationRequest; import software.amazon.awssdk.services.json.model.StreamingOutputOperationResponse; +import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPublisher; +import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPublisher; import software.amazon.awssdk.utils.SdkAutoCloseable; /** @@ -131,7 +133,7 @@ default CompletableFuture aPostOperation(ConsumerAWS API Documentation */ default CompletableFuture aPostOperationWithOutput( - APostOperationWithOutputRequest aPostOperationWithOutputRequest) { + APostOperationWithOutputRequest aPostOperationWithOutputRequest) { throw new UnsupportedOperationException(); } @@ -163,7 +165,7 @@ default CompletableFuture aPostOperationWithOu * target="_top">AWS API Documentation */ default CompletableFuture aPostOperationWithOutput( - Consumer aPostOperationWithOutputRequest) { + Consumer aPostOperationWithOutputRequest) { return aPostOperationWithOutput(APostOperationWithOutputRequest.builder().apply(aPostOperationWithOutputRequest).build()); } @@ -191,7 +193,7 @@ default CompletableFuture aPostOperationWithOu * target="_top">AWS API Documentation */ default CompletableFuture getWithoutRequiredMembers( - GetWithoutRequiredMembersRequest getWithoutRequiredMembersRequest) { + GetWithoutRequiredMembersRequest getWithoutRequiredMembersRequest) { throw new UnsupportedOperationException(); } @@ -249,9 +251,9 @@ default CompletableFuture getWithoutRequiredM * target="_top">AWS API Documentation */ default CompletableFuture getWithoutRequiredMembers( - Consumer getWithoutRequiredMembersRequest) { + Consumer getWithoutRequiredMembersRequest) { return getWithoutRequiredMembers(GetWithoutRequiredMembersRequest.builder().apply(getWithoutRequiredMembersRequest) - .build()); + .build()); } /** @@ -275,10 +277,33 @@ default CompletableFuture getWithoutRequiredM * target="_top">AWS API Documentation */ default CompletableFuture paginatedOperationWithResultKey( - PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) { + PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) { throw new UnsupportedOperationException(); } + /** + * Some paginated operation with result_key in paginators.json file + * + * @return A Java Future containing the result of the PaginatedOperationWithResultKey operation returned by the + * service.
+ * The CompletableFuture returned by this method can be completed exceptionally with the following + * exceptions. + *
    + *
  • SdkException Base class for all exceptions that can be thrown by the SDK (both service and client). + * Can be used for catch all scenarios.
  • + *
  • SdkClientException If any client side error occurs such as an IO related failure, failure to get + * credentials, etc.
  • + *
  • JsonException Base class for all service exceptions. Unknown exceptions will be thrown as an instance + * of this type.
  • + *
+ * @sample JsonAsyncClient.PaginatedOperationWithResultKey + * @see AWS API Documentation + */ + default CompletableFuture paginatedOperationWithResultKey() { + return paginatedOperationWithResultKey(PaginatedOperationWithResultKeyRequest.builder().build()); + } + /** * Some paginated operation with result_key in paginators.json file
* This is a convenience which creates an instance of the {@link PaginatedOperationWithResultKeyRequest.Builder} @@ -303,9 +328,153 @@ default CompletableFuture paginatedOper * target="_top">AWS API Documentation */ default CompletableFuture paginatedOperationWithResultKey( - Consumer paginatedOperationWithResultKeyRequest) { + Consumer paginatedOperationWithResultKeyRequest) { return paginatedOperationWithResultKey(PaginatedOperationWithResultKeyRequest.builder() - .apply(paginatedOperationWithResultKeyRequest).build()); + .apply(paginatedOperationWithResultKeyRequest).build()); + } + + /** + * Some paginated operation with result_key in paginators.json file
+ *

+ * This is a variant of + * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} + * operation. The return type is a custom publisher that can be subscribed to request a stream of response pages. + * SDK will internally handle making service calls for you. + *

+ *

+ * When the operation is called, an instance of this class is returned. At this point, no service calls are made yet + * and so there is no guarantee that the request is valid. If there are errors in your request, you will see the + * failures only after you start streaming the data. The subscribe method should be called as a request to start + * streaming data. For more info, see + * {@link org.reactivestreams.Publisher#subscribe(org.reactivestreams.Subscriber)}. Each call to the subscribe + * method will result in a new {@link org.reactivestreams.Subscription} i.e., a new contract to stream data from the + * starting request. + *

+ * + *

+ * The following are few ways to use the response class: + *

+ * 1) Using the forEach helper method + * + *
+     * {@code
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPublisher publisher = client.paginatedOperationWithResultKeyPaginator(request);
+     * CompletableFuture future = publisher.forEach(res -> { // Do something with the response });
+     * future.get();
+     * }
+     * 
+ * + * 2) Using a custom subscriber + * + *
+     * {@code
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPublisher publisher = client.paginatedOperationWithResultKeyPaginator(request);
+     * publisher.subscribe(new Subscriber() {
+     *
+     * public void onSubscribe(org.reactivestreams.Subscriber subscription) { //... };
+     *
+     *
+     * public void onNext(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyResponse response) { //... };
+     * });}
+     * 
+ * + * As the response is a publisher, it can work well with third party reactive streams implementations like RxJava2. + *

+ * Note: If you prefer to have control on service calls, use the + * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} + * operation. + *

+ * + * @param paginatedOperationWithResultKeyRequest + * @return A custom publisher that can be subscribed to request a stream of response pages.
+ * The CompletableFuture returned by this method can be completed exceptionally with the following + * exceptions. + *
    + *
  • SdkException Base class for all exceptions that can be thrown by the SDK (both service and client). + * Can be used for catch all scenarios.
  • + *
  • SdkClientException If any client side error occurs such as an IO related failure, failure to get + * credentials, etc.
  • + *
  • JsonException Base class for all service exceptions. Unknown exceptions will be thrown as an instance + * of this type.
  • + *
+ * @sample JsonAsyncClient.PaginatedOperationWithResultKey + * @see AWS API Documentation + */ + default PaginatedOperationWithResultKeyPublisher paginatedOperationWithResultKeyPaginator( + PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) { + throw new UnsupportedOperationException(); + } + + /** + * Some paginated operation with result_key in paginators.json file
+ *

+ * This is a variant of + * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} + * operation. The return type is a custom publisher that can be subscribed to request a stream of response pages. + * SDK will internally handle making service calls for you. + *

+ *

+ * When the operation is called, an instance of this class is returned. At this point, no service calls are made yet + * and so there is no guarantee that the request is valid. If there are errors in your request, you will see the + * failures only after you start streaming the data. The subscribe method should be called as a request to start + * streaming data. For more info, see + * {@link org.reactivestreams.Publisher#subscribe(org.reactivestreams.Subscriber)}. Each call to the subscribe + * method will result in a new {@link org.reactivestreams.Subscription} i.e., a new contract to stream data from the + * starting request. + *

+ * + *

+ * The following are few ways to use the response class: + *

+ * 1) Using the forEach helper method + * + *
+     * {@code
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPublisher publisher = client.paginatedOperationWithResultKeyPaginator(request);
+     * CompletableFuture future = publisher.forEach(res -> { // Do something with the response });
+     * future.get();
+     * }
+     * 
+ * + * 2) Using a custom subscriber + * + *
+     * {@code
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPublisher publisher = client.paginatedOperationWithResultKeyPaginator(request);
+     * publisher.subscribe(new Subscriber() {
+     *
+     * public void onSubscribe(org.reactivestreams.Subscriber subscription) { //... };
+     *
+     *
+     * public void onNext(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyResponse response) { //... };
+     * });}
+     * 
+ * + * As the response is a publisher, it can work well with third party reactive streams implementations like RxJava2. + *

+ * Note: If you prefer to have control on service calls, use the + * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} + * operation. + *

+ * + * @return A custom publisher that can be subscribed to request a stream of response pages.
+ * The CompletableFuture returned by this method can be completed exceptionally with the following + * exceptions. + *
    + *
  • SdkException Base class for all exceptions that can be thrown by the SDK (both service and client). + * Can be used for catch all scenarios.
  • + *
  • SdkClientException If any client side error occurs such as an IO related failure, failure to get + * credentials, etc.
  • + *
  • JsonException Base class for all service exceptions. Unknown exceptions will be thrown as an instance + * of this type.
  • + *
+ * @sample JsonAsyncClient.PaginatedOperationWithResultKey + * @see AWS API Documentation + */ + default PaginatedOperationWithResultKeyPublisher paginatedOperationWithResultKeyPaginator() { + return paginatedOperationWithResultKeyPaginator(PaginatedOperationWithResultKeyRequest.builder().build()); } /** @@ -329,7 +498,7 @@ default CompletableFuture paginatedOper * target="_top">AWS API Documentation */ default CompletableFuture paginatedOperationWithoutResultKey( - PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) { + PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) { throw new UnsupportedOperationException(); } @@ -357,9 +526,82 @@ default CompletableFuture paginatedO * target="_top">AWS API Documentation */ default CompletableFuture paginatedOperationWithoutResultKey( - Consumer paginatedOperationWithoutResultKeyRequest) { + Consumer paginatedOperationWithoutResultKeyRequest) { return paginatedOperationWithoutResultKey(PaginatedOperationWithoutResultKeyRequest.builder() - .apply(paginatedOperationWithoutResultKeyRequest).build()); + .apply(paginatedOperationWithoutResultKeyRequest).build()); + } + + /** + * Some paginated operation without result_key in paginators.json file
+ *

+ * This is a variant of + * {@link #paginatedOperationWithoutResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyRequest)} + * operation. The return type is a custom publisher that can be subscribed to request a stream of response pages. + * SDK will internally handle making service calls for you. + *

+ *

+ * When the operation is called, an instance of this class is returned. At this point, no service calls are made yet + * and so there is no guarantee that the request is valid. If there are errors in your request, you will see the + * failures only after you start streaming the data. The subscribe method should be called as a request to start + * streaming data. For more info, see + * {@link org.reactivestreams.Publisher#subscribe(org.reactivestreams.Subscriber)}. Each call to the subscribe + * method will result in a new {@link org.reactivestreams.Subscription} i.e., a new contract to stream data from the + * starting request. + *

+ * + *

+ * The following are few ways to use the response class: + *

+ * 1) Using the forEach helper method + * + *
+     * {@code
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPublisher publisher = client.paginatedOperationWithoutResultKeyPaginator(request);
+     * CompletableFuture future = publisher.forEach(res -> { // Do something with the response });
+     * future.get();
+     * }
+     * 
+ * + * 2) Using a custom subscriber + * + *
+     * {@code
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPublisher publisher = client.paginatedOperationWithoutResultKeyPaginator(request);
+     * publisher.subscribe(new Subscriber() {
+     *
+     * public void onSubscribe(org.reactivestreams.Subscriber subscription) { //... };
+     *
+     *
+     * public void onNext(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyResponse response) { //... };
+     * });}
+     * 
+ * + * As the response is a publisher, it can work well with third party reactive streams implementations like RxJava2. + *

+ * Note: If you prefer to have control on service calls, use the + * {@link #paginatedOperationWithoutResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyRequest)} + * operation. + *

+ * + * @param paginatedOperationWithoutResultKeyRequest + * @return A custom publisher that can be subscribed to request a stream of response pages.
+ * The CompletableFuture returned by this method can be completed exceptionally with the following + * exceptions. + *
    + *
  • SdkException Base class for all exceptions that can be thrown by the SDK (both service and client). + * Can be used for catch all scenarios.
  • + *
  • SdkClientException If any client side error occurs such as an IO related failure, failure to get + * credentials, etc.
  • + *
  • JsonException Base class for all service exceptions. Unknown exceptions will be thrown as an instance + * of this type.
  • + *
+ * @sample JsonAsyncClient.PaginatedOperationWithoutResultKey + * @see AWS API Documentation + */ + default PaginatedOperationWithoutResultKeyPublisher paginatedOperationWithoutResultKeyPaginator( + PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) { + throw new UnsupportedOperationException(); } /** @@ -388,7 +630,7 @@ default CompletableFuture paginatedO * target="_top">AWS API Documentation */ default CompletableFuture streamingInputOperation( - StreamingInputOperationRequest streamingInputOperationRequest, AsyncRequestProvider requestProvider) { + StreamingInputOperationRequest streamingInputOperationRequest, AsyncRequestProvider requestProvider) { throw new UnsupportedOperationException(); } @@ -417,7 +659,7 @@ default CompletableFuture streamingInputOperati * target="_top">AWS API Documentation */ default CompletableFuture streamingInputOperation( - StreamingInputOperationRequest streamingInputOperationRequest, Path path) { + StreamingInputOperationRequest streamingInputOperationRequest, Path path) { return streamingInputOperation(streamingInputOperationRequest, AsyncRequestProvider.fromFile(path)); } @@ -446,8 +688,8 @@ default CompletableFuture streamingInputOperati * target="_top">AWS API Documentation */ default CompletableFuture streamingOutputOperation( - StreamingOutputOperationRequest streamingOutputOperationRequest, - AsyncResponseHandler asyncResponseHandler) { + StreamingOutputOperationRequest streamingOutputOperationRequest, + AsyncResponseHandler asyncResponseHandler) { throw new UnsupportedOperationException(); } @@ -475,7 +717,7 @@ default CompletableFuture streamingOutputOperation( * target="_top">AWS API Documentation */ default CompletableFuture streamingOutputOperation( - StreamingOutputOperationRequest streamingOutputOperationRequest, Path path) { + StreamingOutputOperationRequest streamingOutputOperationRequest, Path path) { return streamingOutputOperation(streamingOutputOperationRequest, AsyncResponseHandler.toFile(path)); } } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-client-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-client-class.java index 84c92e11fbb5..149f0edd4cfb 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-client-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-client-class.java @@ -34,8 +34,8 @@ import software.amazon.awssdk.services.json.model.StreamingInputOperationResponse; import software.amazon.awssdk.services.json.model.StreamingOutputOperationRequest; import software.amazon.awssdk.services.json.model.StreamingOutputOperationResponse; -import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPaginator; -import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPaginator; +import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable; +import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyIterable; import software.amazon.awssdk.services.json.transform.APostOperationRequestMarshaller; import software.amazon.awssdk.services.json.transform.APostOperationResponseUnmarshaller; import software.amazon.awssdk.services.json.transform.APostOperationWithOutputRequestMarshaller; @@ -101,14 +101,14 @@ public APostOperationResponse aPostOperation(APostOperationRequest aPostOperatio SdkServiceException, SdkClientException, JsonException { HttpResponseHandler responseHandler = protocolFactory.createResponseHandler( - new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), - new APostOperationResponseUnmarshaller()); + new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), + new APostOperationResponseUnmarshaller()); HttpResponseHandler errorResponseHandler = createErrorResponseHandler(); return clientHandler.execute(new ClientExecutionParams() - .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) - .withInput(aPostOperationRequest).withMarshaller(new APostOperationRequestMarshaller(protocolFactory))); + .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) + .withInput(aPostOperationRequest).withMarshaller(new APostOperationRequestMarshaller(protocolFactory))); } /** @@ -133,20 +133,20 @@ public APostOperationResponse aPostOperation(APostOperationRequest aPostOperatio */ @Override public APostOperationWithOutputResponse aPostOperationWithOutput( - APostOperationWithOutputRequest aPostOperationWithOutputRequest) throws InvalidInputException, SdkServiceException, - SdkClientException, JsonException { + APostOperationWithOutputRequest aPostOperationWithOutputRequest) throws InvalidInputException, SdkServiceException, + SdkClientException, JsonException { HttpResponseHandler responseHandler = protocolFactory.createResponseHandler( - new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), - new APostOperationWithOutputResponseUnmarshaller()); + new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), + new APostOperationWithOutputResponseUnmarshaller()); HttpResponseHandler errorResponseHandler = createErrorResponseHandler(); return clientHandler - .execute(new ClientExecutionParams() - .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) - .withInput(aPostOperationWithOutputRequest) - .withMarshaller(new APostOperationWithOutputRequestMarshaller(protocolFactory))); + .execute(new ClientExecutionParams() + .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) + .withInput(aPostOperationWithOutputRequest) + .withMarshaller(new APostOperationWithOutputRequestMarshaller(protocolFactory))); } /** @@ -171,20 +171,20 @@ public APostOperationWithOutputResponse aPostOperationWithOutput( */ @Override public GetWithoutRequiredMembersResponse getWithoutRequiredMembers( - GetWithoutRequiredMembersRequest getWithoutRequiredMembersRequest) throws InvalidInputException, SdkServiceException, - SdkClientException, JsonException { + GetWithoutRequiredMembersRequest getWithoutRequiredMembersRequest) throws InvalidInputException, SdkServiceException, + SdkClientException, JsonException { HttpResponseHandler responseHandler = protocolFactory.createResponseHandler( - new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), - new GetWithoutRequiredMembersResponseUnmarshaller()); + new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), + new GetWithoutRequiredMembersResponseUnmarshaller()); HttpResponseHandler errorResponseHandler = createErrorResponseHandler(); return clientHandler - .execute(new ClientExecutionParams() - .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) - .withInput(getWithoutRequiredMembersRequest) - .withMarshaller(new GetWithoutRequiredMembersRequestMarshaller(protocolFactory))); + .execute(new ClientExecutionParams() + .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) + .withInput(getWithoutRequiredMembersRequest) + .withMarshaller(new GetWithoutRequiredMembersRequestMarshaller(protocolFactory))); } /** @@ -205,20 +205,20 @@ public GetWithoutRequiredMembersResponse getWithoutRequiredMembers( */ @Override public PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey( - PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) throws SdkServiceException, - SdkClientException, JsonException { + PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) throws SdkServiceException, + SdkClientException, JsonException { HttpResponseHandler responseHandler = protocolFactory.createResponseHandler( - new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), - new PaginatedOperationWithResultKeyResponseUnmarshaller()); + new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), + new PaginatedOperationWithResultKeyResponseUnmarshaller()); HttpResponseHandler errorResponseHandler = createErrorResponseHandler(); return clientHandler - .execute(new ClientExecutionParams() - .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) - .withInput(paginatedOperationWithResultKeyRequest) - .withMarshaller(new PaginatedOperationWithResultKeyRequestMarshaller(protocolFactory))); + .execute(new ClientExecutionParams() + .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) + .withInput(paginatedOperationWithResultKeyRequest) + .withMarshaller(new PaginatedOperationWithResultKeyRequestMarshaller(protocolFactory))); } /** @@ -243,7 +243,7 @@ public PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey( * *
      * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPaginator responses = client.paginatedOperationWithResultKeyIterable(request);
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable responses = client.paginatedOperationWithResultKeyPaginator(request);
      * responses.stream().forEach(....);
      * }
      * 
@@ -253,8 +253,8 @@ public PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey( *
      * {
      *     @code
-     *     software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPaginator responses = client
-     *             .paginatedOperationWithResultKeyIterable(request);
+     *     software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable responses = client
+     *             .paginatedOperationWithResultKeyPaginator(request);
      *     for (software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyResponse response : responses) {
      *         // do something;
      *     }
@@ -265,7 +265,7 @@ public PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey(
      *
      * 
      * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPaginator responses = client.paginatedOperationWithResultKeyIterable(request);
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable responses = client.paginatedOperationWithResultKeyPaginator(request);
      * responses.iterator().forEachRemaining(....);
      * }
      * 
@@ -276,7 +276,7 @@ public PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey( *

* * @param paginatedOperationWithResultKeyRequest - * @return Result of the PaginatedOperationWithResultKey operation returned by the service. + * @return A custom iterable that can be used to iterate through all the response pages. * @throws SdkException * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for * catch all scenarios. @@ -289,10 +289,10 @@ public PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey( * target="_top">AWS API Documentation */ @Override - public PaginatedOperationWithResultKeyPaginator paginatedOperationWithResultKeyIterable( - PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) throws SdkServiceException, - SdkClientException, JsonException { - return new PaginatedOperationWithResultKeyPaginator(this, paginatedOperationWithResultKeyRequest); + public PaginatedOperationWithResultKeyIterable paginatedOperationWithResultKeyPaginator( + PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) throws SdkServiceException, + SdkClientException, JsonException { + return new PaginatedOperationWithResultKeyIterable(this, paginatedOperationWithResultKeyRequest); } /** @@ -313,20 +313,20 @@ public PaginatedOperationWithResultKeyPaginator paginatedOperationWithResultKeyI */ @Override public PaginatedOperationWithoutResultKeyResponse paginatedOperationWithoutResultKey( - PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) throws SdkServiceException, - SdkClientException, JsonException { + PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) throws SdkServiceException, + SdkClientException, JsonException { HttpResponseHandler responseHandler = protocolFactory.createResponseHandler( - new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), - new PaginatedOperationWithoutResultKeyResponseUnmarshaller()); + new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), + new PaginatedOperationWithoutResultKeyResponseUnmarshaller()); HttpResponseHandler errorResponseHandler = createErrorResponseHandler(); return clientHandler - .execute(new ClientExecutionParams() - .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) - .withInput(paginatedOperationWithoutResultKeyRequest) - .withMarshaller(new PaginatedOperationWithoutResultKeyRequestMarshaller(protocolFactory))); + .execute(new ClientExecutionParams() + .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) + .withInput(paginatedOperationWithoutResultKeyRequest) + .withMarshaller(new PaginatedOperationWithoutResultKeyRequestMarshaller(protocolFactory))); } /** @@ -351,7 +351,7 @@ public PaginatedOperationWithoutResultKeyResponse paginatedOperationWithoutResul * *
      * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPaginator responses = client.paginatedOperationWithoutResultKeyIterable(request);
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyIterable responses = client.paginatedOperationWithoutResultKeyPaginator(request);
      * responses.stream().forEach(....);
      * }
      * 
@@ -361,8 +361,8 @@ public PaginatedOperationWithoutResultKeyResponse paginatedOperationWithoutResul *
      * {
      *     @code
-     *     software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPaginator responses = client
-     *             .paginatedOperationWithoutResultKeyIterable(request);
+     *     software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyIterable responses = client
+     *             .paginatedOperationWithoutResultKeyPaginator(request);
      *     for (software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyResponse response : responses) {
      *         // do something;
      *     }
@@ -373,7 +373,7 @@ public PaginatedOperationWithoutResultKeyResponse paginatedOperationWithoutResul
      *
      * 
      * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPaginator responses = client.paginatedOperationWithoutResultKeyIterable(request);
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyIterable responses = client.paginatedOperationWithoutResultKeyPaginator(request);
      * responses.iterator().forEachRemaining(....);
      * }
      * 
@@ -384,7 +384,7 @@ public PaginatedOperationWithoutResultKeyResponse paginatedOperationWithoutResul *

* * @param paginatedOperationWithoutResultKeyRequest - * @return Result of the PaginatedOperationWithoutResultKey operation returned by the service. + * @return A custom iterable that can be used to iterate through all the response pages. * @throws SdkException * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for * catch all scenarios. @@ -397,10 +397,10 @@ public PaginatedOperationWithoutResultKeyResponse paginatedOperationWithoutResul * target="_top">AWS API Documentation */ @Override - public PaginatedOperationWithoutResultKeyPaginator paginatedOperationWithoutResultKeyIterable( - PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) throws SdkServiceException, - SdkClientException, JsonException { - return new PaginatedOperationWithoutResultKeyPaginator(this, paginatedOperationWithoutResultKeyRequest); + public PaginatedOperationWithoutResultKeyIterable paginatedOperationWithoutResultKeyPaginator( + PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) throws SdkServiceException, + SdkClientException, JsonException { + return new PaginatedOperationWithoutResultKeyIterable(this, paginatedOperationWithoutResultKeyRequest); } /** @@ -435,18 +435,18 @@ public StreamingInputOperationResponse streamingInputOperation(StreamingInputOpe RequestBody requestBody) throws SdkServiceException, SdkClientException, JsonException { HttpResponseHandler responseHandler = protocolFactory.createResponseHandler( - new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), - new StreamingInputOperationResponseUnmarshaller()); + new JsonOperationMetadata().withPayloadJson(true).withHasStreamingSuccessResponse(false), + new StreamingInputOperationResponseUnmarshaller()); HttpResponseHandler errorResponseHandler = createErrorResponseHandler(); return clientHandler.execute(new ClientExecutionParams() - .withResponseHandler(responseHandler) - .withErrorResponseHandler(errorResponseHandler) - .withInput(streamingInputOperationRequest) - .withMarshaller( - new StreamingRequestMarshaller( - new StreamingInputOperationRequestMarshaller(protocolFactory), requestBody))); + .withResponseHandler(responseHandler) + .withErrorResponseHandler(errorResponseHandler) + .withInput(streamingInputOperationRequest) + .withMarshaller( + new StreamingRequestMarshaller( + new StreamingInputOperationRequestMarshaller(protocolFactory), requestBody))); } /** @@ -475,21 +475,21 @@ public StreamingInputOperationResponse streamingInputOperation(StreamingInputOpe @Override public ReturnT streamingOutputOperation(StreamingOutputOperationRequest streamingOutputOperationRequest, StreamingResponseHandler streamingResponseHandler) - throws SdkServiceException, SdkClientException, JsonException { + throws SdkServiceException, SdkClientException, JsonException { HttpResponseHandler responseHandler = protocolFactory.createResponseHandler( - new JsonOperationMetadata().withPayloadJson(false).withHasStreamingSuccessResponse(true), - new StreamingOutputOperationResponseUnmarshaller()); + new JsonOperationMetadata().withPayloadJson(false).withHasStreamingSuccessResponse(true), + new StreamingOutputOperationResponseUnmarshaller()); HttpResponseHandler errorResponseHandler = createErrorResponseHandler(); return clientHandler - .execute( - new ClientExecutionParams() - .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) - .withInput(streamingOutputOperationRequest) - .withMarshaller(new StreamingOutputOperationRequestMarshaller(protocolFactory)), - streamingResponseHandler); + .execute( + new ClientExecutionParams() + .withResponseHandler(responseHandler).withErrorResponseHandler(errorResponseHandler) + .withInput(streamingOutputOperationRequest) + .withMarshaller(new StreamingOutputOperationRequestMarshaller(protocolFactory)), + streamingResponseHandler); } private HttpResponseHandler createErrorResponseHandler() { @@ -498,13 +498,13 @@ private HttpResponseHandler createErrorResponseHandler() { private software.amazon.awssdk.core.protocol.json.SdkJsonProtocolFactory init() { return new SdkJsonProtocolFactory(new JsonClientMetadata() - .withProtocolVersion("1.1") - .withSupportsCbor(false) - .withSupportsIon(false) - .withBaseServiceExceptionClass(software.amazon.awssdk.services.json.model.JsonException.class) - .withContentTypeOverride("") - .addErrorMetadata( - new JsonErrorShapeMetadata().withErrorCode("InvalidInput").withModeledClass(InvalidInputException.class))); + .withProtocolVersion("1.1") + .withSupportsCbor(false) + .withSupportsIon(false) + .withBaseServiceExceptionClass(software.amazon.awssdk.services.json.model.JsonException.class) + .withContentTypeOverride("") + .addErrorMetadata( + new JsonErrorShapeMetadata().withErrorCode("InvalidInput").withModeledClass(InvalidInputException.class))); } @Override diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-client-interface.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-client-interface.java index 5a3018822658..fce4db7ac788 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-client-interface.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-client-interface.java @@ -27,8 +27,8 @@ import software.amazon.awssdk.services.json.model.StreamingInputOperationResponse; import software.amazon.awssdk.services.json.model.StreamingOutputOperationRequest; import software.amazon.awssdk.services.json.model.StreamingOutputOperationResponse; -import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPaginator; -import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPaginator; +import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable; +import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyIterable; import software.amazon.awssdk.utils.SdkAutoCloseable; /** @@ -102,7 +102,7 @@ default APostOperationResponse aPostOperation(APostOperationRequest aPostOperati * API Documentation */ default APostOperationResponse aPostOperation(Consumer aPostOperationRequest) - throws InvalidInputException, SdkServiceException, SdkClientException, JsonException { + throws InvalidInputException, SdkServiceException, SdkClientException, JsonException { return aPostOperation(APostOperationRequest.builder().apply(aPostOperationRequest).build()); } @@ -127,8 +127,8 @@ default APostOperationResponse aPostOperation(ConsumerAWS API Documentation */ default APostOperationWithOutputResponse aPostOperationWithOutput( - APostOperationWithOutputRequest aPostOperationWithOutputRequest) throws InvalidInputException, SdkServiceException, - SdkClientException, JsonException { + APostOperationWithOutputRequest aPostOperationWithOutputRequest) throws InvalidInputException, SdkServiceException, + SdkClientException, JsonException { throw new UnsupportedOperationException(); } @@ -153,8 +153,8 @@ default APostOperationWithOutputResponse aPostOperationWithOutput( * target="_top">AWS API Documentation */ default APostOperationWithOutputResponse aPostOperationWithOutput( - Consumer aPostOperationWithOutputRequest) throws InvalidInputException, - SdkServiceException, SdkClientException, JsonException { + Consumer aPostOperationWithOutputRequest) throws InvalidInputException, + SdkServiceException, SdkClientException, JsonException { return aPostOperationWithOutput(APostOperationWithOutputRequest.builder().apply(aPostOperationWithOutputRequest).build()); } @@ -204,8 +204,8 @@ default GetWithoutRequiredMembersResponse getWithoutRequiredMembers() throws Inv * target="_top">AWS API Documentation */ default GetWithoutRequiredMembersResponse getWithoutRequiredMembers( - GetWithoutRequiredMembersRequest getWithoutRequiredMembersRequest) throws InvalidInputException, SdkServiceException, - SdkClientException, JsonException { + GetWithoutRequiredMembersRequest getWithoutRequiredMembersRequest) throws InvalidInputException, SdkServiceException, + SdkClientException, JsonException { throw new UnsupportedOperationException(); } @@ -230,12 +230,33 @@ default GetWithoutRequiredMembersResponse getWithoutRequiredMembers( * target="_top">AWS API Documentation */ default GetWithoutRequiredMembersResponse getWithoutRequiredMembers( - Consumer getWithoutRequiredMembersRequest) throws InvalidInputException, - SdkServiceException, SdkClientException, JsonException { + Consumer getWithoutRequiredMembersRequest) throws InvalidInputException, + SdkServiceException, SdkClientException, JsonException { return getWithoutRequiredMembers(GetWithoutRequiredMembersRequest.builder().apply(getWithoutRequiredMembersRequest) .build()); } + /** + * Some paginated operation with result_key in paginators.json file + * + * @return Result of the PaginatedOperationWithResultKey operation returned by the service. + * @throws SdkException + * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for + * catch all scenarios. + * @throws SdkClientException + * If any client side error occurs such as an IO related failure, failure to get credentials, etc. + * @throws JsonException + * Base class for all service exceptions. Unknown exceptions will be thrown as an instance of this type. + * @sample JsonClient.PaginatedOperationWithResultKey + * @see #paginatedOperationWithResultKey(PaginatedOperationWithResultKeyRequest) + * @see AWS API Documentation + */ + default PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey() throws SdkServiceException, + SdkClientException, JsonException { + return paginatedOperationWithResultKey(PaginatedOperationWithResultKeyRequest.builder().build()); + } + /** * Some paginated operation with result_key in paginators.json file * @@ -253,8 +274,8 @@ default GetWithoutRequiredMembersResponse getWithoutRequiredMembers( * target="_top">AWS API Documentation */ default PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey( - PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) throws SdkServiceException, - SdkClientException, JsonException { + PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) throws SdkServiceException, + SdkClientException, JsonException { throw new UnsupportedOperationException(); } @@ -275,8 +296,8 @@ default PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey( * target="_top">AWS API Documentation */ default PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey( - Consumer paginatedOperationWithResultKeyRequest) - throws SdkServiceException, SdkClientException, JsonException { + Consumer paginatedOperationWithResultKeyRequest) + throws SdkServiceException, SdkClientException, JsonException { return paginatedOperationWithResultKey(PaginatedOperationWithResultKeyRequest.builder() .apply(paginatedOperationWithResultKeyRequest).build()); } @@ -303,7 +324,7 @@ default PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey( * *
      * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPaginator responses = client.paginatedOperationWithResultKeyIterable(request);
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable responses = client.paginatedOperationWithResultKeyPaginator(request);
      * responses.stream().forEach(....);
      * }
      * 
@@ -313,8 +334,8 @@ default PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey( *
      * {
      *     @code
-     *     software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPaginator responses = client
-     *             .paginatedOperationWithResultKeyIterable(request);
+     *     software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable responses = client
+     *             .paginatedOperationWithResultKeyPaginator(request);
      *     for (software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyResponse response : responses) {
      *         // do something;
      *     }
@@ -325,7 +346,79 @@ default PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey(
      *
      * 
      * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPaginator responses = client.paginatedOperationWithResultKeyIterable(request);
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable responses = client.paginatedOperationWithResultKeyPaginator(request);
+     * responses.iterator().forEachRemaining(....);
+     * }
+     * 
+ *

+ * Note: If you prefer to have control on service calls, use the + * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} + * operation. + *

+ * + * @return A custom iterable that can be used to iterate through all the response pages. + * @throws SdkException + * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for + * catch all scenarios. + * @throws SdkClientException + * If any client side error occurs such as an IO related failure, failure to get credentials, etc. + * @throws JsonException + * Base class for all service exceptions. Unknown exceptions will be thrown as an instance of this type. + * @sample JsonClient.PaginatedOperationWithResultKey + * @see #paginatedOperationWithResultKeyPaginator(PaginatedOperationWithResultKeyRequest) + * @see AWS API Documentation + */ + default PaginatedOperationWithResultKeyIterable paginatedOperationWithResultKeyPaginator() throws SdkServiceException, + SdkClientException, JsonException { + return paginatedOperationWithResultKeyPaginator(PaginatedOperationWithResultKeyRequest.builder().build()); + } + + /** + * Some paginated operation with result_key in paginators.json file
+ *

+ * This is a variant of + * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} + * operation. The return type is a custom iterable that can be used to iterate through all the pages. SDK will + * internally handle making service calls for you. + *

+ *

+ * When this operation is called, a custom iterable is returned but no service calls are made yet. So there is no + * guarantee that the request is valid. As you iterate through the iterable, SDK will start lazily loading response + * pages by making service calls until there are no pages left or your iteration stops. If there are errors in your + * request, you will see the failures only after you start iterating through the iterable. + *

+ * + *

+ * The following are few ways to iterate through the response pages: + *

+ * 1) Using a Stream + * + *
+     * {@code
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable responses = client.paginatedOperationWithResultKeyPaginator(request);
+     * responses.stream().forEach(....);
+     * }
+     * 
+ * + * 2) Using For loop + * + *
+     * {
+     *     @code
+     *     software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable responses = client
+     *             .paginatedOperationWithResultKeyPaginator(request);
+     *     for (software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyResponse response : responses) {
+     *         // do something;
+     *     }
+     * }
+     * 
+ * + * 3) Use iterator directly + * + *
+     * {@code
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable responses = client.paginatedOperationWithResultKeyPaginator(request);
      * responses.iterator().forEachRemaining(....);
      * }
      * 
@@ -336,7 +429,7 @@ default PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey( *

* * @param paginatedOperationWithResultKeyRequest - * @return Result of the PaginatedOperationWithResultKey operation returned by the service. + * @return A custom iterable that can be used to iterate through all the response pages. * @throws SdkException * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for * catch all scenarios. @@ -348,9 +441,9 @@ default PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey( * @see AWS API Documentation */ - default PaginatedOperationWithResultKeyPaginator paginatedOperationWithResultKeyIterable( - PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) throws SdkServiceException, - SdkClientException, JsonException { + default PaginatedOperationWithResultKeyIterable paginatedOperationWithResultKeyPaginator( + PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) throws SdkServiceException, + SdkClientException, JsonException { throw new UnsupportedOperationException(); } @@ -371,8 +464,8 @@ default PaginatedOperationWithResultKeyPaginator paginatedOperationWithResultKey * target="_top">AWS API Documentation */ default PaginatedOperationWithoutResultKeyResponse paginatedOperationWithoutResultKey( - PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) throws SdkServiceException, - SdkClientException, JsonException { + PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) throws SdkServiceException, + SdkClientException, JsonException { throw new UnsupportedOperationException(); } @@ -393,8 +486,8 @@ default PaginatedOperationWithoutResultKeyResponse paginatedOperationWithoutResu * target="_top">AWS API Documentation */ default PaginatedOperationWithoutResultKeyResponse paginatedOperationWithoutResultKey( - Consumer paginatedOperationWithoutResultKeyRequest) - throws SdkServiceException, SdkClientException, JsonException { + Consumer paginatedOperationWithoutResultKeyRequest) + throws SdkServiceException, SdkClientException, JsonException { return paginatedOperationWithoutResultKey(PaginatedOperationWithoutResultKeyRequest.builder() .apply(paginatedOperationWithoutResultKeyRequest).build()); } @@ -421,7 +514,7 @@ default PaginatedOperationWithoutResultKeyResponse paginatedOperationWithoutResu * *
      * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPaginator responses = client.paginatedOperationWithoutResultKeyIterable(request);
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyIterable responses = client.paginatedOperationWithoutResultKeyPaginator(request);
      * responses.stream().forEach(....);
      * }
      * 
@@ -431,8 +524,8 @@ default PaginatedOperationWithoutResultKeyResponse paginatedOperationWithoutResu *
      * {
      *     @code
-     *     software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPaginator responses = client
-     *             .paginatedOperationWithoutResultKeyIterable(request);
+     *     software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyIterable responses = client
+     *             .paginatedOperationWithoutResultKeyPaginator(request);
      *     for (software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyResponse response : responses) {
      *         // do something;
      *     }
@@ -443,7 +536,7 @@ default PaginatedOperationWithoutResultKeyResponse paginatedOperationWithoutResu
      *
      * 
      * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPaginator responses = client.paginatedOperationWithoutResultKeyIterable(request);
+     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyIterable responses = client.paginatedOperationWithoutResultKeyPaginator(request);
      * responses.iterator().forEachRemaining(....);
      * }
      * 
@@ -454,7 +547,7 @@ default PaginatedOperationWithoutResultKeyResponse paginatedOperationWithoutResu *

* * @param paginatedOperationWithoutResultKeyRequest - * @return Result of the PaginatedOperationWithoutResultKey operation returned by the service. + * @return A custom iterable that can be used to iterate through all the response pages. * @throws SdkException * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for * catch all scenarios. @@ -466,9 +559,9 @@ default PaginatedOperationWithoutResultKeyResponse paginatedOperationWithoutResu * @see AWS API Documentation */ - default PaginatedOperationWithoutResultKeyPaginator paginatedOperationWithoutResultKeyIterable( - PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) throws SdkServiceException, - SdkClientException, JsonException { + default PaginatedOperationWithoutResultKeyIterable paginatedOperationWithoutResultKeyPaginator( + PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) throws SdkServiceException, + SdkClientException, JsonException { throw new UnsupportedOperationException(); } @@ -500,8 +593,8 @@ default PaginatedOperationWithoutResultKeyPaginator paginatedOperationWithoutRes * target="_top">AWS API Documentation */ default StreamingInputOperationResponse streamingInputOperation( - StreamingInputOperationRequest streamingInputOperationRequest, RequestBody requestBody) throws SdkServiceException, - SdkClientException, JsonException { + StreamingInputOperationRequest streamingInputOperationRequest, RequestBody requestBody) throws SdkServiceException, + SdkClientException, JsonException { throw new UnsupportedOperationException(); } @@ -528,8 +621,8 @@ default StreamingInputOperationResponse streamingInputOperation( * target="_top">AWS API Documentation */ default StreamingInputOperationResponse streamingInputOperation( - StreamingInputOperationRequest streamingInputOperationRequest, Path filePath) throws SdkServiceException, - SdkClientException, JsonException { + StreamingInputOperationRequest streamingInputOperationRequest, Path filePath) throws SdkServiceException, + SdkClientException, JsonException { return streamingInputOperation(streamingInputOperationRequest, RequestBody.of(filePath)); } @@ -558,7 +651,7 @@ default StreamingInputOperationResponse streamingInputOperation( */ default ReturnT streamingOutputOperation(StreamingOutputOperationRequest streamingOutputOperationRequest, StreamingResponseHandler streamingResponseHandler) - throws SdkServiceException, SdkClientException, JsonException { + throws SdkServiceException, SdkClientException, JsonException { throw new UnsupportedOperationException(); } @@ -584,8 +677,8 @@ default ReturnT streamingOutputOperation(StreamingOutputOperationReque * target="_top">AWS API Documentation */ default StreamingOutputOperationResponse streamingOutputOperation( - StreamingOutputOperationRequest streamingOutputOperationRequest, Path filePath) throws SdkServiceException, - SdkClientException, JsonException { + StreamingOutputOperationRequest streamingOutputOperationRequest, Path filePath) throws SdkServiceException, + SdkClientException, JsonException { return streamingOutputOperation(streamingOutputOperationRequest, StreamingResponseHandler.toFile(filePath)); } @@ -612,8 +705,8 @@ default StreamingOutputOperationResponse streamingOutputOperation( * target="_top">AWS API Documentation */ default ResponseInputStream streamingOutputOperation( - StreamingOutputOperationRequest streamingOutputOperationRequest) throws SdkServiceException, SdkClientException, - JsonException { + StreamingOutputOperationRequest streamingOutputOperationRequest) throws SdkServiceException, SdkClientException, + JsonException { return streamingOutputOperation(streamingOutputOperationRequest, StreamingResponseHandler.toInputStream()); } @@ -638,8 +731,8 @@ default ResponseInputStream streamingOutputOpe * target="_top">AWS API Documentation */ default ResponseBytes streamingOutputOperationBytes( - StreamingOutputOperationRequest streamingOutputOperationRequest) throws SdkServiceException, SdkClientException, - JsonException { + StreamingOutputOperationRequest streamingOutputOperationRequest) throws SdkServiceException, SdkClientException, + JsonException { return streamingOutputOperation(streamingOutputOperationRequest, StreamingResponseHandler.toBytes()); } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithResultKeyPaginator.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithResultKeyIterable.java similarity index 65% rename from codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithResultKeyPaginator.java rename to codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithResultKeyIterable.java index 88343f47d010..4092395e373a 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithResultKeyPaginator.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithResultKeyIterable.java @@ -1,12 +1,13 @@ package software.amazon.awssdk.services.jsonprotocoltests.paginators; +import java.util.Collections; import java.util.Iterator; import java.util.function.Function; import javax.annotation.Generated; -import software.amazon.awssdk.core.pagination.NextPageFetcher; import software.amazon.awssdk.core.pagination.PaginatedItemsIterable; import software.amazon.awssdk.core.pagination.PaginatedResponsesIterator; import software.amazon.awssdk.core.pagination.SdkIterable; +import software.amazon.awssdk.core.pagination.SyncPageFetcher; import software.amazon.awssdk.services.jsonprotocoltests.JsonProtocolTestsClient; import software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyRequest; import software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyResponse; @@ -15,7 +16,7 @@ /** *

* Represents the output for the - * {@link software.amazon.awssdk.services.jsonprotocoltests.JsonProtocolTestsClient#paginatedOperationWithResultKeyIterable(software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyRequest)} + * {@link software.amazon.awssdk.services.jsonprotocoltests.JsonProtocolTestsClient#paginatedOperationWithResultKeyPaginator(software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyRequest)} * operation which is a paginated operation. This class is an iterable of * {@link software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyResponse} that can be * used to iterate through all the response pages of the operation. @@ -34,7 +35,7 @@ * *

  * {@code
- * software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithResultKeyPaginator responses = client.paginatedOperationWithResultKeyIterable(request);
+ * software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithResultKeyIterable responses = client.paginatedOperationWithResultKeyPaginator(request);
  * responses.stream().forEach(....);
  * }
  * 
@@ -44,8 +45,8 @@ *
  * {
  *     @code
- *     software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithResultKeyPaginator responses = client
- *             .paginatedOperationWithResultKeyIterable(request);
+ *     software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithResultKeyIterable responses = client
+ *             .paginatedOperationWithResultKeyPaginator(request);
  *     for (software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyResponse response : responses) {
  *         // do something;
  *     }
@@ -56,7 +57,7 @@
  *
  * 
  * {@code
- * software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithResultKeyPaginator responses = client.paginatedOperationWithResultKeyIterable(request);
+ * software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithResultKeyIterable responses = client.paginatedOperationWithResultKeyPaginator(request);
  * responses.iterator().forEachRemaining(....);
  * }
  * 
@@ -67,15 +68,15 @@ *

*/ @Generated("software.amazon.awssdk:codegen") -public final class PaginatedOperationWithResultKeyPaginator implements SdkIterable { +public class PaginatedOperationWithResultKeyIterable implements SdkIterable { private final JsonProtocolTestsClient client; private final PaginatedOperationWithResultKeyRequest firstRequest; - private final NextPageFetcher nextPageFetcher; + private final SyncPageFetcher nextPageFetcher; - public PaginatedOperationWithResultKeyPaginator(final JsonProtocolTestsClient client, - final PaginatedOperationWithResultKeyRequest firstRequest) { + public PaginatedOperationWithResultKeyIterable(final JsonProtocolTestsClient client, + final PaginatedOperationWithResultKeyRequest firstRequest) { this.client = client; this.firstRequest = firstRequest; this.nextPageFetcher = new PaginatedOperationWithResultKeyResponseFetcher(); @@ -95,15 +96,38 @@ public Iterator iterator() { * of the top level pages. Similar to iteration over pages, this method internally makes service calls to get the * next list of results until the iteration stops or there are no more results. */ - public SdkIterable items() { - Function> getIterator = response -> response != null ? response - .items().iterator() : null; - + public final SdkIterable items() { + Function> getIterator = response -> { + if (response != null && response.items() != null) { + return response.items().iterator(); + } + return Collections.emptyIterator(); + }; return new PaginatedItemsIterable(this, getIterator); } + /** + *

+ * A helper method to resume the pages in case of unexpected failures. The method takes the last successful response + * page as input and returns an instance of {@link PaginatedOperationWithResultKeyIterable} that can be used to + * retrieve the consecutive pages that follows the input page. + *

+ */ + public final PaginatedOperationWithResultKeyIterable resume(final PaginatedOperationWithResultKeyResponse lastSuccessfulPage) { + if (nextPageFetcher.hasNextPage(lastSuccessfulPage)) { + return new PaginatedOperationWithResultKeyIterable(client, firstRequest.toBuilder() + .nextToken(lastSuccessfulPage.nextToken()).build()); + } + return new PaginatedOperationWithResultKeyIterable(client, firstRequest) { + @Override + public Iterator iterator() { + return Collections.emptyIterator(); + } + }; + } + private class PaginatedOperationWithResultKeyResponseFetcher implements - NextPageFetcher { + SyncPageFetcher { @Override public boolean hasNextPage(PaginatedOperationWithResultKeyResponse previousPage) { return previousPage.nextToken() != null; diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithResultKeyPublisher.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithResultKeyPublisher.java new file mode 100644 index 000000000000..6b1255321fd9 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithResultKeyPublisher.java @@ -0,0 +1,150 @@ +package software.amazon.awssdk.services.jsonprotocoltests.paginators; + +import java.util.Collections; +import java.util.Iterator; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import javax.annotation.Generated; +import org.reactivestreams.Subscriber; +import software.amazon.awssdk.core.pagination.async.AsyncPageFetcher; +import software.amazon.awssdk.core.pagination.async.EmptySubscription; +import software.amazon.awssdk.core.pagination.async.PaginatedItemsPublisher; +import software.amazon.awssdk.core.pagination.async.ResponsesSubscription; +import software.amazon.awssdk.core.pagination.async.SdkPublisher; +import software.amazon.awssdk.services.jsonprotocoltests.JsonProtocolTestsAsyncClient; +import software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyRequest; +import software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyResponse; +import software.amazon.awssdk.services.jsonprotocoltests.model.SimpleStruct; + +/** + *

+ * Represents the output for the + * {@link software.amazon.awssdk.services.jsonprotocoltests.JsonProtocolTestsAsyncClient#paginatedOperationWithResultKeyPaginator(software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyRequest)} + * operation which is a paginated operation. This class is a type of {@link org.reactivestreams.Publisher} which can be + * used to provide a sequence of + * {@link software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyResponse} response + * pages as per demand from the subscriber. + *

+ *

+ * When the operation is called, an instance of this class is returned. At this point, no service calls are made yet and + * so there is no guarantee that the request is valid. If there are errors in your request, you will see the failures + * only after you start streaming the data. The subscribe method should be called as a request to start streaming data. + * For more info, see {@link org.reactivestreams.Publisher#subscribe(org.reactivestreams.Subscriber)}. Each call to the + * subscribe method will result in a new {@link org.reactivestreams.Subscription} i.e., a new contract to stream data + * from the starting request. + *

+ * + *

+ * The following are few ways to use the response class: + *

+ * 1) Using the forEach helper method + * + *
+ * {@code
+ * software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithResultKeyPublisher publisher = client.paginatedOperationWithResultKeyPaginator(request);
+ * CompletableFuture future = publisher.forEach(res -> { // Do something with the response });
+ * future.get();
+ * }
+ * 
+ * + * 2) Using a custom subscriber + * + *
+ * {@code
+ * software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithResultKeyPublisher publisher = client.paginatedOperationWithResultKeyPaginator(request);
+ * publisher.subscribe(new Subscriber() {
+ *
+ * public void onSubscribe(org.reactivestreams.Subscriber subscription) { //... };
+ *
+ *
+ * public void onNext(software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyResponse response) { //... };
+ * });}
+ * 
+ * + * As the response is a publisher, it can work well with third party reactive streams implementations like RxJava2. + *

+ * Note: If you prefer to have control on service calls, use the + * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyRequest)} + * operation. + *

+ */ +@Generated("software.amazon.awssdk:codegen") +public class PaginatedOperationWithResultKeyPublisher implements SdkPublisher { + private final JsonProtocolTestsAsyncClient client; + + private final PaginatedOperationWithResultKeyRequest firstRequest; + + private final AsyncPageFetcher nextPageFetcher; + + private boolean isLastPage; + + public PaginatedOperationWithResultKeyPublisher(final JsonProtocolTestsAsyncClient client, + final PaginatedOperationWithResultKeyRequest firstRequest) { + this(client, firstRequest, false); + } + + private PaginatedOperationWithResultKeyPublisher(final JsonProtocolTestsAsyncClient client, + final PaginatedOperationWithResultKeyRequest firstRequest, final boolean isLastPage) { + this.client = client; + this.firstRequest = firstRequest; + this.isLastPage = isLastPage; + this.nextPageFetcher = new PaginatedOperationWithResultKeyResponseFetcher(); + } + + @Override + public void subscribe(Subscriber subscriber) { + subscriber.onSubscribe(new ResponsesSubscription(subscriber, nextPageFetcher)); + } + + /** + * Returns a publisher that can be used to get a stream of data. You need to subscribe to the publisher to request + * the stream of data. The publisher has a helper forEach method that takes in a {@link java.util.function.Consumer} + * and then applies that consumer to each response returned by the service. + */ + public final SdkPublisher items() { + Function> getIterator = response -> { + if (response != null && response.items() != null) { + return response.items().iterator(); + } + return Collections.emptyIterator(); + }; + return new PaginatedItemsPublisher(new PaginatedOperationWithResultKeyResponseFetcher(), getIterator, isLastPage); + } + + /** + *

+ * A helper method to resume the pages in case of unexpected failures. The method takes the last successful response + * page as input and returns an instance of {@link PaginatedOperationWithResultKeyPublisher} that can be used to + * retrieve the consecutive pages that follows the input page. + *

+ */ + public final PaginatedOperationWithResultKeyPublisher resume(final PaginatedOperationWithResultKeyResponse lastSuccessfulPage) { + if (nextPageFetcher.hasNextPage(lastSuccessfulPage)) { + return new PaginatedOperationWithResultKeyPublisher(client, firstRequest.toBuilder() + .nextToken(lastSuccessfulPage.nextToken()).build()); + } + return new PaginatedOperationWithResultKeyPublisher(client, firstRequest, true) { + @Override + public void subscribe(Subscriber subscriber) { + subscriber.onSubscribe(new EmptySubscription(subscriber)); + } + }; + } + + private class PaginatedOperationWithResultKeyResponseFetcher implements + AsyncPageFetcher { + @Override + public boolean hasNextPage(final PaginatedOperationWithResultKeyResponse previousPage) { + return previousPage.nextToken() != null; + } + + @Override + public CompletableFuture nextPage( + final PaginatedOperationWithResultKeyResponse previousPage) { + if (previousPage == null) { + return client.paginatedOperationWithResultKey(firstRequest); + } + return client.paginatedOperationWithResultKey(firstRequest.toBuilder().nextToken(previousPage.nextToken()).build()); + } + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithoutResultKeyPaginator.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithoutResultKeyIterable.java similarity index 62% rename from codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithoutResultKeyPaginator.java rename to codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithoutResultKeyIterable.java index 7a77a8114b8d..22293415188d 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithoutResultKeyPaginator.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithoutResultKeyIterable.java @@ -1,10 +1,11 @@ package software.amazon.awssdk.services.jsonprotocoltests.paginators; +import java.util.Collections; import java.util.Iterator; import javax.annotation.Generated; -import software.amazon.awssdk.core.pagination.NextPageFetcher; import software.amazon.awssdk.core.pagination.PaginatedResponsesIterator; import software.amazon.awssdk.core.pagination.SdkIterable; +import software.amazon.awssdk.core.pagination.SyncPageFetcher; import software.amazon.awssdk.services.jsonprotocoltests.JsonProtocolTestsClient; import software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithoutResultKeyRequest; import software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithoutResultKeyResponse; @@ -12,7 +13,7 @@ /** *

* Represents the output for the - * {@link software.amazon.awssdk.services.jsonprotocoltests.JsonProtocolTestsClient#paginatedOperationWithoutResultKeyIterable(software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithoutResultKeyRequest)} + * {@link software.amazon.awssdk.services.jsonprotocoltests.JsonProtocolTestsClient#paginatedOperationWithoutResultKeyPaginator(software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithoutResultKeyRequest)} * operation which is a paginated operation. This class is an iterable of * {@link software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithoutResultKeyResponse} that can * be used to iterate through all the response pages of the operation. @@ -31,7 +32,7 @@ * *

  * {@code
- * software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithoutResultKeyPaginator responses = client.paginatedOperationWithoutResultKeyIterable(request);
+ * software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithoutResultKeyIterable responses = client.paginatedOperationWithoutResultKeyPaginator(request);
  * responses.stream().forEach(....);
  * }
  * 
@@ -41,8 +42,8 @@ *
  * {
  *     @code
- *     software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithoutResultKeyPaginator responses = client
- *             .paginatedOperationWithoutResultKeyIterable(request);
+ *     software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithoutResultKeyIterable responses = client
+ *             .paginatedOperationWithoutResultKeyPaginator(request);
  *     for (software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithoutResultKeyResponse response : responses) {
  *         // do something;
  *     }
@@ -53,7 +54,7 @@
  *
  * 
  * {@code
- * software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithoutResultKeyPaginator responses = client.paginatedOperationWithoutResultKeyIterable(request);
+ * software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithoutResultKeyIterable responses = client.paginatedOperationWithoutResultKeyPaginator(request);
  * responses.iterator().forEachRemaining(....);
  * }
  * 
@@ -64,15 +65,15 @@ *

*/ @Generated("software.amazon.awssdk:codegen") -public final class PaginatedOperationWithoutResultKeyPaginator implements SdkIterable { +public class PaginatedOperationWithoutResultKeyIterable implements SdkIterable { private final JsonProtocolTestsClient client; private final PaginatedOperationWithoutResultKeyRequest firstRequest; - private final NextPageFetcher nextPageFetcher; + private final SyncPageFetcher nextPageFetcher; - public PaginatedOperationWithoutResultKeyPaginator(final JsonProtocolTestsClient client, - final PaginatedOperationWithoutResultKeyRequest firstRequest) { + public PaginatedOperationWithoutResultKeyIterable(final JsonProtocolTestsClient client, + final PaginatedOperationWithoutResultKeyRequest firstRequest) { this.client = client; this.firstRequest = firstRequest; this.nextPageFetcher = new PaginatedOperationWithoutResultKeyResponseFetcher(); @@ -83,8 +84,29 @@ public Iterator iterator() { return new PaginatedResponsesIterator(nextPageFetcher); } + /** + *

+ * A helper method to resume the pages in case of unexpected failures. The method takes the last successful response + * page as input and returns an instance of {@link PaginatedOperationWithoutResultKeyIterable} that can be used to + * retrieve the consecutive pages that follows the input page. + *

+ */ + public final PaginatedOperationWithoutResultKeyIterable resume( + final PaginatedOperationWithoutResultKeyResponse lastSuccessfulPage) { + if (nextPageFetcher.hasNextPage(lastSuccessfulPage)) { + return new PaginatedOperationWithoutResultKeyIterable(client, firstRequest.toBuilder() + .nextToken(lastSuccessfulPage.nextToken()).build()); + } + return new PaginatedOperationWithoutResultKeyIterable(client, firstRequest) { + @Override + public Iterator iterator() { + return Collections.emptyIterator(); + } + }; + } + private class PaginatedOperationWithoutResultKeyResponseFetcher implements - NextPageFetcher { + SyncPageFetcher { @Override public boolean hasNextPage(PaginatedOperationWithoutResultKeyResponse previousPage) { return previousPage.nextToken() != null; diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithoutResultKeyPublisher.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithoutResultKeyPublisher.java new file mode 100644 index 000000000000..d21c39253759 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithoutResultKeyPublisher.java @@ -0,0 +1,132 @@ +package software.amazon.awssdk.services.jsonprotocoltests.paginators; + +import java.util.concurrent.CompletableFuture; +import javax.annotation.Generated; +import org.reactivestreams.Subscriber; +import software.amazon.awssdk.core.pagination.async.AsyncPageFetcher; +import software.amazon.awssdk.core.pagination.async.EmptySubscription; +import software.amazon.awssdk.core.pagination.async.ResponsesSubscription; +import software.amazon.awssdk.core.pagination.async.SdkPublisher; +import software.amazon.awssdk.services.jsonprotocoltests.JsonProtocolTestsAsyncClient; +import software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithoutResultKeyRequest; +import software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithoutResultKeyResponse; + +/** + *

+ * Represents the output for the + * {@link software.amazon.awssdk.services.jsonprotocoltests.JsonProtocolTestsAsyncClient#paginatedOperationWithoutResultKeyPaginator(software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithoutResultKeyRequest)} + * operation which is a paginated operation. This class is a type of {@link org.reactivestreams.Publisher} which can be + * used to provide a sequence of + * {@link software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithoutResultKeyResponse} response + * pages as per demand from the subscriber. + *

+ *

+ * When the operation is called, an instance of this class is returned. At this point, no service calls are made yet and + * so there is no guarantee that the request is valid. If there are errors in your request, you will see the failures + * only after you start streaming the data. The subscribe method should be called as a request to start streaming data. + * For more info, see {@link org.reactivestreams.Publisher#subscribe(org.reactivestreams.Subscriber)}. Each call to the + * subscribe method will result in a new {@link org.reactivestreams.Subscription} i.e., a new contract to stream data + * from the starting request. + *

+ * + *

+ * The following are few ways to use the response class: + *

+ * 1) Using the forEach helper method + * + *
+ * {@code
+ * software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithoutResultKeyPublisher publisher = client.paginatedOperationWithoutResultKeyPaginator(request);
+ * CompletableFuture future = publisher.forEach(res -> { // Do something with the response });
+ * future.get();
+ * }
+ * 
+ * + * 2) Using a custom subscriber + * + *
+ * {@code
+ * software.amazon.awssdk.services.jsonprotocoltests.paginators.PaginatedOperationWithoutResultKeyPublisher publisher = client.paginatedOperationWithoutResultKeyPaginator(request);
+ * publisher.subscribe(new Subscriber() {
+ *
+ * public void onSubscribe(org.reactivestreams.Subscriber subscription) { //... };
+ *
+ *
+ * public void onNext(software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithoutResultKeyResponse response) { //... };
+ * });}
+ * 
+ * + * As the response is a publisher, it can work well with third party reactive streams implementations like RxJava2. + *

+ * Note: If you prefer to have control on service calls, use the + * {@link #paginatedOperationWithoutResultKey(software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithoutResultKeyRequest)} + * operation. + *

+ */ +@Generated("software.amazon.awssdk:codegen") +public class PaginatedOperationWithoutResultKeyPublisher implements SdkPublisher { + private final JsonProtocolTestsAsyncClient client; + + private final PaginatedOperationWithoutResultKeyRequest firstRequest; + + private final AsyncPageFetcher nextPageFetcher; + + private boolean isLastPage; + + public PaginatedOperationWithoutResultKeyPublisher(final JsonProtocolTestsAsyncClient client, + final PaginatedOperationWithoutResultKeyRequest firstRequest) { + this(client, firstRequest, false); + } + + private PaginatedOperationWithoutResultKeyPublisher(final JsonProtocolTestsAsyncClient client, + final PaginatedOperationWithoutResultKeyRequest firstRequest, final boolean isLastPage) { + this.client = client; + this.firstRequest = firstRequest; + this.isLastPage = isLastPage; + this.nextPageFetcher = new PaginatedOperationWithoutResultKeyResponseFetcher(); + } + + @Override + public void subscribe(Subscriber subscriber) { + subscriber.onSubscribe(new ResponsesSubscription(subscriber, nextPageFetcher)); + } + + /** + *

+ * A helper method to resume the pages in case of unexpected failures. The method takes the last successful response + * page as input and returns an instance of {@link PaginatedOperationWithoutResultKeyPublisher} that can be used to + * retrieve the consecutive pages that follows the input page. + *

+ */ + public final PaginatedOperationWithoutResultKeyPublisher resume( + final PaginatedOperationWithoutResultKeyResponse lastSuccessfulPage) { + if (nextPageFetcher.hasNextPage(lastSuccessfulPage)) { + return new PaginatedOperationWithoutResultKeyPublisher(client, firstRequest.toBuilder() + .nextToken(lastSuccessfulPage.nextToken()).build()); + } + return new PaginatedOperationWithoutResultKeyPublisher(client, firstRequest, true) { + @Override + public void subscribe(Subscriber subscriber) { + subscriber.onSubscribe(new EmptySubscription(subscriber)); + } + }; + } + + private class PaginatedOperationWithoutResultKeyResponseFetcher implements + AsyncPageFetcher { + @Override + public boolean hasNextPage(final PaginatedOperationWithoutResultKeyResponse previousPage) { + return previousPage.nextToken() != null; + } + + @Override + public CompletableFuture nextPage( + final PaginatedOperationWithoutResultKeyResponse previousPage) { + if (previousPage == null) { + return client.paginatedOperationWithoutResultKey(firstRequest); + } + return client + .paginatedOperationWithoutResultKey(firstRequest.toBuilder().nextToken(previousPage.nextToken()).build()); + } + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/customization.config b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/customization.config index c90352b1dd80..53d1e1898170 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/customization.config +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/customization.config @@ -3,5 +3,6 @@ "allTypes", "nestedContainers", "operationWithNoInputOrOutput" - ] + ], + "verifiedSimpleMethods" : ["paginatedOperationWithResultKey"] } \ No newline at end of file diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/service-2.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/service-2.json index 8dff2991dccb..49e9ca1caa8b 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/service-2.json +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/service-2.json @@ -294,9 +294,6 @@ "Timestamp":{"type":"timestamp"}, "PaginatedOperationWithResultKeyRequest": { "type": "structure", - "required": [ - "NextToken" - ], "members": { "NextToken": { "shape": "String", diff --git a/core/src/main/java/software/amazon/awssdk/core/pagination/PaginatedItemsIterable.java b/core/src/main/java/software/amazon/awssdk/core/pagination/PaginatedItemsIterable.java index 1b241bfaa261..98931eb027d3 100644 --- a/core/src/main/java/software/amazon/awssdk/core/pagination/PaginatedItemsIterable.java +++ b/core/src/main/java/software/amazon/awssdk/core/pagination/PaginatedItemsIterable.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.core.pagination; +import java.util.Collections; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.function.Function; @@ -51,17 +52,17 @@ private class ItemsIterator implements Iterator { ItemsIterator(final Iterator pagesIterator) { this.pagesIterator = pagesIterator; - this.singlePageItemsIterator = getItemIterator.apply(pagesIterator.next()); + this.singlePageItemsIterator = pagesIterator.hasNext() ? getItemIterator.apply(pagesIterator.next()) + : Collections.emptyIterator(); } @Override public boolean hasNext() { - while ((singlePageItemsIterator == null || !singlePageItemsIterator.hasNext()) - && pagesIterator.hasNext()) { + while (!hasMoreItems() && pagesIterator.hasNext()) { singlePageItemsIterator = getItemIterator.apply(pagesIterator.next()); } - if (singlePageItemsIterator != null && singlePageItemsIterator.hasNext()) { + if (hasMoreItems()) { return true; } @@ -76,6 +77,10 @@ public ItemT next() { return singlePageItemsIterator.next(); } + + private boolean hasMoreItems() { + return singlePageItemsIterator.hasNext(); + } } } diff --git a/core/src/main/java/software/amazon/awssdk/core/pagination/PaginatedResponsesIterator.java b/core/src/main/java/software/amazon/awssdk/core/pagination/PaginatedResponsesIterator.java index 05cdfaa2fd51..b0f09bc28a9f 100644 --- a/core/src/main/java/software/amazon/awssdk/core/pagination/PaginatedResponsesIterator.java +++ b/core/src/main/java/software/amazon/awssdk/core/pagination/PaginatedResponsesIterator.java @@ -28,13 +28,13 @@ */ public class PaginatedResponsesIterator implements Iterator { - private final NextPageFetcher nextPageFetcher; + private final SyncPageFetcher nextPageFetcher; // This is null when the object is created. It gets initialized in next() method // where SDK make service calls. private ResponseT oldResponse; - public PaginatedResponsesIterator(NextPageFetcher nextPageFetcher) { + public PaginatedResponsesIterator(SyncPageFetcher nextPageFetcher) { this.nextPageFetcher = nextPageFetcher; } diff --git a/core/src/main/java/software/amazon/awssdk/core/pagination/NextPageFetcher.java b/core/src/main/java/software/amazon/awssdk/core/pagination/SyncPageFetcher.java similarity index 96% rename from core/src/main/java/software/amazon/awssdk/core/pagination/NextPageFetcher.java rename to core/src/main/java/software/amazon/awssdk/core/pagination/SyncPageFetcher.java index de4db5a6f49a..a628ad4e3a39 100644 --- a/core/src/main/java/software/amazon/awssdk/core/pagination/NextPageFetcher.java +++ b/core/src/main/java/software/amazon/awssdk/core/pagination/SyncPageFetcher.java @@ -18,7 +18,7 @@ import software.amazon.awssdk.annotations.SdkInternalApi; @SdkInternalApi -public interface NextPageFetcher { +public interface SyncPageFetcher { /** * Returns a boolean value indicating if a next page is available. diff --git a/core/src/main/java/software/amazon/awssdk/core/pagination/async/AsyncPageFetcher.java b/core/src/main/java/software/amazon/awssdk/core/pagination/async/AsyncPageFetcher.java new file mode 100644 index 000000000000..024bb25588ba --- /dev/null +++ b/core/src/main/java/software/amazon/awssdk/core/pagination/async/AsyncPageFetcher.java @@ -0,0 +1,44 @@ +/* + * Copyright 2010-2018 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.core.pagination.async; + +import java.util.concurrent.CompletableFuture; +import software.amazon.awssdk.annotations.SdkInternalApi; + +/** + * Interface to deal with async paginated responses. + * @param Type of Response + */ +@SdkInternalApi +public interface AsyncPageFetcher { + + /** + * Returns a boolean value indicating if a next page is available. + * + * @param oldPage last page sent by service in a paginated operation + * @return True if there is a next page available. Otherwise false. + */ + boolean hasNextPage(ResponseT oldPage); + + /** + * Method that uses the information in #oldPage and returns a + * completable future for the next page. This method makes service calls. + * + * @param oldPage last page sent by service in a paginated operation + * @return A CompletableFuture that can be used to get the next response page + */ + CompletableFuture nextPage(ResponseT oldPage); +} diff --git a/core/src/main/java/software/amazon/awssdk/core/pagination/async/EmptySubscription.java b/core/src/main/java/software/amazon/awssdk/core/pagination/async/EmptySubscription.java new file mode 100644 index 000000000000..321788750ada --- /dev/null +++ b/core/src/main/java/software/amazon/awssdk/core/pagination/async/EmptySubscription.java @@ -0,0 +1,63 @@ +/* + * Copyright 2010-2018 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.core.pagination.async; + +import java.util.concurrent.atomic.AtomicBoolean; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +/** + * A NoOp implementation of {@link Subscription} interface. + * + * This subscription calls {@link Subscriber#onComplete()} on first request for data and then terminates the subscription. + */ +public class EmptySubscription implements Subscription { + + private final AtomicBoolean isTerminated = new AtomicBoolean(false); + private final Subscriber subscriber; + + public EmptySubscription(Subscriber subscriber) { + this.subscriber = subscriber; + } + + @Override + public void request(long n) { + if (isTerminated()) { + return; + } + + if (n <= 0) { + throw new IllegalArgumentException("Non-positive request signals are illegal"); + } + + if (terminate()) { + subscriber.onComplete(); + } + } + + @Override + public void cancel() { + terminate(); + } + + private boolean terminate() { + return isTerminated.compareAndSet(false, true); + } + + private boolean isTerminated() { + return isTerminated.get(); + } +} diff --git a/core/src/main/java/software/amazon/awssdk/core/pagination/async/ItemsSubscription.java b/core/src/main/java/software/amazon/awssdk/core/pagination/async/ItemsSubscription.java new file mode 100644 index 000000000000..4c6a6ba35f6d --- /dev/null +++ b/core/src/main/java/software/amazon/awssdk/core/pagination/async/ItemsSubscription.java @@ -0,0 +1,103 @@ +/* + * Copyright 2010-2018 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.core.pagination.async; + +import java.util.Iterator; +import java.util.function.Function; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +/** + * An implementation of the {@link Subscription} interface that can be used to signal and cancel demand for + * paginated items across pages + * + * @param The type of a single response page + * @param The type of paginated member in a response page + */ +public class ItemsSubscription extends PaginationSubscription { + private final Function> getIteratorFunction; + private volatile Iterator singlePageItemsIterator; + + public ItemsSubscription(Subscriber subscriber, + AsyncPageFetcher nextPageFetcher, + Function> getIteratorFunction) { + super(subscriber, nextPageFetcher); + this.getIteratorFunction = getIteratorFunction; + } + + + protected void handleRequests() { + if (!hasMoreItems() && !hasNextPage()) { + completeSubscription(); + return; + } + + synchronized (this) { + if (outstandingRequests.get() <= 0) { + stopTask(); + return; + } + } + + if (!isTerminated()) { + /** + * Current page is null only the first time the method is called. + * Once initialized, current page will never be null + */ + if (currentPage == null || (!hasMoreItems() && hasNextPage())) { + fetchNextPage(); + + } else if (hasMoreItems()) { + sendNextElement(); + + // All valid cases are covered above. Throw an exception if any combination is missed + } else { + throw new IllegalStateException("Execution should have not reached here"); + } + } + } + + private void fetchNextPage() { + nextPageFetcher.nextPage(currentPage) + .whenComplete(((response, error) -> { + if (response != null) { + currentPage = response; + singlePageItemsIterator = getIteratorFunction.apply(response); + sendNextElement(); + } + if (error != null) { + subscriber.onError(error); + cleanup(); + } + })); + } + + /** + * Calls onNext and calls the recursive method. + */ + private void sendNextElement() { + if (singlePageItemsIterator.hasNext()) { + subscriber.onNext(singlePageItemsIterator.next()); + outstandingRequests.getAndDecrement(); + } + + handleRequests(); + } + + private boolean hasMoreItems() { + return singlePageItemsIterator != null && singlePageItemsIterator.hasNext(); + } +} diff --git a/core/src/main/java/software/amazon/awssdk/core/pagination/async/PaginatedItemsPublisher.java b/core/src/main/java/software/amazon/awssdk/core/pagination/async/PaginatedItemsPublisher.java new file mode 100644 index 000000000000..c69d251882f1 --- /dev/null +++ b/core/src/main/java/software/amazon/awssdk/core/pagination/async/PaginatedItemsPublisher.java @@ -0,0 +1,50 @@ +/* + * Copyright 2010-2018 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.core.pagination.async; + +import java.util.Iterator; +import java.util.function.Function; +import org.reactivestreams.Subscriber; + +/** + * A publisher to request for a stream of paginated items. The class can be used to request data for paginated items + * across multiple pages. + * + * @param The type of a single response page + * @param The type of paginated member in a response page + */ +public class PaginatedItemsPublisher implements SdkPublisher { + + private final AsyncPageFetcher nextPageFetcher; + + private final Function> getIteratorFunction; + + private final boolean isLastPage; + + public PaginatedItemsPublisher(AsyncPageFetcher nextPageFetcher, + Function> getIteratorFunction, + boolean isLastPage) { + this.nextPageFetcher = nextPageFetcher; + this.getIteratorFunction = getIteratorFunction; + this.isLastPage = isLastPage; + } + + @Override + public void subscribe(Subscriber subscriber) { + subscriber.onSubscribe(isLastPage ? new EmptySubscription(subscriber) + : new ItemsSubscription(subscriber, nextPageFetcher, getIteratorFunction)); + } +} diff --git a/core/src/main/java/software/amazon/awssdk/core/pagination/async/PaginationSubscription.java b/core/src/main/java/software/amazon/awssdk/core/pagination/async/PaginationSubscription.java new file mode 100644 index 000000000000..e482e5eee0e0 --- /dev/null +++ b/core/src/main/java/software/amazon/awssdk/core/pagination/async/PaginationSubscription.java @@ -0,0 +1,104 @@ +/* + * Copyright 2010-2018 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.core.pagination.async; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +public abstract class PaginationSubscription implements Subscription { + + protected AtomicLong outstandingRequests = new AtomicLong(0); + protected final Subscriber subscriber; + protected final AsyncPageFetcher nextPageFetcher; + protected volatile ResponseT currentPage; + + // boolean indicating whether subscription is terminated + private AtomicBoolean isTerminated = new AtomicBoolean(false); + + // boolean indicating whether task to handle requests is running + private AtomicBoolean isTaskRunning = new AtomicBoolean(false); + + public PaginationSubscription(Subscriber subscriber, AsyncPageFetcher nextPageFetcher) { + this.subscriber = subscriber; + this.nextPageFetcher = nextPageFetcher; + } + + @Override + public void request(long n) { + if (isTerminated()) { + return; + } + + if (n <= 0) { + throw new IllegalArgumentException("Non-positive request signals are illegal"); + } + + AtomicBoolean startTask = new AtomicBoolean(false); + synchronized (this) { + outstandingRequests.addAndGet(n); + startTask.set(startTask()); + } + + if (startTask.get()) { + handleRequests(); + } + } + + /** + * Recursive method to deal with requests until there are no outstandingRequests or + * no more pages. + */ + protected abstract void handleRequests(); + + @Override + public void cancel() { + cleanup(); + } + + protected boolean hasNextPage() { + return currentPage == null || nextPageFetcher.hasNextPage(currentPage); + } + + protected void completeSubscription() { + if (!isTerminated()) { + subscriber.onComplete(); + cleanup(); + } + } + + private void terminate() { + isTerminated.compareAndSet(false, true); + } + + protected boolean isTerminated() { + return isTerminated.get(); + } + + protected void stopTask() { + isTaskRunning.set(false); + } + + private synchronized boolean startTask() { + return !isTerminated() && isTaskRunning.compareAndSet(false, true); + } + + protected synchronized void cleanup() { + terminate(); + stopTask(); + } +} diff --git a/core/src/main/java/software/amazon/awssdk/core/pagination/async/ResponsesSubscription.java b/core/src/main/java/software/amazon/awssdk/core/pagination/async/ResponsesSubscription.java new file mode 100644 index 000000000000..417d43e0eb65 --- /dev/null +++ b/core/src/main/java/software/amazon/awssdk/core/pagination/async/ResponsesSubscription.java @@ -0,0 +1,62 @@ +/* + * Copyright 2010-2018 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.core.pagination.async; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +/** + * An implementation of the {@link Subscription} interface that can be used to signal and cancel demand for + * paginated response pages. + * + * @param The type of a single response page + */ +public class ResponsesSubscription extends PaginationSubscription { + + public ResponsesSubscription(Subscriber subscriber, AsyncPageFetcher nextPageFetcher) { + super(subscriber, nextPageFetcher); + } + + protected void handleRequests() { + if (!hasNextPage()) { + completeSubscription(); + return; + } + + synchronized (this) { + if (outstandingRequests.get() <= 0) { + stopTask(); + return; + } + } + + if (!isTerminated()) { + outstandingRequests.getAndDecrement(); + nextPageFetcher.nextPage(currentPage) + .whenComplete(((response, error) -> { + if (response != null) { + currentPage = response; + subscriber.onNext(response); + handleRequests(); + } + if (error != null) { + subscriber.onError(error); + cleanup(); + } + })); + } + } +} diff --git a/core/src/main/java/software/amazon/awssdk/core/pagination/async/SdkPublisher.java b/core/src/main/java/software/amazon/awssdk/core/pagination/async/SdkPublisher.java new file mode 100644 index 000000000000..8ae681e9bf35 --- /dev/null +++ b/core/src/main/java/software/amazon/awssdk/core/pagination/async/SdkPublisher.java @@ -0,0 +1,33 @@ +/* + * Copyright 2010-2018 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.core.pagination.async; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import org.reactivestreams.Publisher; + +/** + * Interface that is implemented by the Async auto-paginated responses. + */ +public interface SdkPublisher extends Publisher { + + // TODO Should we return the last response instead of Void? + default CompletableFuture forEach(Consumer consumer) { + CompletableFuture future = new CompletableFuture<>(); + subscribe(new SequentialSubscriber<>(consumer, future)); + return future; + } +} diff --git a/core/src/main/java/software/amazon/awssdk/core/pagination/async/SequentialSubscriber.java b/core/src/main/java/software/amazon/awssdk/core/pagination/async/SequentialSubscriber.java new file mode 100644 index 000000000000..e6f53f5f76a8 --- /dev/null +++ b/core/src/main/java/software/amazon/awssdk/core/pagination/async/SequentialSubscriber.java @@ -0,0 +1,63 @@ +/* + * Copyright 2010-2018 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.core.pagination.async; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +/** + * A sample implementation of {@link Subscriber} that requests data one at a time. + * + * @param Type of data requested + */ +public class SequentialSubscriber implements Subscriber { + + private final Consumer consumer; + private final CompletableFuture future; + private Subscription subscription; + + public SequentialSubscriber(Consumer consumer, CompletableFuture future) { + this.consumer = consumer; + this.future = future; + } + + @Override + public void onSubscribe(Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } + + @Override + public void onNext(T t) { + consumer.accept(t); + subscription.request(1); + } + + @Override + public void onError(Throwable t) { + future.completeExceptionally(t); + } + + @Override + public void onComplete() { + // TODO Update spotbugs version when new version is released and remove this filter the spotbugs-suppressions.xml file + // SpotBugs incorrectly reports NP_NONNULL_PARAM_VIOLATION when passing null. The fix is not released yet + // https://github.com/spotbugs/spotbugs/issues/484 + future.complete(null); + } +} diff --git a/core/src/test/java/software/amazon/awssdk/core/pagination/PaginatedItemsIterableTest.java b/core/src/test/java/software/amazon/awssdk/core/pagination/PaginatedItemsIterableTest.java index ebf4ed184d9c..d65477018aa4 100644 --- a/core/src/test/java/software/amazon/awssdk/core/pagination/PaginatedItemsIterableTest.java +++ b/core/src/test/java/software/amazon/awssdk/core/pagination/PaginatedItemsIterableTest.java @@ -67,14 +67,18 @@ public void hasNext_ReturnsFalse_WhenItemsAndPagesIteratorHasNoNextElement() { @Test public void hasNext_ReturnsTrue_WhenItemsIteratorHasNextElement() { + when(pagesIterator.hasNext()).thenReturn(true); + when(getItemIteratorFunction.apply(any())).thenReturn(singlePageItemsIterator); when(singlePageItemsIterator.hasNext()).thenReturn(true); - when(pagesIterator.hasNext()).thenReturn(false); assertTrue(itemsIterable.iterator().hasNext()); } @Test public void hasNextMethodDoesNotRetrieveNextPage_WhenItemsIteratorHasAnElement() { + when(pagesIterator.hasNext()).thenReturn(true); + when(getItemIteratorFunction.apply(any())).thenReturn(singlePageItemsIterator); + when(singlePageItemsIterator.hasNext()).thenReturn(true); Iterator itemsIterator = itemsIterable.iterator(); @@ -102,12 +106,12 @@ public void hasNextMethodGetsNextPage_WhenCurrentItemsIteratorHasNoElements() { } @Test - public void hasNextMethodGetsNextPage_WhenCurrentItemsIteratorIsNull() { + public void hasNextMethodGetsNextPage_WhenCurrentItemsIteratorIsEmpty() { when(pagesIterator.hasNext()).thenReturn(true); - when(getItemIteratorFunction.apply(any())).thenReturn(null, singlePageItemsIterator); + when(getItemIteratorFunction.apply(any())).thenReturn(singlePageItemsIterator); - when(singlePageItemsIterator.hasNext()).thenReturn(true); + when(singlePageItemsIterator.hasNext()).thenReturn(false, true); itemsIterable.iterator().hasNext(); diff --git a/pom.xml b/pom.xml index d3119b4bb69c..ca6510c80f76 100644 --- a/pom.xml +++ b/pom.xml @@ -247,6 +247,11 @@ netty-reactive-streams-http 2.0.0 + + io.reactivex.rxjava2 + rxjava + 2.1.9 + diff --git a/services/dynamodb/src/it/java/software/amazon/awssdk/services/dynamodb/ScanPaginatorIntegrationTest.java b/services/dynamodb/src/it/java/software/amazon/awssdk/services/dynamodb/ScanPaginatorIntegrationTest.java index b802f81e54f3..0ab7419ff0e3 100644 --- a/services/dynamodb/src/it/java/software/amazon/awssdk/services/dynamodb/ScanPaginatorIntegrationTest.java +++ b/services/dynamodb/src/it/java/software/amazon/awssdk/services/dynamodb/ScanPaginatorIntegrationTest.java @@ -15,12 +15,14 @@ package software.amazon.awssdk.services.dynamodb; +import static org.junit.Assert.assertEquals; + import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import java.util.Random; import java.util.stream.Stream; import org.junit.AfterClass; -import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import software.amazon.awssdk.core.pagination.SdkIterable; @@ -35,7 +37,7 @@ import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; import software.amazon.awssdk.services.dynamodb.model.ScanRequest; import software.amazon.awssdk.services.dynamodb.model.ScanResponse; -import software.amazon.awssdk.services.dynamodb.paginators.ScanPaginator; +import software.amazon.awssdk.services.dynamodb.paginators.ScanIterable; import utils.resources.tables.BasicTempTable; import utils.test.util.DynamoDBTestBase; import utils.test.util.TableUtils; @@ -78,7 +80,7 @@ public static void cleanUpFixture() { @Test public void test_MultipleIteration_On_Responses_Iterable() { ScanRequest request = ScanRequest.builder().tableName(TABLE_NAME).limit(2).build(); - ScanPaginator scanResponses = dynamo.scanIterable(request); + ScanIterable scanResponses = dynamo.scanPaginator(request); int count = 0; @@ -86,20 +88,20 @@ public void test_MultipleIteration_On_Responses_Iterable() { for (ScanResponse response : scanResponses) { count += response.count(); } - Assert.assertEquals(ITEM_COUNT, count); + assertEquals(ITEM_COUNT, count); // Iterate second time count = 0; for (ScanResponse response : scanResponses) { count += response.count(); } - Assert.assertEquals(ITEM_COUNT, count); + assertEquals(ITEM_COUNT, count); } @Test public void test_MultipleIteration_On_PaginatedMember_Iterable() { ScanRequest request = ScanRequest.builder().tableName(TABLE_NAME).limit(2).build(); - SdkIterable> items = dynamo.scanIterable(request).items(); + SdkIterable> items = dynamo.scanPaginator(request).items(); int count = 0; @@ -107,80 +109,118 @@ public void test_MultipleIteration_On_PaginatedMember_Iterable() { for (Map item : items) { count++; } - Assert.assertEquals(ITEM_COUNT, count); + assertEquals(ITEM_COUNT, count); // Iterate second time count = 0; for (Map item : items) { count++; } - Assert.assertEquals(ITEM_COUNT, count); + assertEquals(ITEM_COUNT, count); } @Test public void test_MultipleIteration_On_Responses_Stream() { int results_per_page = 2; ScanRequest request = ScanRequest.builder().tableName(TABLE_NAME).limit(results_per_page).build(); - ScanPaginator scanResponses = dynamo.scanIterable(request); + ScanIterable scanResponses = dynamo.scanPaginator(request); // Iterate once - Assert.assertEquals(ITEM_COUNT, scanResponses.stream() + assertEquals(ITEM_COUNT, scanResponses.stream() .mapToInt(response -> response.count()) .sum()); // Iterate second time - Assert.assertEquals(ITEM_COUNT, scanResponses.stream() + assertEquals(ITEM_COUNT, scanResponses.stream() .mapToInt(response -> response.count()) .sum()); // Number of pages - Assert.assertEquals(Math.ceil((double) ITEM_COUNT/results_per_page), scanResponses.stream().count(), 0); + assertEquals(Math.ceil((double) ITEM_COUNT/results_per_page), scanResponses.stream().count(), 0); } @Test public void test_MultipleIteration_On_PaginatedMember_Stream() { int results_per_page = 2; ScanRequest request = ScanRequest.builder().tableName(TABLE_NAME).limit(results_per_page).build(); - SdkIterable> items = dynamo.scanIterable(request).items(); + SdkIterable> items = dynamo.scanPaginator(request).items(); // Iterate once - Assert.assertEquals(ITEM_COUNT, items.stream().distinct().count()); + assertEquals(ITEM_COUNT, items.stream().distinct().count()); // Iterate second time - Assert.assertEquals(ITEM_COUNT, items.stream().distinct().count()); + assertEquals(ITEM_COUNT, items.stream().distinct().count()); } @Test (expected = IllegalStateException.class) public void iteration_On_SameStream_ThrowsError() { int results_per_page = 2; ScanRequest request = ScanRequest.builder().tableName(TABLE_NAME).limit(results_per_page).build(); - Stream> itemsStream = dynamo.scanIterable(request).items().stream(); + Stream> itemsStream = dynamo.scanPaginator(request).items().stream(); // Iterate once - Assert.assertEquals(ITEM_COUNT, itemsStream.distinct().count()); + assertEquals(ITEM_COUNT, itemsStream.distinct().count()); // Iterate second time - Assert.assertEquals(ITEM_COUNT, itemsStream.distinct().count()); + assertEquals(ITEM_COUNT, itemsStream.distinct().count()); } @Test public void mix_Iterator_And_Stream_Calls() { ScanRequest request = ScanRequest.builder().tableName(TABLE_NAME).limit(2).build(); - ScanPaginator scanResponses = dynamo.scanIterable(request); + ScanIterable scanResponses = dynamo.scanPaginator(request); - Assert.assertEquals(ITEM_COUNT, scanResponses.stream().flatMap(r -> r.items().stream()) + assertEquals(ITEM_COUNT, scanResponses.stream().flatMap(r -> r.items().stream()) .distinct() .count()); - Assert.assertEquals(ITEM_COUNT, scanResponses.stream().mapToInt(response -> response.count()).sum()); + assertEquals(ITEM_COUNT, scanResponses.stream().mapToInt(response -> response.count()).sum()); int count = 0; for (ScanResponse response : scanResponses) { count += response.count(); } - Assert.assertEquals(ITEM_COUNT, count); + assertEquals(ITEM_COUNT, count); + } + + @Test + public void test_resume_On_IntermediatePage() { + ScanRequest request = ScanRequest.builder().tableName(TABLE_NAME).limit(5).build(); + ScanIterable scanResponses = dynamo.scanPaginator(request); + + Iterator scanResponsesIterator = scanResponses.iterator(); + + int scannedCount = 0; + scannedCount += scanResponsesIterator.next().count(); + + ScanResponse lastResponse = scanResponsesIterator.next(); + scannedCount += lastResponse.count(); + + // Resume from last page + assertEquals(ITEM_COUNT, scannedCount + scanResponses.resume(lastResponse).stream() + .flatMap(r -> r.items().stream()) + .count()); + + + assertEquals(ITEM_COUNT, scannedCount + scanResponses.resume(lastResponse).items().stream().count()); + } + + + @Test + public void test_resume_On_LastPage() { + ScanRequest request = ScanRequest.builder().tableName(TABLE_NAME).limit(5).build(); + ScanIterable scanResponses = dynamo.scanPaginator(request); + + ScanResponse lastPage = scanResponses.stream().reduce((first, second) -> second).get(); + + // Resume from last page + assertEquals(0, scanResponses.resume(lastPage).stream() + .flatMap(r -> r.items().stream()) + .count()); + + assertEquals(0, scanResponses.resume(lastPage).items().stream().count()); } private static void putTestData() { diff --git a/services/pom.xml b/services/pom.xml index 8237d3e8a955..dd876af5e376 100644 --- a/services/pom.xml +++ b/services/pom.xml @@ -187,6 +187,11 @@ slf4j-log4j12 test + + io.reactivex.rxjava2 + rxjava + test + diff --git a/services/s3/src/it/java/software/amazon/awssdk/services/s3/ListObjectsV2PaginatorsIntegrationTest.java b/services/s3/src/it/java/software/amazon/awssdk/services/s3/ListObjectsV2PaginatorsIntegrationTest.java new file mode 100644 index 000000000000..12b881a3a42e --- /dev/null +++ b/services/s3/src/it/java/software/amazon/awssdk/services/s3/ListObjectsV2PaginatorsIntegrationTest.java @@ -0,0 +1,307 @@ +/* + * Copyright 2010-2018 Amazon.com, Inc. or its affiliates. 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.services.s3; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.fail; +import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName; + +import io.reactivex.Flowable; +import io.reactivex.Single; +import java.io.File; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.S3Object; +import software.amazon.awssdk.services.s3.paginators.ListObjectsV2Iterable; +import software.amazon.awssdk.services.s3.paginators.ListObjectsV2Publisher; +import software.amazon.awssdk.testutils.RandomTempFile; + +public class ListObjectsV2PaginatorsIntegrationTest extends S3IntegrationTestBase { + + private static final long OBJECT_COUNT = 15; + + /** + * Content length for sample keys created by these tests. + */ + private static final long CONTENT_LENGTH = 123; + + /** + * The name of the bucket created, used, and deleted by these tests. + */ + private static String bucketName = temporaryBucketName("list-objects-v2-integ-test"); + + private static String emptyBucketName = temporaryBucketName("list-objects-integ-test-emptybucket"); + + /** + * List of all keys created by these tests. + */ + private static List keys = new ArrayList<>(); + + private static final ListObjectsV2Request.Builder requestBuilder = ListObjectsV2Request.builder().maxKeys(3); + + /** + * Releases all resources created in this test. + */ + @AfterClass + public static void tearDown() { + deleteBucketAndAllContents(bucketName); + deleteBucketAndAllContents(emptyBucketName); + } + + /** + * Creates all the test resources for the tests. + */ + @BeforeClass + public static void createResources() throws Exception { + createBucket(bucketName); + createBucket(emptyBucketName); + + NumberFormat numberFormatter = new DecimalFormat("##00"); + for (int i = 1; i <= OBJECT_COUNT; i++) { + createKey("key-" + numberFormatter.format(i)); + } + } + + /** + * Creates a test object in S3 with the specified name, using random ASCII + * data of the default content length as defined in this test class. + * + * @param key The key under which to create the object in this test class' + * test bucket. + */ + private static void createKey(String key) throws Exception { + + File file = new RandomTempFile("list-objects-integ-test-" + new Date().getTime(), CONTENT_LENGTH); + + s3.putObject(PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build(), + RequestBody.of(file)); + keys.add(key); + } + + @Test + public void test_SyncResponse_onEmptyBucket() { + ListObjectsV2Iterable iterable = s3.listObjectsV2Paginator(requestBuilder.bucket(emptyBucketName).build()); + + assertThat(iterable.contents().stream().count(), equalTo(0L)); + + assertThat(iterable.stream() + .flatMap(r -> r.contents() != null ? r.contents().stream() : Stream.empty()) + .count(), + equalTo(0L)); + } + + @Test + public void test_SyncResponse_onNonEmptyBucket() { + ListObjectsV2Iterable iterable = s3.listObjectsV2Paginator(requestBuilder.bucket(bucketName).build()); + + assertThat(iterable.contents().stream().count(), equalTo(OBJECT_COUNT)); + + assertThat(iterable.stream().flatMap(r -> r.contents().stream()).count(), equalTo(OBJECT_COUNT)); + } + + @Test + public void test_AsyncResponse_OnNonEmptyBucket() throws ExecutionException, InterruptedException { + ListObjectsV2Publisher publisher = s3Async.listObjectsV2Paginator(requestBuilder.bucket(bucketName).build()); + + publisher.subscribe(new Subscriber() { + private Subscription subscription; + private int keyCount; + + @Override + public void onSubscribe(Subscription s) { + subscription = s; + keyCount = 0; + subscription.request(2); + } + + @Override + public void onNext(ListObjectsV2Response response) { + keyCount += response.keyCount(); + subscription.request(1); + subscription.request(2); + } + + @Override + public void onError(Throwable t) { + fail("Error receiving response" + t.getMessage()); + } + + @Override + public void onComplete() { + assertThat(keyCount, equalTo(OBJECT_COUNT)); + } + }); + Thread.sleep(3_000); + + + // count objects using forEach + final long[] count = {0}; + CompletableFuture future = publisher.forEach(response -> { + count[0] += response.keyCount(); + }); + future.get(); + assertThat(count[0], equalTo(OBJECT_COUNT)); + + + // Use ForEach: collect objects into a list + List objects = new ArrayList<>(); + CompletableFuture future2 = publisher.forEach(response -> { + objects.addAll(response.contents()); + }); + future2.get(); + assertThat(Long.valueOf(objects.size()), equalTo(OBJECT_COUNT)); + } + + @Test + public void test_AsyncResponse_OnEmptyBucket() throws ExecutionException, InterruptedException { + ListObjectsV2Publisher publisher = s3Async.listObjectsV2Paginator(requestBuilder.bucket(emptyBucketName).build()); + + final int[] count = {0}; + CompletableFuture future = publisher.forEach(response -> { + count[0] += response.keyCount(); + }); + future.get(); + assertThat(count[0], equalTo(0)); + } + + @Test + public void test_AsyncResponse_UsingRxJava() { + ListObjectsV2Publisher publisher = s3Async.listObjectsV2Paginator(requestBuilder.bucket(bucketName).build()); + + Single> objects = Flowable.fromPublisher(publisher) + .flatMapIterable(ListObjectsV2Response::contents) + .toList(); + + // There are multiple fluent methods to convert Single type to a different form + List objectList = objects.blockingGet(); + assertThat(Long.valueOf(objectList.size()), equalTo(OBJECT_COUNT)); + } + + @Test + public void test_Resume_Using_IntermediatePage_OnAsyncResponse() throws ExecutionException, InterruptedException { + ListObjectsV2Publisher publisher = s3Async.listObjectsV2Paginator(requestBuilder.bucket(bucketName).build()); + + TestSubscriber firstSubscriber = new TestSubscriber(2); + publisher.subscribe(firstSubscriber); + while (!firstSubscriber.isDone()) { + Thread.sleep(1000); + } + + // Call resume + ListObjectsV2Publisher resumedPublisher = publisher.resume(firstSubscriber.getLastPage()); + TestSubscriber secondSubscriber = new TestSubscriber(10); + resumedPublisher.subscribe(secondSubscriber); + while (!secondSubscriber.isDone()) { + Thread.sleep(1000); + } + + assertThat(firstSubscriber.getKeyCount() + secondSubscriber.getKeyCount(), + equalTo(OBJECT_COUNT)); + } + + @Test + public void test_Resume_Using_LastPage_OnAsyncResponse() throws ExecutionException, InterruptedException { + ListObjectsV2Publisher publisher = s3Async.listObjectsV2Paginator(requestBuilder.bucket(bucketName).build()); + + final ListObjectsV2Response[] lastPage = new ListObjectsV2Response[1]; + CompletableFuture future = publisher.forEach(response -> { + lastPage[0] = response; + }); + future.get(); + + // Resume + ListObjectsV2Publisher resumedPublisher = publisher.resume(lastPage[0]); + + // count using pages + final long[] count = {0}; + resumedPublisher.forEach(response -> count[0] += response.contents().size()) + .get(); + assertThat(count[0], equalTo(0L)); + + // count using items + List objects = new ArrayList<>(); + resumedPublisher.contents().forEach(s3Object -> objects.add(s3Object)) + .get(); + assertThat(Long.valueOf(objects.size()), equalTo(0L)); + } + + private class TestSubscriber implements Subscriber { + private Subscription subscription; + private ListObjectsV2Response lastPage; + private final long requestCount; + private long keyCount; + private long requestsCompleted; + private boolean isDone; + + public TestSubscriber(long requestCount) { + this.requestCount = requestCount; + } + + @Override + public void onSubscribe(Subscription s) { + subscription = s; + subscription.request(requestCount); + } + + @Override + public void onNext(ListObjectsV2Response response) { + lastPage = response; + keyCount += response.keyCount(); + requestsCompleted++; + } + + @Override + public void onError(Throwable t) { + fail("Error receiving response" + t.getMessage()); + } + + @Override + public void onComplete() { + isDone = true; + } + + public long getKeyCount() { + return keyCount; + } + + public ListObjectsV2Response getLastPage() { + return lastPage; + } + + public boolean isDone() { + return isDone || requestCount == requestsCompleted; + } + } +}