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