diff --git a/codegen-lite-maven-plugin/src/main/java/software/amazon/awssdk/codegen/lite/maven/plugin/DefaultsModeGenerationMojo.java b/codegen-lite-maven-plugin/src/main/java/software/amazon/awssdk/codegen/lite/maven/plugin/DefaultsModeGenerationMojo.java new file mode 100644 index 000000000000..0f3217b4cd1f --- /dev/null +++ b/codegen-lite-maven-plugin/src/main/java/software/amazon/awssdk/codegen/lite/maven/plugin/DefaultsModeGenerationMojo.java @@ -0,0 +1,65 @@ +/* + * 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.lite.maven.plugin; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; +import software.amazon.awssdk.codegen.lite.CodeGenerator; +import software.amazon.awssdk.codegen.lite.defaultsmode.DefaultConfiguration; +import software.amazon.awssdk.codegen.lite.defaultsmode.DefaultsLoader; +import software.amazon.awssdk.codegen.lite.defaultsmode.DefaultsModeGenerator; + +/** + * The Maven mojo to generate defaults mode related classes. + */ +@Mojo(name = "generate-defaults-mode") +public class DefaultsModeGenerationMojo extends AbstractMojo { + + private static final String DEFAULTS_MODE_BASE = "software.amazon.awssdk.defaultsmode"; + + @Parameter(property = "outputDirectory", defaultValue = "${project.build.directory}") + private String outputDirectory; + + @Parameter(defaultValue = "${project}", readonly = true) + private MavenProject project; + + @Parameter(property = "defaultConfigurationFile", defaultValue = + "${basedir}/src/main/resources/software/amazon/awssdk/internal/defaults/sdk-default-configuration.json") + private File defaultConfigurationFile; + + public void execute() { + Path baseSourcesDirectory = Paths.get(outputDirectory).resolve("generated-sources").resolve("sdk"); + Path testsDirectory = Paths.get(outputDirectory).resolve("generated-test-sources").resolve("sdk-tests"); + + DefaultConfiguration configuration = DefaultsLoader.load(defaultConfigurationFile); + + generateDefaultsModeClass(baseSourcesDirectory, configuration); + + project.addCompileSourceRoot(baseSourcesDirectory.toFile().getAbsolutePath()); + project.addTestCompileSourceRoot(testsDirectory.toFile().getAbsolutePath()); + } + + public void generateDefaultsModeClass(Path baseSourcesDirectory, DefaultConfiguration configuration) { + Path sourcesDirectory = baseSourcesDirectory.resolve(DEFAULTS_MODE_BASE.replace(".", "/")); + new CodeGenerator(sourcesDirectory.toString(), new DefaultsModeGenerator(DEFAULTS_MODE_BASE, configuration)).generate(); + } + +} diff --git a/codegen-lite/pom.xml b/codegen-lite/pom.xml index 32682a7f93e8..f3cddc67b4fa 100644 --- a/codegen-lite/pom.xml +++ b/codegen-lite/pom.xml @@ -57,6 +57,11 @@ utils ${awsjavasdk.version} + + software.amazon.awssdk + json-utils + ${awsjavasdk.version} + com.squareup javapoet diff --git a/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultConfiguration.java b/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultConfiguration.java new file mode 100644 index 000000000000..c18a68996c72 --- /dev/null +++ b/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultConfiguration.java @@ -0,0 +1,65 @@ +/* + * 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.lite.defaultsmode; + +import java.util.Map; + +/** + * Container for default configuration + */ +public class DefaultConfiguration { + /** + * The transformed configuration values for each mode + */ + private Map> modeDefaults; + + /** + * The documentation for each mode + */ + private Map modesDocumentation; + + /* + * The documentation for each configuration option + */ + private Map configurationDocumentation; + + public Map> modeDefaults() { + return modeDefaults; + } + + public DefaultConfiguration modeDefaults(Map> modeDefaults) { + this.modeDefaults = modeDefaults; + return this; + } + + public Map modesDocumentation() { + return modesDocumentation; + } + + public DefaultConfiguration modesDocumentation(Map documentation) { + this.modesDocumentation = documentation; + return this; + } + + public Map configurationDocumentation() { + return configurationDocumentation; + } + + public DefaultConfiguration configurationDocumentation(Map configurationDocumentation) { + this.configurationDocumentation = configurationDocumentation; + return this; + } +} diff --git a/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsLoader.java b/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsLoader.java new file mode 100644 index 000000000000..05d938c65a13 --- /dev/null +++ b/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsLoader.java @@ -0,0 +1,206 @@ +/* + * 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.lite.defaultsmode; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.protocols.jsoncore.JsonNode; +import software.amazon.awssdk.protocols.jsoncore.JsonNodeParser; +import software.amazon.awssdk.protocols.jsoncore.JsonNodeVisitor; +import software.amazon.awssdk.utils.Logger; + +/** + * Loads sdk-default-configuration.json into memory. It filters out unsupported configuration options from the file + */ +@SdkInternalApi +public final class DefaultsLoader { + private static final Logger log = Logger.loggerFor(DefaultsLoader.class); + + private static final Set UNSUPPORTED_OPTIONS = new HashSet<>(); + + static { + UNSUPPORTED_OPTIONS.add("stsRegionalEndpoints"); + UNSUPPORTED_OPTIONS.add("tlsNegotiationTimeoutInMillis"); + } + + private DefaultsLoader() { + } + + public static DefaultConfiguration load(File path) { + return loadDefaultsFromFile(path); + } + + private static DefaultConfiguration loadDefaultsFromFile(File path) { + DefaultConfiguration defaultsResolution = new DefaultConfiguration(); + Map> resolvedDefaults = new HashMap<>(); + + try (FileInputStream fileInputStream = new FileInputStream(path)) { + JsonNodeParser jsonNodeParser = JsonNodeParser.builder().build(); + + Map sdkDefaultConfiguration = jsonNodeParser.parse(fileInputStream) + .asObject(); + + Map base = sdkDefaultConfiguration.get("base").asObject(); + Map modes = sdkDefaultConfiguration.get("modes").asObject(); + + modes.forEach((mode, modifiers) -> applyModificationToOneMode(resolvedDefaults, base, mode, modifiers)); + + Map documentation = sdkDefaultConfiguration.get("documentation").asObject(); + Map modesDocumentation = documentation.get("modes").asObject(); + Map configDocumentation = documentation.get("configuration").asObject(); + + defaultsResolution.modesDocumentation( + modesDocumentation.entrySet() + .stream() + .collect(HashMap::new, (m, e) -> m.put(e.getKey(), e.getValue().asString()), Map::putAll)); + defaultsResolution.configurationDocumentation( + configDocumentation.entrySet() + .stream() + .filter(e -> !UNSUPPORTED_OPTIONS.contains(e.getKey())) + .collect(HashMap::new, (m, e) -> m.put(e.getKey(), e.getValue().asString()), Map::putAll)); + + } catch (IOException e) { + throw new RuntimeException(e); + } + + defaultsResolution.modeDefaults(resolvedDefaults); + + return defaultsResolution; + } + + private static void applyModificationToOneConfigurationOption(Map resolvedDefaultsForCurrentMode, + String option, + JsonNode modifier) { + String resolvedValue; + String baseValue = resolvedDefaultsForCurrentMode.get(option); + + if (UNSUPPORTED_OPTIONS.contains(option)) { + return; + } + + Map modifierMap = modifier.asObject(); + + if (modifierMap.size() != 1) { + throw new IllegalStateException("More than one modifier exists for option " + option); + } + + String modifierString = modifierMap.keySet().iterator().next(); + + switch (modifierString) { + case "override": + resolvedValue = modifierMap.get("override").visit(new StringJsonNodeVisitor()); + break; + case "multiply": + resolvedValue = processMultiply(baseValue, modifierMap); + break; + case "add": + resolvedValue = processAdd(baseValue, modifierMap); + break; + default: + throw new UnsupportedOperationException("Unsupported modifier: " + modifierString); + } + + resolvedDefaultsForCurrentMode.put(option, resolvedValue); + } + + private static void applyModificationToOneMode(Map> resolvedDefaults, + Map base, + String mode, + JsonNode modifiers) { + + log.info(() -> "Apply modification for mode: " + mode); + Map resolvedDefaultsForCurrentMode = + base.entrySet().stream().filter(e -> !UNSUPPORTED_OPTIONS.contains(e.getKey())) + .collect(HashMap::new, (m, e) -> m.put(e.getKey(), + e.getValue().visit(new StringJsonNodeVisitor())), Map::putAll); + + + // Iterate the configuration options and apply modification. + modifiers.asObject().forEach((option, modifier) -> applyModificationToOneConfigurationOption( + resolvedDefaultsForCurrentMode, option, modifier)); + + resolvedDefaults.put(mode, resolvedDefaultsForCurrentMode); + } + + private static String processAdd(String baseValue, Map modifierMap) { + String resolvedValue; + String add = modifierMap.get("add").asNumber(); + int parsedAdd = Integer.parseInt(add); + int number = Math.addExact(Integer.parseInt(baseValue), parsedAdd); + resolvedValue = String.valueOf(number); + return resolvedValue; + } + + private static String processMultiply(String baseValue, Map modifierMap) { + String resolvedValue; + String multiply = modifierMap.get("multiply").asNumber(); + double parsedValue = Double.parseDouble(multiply); + + double resolvedNumber = Integer.parseInt(baseValue) * parsedValue; + int castValue = (int) resolvedNumber; + + if (castValue != resolvedNumber) { + throw new IllegalStateException("The transformed value must be be a float number: " + castValue); + } + + resolvedValue = String.valueOf(castValue); + return resolvedValue; + } + + private static final class StringJsonNodeVisitor implements JsonNodeVisitor { + @Override + public String visitNull() { + throw new IllegalStateException("Invalid type encountered"); + } + + @Override + public String visitBoolean(boolean b) { + throw new IllegalStateException("Invalid type (boolean) encountered " + b); + } + + @Override + public String visitNumber(String s) { + return s; + } + + @Override + public String visitString(String s) { + return s; + } + + @Override + public String visitArray(List list) { + throw new IllegalStateException("Invalid type (list) encountered: " + list); + } + + @Override + public String visitObject(Map map) { + throw new IllegalStateException("Invalid type (map) encountered: " + map); + } + + @Override + public String visitEmbeddedObject(Object o) { + throw new IllegalStateException("Invalid type (embedded) encountered: " + o); + } + } +} diff --git a/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsModeGenerator.java b/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsModeGenerator.java new file mode 100644 index 000000000000..0a318e1f69c9 --- /dev/null +++ b/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsModeGenerator.java @@ -0,0 +1,189 @@ +/* + * 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.lite.defaultsmode; + +import static javax.lang.model.element.Modifier.FINAL; +import static javax.lang.model.element.Modifier.PRIVATE; +import static javax.lang.model.element.Modifier.PUBLIC; +import static javax.lang.model.element.Modifier.STATIC; + +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.Locale; +import java.util.Map; +import javax.lang.model.element.Modifier; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.codegen.lite.PoetClass; +import software.amazon.awssdk.utils.Validate; +import software.amazon.awssdk.utils.internal.EnumUtils; + +/** + * Generates DefaultsMode enum + */ +public class DefaultsModeGenerator implements PoetClass { + + private static final String VALUE = "value"; + private static final String VALUE_MAP = "VALUE_MAP"; + + private final String basePackage; + private final DefaultConfiguration configuration; + + public DefaultsModeGenerator(String basePackage, DefaultConfiguration configuration) { + this.basePackage = basePackage; + this.configuration = configuration; + } + + @Override + public TypeSpec poetClass() { + TypeSpec.Builder builder = TypeSpec.enumBuilder(className()) + .addField(valueMapField()) + .addField(String.class, VALUE, Modifier.PRIVATE, Modifier.FINAL) + .addModifiers(PUBLIC) + .addJavadoc(documentation()) + .addAnnotation(SdkPublicApi.class) + .addAnnotation(AnnotationSpec.builder(Generated.class) + .addMember(VALUE, + "$S", + "software.amazon.awssdk:codegen") + .build()) + .addMethod(fromValueSpec()) + .addMethod(toStringBuilder().addStatement("return $T.valueOf($N)", String.class, + VALUE).build()) + .addMethod(createConstructor()); + + builder.addEnumConstant("LEGACY", enumValueTypeSpec("legacy", javaDocForMode("legacy"))); + + configuration.modeDefaults().keySet().forEach(k -> { + String enumKey = sanitizeEnum(k); + builder.addEnumConstant(enumKey, enumValueTypeSpec(k, javaDocForMode(k))); + }); + + builder.addEnumConstant("AUTO", enumValueTypeSpec("auto", javaDocForMode("auto"))); + + return builder.build(); + } + + @Override + public ClassName className() { + return ClassName.get(basePackage, "DefaultsMode"); + } + + private TypeSpec enumValueTypeSpec(String value, String documentation) { + return TypeSpec.anonymousClassBuilder("$S", value) + .addJavadoc(documentation) + .build(); + } + + private FieldSpec valueMapField() { + ParameterizedTypeName mapType = ParameterizedTypeName.get(ClassName.get(Map.class), + ClassName.get(String.class), + className()); + return FieldSpec.builder(mapType, VALUE_MAP) + .addModifiers(PRIVATE, STATIC, FINAL) + .initializer("$1T.uniqueIndex($2T.class, $2T::toString)", EnumUtils.class, className()) + .build(); + } + + private String sanitizeEnum(String str) { + return str.replace('-', '_').toUpperCase(Locale.US); + } + + private String javaDocForMode(String mode) { + return configuration.modesDocumentation().getOrDefault(mode, ""); + } + + private CodeBlock documentation() { + CodeBlock.Builder builder = CodeBlock.builder() + .add("A defaults mode determines how certain default configuration options are " + + "resolved in " + + "the SDK. " + + "Based on the provided " + + "mode, the SDK will vend sensible default values tailored to the mode for " + + "the following settings:") + .add(System.lineSeparator()); + + builder.add("
    "); + configuration.configurationDocumentation().forEach((k, v) -> { + builder.add("
  • " + k + ": " + v + "
  • "); + }); + builder.add("
").add(System.lineSeparator()); + + builder.add("

All options above can be configured by users, and the overridden value will take precedence.") + .add("

Note: for any mode other than {@link #LEGACY}, the vended default values might change " + + "as best practices may evolve. As a result, it is encouraged to perform testing when upgrading the SDK if" + + " you are using a mode other than {@link #LEGACY}") + .add(System.lineSeparator()); + + return builder.add("

While the {@link #LEGACY} defaults mode is specific to Java, other modes are " + + "standardized across " + + "all of the AWS SDKs

") + .add(System.lineSeparator()) + .add("

The defaults mode can be configured:") + .add(System.lineSeparator()) + .add("

    ") + .add("
  1. Directly on a client via {@code ClientOverrideConfiguration.Builder#defaultsMode" + + "(DefaultsMode)}.
  2. ") + .add(System.lineSeparator()) + .add("
  3. On a configuration profile via the \"defaults_mode\" profile file property.
  4. ") + .add(System.lineSeparator()) + .add("
  5. Globally via the \"aws.defaultsMode\" system property.
  6. ") + .add("
  7. Globally via the \"AWS_DEFAULTS_MODE\" environment variable.
  8. ") + .add("
") + .build(); + } + + + private MethodSpec fromValueSpec() { + return MethodSpec.methodBuilder("fromValue") + .returns(className()) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addJavadoc("Use this in place of valueOf to convert the raw string returned by the service into the " + + "enum value.\n\n" + + "@param $N real value\n" + + "@return $T corresponding to the value\n", VALUE, className()) + .addParameter(String.class, VALUE) + .addStatement("$T.paramNotNull(value, $S)", Validate.class, VALUE) + .beginControlFlow("if (!VALUE_MAP.containsKey(value))") + .addStatement("throw new IllegalArgumentException($S + value)", "The provided value is not a" + + " valid " + + "defaults mode ") + .endControlFlow() + .addStatement("return $N.get($N)", VALUE_MAP, VALUE) + .build(); + } + + private MethodSpec createConstructor() { + return MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(String.class, VALUE) + .addStatement("this.$1N = $1N", VALUE) + .build(); + } + + private static MethodSpec.Builder toStringBuilder() { + return MethodSpec.methodBuilder("toString") + .returns(String.class) + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class); + } + +} diff --git a/codegen-lite/src/test/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsModeGenerationTest.java b/codegen-lite/src/test/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsModeGenerationTest.java new file mode 100644 index 000000000000..5a753db0af1d --- /dev/null +++ b/codegen-lite/src/test/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsModeGenerationTest.java @@ -0,0 +1,45 @@ +/* + * 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.lite.defaultsmode; + +import static org.hamcrest.MatcherAssert.assertThat; +import static software.amazon.awssdk.codegen.lite.PoetMatchers.generatesTo; + +import java.io.File; +import java.nio.file.Paths; +import org.junit.Before; +import org.junit.Test; + +public class DefaultsModeGenerationTest { + + private static final String DEFAULT_CONFIGURATION = "/software/amazon/awssdk/codegen/lite/test-sdk-default-configuration.json"; + private static final String DEFAULTS_MODE_BASE = "software.amazon.awssdk.defaultsmode"; + + private File file; + private DefaultConfiguration defaultConfiguration; + + @Before + public void before() throws Exception { + this.file = Paths.get(getClass().getResource(DEFAULT_CONFIGURATION).toURI()).toFile(); + this.defaultConfiguration = DefaultsLoader.load(file); + } + + @Test + public void defaultsModeEnum() { + DefaultsModeGenerator generator = new DefaultsModeGenerator(DEFAULTS_MODE_BASE, defaultConfiguration); + assertThat(generator, generatesTo("defaults-mode.java")); + } + +} diff --git a/codegen-lite/src/test/resources/software/amazon/awssdk/codegen/lite/defaultsmode/defaults-mode.java b/codegen-lite/src/test/resources/software/amazon/awssdk/codegen/lite/defaultsmode/defaults-mode.java new file mode 100644 index 000000000000..0d78d3a2fd2f --- /dev/null +++ b/codegen-lite/src/test/resources/software/amazon/awssdk/codegen/lite/defaultsmode/defaults-mode.java @@ -0,0 +1,95 @@ +package software.amazon.awssdk.defaultsmode; + +import java.util.Map; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.utils.Validate; +import software.amazon.awssdk.utils.internal.EnumUtils; + +/** + * A defaults mode determines how certain default configuration options are resolved in the SDK. Based on the provided + * mode, the SDK will vend sensible default values tailored to the mode for the following settings: + *
    + *
  • retryMode: PLACEHOLDER
  • + *
  • s3UsEast1RegionalEndpoints: PLACEHOLDER
  • + *
  • connectTimeoutInMillis: PLACEHOLDER
  • + *
+ *

+ * All options above can be configured by users, and the overridden value will take precedence. + *

+ * Note: for any mode other than {@link #LEGACY}, the vended default values might change as best practices may + * evolve. As a result, it is encouraged to perform testing when upgrading the SDK if you are using a mode other than + * {@link #LEGACY} + *

+ * While the {@link #LEGACY} defaults mode is specific to Java, other modes are standardized across all of the AWS SDKs + *

+ *

+ * The defaults mode can be configured: + *

    + *
  1. Directly on a client via {@code ClientOverrideConfiguration.Builder#defaultsMode(DefaultsMode)}.
  2. + *
  3. On a configuration profile via the "defaults_mode" profile file property.
  4. + *
  5. Globally via the "aws.defaultsMode" system property.
  6. + *
  7. Globally via the "AWS_DEFAULTS_MODE" environment variable.
  8. + *
+ */ +@SdkPublicApi +@Generated("software.amazon.awssdk:codegen") +public enum DefaultsMode { + /** + * PLACEHOLDER + */ + LEGACY("legacy"), + + /** + * PLACEHOLDER + */ + STANDARD("standard"), + + /** + * PLACEHOLDER + */ + MOBILE("mobile"), + + /** + * PLACEHOLDER + */ + CROSS_REGION("cross-region"), + + /** + * PLACEHOLDER + */ + IN_REGION("in-region"), + + /** + * PLACEHOLDER + */ + AUTO("auto"); + + private static final Map VALUE_MAP = EnumUtils.uniqueIndex(DefaultsMode.class, DefaultsMode::toString); + + private final String value; + + private DefaultsMode(String value) { + this.value = value; + } + + /** + * Use this in place of valueOf to convert the raw string returned by the service into the enum value. + * + * @param value + * real value + * @return DefaultsMode corresponding to the value + */ + public static DefaultsMode fromValue(String value) { + Validate.paramNotNull(value, "value"); + if (!VALUE_MAP.containsKey(value)) { + throw new IllegalArgumentException("The provided value is not a valid defaults mode " + value); + } + return VALUE_MAP.get(value); + } + + @Override + public String toString() { + return String.valueOf(value); + } +} diff --git a/codegen-lite/src/test/resources/software/amazon/awssdk/codegen/lite/test-sdk-default-configuration.json b/codegen-lite/src/test/resources/software/amazon/awssdk/codegen/lite/test-sdk-default-configuration.json new file mode 100644 index 000000000000..5c9f0fab48b7 --- /dev/null +++ b/codegen-lite/src/test/resources/software/amazon/awssdk/codegen/lite/test-sdk-default-configuration.json @@ -0,0 +1,64 @@ +{ + "version": 1, + "base": { + "retryMode": "standard", + "stsRegionalEndpoints": "regional", + "s3UsEast1RegionalEndpoints": "regional", + "connectTimeoutInMillis": 1000, + "tlsNegotiationTimeoutInMillis": 1000 + }, + "modes": { + "standard": { + "connectTimeoutInMillis": { + "multiply":2 + }, + "tlsNegotiationTimeoutInMillis": { + "multiply":2 + } + }, + "in-region": { + "connectTimeoutInMillis": { + "multiply": 1 + }, + "tlsNegotiationTimeoutInMillis": { + "multiply": 1 + } + }, + "cross-region": { + "connectTimeoutInMillis": { + "multiply": 2.8 + }, + "tlsNegotiationTimeoutInMillis": { + "multiply": 2.8 + } + }, + "mobile": { + "connectTimeoutInMillis": { + "override": 10000 + }, + "tlsNegotiationTimeoutInMillis": { + "add": 10000 + }, + "retryMode": { + "override": "adaptive" + } + } + }, + "documentation": { + "modes": { + "standard": "PLACEHOLDER", + "in-region": "PLACEHOLDER", + "cross-region": "PLACEHOLDER", + "mobile": "PLACEHOLDER", + "auto": "PLACEHOLDER", + "legacy": "PLACEHOLDER" + }, + "configuration": { + "retryMode": "PLACEHOLDER", + "stsRegionalEndpoints": "PLACEHOLDER", + "s3UsEast1RegionalEndpoints": "PLACEHOLDER", + "connectTimeoutInMillis": "PLACEHOLDER", + "tlsNegotiationTimeoutInMillis": "PLACEHOLDER" + } + } +} \ No newline at end of file diff --git a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java index 88b0328871b9..6faf0cad42f2 100644 --- a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java +++ b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileProperty.java @@ -99,6 +99,13 @@ public final class ProfileProperty { */ public static final String RETRY_MODE = "retry_mode"; + /** + * The "defaults mode" to be used for clients created using the currently-configured profile. Defaults mode determins how SDK + * default configuration should be resolved. See the {@code DefaultsMode} class JavaDoc for more + * information. + */ + public static final String DEFAULTS_MODE = "defaults_mode"; + /** * Aws region where the SSO directory for the given 'sso_start_url' is hosted. This is independent of the general 'region'. */ diff --git a/core/sdk-core/pom.xml b/core/sdk-core/pom.xml index 3f59dc6a2e1a..204b49d4213d 100644 --- a/core/sdk-core/pom.xml +++ b/core/sdk-core/pom.xml @@ -238,6 +238,19 @@ + + software.amazon.awssdk + codegen-lite-maven-plugin + ${awsjavasdk.version} + + + generate-sources + + generate-defaults-mode + + + + diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java index 8fe7845d2f71..be9ba3629161 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java @@ -19,6 +19,7 @@ import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.defaultsmode.DefaultsMode; import software.amazon.awssdk.utils.SystemSetting; /** @@ -168,6 +169,12 @@ public enum SdkSystemSetting implements SystemSetting { */ AWS_MAX_ATTEMPTS("aws.maxAttempts", null), + /** + * Which {@link DefaultsMode} to use, case insensitive + * @see DefaultsMode + */ + AWS_DEFAULTS_MODE("aws.defaultsMode", null), + ; private final String systemProperty; diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java index 81b99d999667..4a57d3885456 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/ClientOverrideConfiguration.java @@ -31,6 +31,7 @@ import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.core.sync.ResponseTransformer; +import software.amazon.awssdk.defaultsmode.DefaultsMode; import software.amazon.awssdk.metrics.MetricPublisher; import software.amazon.awssdk.profiles.ProfileFile; import software.amazon.awssdk.profiles.ProfileFileSystemSetting; @@ -60,6 +61,7 @@ public final class ClientOverrideConfiguration private final String defaultProfileName; private final List metricPublishers; private final ExecutionAttributes executionAttributes; + private final DefaultsMode defaultsMode; /** * Initialize this configuration. Private to require use of {@link #builder()}. @@ -75,6 +77,7 @@ private ClientOverrideConfiguration(Builder builder) { this.defaultProfileName = builder.defaultProfileName(); this.metricPublishers = Collections.unmodifiableList(new ArrayList<>(builder.metricPublishers())); this.executionAttributes = ExecutionAttributes.unmodifiableExecutionAttributes(builder.executionAttributes()); + this.defaultsMode = builder.defaultsMode(); } @Override @@ -201,6 +204,15 @@ public List metricPublishers() { return metricPublishers; } + /** + * The optional defaults mode that should be used to determine the default configuration + * @return the optional defaults mode + * @see Builder#defaultsMode(DefaultsMode) + */ + public Optional defaultsMode() { + return Optional.of(defaultsMode); + } + /** * Returns the additional execution attributes to be added for this client. * @@ -469,6 +481,16 @@ default Builder retryPolicy(RetryMode retryMode) { Builder putExecutionAttribute(ExecutionAttribute attribute, T value); ExecutionAttributes executionAttributes(); + + /** + * Sets the defaults mode that will be used to determine the default configuration + * @param defaultsMode the defaultsMode to use + * @return This object for method chaining. + * @see DefaultsMode + */ + Builder defaultsMode(DefaultsMode defaultsMode); + + DefaultsMode defaultsMode(); } /** @@ -485,6 +507,7 @@ private static final class DefaultClientOverrideConfigurationBuilder implements private String defaultProfileName; private List metricPublishers = new ArrayList<>(); private ExecutionAttributes.Builder executionAttributesBuilder = ExecutionAttributes.builder(); + private DefaultsMode defaultsMode; @Override public Builder headers(Map> headers) { @@ -667,6 +690,21 @@ public ExecutionAttributes executionAttributes() { return executionAttributesBuilder.build(); } + @Override + public Builder defaultsMode(DefaultsMode mode) { + this.defaultsMode = mode; + return this; + } + + @Override + public DefaultsMode defaultsMode() { + return defaultsMode; + } + + public void setDefaultsMode(DefaultsMode mode) { + defaultsMode(mode); + } + @Override public ClientOverrideConfiguration build() { return new ClientOverrideConfiguration(this); diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/defaultsmode/DefaultsModeResolver.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/defaultsmode/DefaultsModeResolver.java new file mode 100644 index 000000000000..f0a71f5df37d --- /dev/null +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/defaultsmode/DefaultsModeResolver.java @@ -0,0 +1,99 @@ +/* + * 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.core.internal.defaultsmode; + +import java.util.Locale; +import java.util.Optional; +import java.util.function.Supplier; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.defaultsmode.DefaultsMode; +import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.profiles.ProfileFileSystemSetting; +import software.amazon.awssdk.profiles.ProfileProperty; +import software.amazon.awssdk.utils.OptionalUtils; + +/** + * Allows customizing the variables used during determination of a {@link DefaultsMode}. Created via {@link #create()}. + */ +@SdkInternalApi +public final class DefaultsModeResolver { + + private static final DefaultsMode SDK_DEFAULT_DEFAULTS_MODE = DefaultsMode.LEGACY; + private Supplier profileFile; + private String profileName; + private DefaultsMode mode; + + private DefaultsModeResolver() { + } + + public static DefaultsModeResolver create() { + return new DefaultsModeResolver(); + } + + /** + * Configure the profile file that should be used when determining the {@link RetryMode}. The supplier is only consulted + * if a higher-priority determinant (e.g. environment variables) does not find the setting. + */ + public DefaultsModeResolver profileFile(Supplier profileFile) { + this.profileFile = profileFile; + return this; + } + + /** + * Configure the profile file name should be used when determining the {@link RetryMode}. + */ + public DefaultsModeResolver profileName(String profileName) { + this.profileName = profileName; + return this; + } + + /** + * Configure the {@link DefaultsMode} that should be used if the mode is not specified anywhere else. + */ + public DefaultsModeResolver defaultMode(DefaultsMode mode) { + this.mode = mode; + return this; + } + + /** + * Resolve which defaults mode should be used, based on the configured values. + */ + public DefaultsMode resolve() { + return OptionalUtils.firstPresent(DefaultsModeResolver.fromSystemSettings(), () -> fromProfileFile(profileFile, + profileName)) + .orElseGet(this::fromDefaultMode); + } + + private static Optional fromSystemSettings() { + return SdkSystemSetting.AWS_DEFAULTS_MODE.getStringValue() + .map(value -> DefaultsMode.fromValue(value.toLowerCase(Locale.US))); + } + + private static Optional fromProfileFile(Supplier profileFile, String profileName) { + profileFile = profileFile != null ? profileFile : ProfileFile::defaultProfileFile; + profileName = profileName != null ? profileName : ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow(); + return profileFile.get() + .profile(profileName) + .flatMap(p -> p.property(ProfileProperty.DEFAULTS_MODE)) + .map(value -> DefaultsMode.fromValue(value.toLowerCase(Locale.US))); + } + + private DefaultsMode fromDefaultMode() { + return mode != null ? mode : SDK_DEFAULT_DEFAULTS_MODE; + } +} diff --git a/core/sdk-core/src/main/resources/software/amazon/awssdk/internal/defaults/sdk-default-configuration.json b/core/sdk-core/src/main/resources/software/amazon/awssdk/internal/defaults/sdk-default-configuration.json new file mode 100644 index 000000000000..e99d2b038098 --- /dev/null +++ b/core/sdk-core/src/main/resources/software/amazon/awssdk/internal/defaults/sdk-default-configuration.json @@ -0,0 +1,53 @@ +{ + "version": 1, + "base": { + "retryMode": "standard", + "stsRegionalEndpoints": "regional", + "s3UsEast1RegionalEndpoints": "regional", + "connectTimeoutInMillis": 1100, + "tlsNegotiationTimeoutInMillis": 1100 + }, + "modes": { + "standard": { + "connectTimeoutInMillis": { + "override": 3100 + }, + "tlsNegotiationTimeoutInMillis": { + "override": 3100 + } + }, + "in-region": { + "connectTimeoutInMillis": { + "multiply": 1 + }, + "tlsNegotiationTimeoutInMillis": { + "multiply": 1 + } + }, + "cross-region": { + "connectTimeoutInMillis": { + "override": 3100 + }, + "tlsNegotiationTimeoutInMillis": { + "override": 3100 + } + } + }, + "documentation": { + "modes": { + "standard": "PLACEHOLDER", + "in-region": "PLACEHOLDER", + "cross-region": "PLACEHOLDER", + "mobile": "PLACEHOLDER", + "auto": "PLACEHOLDER", + "legacy": "PLACEHOLDER" + }, + "configuration": { + "retryMode": "PLACEHOLDER", + "stsRegionalEndpoints": "PLACEHOLDER", + "s3UsEast1RegionalEndpoints": "PLACEHOLDER", + "connectTimeoutInMillis": "PLACEHOLDER", + "tlsNegotiationTimeoutInMillis": "PLACEHOLDER" + } + } +} \ No newline at end of file diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/defaultsmode/DefaultsModeTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/defaultsmode/DefaultsModeTest.java new file mode 100644 index 000000000000..60e1538c83f9 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/defaultsmode/DefaultsModeTest.java @@ -0,0 +1,135 @@ +/* + * 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.core.internal.defaultsmode; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.Callable; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.defaultsmode.DefaultsMode; +import software.amazon.awssdk.profiles.ProfileFileSystemSetting; +import software.amazon.awssdk.testutils.EnvironmentVariableHelper; +import software.amazon.awssdk.utils.Validate; + +@RunWith(Parameterized.class) +public class DefaultsModeTest { + private static final EnvironmentVariableHelper ENVIRONMENT_VARIABLE_HELPER = new EnvironmentVariableHelper(); + + @Parameterized.Parameter + public TestData testData; + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[] { + // Test defaults + new TestData(null, null, null, null, DefaultsMode.LEGACY), + new TestData(null, null, "PropertyNotSet", null, DefaultsMode.LEGACY), + + // Test resolution + new TestData("legacy", null, null, null, DefaultsMode.LEGACY), + new TestData("standard", null, null, null, DefaultsMode.STANDARD), + new TestData("auto", null, null, null, DefaultsMode.AUTO), + new TestData("lEgAcY", null, null, null, DefaultsMode.LEGACY), + new TestData("sTanDaRd", null, null, null, DefaultsMode.STANDARD), + new TestData("AUtO", null, null, null, DefaultsMode.AUTO), + + // Test precedence + new TestData("standard", "legacy", "PropertySetToLegacy", DefaultsMode.LEGACY, DefaultsMode.STANDARD), + new TestData("standard", null, null, DefaultsMode.LEGACY, DefaultsMode.STANDARD), + new TestData(null, "standard", "PropertySetToLegacy", DefaultsMode.LEGACY, DefaultsMode.STANDARD), + new TestData(null, "standard", null, DefaultsMode.LEGACY, DefaultsMode.STANDARD), + new TestData(null, null, "PropertySetToStandard", DefaultsMode.LEGACY, DefaultsMode.STANDARD), + new TestData(null, null, "PropertySetToAuto", DefaultsMode.LEGACY, DefaultsMode.AUTO), + new TestData(null, null, null, DefaultsMode.STANDARD, DefaultsMode.STANDARD), + + // Test invalid values + new TestData("wrongValue", null, null, null, IllegalArgumentException.class), + new TestData(null, "wrongValue", null, null, IllegalArgumentException.class), + new TestData(null, null, "PropertySetToUnsupportedValue", null, IllegalArgumentException.class), + + // Test capitalization standardization + new TestData("sTaNdArD", null, null, null, DefaultsMode.STANDARD), + new TestData(null, "sTaNdArD", null, null, DefaultsMode.STANDARD), + new TestData(null, null, "PropertyMixedCase", null, DefaultsMode.STANDARD), + }); + } + + @Before + @After + public void methodSetup() { + ENVIRONMENT_VARIABLE_HELPER.reset(); + System.clearProperty(SdkSystemSetting.AWS_DEFAULTS_MODE.property()); + System.clearProperty(ProfileFileSystemSetting.AWS_PROFILE.property()); + System.clearProperty(ProfileFileSystemSetting.AWS_CONFIG_FILE.property()); + } + + @Test + public void differentCombinationOfConfigs_shouldResolveCorrectly() throws Exception { + if (testData.envVarValue != null) { + ENVIRONMENT_VARIABLE_HELPER.set(SdkSystemSetting.AWS_DEFAULTS_MODE.environmentVariable(), testData.envVarValue); + } + + if (testData.systemProperty != null) { + System.setProperty(SdkSystemSetting.AWS_DEFAULTS_MODE.property(), testData.systemProperty); + } + + if (testData.configFile != null) { + String diskLocationForFile = diskLocationForConfig(testData.configFile); + Validate.isTrue(Files.isReadable(Paths.get(diskLocationForFile)), diskLocationForFile + " is not readable."); + System.setProperty(ProfileFileSystemSetting.AWS_PROFILE.property(), "default"); + System.setProperty(ProfileFileSystemSetting.AWS_CONFIG_FILE.property(), diskLocationForFile); + } + + Callable result = DefaultsModeResolver.create().defaultMode(testData.defaultMode)::resolve; + if (testData.expected instanceof Class) { + Class expectedClassType = (Class) testData.expected; + assertThatThrownBy(result::call).isInstanceOf(expectedClassType); + } else { + assertThat(result.call()).isEqualTo(testData.expected); + } + } + + private String diskLocationForConfig(String configFileName) { + return getClass().getResource(configFileName).getFile(); + } + + + private static class TestData { + private final String envVarValue; + private final String systemProperty; + private final String configFile; + private final DefaultsMode defaultMode; + private final Object expected; + + TestData(String systemProperty, String envVarValue, String configFile, DefaultsMode defaultMode, Object expected) { + this.envVarValue = envVarValue; + this.systemProperty = systemProperty; + this.configFile = configFile; + this.defaultMode = defaultMode; + this.expected = expected; + } + } +} \ No newline at end of file diff --git a/core/sdk-core/src/test/resources/software/amazon/awssdk/core/internal/defaultsmode/PropertyMixedCase b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/internal/defaultsmode/PropertyMixedCase new file mode 100644 index 000000000000..b400595208f4 --- /dev/null +++ b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/internal/defaultsmode/PropertyMixedCase @@ -0,0 +1,2 @@ +[default] +defaults_mode = sTanDard \ No newline at end of file diff --git a/core/sdk-core/src/test/resources/software/amazon/awssdk/core/internal/defaultsmode/PropertyNotSet b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/internal/defaultsmode/PropertyNotSet new file mode 100644 index 000000000000..399487f9b5e5 --- /dev/null +++ b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/internal/defaultsmode/PropertyNotSet @@ -0,0 +1 @@ +[default] \ No newline at end of file diff --git a/core/sdk-core/src/test/resources/software/amazon/awssdk/core/internal/defaultsmode/PropertySetToAuto b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/internal/defaultsmode/PropertySetToAuto new file mode 100644 index 000000000000..353d1deb69e6 --- /dev/null +++ b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/internal/defaultsmode/PropertySetToAuto @@ -0,0 +1,2 @@ +[default] +defaults_mode = auto \ No newline at end of file diff --git a/core/sdk-core/src/test/resources/software/amazon/awssdk/core/internal/defaultsmode/PropertySetToLegacy b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/internal/defaultsmode/PropertySetToLegacy new file mode 100644 index 000000000000..db8ae0695416 --- /dev/null +++ b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/internal/defaultsmode/PropertySetToLegacy @@ -0,0 +1,2 @@ +[default] +defaults_mode = legacy \ No newline at end of file diff --git a/core/sdk-core/src/test/resources/software/amazon/awssdk/core/internal/defaultsmode/PropertySetToStandard b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/internal/defaultsmode/PropertySetToStandard new file mode 100644 index 000000000000..fe9ff9fce00e --- /dev/null +++ b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/internal/defaultsmode/PropertySetToStandard @@ -0,0 +1,2 @@ +[default] +defaults_mode = standard \ No newline at end of file diff --git a/core/sdk-core/src/test/resources/software/amazon/awssdk/core/internal/defaultsmode/PropertySetToUnsupportedValue b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/internal/defaultsmode/PropertySetToUnsupportedValue new file mode 100644 index 000000000000..7f88da4108ab --- /dev/null +++ b/core/sdk-core/src/test/resources/software/amazon/awssdk/core/internal/defaultsmode/PropertySetToUnsupportedValue @@ -0,0 +1,2 @@ +[default] +defaults_mode = unsupported-value \ No newline at end of file