software.amazon.awssdk.codegen.poet.paginators.PaginatorsClassSpec Maven / Gradle / Ivy
/*
* Copyright 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.PoetExtension;
import software.amazon.awssdk.codegen.poet.model.TypeProvider;
import software.amazon.awssdk.core.util.PaginatorUtils;
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 PoetExtension 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 PoetExtension(model);
this.typeProvider = new TypeProvider(model);
this.operationModel = model.getOperation(c2jOperationName);
if (operationModel == null) {
throw new IllegalArgumentException("The service model does not model an operation '" + c2jOperationName + "'");
}
this.paginationDocs = new PaginationDocs(model, operationModel, paginatorDefinition);
}
/**
* @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.PRIVATE, Modifier.FINAL)
.addParameter(responseType(), LAST_SUCCESSFUL_PAGE_LITERAL)
.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) {
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 CodeBlock hasNextPageMethodBody() {
if (paginatorDefinition.getMoreResults() != null) {
return CodeBlock.builder()
.add("return $1N.$2L != null && $1N.$2L.booleanValue()",
PREVIOUS_PAGE_METHOD_ARGUMENT,
fluentGetterMethodForResponseMember(paginatorDefinition.getMoreResults()))
.build();
}
// If there is no more_results token, then output_token will be a single value
return CodeBlock.builder()
.add("return $3T.isOutputTokenAvailable($1N.$2L)",
PREVIOUS_PAGE_METHOD_ARGUMENT,
fluentGetterMethodsForOutputToken().get(0),
PaginatorUtils.class)
.build();
}
/*
* 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());
*/
protected String codeToGetNextPageIfOldResponseIsNotNull() {
return String.format("return %s.%s(%s)", CLIENT_MEMBER,
operationModel.getMethodName(),
constructRequestFromLastPage(PREVIOUS_PAGE_METHOD_ARGUMENT));
}
/**
* 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.
*/
protected 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}.
*/
protected String fluentGetterMethodForResponseMember(String member) {
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();
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) {
String conditionalStatement = getConditionalStatementforIteratorLambda(resultKey);
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) {
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();
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();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy