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