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.rules; 17 18 import com.fasterxml.jackson.core.TreeNode; 19 import com.fasterxml.jackson.jr.stree.JrsBoolean; 20 import com.fasterxml.jackson.jr.stree.JrsString; 21 import com.squareup.javapoet.ClassName; 22 import com.squareup.javapoet.CodeBlock; 23 import com.squareup.javapoet.FieldSpec; 24 import com.squareup.javapoet.MethodSpec; 25 import com.squareup.javapoet.ParameterSpec; 26 import com.squareup.javapoet.ParameterizedTypeName; 27 import com.squareup.javapoet.TypeName; 28 import java.io.IOException; 29 import java.io.UncheckedIOException; 30 import java.net.URL; 31 import java.util.List; 32 import java.util.Locale; 33 import java.util.Map; 34 import java.util.concurrent.CompletableFuture; 35 import java.util.jar.JarFile; 36 import java.util.stream.Collectors; 37 import java.util.zip.ZipEntry; 38 import javax.lang.model.element.Modifier; 39 import software.amazon.awssdk.codegen.internal.Utils; 40 import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; 41 import software.amazon.awssdk.codegen.model.intermediate.Metadata; 42 import software.amazon.awssdk.codegen.model.rules.endpoints.BuiltInParameter; 43 import software.amazon.awssdk.codegen.model.rules.endpoints.ParameterModel; 44 import software.amazon.awssdk.endpoints.Endpoint; 45 import software.amazon.awssdk.regions.Region; 46 import software.amazon.awssdk.utils.Validate; 47 import software.amazon.awssdk.utils.internal.CodegenNamingUtils; 48 49 public class EndpointRulesSpecUtils { 50 private final IntermediateModel intermediateModel; 51 EndpointRulesSpecUtils(IntermediateModel intermediateModel)52 public EndpointRulesSpecUtils(IntermediateModel intermediateModel) { 53 this.intermediateModel = intermediateModel; 54 } 55 basePackage()56 public String basePackage() { 57 return intermediateModel.getMetadata().getFullEndpointRulesPackageName(); 58 } 59 rulesRuntimeClassName(String name)60 public ClassName rulesRuntimeClassName(String name) { 61 return ClassName.get(intermediateModel.getMetadata().getFullInternalEndpointRulesPackageName(), 62 name); 63 } 64 parametersClassName()65 public ClassName parametersClassName() { 66 return ClassName.get(basePackage(), intermediateModel.getMetadata().getServiceName() + "EndpointParams"); 67 } 68 providerInterfaceName()69 public ClassName providerInterfaceName() { 70 return ClassName.get(basePackage(), intermediateModel.getMetadata().getServiceName() + "EndpointProvider"); 71 } 72 providerDefaultImplName()73 public ClassName providerDefaultImplName() { 74 Metadata md = intermediateModel.getMetadata(); 75 return ClassName.get(md.getFullInternalEndpointRulesPackageName(), 76 "Default" + providerInterfaceName().simpleName()); 77 } 78 resolverInterceptorName()79 public ClassName resolverInterceptorName() { 80 Metadata md = intermediateModel.getMetadata(); 81 return ClassName.get(md.getFullInternalEndpointRulesPackageName(), 82 md.getServiceName() + "ResolveEndpointInterceptor"); 83 } 84 requestModifierInterceptorName()85 public ClassName requestModifierInterceptorName() { 86 Metadata md = intermediateModel.getMetadata(); 87 return ClassName.get(md.getFullInternalEndpointRulesPackageName(), 88 md.getServiceName() + "RequestSetEndpointInterceptor"); 89 } 90 clientEndpointTestsName()91 public ClassName clientEndpointTestsName() { 92 Metadata md = intermediateModel.getMetadata(); 93 return ClassName.get(md.getFullEndpointRulesPackageName(), 94 md.getServiceName() + "ClientEndpointTests"); 95 } 96 endpointProviderTestsName()97 public ClassName endpointProviderTestsName() { 98 Metadata md = intermediateModel.getMetadata(); 99 return ClassName.get(md.getFullEndpointRulesPackageName(), 100 md.getServiceName() + "EndpointProviderTests"); 101 } 102 clientContextParamsName()103 public ClassName clientContextParamsName() { 104 Metadata md = intermediateModel.getMetadata(); 105 return ClassName.get(md.getFullEndpointRulesPackageName(), 106 md.getServiceName() + "ClientContextParams"); 107 } 108 paramMethodName(String param)109 public String paramMethodName(String param) { 110 return Utils.unCapitalize(CodegenNamingUtils.pascalCase(param)); 111 } 112 clientContextParamMethodName(String param)113 public String clientContextParamMethodName(String param) { 114 return Utils.unCapitalize(CodegenNamingUtils.pascalCase(param)); 115 } 116 clientContextParamName(String paramName)117 public String clientContextParamName(String paramName) { 118 return intermediateModel.getNamingStrategy().getEnumValueName(paramName); 119 } 120 toJavaType(String type)121 public TypeName toJavaType(String type) { 122 switch (type.toLowerCase(Locale.ENGLISH)) { 123 case "boolean": 124 return TypeName.get(Boolean.class); 125 case "string": 126 return TypeName.get(String.class); 127 default: 128 throw new RuntimeException("Unknown type: " + type); 129 } 130 } 131 valueCreationCode(String type, CodeBlock param)132 public CodeBlock valueCreationCode(String type, CodeBlock param) { 133 String methodName; 134 switch (type.toLowerCase(Locale.ENGLISH)) { 135 case "boolean": 136 methodName = "fromBool"; 137 break; 138 case "string": 139 methodName = "fromStr"; 140 break; 141 default: 142 throw new RuntimeException("Don't know how to create a Value instance from type " + type); 143 } 144 145 return CodeBlock.builder() 146 .add("$T.$N($L)", rulesRuntimeClassName("Value"), methodName, param) 147 .build(); 148 } 149 parameterType(ParameterModel param)150 public TypeName parameterType(ParameterModel param) { 151 if (param.getBuiltInEnum() == null || param.getBuiltInEnum() != BuiltInParameter.AWS_REGION) { 152 return toJavaType(param.getType()); 153 } 154 155 if (param.getBuiltInEnum() == BuiltInParameter.AWS_REGION) { 156 return ClassName.get(Region.class); 157 } 158 return toJavaType(param.getType()); 159 } 160 treeNodeToLiteral(TreeNode treeNode)161 public CodeBlock treeNodeToLiteral(TreeNode treeNode) { 162 CodeBlock.Builder b = CodeBlock.builder(); 163 164 switch (treeNode.asToken()) { 165 case VALUE_STRING: 166 b.add("$S", Validate.isInstanceOf(JrsString.class, treeNode, "Expected string").getValue()); 167 break; 168 case VALUE_TRUE: 169 case VALUE_FALSE: 170 b.add("$L", Validate.isInstanceOf(JrsBoolean.class, treeNode, "Expected boolean").booleanValue()); 171 break; 172 default: 173 throw new RuntimeException("Don't know how to set default value for parameter of type " 174 + treeNode.asToken()); 175 } 176 return b.build(); 177 } 178 179 isS3()180 public boolean isS3() { 181 return "S3".equals(intermediateModel.getMetadata().getServiceName()); 182 } 183 isS3Control()184 public boolean isS3Control() { 185 return "S3Control".equals(intermediateModel.getMetadata().getServiceName()); 186 } 187 useS3Express()188 public boolean useS3Express() { 189 return intermediateModel.getCustomizationConfig().getS3ExpressAuthSupport(); 190 } 191 resolverReturnType()192 public TypeName resolverReturnType() { 193 return ParameterizedTypeName.get(CompletableFuture.class, Endpoint.class); 194 } 195 rulesEngineResourceFiles()196 public List<String> rulesEngineResourceFiles() { 197 URL currentJarUrl = EndpointRulesSpecUtils.class.getProtectionDomain().getCodeSource().getLocation(); 198 try (JarFile jarFile = new JarFile(currentJarUrl.getFile())) { 199 return jarFile.stream() 200 .map(ZipEntry::getName) 201 .filter(e -> e.startsWith("software/amazon/awssdk/codegen/rules/")) 202 .collect(Collectors.toList()); 203 } catch (IOException e) { 204 throw new UncheckedIOException(e); 205 } 206 } 207 rulesEngineResourceFiles2()208 public List<String> rulesEngineResourceFiles2() { 209 URL currentJarUrl = EndpointRulesSpecUtils.class.getProtectionDomain().getCodeSource().getLocation(); 210 try (JarFile jarFile = new JarFile(currentJarUrl.getFile())) { 211 return jarFile.stream() 212 .map(ZipEntry::getName) 213 .filter(e -> e.startsWith("software/amazon/awssdk/codegen/rules2/")) 214 .collect(Collectors.toList()); 215 } catch (IOException e) { 216 throw new UncheckedIOException(e); 217 } 218 } 219 parameters()220 public Map<String, ParameterModel> parameters() { 221 return intermediateModel.getEndpointRuleSetModel().getParameters(); 222 } 223 isDeclaredParam(String paramName)224 public boolean isDeclaredParam(String paramName) { 225 Map<String, ParameterModel> parameters = intermediateModel.getEndpointRuleSetModel().getParameters(); 226 return parameters.containsKey(paramName); 227 } 228 229 /** 230 * Creates a data-class level field for the given parameter. For instance 231 * 232 * <pre> 233 * private final Region region; 234 * </pre> 235 */ parameterClassField(String name, ParameterModel model)236 public FieldSpec parameterClassField(String name, ParameterModel model) { 237 return parameterFieldSpecBuilder(name, model) 238 .addModifiers(Modifier.PRIVATE) 239 .addModifiers(Modifier.FINAL) 240 .build(); 241 } 242 243 /** 244 * Creates a data-class method to access the given parameter. For instance 245 * 246 * <pre> 247 * public Region region() {…}; 248 * </pre> 249 */ parameterClassAccessorMethod(String name, ParameterModel model)250 public MethodSpec parameterClassAccessorMethod(String name, ParameterModel model) { 251 MethodSpec.Builder b = parameterMethodBuilder(name, model); 252 b.returns(parameterType(model)); 253 b.addStatement("return $N", variableName(name)); 254 return b.build(); 255 } 256 257 258 /** 259 * Creates a data-interface method to access the given parameter. For instance 260 * 261 * <pre> 262 * Region region(); 263 * </pre> 264 */ parameterInterfaceAccessorMethod(String name, ParameterModel model)265 public MethodSpec parameterInterfaceAccessorMethod(String name, ParameterModel model) { 266 MethodSpec.Builder b = parameterMethodBuilder(name, model); 267 b.returns(parameterType(model)); 268 b.addModifiers(Modifier.ABSTRACT); 269 return b.build(); 270 } 271 272 /** 273 * Creates a builder-class level field for the given parameter initialized to its default value when present. For instance 274 * 275 * <pre> 276 * private Boolean useGlobalEndpoint = false; 277 * </pre> 278 */ parameterBuilderFieldSpec(String name, ParameterModel model)279 public FieldSpec parameterBuilderFieldSpec(String name, ParameterModel model) { 280 return parameterFieldSpecBuilder(name, model) 281 .initializer(parameterDefaultValueCode(model)) 282 .build(); 283 } 284 285 /** 286 * Creates a builder-interface method to set the given parameter. For instance 287 * 288 * <pre> 289 * Builder region(Region region); 290 * </pre> 291 * 292 */ parameterBuilderSetterMethodDeclaration(ClassName containingClass, String name, ParameterModel model)293 public MethodSpec parameterBuilderSetterMethodDeclaration(ClassName containingClass, String name, ParameterModel model) { 294 MethodSpec.Builder b = parameterMethodBuilder(name, model); 295 b.addModifiers(Modifier.ABSTRACT); 296 b.addParameter(parameterSpec(name, model)); 297 b.returns(containingClass.nestedClass("Builder")); 298 return b.build(); 299 } 300 301 /** 302 * Creates a builder-class method to set the given parameter. For instance 303 * 304 * <pre> 305 * public Builder region(Region region) {…}; 306 * </pre> 307 */ parameterBuilderSetterMethod(ClassName containingClass, String name, ParameterModel model)308 public MethodSpec parameterBuilderSetterMethod(ClassName containingClass, String name, ParameterModel model) { 309 String memberName = variableName(name); 310 311 MethodSpec.Builder b = parameterMethodBuilder(name, model) 312 .addAnnotation(Override.class) 313 .addParameter(parameterSpec(name, model)) 314 .returns(containingClass.nestedClass("Builder")) 315 .addStatement("this.$1N = $1N", memberName); 316 317 TreeNode defaultValue = model.getDefault(); 318 if (defaultValue != null) { 319 b.beginControlFlow("if (this.$N == null)", memberName); 320 b.addStatement("this.$N = $L", memberName, parameterDefaultValueCode(model)); 321 b.endControlFlow(); 322 } 323 324 b.addStatement("return this"); 325 return b.build(); 326 } 327 328 /** 329 * Used internally to create a field for the given parameter. Returns the builder that can be further tailor to be used for 330 * data-classes or for builder-classes. 331 */ parameterFieldSpecBuilder(String name, ParameterModel model)332 private FieldSpec.Builder parameterFieldSpecBuilder(String name, ParameterModel model) { 333 return FieldSpec.builder(parameterType(model), variableName(name)) 334 .addModifiers(Modifier.PRIVATE); 335 } 336 337 /** 338 * Used internally to create the spec for a parameter to be used in a method for the given param model. For instance, for 339 * {@code ParameterModel} for {@code Region} it creates this parameter for the builder setter. 340 * 341 * <pre> 342 * public Builder region( 343 * Region region // <<--- This 344 * ) {…}; 345 * </pre> 346 */ parameterSpec(String name, ParameterModel model)347 private ParameterSpec parameterSpec(String name, ParameterModel model) { 348 return ParameterSpec.builder(parameterType(model), variableName(name)).build(); 349 } 350 351 /** 352 * Used internally to create a accessor method for the given parameter model. Returns the builder that can be further 353 * tailor to be used for data-classes/interfaces and builder-classes/interfaces. 354 */ parameterMethodBuilder(String name, ParameterModel model)355 private MethodSpec.Builder parameterMethodBuilder(String name, ParameterModel model) { 356 MethodSpec.Builder b = MethodSpec.methodBuilder(paramMethodName(name)); 357 b.addModifiers(Modifier.PUBLIC); 358 if (model.getDeprecated() != null) { 359 b.addAnnotation(Deprecated.class); 360 } 361 return b; 362 } 363 364 /** 365 * Used internally to create the code to initialize the default value modeled for the given parameter. For instance, if the 366 * modeled default for region is "us-east-1", it will create 367 * 368 * <pre> 369 * Region.of("us-east-1") 370 * </pre> 371 * 372 * and if the modeled default value for a boolean parameter useGlobalEndpoint is "false", it will create 373 * 374 * <pre> 375 * false 376 * </pre> 377 */ parameterDefaultValueCode(ParameterModel parameterModel)378 private CodeBlock parameterDefaultValueCode(ParameterModel parameterModel) { 379 CodeBlock.Builder b = CodeBlock.builder(); 380 381 TreeNode defaultValue = parameterModel.getDefault(); 382 383 if (defaultValue == null) { 384 return b.build(); 385 } 386 387 switch (defaultValue.asToken()) { 388 case VALUE_STRING: 389 String stringValue = ((JrsString) defaultValue).getValue(); 390 if (parameterModel.getBuiltInEnum() == BuiltInParameter.AWS_REGION) { 391 b.add("$T.of($S)", Region.class, stringValue); 392 } else { 393 b.add("$S", stringValue); 394 } 395 break; 396 case VALUE_TRUE: 397 case VALUE_FALSE: 398 b.add("$L", ((JrsBoolean) defaultValue).booleanValue()); 399 break; 400 default: 401 throw new RuntimeException("Don't know how to set default value for parameter of type " 402 + defaultValue.asToken()); 403 } 404 return b.build(); 405 } 406 407 /** 408 * Returns the name as a variable name using the intermediate model naming strategy. 409 */ variableName(String name)410 public String variableName(String name) { 411 return intermediateModel.getNamingStrategy().getVariableName(name); 412 } 413 } 414