-
Notifications
You must be signed in to change notification settings - Fork 793
/
PaginatorsClassSpec.java
365 lines (317 loc) · 17.1 KB
/
PaginatorsClassSpec.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
/*
* 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);
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("<p>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.</p>", className())
.build());
}
/*
* Returns the {@link TypeName} for a value in the {@link PaginatorDefinition#getResultKey()} list.
*
* Examples:
* If paginated item is represented as List<String>, then member type is String.
* If paginated item is represented as List<Foo>, then member type is Foo.
* If paginated item is represented as Map<String, List<Foo>>,
* then member type is Map.Entry<String, List<Foo>>.
*/
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<String> requestSetterNames = fluentSetterMethodNamesForInputToken();
List<String> 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<String> 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<String> 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();
}
}