1 /*
2  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License").
5  * You may not use this file except in compliance with the License.
6  * A copy of the License is located at
7  *
8  *  http://aws.amazon.com/apache2.0
9  *
10  * or in the "license" file accompanying this file. This file is distributed
11  * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12  * express or implied. See the License for the specific language governing
13  * permissions and limitations under the License.
14  */
15 
16 package software.amazon.awssdk.codegen.poet.model;
17 
18 import static software.amazon.awssdk.codegen.poet.model.TypeProvider.ShapeTransformation.NONE;
19 import static software.amazon.awssdk.codegen.poet.model.TypeProvider.ShapeTransformation.USE_BUILDER;
20 
21 import com.squareup.javapoet.ClassName;
22 import com.squareup.javapoet.CodeBlock;
23 import com.squareup.javapoet.MethodSpec;
24 import com.squareup.javapoet.TypeName;
25 import com.squareup.javapoet.TypeSpec;
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.LinkedHashMap;
30 import java.util.Map;
31 import java.util.stream.Collectors;
32 import javax.lang.model.element.Modifier;
33 import software.amazon.awssdk.codegen.internal.Utils;
34 import software.amazon.awssdk.codegen.model.intermediate.MapModel;
35 import software.amazon.awssdk.codegen.model.intermediate.MemberModel;
36 import software.amazon.awssdk.codegen.poet.ClassSpec;
37 import software.amazon.awssdk.codegen.poet.PoetExtension;
38 import software.amazon.awssdk.codegen.poet.PoetUtils;
39 import software.amazon.awssdk.codegen.poet.StaticImport;
40 import software.amazon.awssdk.codegen.poet.model.TypeProvider.TypeNameOptions;
41 import software.amazon.awssdk.core.util.DefaultSdkAutoConstructList;
42 import software.amazon.awssdk.core.util.DefaultSdkAutoConstructMap;
43 import software.amazon.awssdk.core.util.SdkAutoConstructList;
44 import software.amazon.awssdk.core.util.SdkAutoConstructMap;
45 
46 class MemberCopierSpec implements ClassSpec {
47     private final MemberModel memberModel;
48     private final ServiceModelCopiers serviceModelCopiers;
49     private final TypeProvider typeProvider;
50     private final PoetExtension poetExtensions;
51 
52     private enum EnumTransform {
53         /** Copy enums as strings */
54         STRING_TO_ENUM,
55         /** Copy strings as enums */
56         ENUM_TO_STRING,
57         /** Copy without a transformation */
58         NONE
59     }
60 
61     private enum BuilderTransform {
62         BUILDER_TO_BUILDABLE,
63         BUILDABLE_TO_BUILDER,
64         NONE
65     }
66 
MemberCopierSpec(MemberModel memberModel, ServiceModelCopiers serviceModelCopiers, TypeProvider typeProvider, PoetExtension poetExtensions)67     MemberCopierSpec(MemberModel memberModel,
68                      ServiceModelCopiers serviceModelCopiers,
69                      TypeProvider typeProvider,
70                      PoetExtension poetExtensions) {
71         this.memberModel = memberModel;
72         this.serviceModelCopiers = serviceModelCopiers;
73         this.typeProvider = typeProvider;
74         this.poetExtensions = poetExtensions;
75     }
76 
77     @Override
poetSpec()78     public TypeSpec poetSpec() {
79         TypeSpec.Builder builder = TypeSpec.classBuilder(className())
80                 .addModifiers(Modifier.FINAL)
81                 .addAnnotation(PoetUtils.generatedAnnotation())
82                 .addMethod(copyMethod());
83 
84         if (memberModel.containsBuildable()) {
85             builder.addMethod(copyFromBuilderMethod());
86             builder.addMethod(copyToBuilderMethod());
87         }
88 
89         // If this is a collection, and it contains enums, or recursively
90         // contains enums, add extra methods for copying the elements from an
91         // enum to string and vice versa
92         if (isEnumCopyAvailable(memberModel)) {
93             builder.addMethod(enumToStringCopyMethod());
94             builder.addMethod(stringToEnumCopyMethod());
95         }
96 
97         return builder.build();
98     }
99 
100     @Override
className()101     public ClassName className() {
102         return serviceModelCopiers.copierClassFor(memberModel).get();
103     }
104 
105     @Override
staticImports()106     public Iterable<StaticImport> staticImports() {
107         if (memberModel.isList()) {
108             return Collections.singletonList(StaticImport.staticMethodImport(Collectors.class, "toList"));
109         }
110 
111         if (memberModel.isMap()) {
112             return Collections.singletonList(StaticImport.staticMethodImport(Collectors.class, "toMap"));
113         }
114 
115         return Collections.emptyList();
116     }
117 
isEnumCopyAvailable(MemberModel memberModel)118     public static boolean isEnumCopyAvailable(MemberModel memberModel) {
119         if (!(memberModel.isMap() || memberModel.isList())) {
120             return false;
121         }
122 
123         if (memberModel.isMap()) {
124             MapModel mapModel = memberModel.getMapModel();
125             MemberModel keyModel = mapModel.getKeyModel();
126             MemberModel valueModel = mapModel.getValueModel();
127             if (keyModel.getEnumType() != null || valueModel.getEnumType() != null) {
128                 return true;
129             }
130 
131             if (valueModel.isList() || valueModel.isMap()) {
132                 return isEnumCopyAvailable(valueModel);
133             }
134             // Keys are always simple, don't need to check
135         } else {
136             MemberModel element = memberModel.getListModel().getListMemberModel();
137             if (element.getEnumType() != null) {
138                 return true;
139             }
140             if (element.isList() || element.isMap()) {
141                 return isEnumCopyAvailable(element);
142             }
143         }
144 
145         return false;
146     }
147 
copyMethod()148     private MethodSpec copyMethod() {
149         return MethodSpec.methodBuilder(serviceModelCopiers.copyMethodName())
150                          .addModifiers(Modifier.STATIC)
151                          .addParameter(typeName(memberModel, true, true, BuilderTransform.NONE, EnumTransform.NONE),
152                                        memberParamName())
153                          .returns(typeName(memberModel, false, false, BuilderTransform.NONE, EnumTransform.NONE))
154                          .addCode(copyMethodBody(BuilderTransform.NONE, EnumTransform.NONE))
155                          .build();
156     }
157 
enumToStringCopyMethod()158     private MethodSpec enumToStringCopyMethod() {
159         return MethodSpec.methodBuilder(serviceModelCopiers.enumToStringCopyMethodName())
160                          .addModifiers(Modifier.STATIC)
161                          .addParameter(typeName(memberModel, true, true, BuilderTransform.NONE, EnumTransform.ENUM_TO_STRING),
162                                        memberParamName())
163                          .returns(typeName(memberModel, false, false, BuilderTransform.NONE, EnumTransform.ENUM_TO_STRING))
164                          .addCode(copyMethodBody(BuilderTransform.NONE, EnumTransform.ENUM_TO_STRING))
165                          .build();
166     }
167 
stringToEnumCopyMethod()168     private MethodSpec stringToEnumCopyMethod() {
169         return MethodSpec.methodBuilder(serviceModelCopiers.stringToEnumCopyMethodName())
170                          .addModifiers(Modifier.STATIC)
171                          .addParameter(typeName(memberModel, true, true, BuilderTransform.NONE, EnumTransform.STRING_TO_ENUM),
172                                        memberParamName())
173                          .returns(typeName(memberModel, false, false, BuilderTransform.NONE, EnumTransform.STRING_TO_ENUM))
174                          .addCode(copyMethodBody(BuilderTransform.NONE, EnumTransform.STRING_TO_ENUM))
175                          .build();
176     }
177 
copyFromBuilderMethod()178     private MethodSpec copyFromBuilderMethod() {
179         return MethodSpec.methodBuilder(serviceModelCopiers.copyFromBuilderMethodName())
180                          .addModifiers(Modifier.STATIC)
181                          .returns(typeName(memberModel, false, false, BuilderTransform.BUILDER_TO_BUILDABLE,
182                                            EnumTransform.NONE))
183                          .addParameter(typeName(memberModel, true, true, BuilderTransform.BUILDER_TO_BUILDABLE,
184                                                 EnumTransform.NONE),
185                                        memberParamName())
186                          .addCode(copyMethodBody(BuilderTransform.BUILDER_TO_BUILDABLE, EnumTransform.NONE))
187                          .build();
188     }
189 
copyToBuilderMethod()190     private MethodSpec copyToBuilderMethod() {
191         return MethodSpec.methodBuilder(serviceModelCopiers.copyToBuilderMethodName())
192                          .addModifiers(Modifier.STATIC)
193                          .returns(typeName(memberModel, false, false, BuilderTransform.BUILDABLE_TO_BUILDER, EnumTransform.NONE))
194                          .addParameter(typeName(memberModel, true, true, BuilderTransform.BUILDABLE_TO_BUILDER,
195                                                 EnumTransform.NONE),
196                                        memberParamName())
197                          .addCode(copyMethodBody(BuilderTransform.BUILDABLE_TO_BUILDER, EnumTransform.NONE))
198                          .build();
199     }
200 
copyMethodBody(BuilderTransform builderTransform, EnumTransform enumTransform)201     private CodeBlock copyMethodBody(BuilderTransform builderTransform, EnumTransform enumTransform) {
202         CodeBlock.Builder code = CodeBlock.builder();
203 
204         if (!memberModel.getAutoConstructClassIfExists().isPresent()) {
205             code.add("if ($N == null) {", memberParamName())
206                 .add("return null;")
207                 .add("}");
208         }
209 
210         String outputVariable = copyMethodBody(code, builderTransform, new UniqueVariableSource(),
211                                                enumTransform, memberParamName(), memberModel);
212 
213         code.add("return $N;", outputVariable);
214 
215         return code.build();
216     }
217 
copyMethodBody(CodeBlock.Builder code, BuilderTransform builderTransform, UniqueVariableSource variableSource, EnumTransform enumTransform, String inputVariableName, MemberModel inputMember)218     private String copyMethodBody(CodeBlock.Builder code, BuilderTransform builderTransform, UniqueVariableSource variableSource,
219                                   EnumTransform enumTransform, String inputVariableName, MemberModel inputMember) {
220         if (inputMember.getEnumType() != null) {
221             String outputVariableName = variableSource.getNew("result");
222             ClassName enumType = poetExtensions.getModelClass(inputMember.getEnumType());
223             switch (enumTransform) {
224                 case NONE:
225                     return inputVariableName;
226                 case ENUM_TO_STRING:
227                     code.add("$T $N = $N.toString();", String.class, outputVariableName, inputVariableName);
228                     return outputVariableName;
229                 case STRING_TO_ENUM:
230                     code.add("$1T $2N = $1T.fromValue($3N);", enumType, outputVariableName, inputVariableName);
231                     return outputVariableName;
232                 default:
233                     throw new IllegalStateException();
234             }
235         }
236 
237         if (inputMember.isSimple()) {
238             return inputVariableName;
239         }
240 
241         if (inputMember.hasBuilder()) {
242             switch (builderTransform) {
243                 case NONE:
244                     return inputVariableName;
245                 case BUILDER_TO_BUILDABLE:
246                     String buildableOutput = variableSource.getNew("member");
247                     TypeName buildableOutputType = typeName(inputMember, false, false, builderTransform, enumTransform);
248                     code.add("$T $N = $N == null ? null : $N.build();", buildableOutputType, buildableOutput, inputVariableName,
249                              inputVariableName);
250                     return buildableOutput;
251                 case BUILDABLE_TO_BUILDER:
252                     String builderOutput = variableSource.getNew("member");
253                     TypeName builderOutputType = typeName(inputMember, false, false, builderTransform, enumTransform);
254                     code.add("$T $N = $N == null ? null : $N.toBuilder();", builderOutputType, builderOutput, inputVariableName,
255                              inputVariableName);
256                     return builderOutput;
257                 default:
258                     throw new IllegalStateException();
259             }
260         }
261 
262         if (inputMember.isList()) {
263             String outputVariableName = variableSource.getNew("list");
264             String modifiableVariableName = variableSource.getNew("modifiableList");
265 
266             MemberModel listEntryModel = inputMember.getListModel().getListMemberModel();
267             TypeName listType = typeName(inputMember, false, false, builderTransform, enumTransform);
268 
269             code.add("$T $N;", listType, outputVariableName)
270                 .add("if ($1N == null || $1N instanceof $2T) {", inputVariableName, SdkAutoConstructList.class)
271                 .add("$N = $T.getInstance();", outputVariableName, DefaultSdkAutoConstructList.class)
272                 .add("} else {")
273                 .add("$T $N = new $T<>();", listType, modifiableVariableName, ArrayList.class);
274 
275             String entryInputVariable = variableSource.getNew("entry");
276             code.add("$N.forEach($N -> {", inputVariableName, entryInputVariable);
277 
278             String entryOutputVariable =
279                 copyMethodBody(code, builderTransform, variableSource, enumTransform, entryInputVariable, listEntryModel);
280 
281             code.add("$N.add($N);", modifiableVariableName, entryOutputVariable)
282                 .add("});")
283                 .add("$N = $T.unmodifiableList($N);", outputVariableName, Collections.class, modifiableVariableName)
284                 .add("}");
285 
286             return outputVariableName;
287         }
288 
289         if (inputMember.isMap()) {
290             String outputVariableName = variableSource.getNew("map");
291             String modifiableVariableName = variableSource.getNew("modifiableMap");
292 
293             MemberModel keyModel = inputMember.getMapModel().getKeyModel();
294             MemberModel valueModel = inputMember.getMapModel().getValueModel();
295             TypeName keyType = typeName(keyModel, false, false, builderTransform, enumTransform);
296             TypeName outputMapType = typeName(inputMember, false, false, builderTransform, enumTransform);
297 
298             code.add("$T $N;", outputMapType, outputVariableName)
299                 .add("if ($1N == null || $1N instanceof $2T) {", inputVariableName, SdkAutoConstructMap.class)
300                 .add("$N = $T.getInstance();", outputVariableName, DefaultSdkAutoConstructMap.class)
301                 .add("} else {")
302                 .add("$T $N = new $T<>();", outputMapType, modifiableVariableName, LinkedHashMap.class);
303 
304             String keyInputVariable = variableSource.getNew("key");
305             String valueInputVariable = variableSource.getNew("value");
306             code.add("$N.forEach(($N, $N) -> {", inputVariableName, keyInputVariable, valueInputVariable);
307 
308             String keyOutputVariable =
309                 copyMethodBody(code, builderTransform, variableSource, enumTransform, keyInputVariable, keyModel);
310 
311             String valueOutputVariable =
312                 copyMethodBody(code, builderTransform, variableSource, enumTransform, valueInputVariable, valueModel);
313 
314             if (keyModel.getEnumType() != null && !keyType.toString().equals("java.lang.String")) {
315                 // When enums are used as keys, drop any entries with unknown keys
316                 code.add("if ($N != $T.UNKNOWN_TO_SDK_VERSION) {", keyOutputVariable, keyType)
317                     .add("$N.put($N, $N);", modifiableVariableName, keyOutputVariable, valueOutputVariable)
318                     .add("}");
319             } else {
320                 code.add("$N.put($N, $N);", modifiableVariableName, keyOutputVariable, valueOutputVariable);
321             }
322 
323             code.add("});")
324                 .add("$N = $T.unmodifiableMap($N);", outputVariableName, Collections.class, modifiableVariableName)
325                 .add("}");
326 
327             return outputVariableName;
328         }
329 
330         throw new UnsupportedOperationException("Unable to generate copier for member '" + inputMember + "'");
331     }
332 
typeName(MemberModel model, boolean isInputType, boolean useCollectionForList, BuilderTransform builderTransform, EnumTransform enumTransform)333     private TypeName typeName(MemberModel model, boolean isInputType, boolean useCollectionForList,
334                               BuilderTransform builderTransform, EnumTransform enumTransform) {
335 
336         boolean useEnumTypes = (isInputType && enumTransform == EnumTransform.ENUM_TO_STRING) ||
337                                (!isInputType && enumTransform == EnumTransform.STRING_TO_ENUM);
338 
339         boolean useBuilderTypes = (isInputType && builderTransform == BuilderTransform.BUILDER_TO_BUILDABLE) ||
340                                   (!isInputType && builderTransform == BuilderTransform.BUILDABLE_TO_BUILDER);
341 
342         return typeProvider.typeName(model, new TypeNameOptions().useEnumTypes(useEnumTypes)
343                                                                  .shapeTransformation(useBuilderTypes ? USE_BUILDER : NONE)
344                                                                  .useSubtypeWildcardsForCollections(isInputType)
345                                                                  .useSubtypeWildcardsForBuilders(isInputType)
346                                                                  .useCollectionForList(useCollectionForList));
347     }
348 
memberParamName()349     private String memberParamName() {
350         if (memberModel.isSimple()) {
351             return Utils.unCapitalize(memberModel.getVariable().getSimpleType()) + "Param";
352         }
353         return Utils.unCapitalize(memberModel.getC2jShape()) + "Param";
354     }
355 
356     private static final class UniqueVariableSource {
357         private final Map<String, Integer> suffixes = new HashMap<>();
358 
getNew(String prefix)359         private String getNew(String prefix) {
360             return prefix + suffix(prefix);
361         }
362 
suffix(String prefix)363         private String suffix(String prefix) {
364             Integer suffixNumber = suffixes.compute(prefix, (k, v) -> v == null ? 0 : v + 1);
365             return suffixNumber == 0 ? "" : suffixNumber.toString();
366         }
367     }
368 }
369