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 index 0f3217b4cd1f..369595e1a246 100644 --- 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 @@ -25,6 +25,7 @@ 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.DefaultsModeConfigurationGenerator; import software.amazon.awssdk.codegen.lite.defaultsmode.DefaultsModeGenerator; /** @@ -34,6 +35,7 @@ public class DefaultsModeGenerationMojo extends AbstractMojo { private static final String DEFAULTS_MODE_BASE = "software.amazon.awssdk.defaultsmode"; + private static final String DEFAULTS_MODE_CONFIGURATION_BASE = "software.amazon.awssdk.internal.defaultsmode"; @Parameter(property = "outputDirectory", defaultValue = "${project.build.directory}") private String outputDirectory; @@ -52,6 +54,7 @@ public void execute() { DefaultConfiguration configuration = DefaultsLoader.load(defaultConfigurationFile); generateDefaultsModeClass(baseSourcesDirectory, configuration); + generateDefaultsModeConfiguartionClass(baseSourcesDirectory, configuration); project.addCompileSourceRoot(baseSourcesDirectory.toFile().getAbsolutePath()); project.addTestCompileSourceRoot(testsDirectory.toFile().getAbsolutePath()); @@ -62,4 +65,10 @@ public void generateDefaultsModeClass(Path baseSourcesDirectory, DefaultConfigur new CodeGenerator(sourcesDirectory.toString(), new DefaultsModeGenerator(DEFAULTS_MODE_BASE, configuration)).generate(); } + public void generateDefaultsModeConfiguartionClass(Path baseSourcesDirectory, DefaultConfiguration configuration) { + Path sourcesDirectory = baseSourcesDirectory.resolve(DEFAULTS_MODE_CONFIGURATION_BASE.replace(".", "/")); + new CodeGenerator(sourcesDirectory.toString(), new DefaultsModeConfigurationGenerator(DEFAULTS_MODE_CONFIGURATION_BASE, + DEFAULTS_MODE_BASE, + configuration)).generate(); + } } diff --git a/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsModeConfigurationGenerator.java b/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsModeConfigurationGenerator.java new file mode 100644 index 000000000000..e049f2276063 --- /dev/null +++ b/codegen-lite/src/main/java/software/amazon/awssdk/codegen/lite/defaultsmode/DefaultsModeConfigurationGenerator.java @@ -0,0 +1,255 @@ +/* + * 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.EnumMap; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import javax.lang.model.element.Modifier; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.codegen.lite.PoetClass; +import software.amazon.awssdk.utils.AttributeMap; + +/** + * Generates DefaultsModeConfiguration class that contains default options for each mode + */ +public class DefaultsModeConfigurationGenerator implements PoetClass { + + private static final String DEFAULT_CONFIG_BY_MODE_ENUM_MAP = "DEFAULT_CONFIG_BY_MODE"; + private static final String DEFAULT_HTTP_CONFIG_BY_MODE_ENUM_MAP = "DEFAULT_HTTP_CONFIG_BY_MODE"; + private static final String DEFAULTS_VAR_SUFFIX = "_DEFAULTS"; + private static final String HTTP_DEFAULTS_VAR_SUFFIX = "_HTTP_DEFAULTS"; + private static final Map CONFIGURATION_MAPPING = new HashMap<>(); + private static final Map HTTP_CONFIGURATION_MAPPING = new HashMap<>(); + private final String basePackage; + private final String defaultsModeBase; + private final DefaultConfiguration configuration; + + static { + HTTP_CONFIGURATION_MAPPING.put("connectTimeoutInMillis", + new OptionMetadata(ClassName.get("java.time", "Duration"), + ClassName.get("software.amazon.awssdk.http", + "SdkHttpConfigurationOption", "CONNECTION_TIMEOUT"))); + CONFIGURATION_MAPPING.put("retryMode", new OptionMetadata(ClassName.get("software.amazon.awssdk.core.retry", "RetryMode" + ), ClassName.get("software.amazon.awssdk.core.client.config", "SdkClientOption", "DEFAULT_RETRY_MODE"))); + } + + public DefaultsModeConfigurationGenerator(String basePackage, String defaultsModeBase, DefaultConfiguration configuration) { + this.basePackage = basePackage; + this.configuration = configuration; + this.defaultsModeBase = defaultsModeBase; + } + + @Override + public TypeSpec poetClass() { + TypeSpec.Builder builder = TypeSpec.classBuilder(className()) + .addModifiers(PUBLIC, FINAL) + .addJavadoc(documentation()) + .addAnnotation(SdkInternalApi.class) + .addAnnotation(AnnotationSpec.builder(Generated.class) + .addMember("value", + "$S", + "software.amazon.awssdk:codegen") + .build()) + .addMethod(defaultConfigMethod(DEFAULT_CONFIG_BY_MODE_ENUM_MAP, "defaultConfig")) + .addMethod(defaultConfigMethod(DEFAULT_HTTP_CONFIG_BY_MODE_ENUM_MAP, + "defaultHttpConfig")) + .addMethod(createConstructor()); + + + configuration.modeDefaults().entrySet().forEach(entry -> { + builder.addField(addDefaultsFieldForMode(entry)); + builder.addField(addHttpDefaultsFieldForMode(entry)); + }); + + addDefaultsFieldForLegacy(builder, "LEGACY_DEFAULTS"); + addDefaultsFieldForLegacy(builder, "LEGACY_HTTP_DEFAULTS"); + + addEnumMapField(builder, DEFAULT_CONFIG_BY_MODE_ENUM_MAP); + addEnumMapField(builder, DEFAULT_HTTP_CONFIG_BY_MODE_ENUM_MAP); + + addStaticEnumMapBlock(builder); + return builder.build(); + } + + private void addStaticEnumMapBlock(TypeSpec.Builder builder) { + CodeBlock.Builder staticCodeBlock = CodeBlock.builder(); + + putItemsToEnumMap(staticCodeBlock, configuration.modeDefaults().keySet(), DEFAULTS_VAR_SUFFIX, + DEFAULT_CONFIG_BY_MODE_ENUM_MAP); + putItemsToEnumMap(staticCodeBlock, configuration.modeDefaults().keySet(), HTTP_DEFAULTS_VAR_SUFFIX, + DEFAULT_HTTP_CONFIG_BY_MODE_ENUM_MAP); + + builder.addStaticBlock(staticCodeBlock.build()); + } + + private void addEnumMapField(TypeSpec.Builder builder, String name) { + ParameterizedTypeName map = ParameterizedTypeName.get(ClassName.get(Map.class), + defaultsModeClassName(), + ClassName.get(AttributeMap.class)); + FieldSpec field = FieldSpec.builder(map, name, PRIVATE, STATIC, FINAL) + .initializer("new $T<>(DefaultsMode.class)", EnumMap.class).build(); + builder.addField(field); + } + + private void putItemsToEnumMap(CodeBlock.Builder codeBlock, Set modes, String suffix, String mapName) { + modes.forEach(m -> { + String mode = sanitizeMode(m); + codeBlock.addStatement("$N.put(DefaultsMode.$N, $N)", mapName, mode, mode + suffix); + }); + + // Add LEGACY since LEGACY is not in the modes set + codeBlock.addStatement("$N.put(DefaultsMode.LEGACY, LEGACY$N)", mapName, suffix); + } + + @Override + public ClassName className() { + return ClassName.get(basePackage, "DefaultsModeConfiguration"); + } + + private FieldSpec addDefaultsFieldForMode(Map.Entry> modeEntry) { + String mode = modeEntry.getKey(); + String fieldName = sanitizeMode(mode) + DEFAULTS_VAR_SUFFIX; + + CodeBlock.Builder attributeBuilder = CodeBlock.builder() + .add("$T.builder()", AttributeMap.class); + + modeEntry.getValue() + .entrySet() + .stream() + .filter(e -> CONFIGURATION_MAPPING.containsKey(e.getKey())) + .forEach(e -> attributeMapBuilder(e.getKey(), e.getValue(), attributeBuilder)); + + + FieldSpec.Builder fieldSpec = FieldSpec.builder(AttributeMap.class, fieldName, PRIVATE, STATIC, FINAL) + .initializer(attributeBuilder + .add(".build()") + .build()); + + + return fieldSpec.build(); + } + + private void addDefaultsFieldForLegacy(TypeSpec.Builder builder, String name) { + FieldSpec field = FieldSpec.builder(AttributeMap.class, name, PRIVATE, STATIC, FINAL) + .initializer("$T.empty()", AttributeMap.class).build(); + builder.addField(field); + } + + private void attributeMapBuilder(String option, String value, CodeBlock.Builder attributeBuilder) { + OptionMetadata optionMetadata = CONFIGURATION_MAPPING.get(option); + switch (option) { + case "retryMode": + attributeBuilder.add(".put($T, $T.$N)", optionMetadata.attribute, optionMetadata.type, + value.toUpperCase(Locale.US)); + break; + default: + throw new IllegalStateException("Unsupported option " + option); + } + } + + private void httpAttributeMapBuilder(String option, String value, CodeBlock.Builder attributeBuilder) { + OptionMetadata optionMetadata = HTTP_CONFIGURATION_MAPPING.get(option); + switch (option) { + case "connectTimeoutInMillis": + attributeBuilder.add(".put($T, $T.ofMillis($N))", optionMetadata.attribute, optionMetadata.type, value); + break; + default: + throw new IllegalStateException("Unsupported option " + option); + } + } + + private FieldSpec addHttpDefaultsFieldForMode(Map.Entry> modeEntry) { + String mode = modeEntry.getKey(); + String fieldName = sanitizeMode(mode) + HTTP_DEFAULTS_VAR_SUFFIX; + + CodeBlock.Builder attributeBuilder = CodeBlock.builder() + .add("$T.builder()", AttributeMap.class); + + modeEntry.getValue() + .entrySet() + .stream() + .filter(e -> HTTP_CONFIGURATION_MAPPING.containsKey(e.getKey())) + .forEach(e -> httpAttributeMapBuilder(e.getKey(), e.getValue(), attributeBuilder)); + + FieldSpec.Builder fieldSpec = FieldSpec.builder(AttributeMap.class, fieldName, PRIVATE, STATIC, FINAL) + .initializer(attributeBuilder + .add(".build()") + .build()); + + return fieldSpec.build(); + } + + private String sanitizeMode(String str) { + return str.replace('-', '_').toUpperCase(Locale.US); + } + + private CodeBlock documentation() { + CodeBlock.Builder builder = CodeBlock.builder() + .add("Contains a collection of default configuration options for each " + + "DefaultsMode"); + + return builder.build(); + } + + private MethodSpec defaultConfigMethod(String enumMap, String methodName) { + MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(methodName) + .returns(AttributeMap.class) + .addModifiers(PUBLIC, STATIC) + .addJavadoc("Return the default config options for a given defaults " + + "mode") + .addParameter(defaultsModeClassName(), "mode") + .addStatement("return $N.getOrDefault(mode, $T.empty())", + enumMap, AttributeMap.class); + + return methodBuilder.build(); + } + + private ClassName defaultsModeClassName() { + return ClassName.get(defaultsModeBase, "DefaultsMode"); + } + + private MethodSpec createConstructor() { + return MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .build(); + } + + private static final class OptionMetadata { + private final ClassName type; + private final ClassName attribute; + + OptionMetadata(ClassName type, ClassName attribute) { + this.type = type; + this.attribute = attribute; + } + } +} 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 index 5a753db0af1d..9ceb3c9e40d4 100644 --- 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 @@ -42,4 +42,10 @@ public void defaultsModeEnum() { assertThat(generator, generatesTo("defaults-mode.java")); } + @Test + public void defaultsModeConfigurationClass() { + DefaultsModeConfigurationGenerator generator = new DefaultsModeConfigurationGenerator(DEFAULTS_MODE_BASE, DEFAULTS_MODE_BASE, defaultConfiguration); + assertThat(generator, generatesTo("defaults-mode-configuration.java")); + } + } diff --git a/codegen-lite/src/test/resources/software/amazon/awssdk/codegen/lite/defaultsmode/defaults-mode-configuration.java b/codegen-lite/src/test/resources/software/amazon/awssdk/codegen/lite/defaultsmode/defaults-mode-configuration.java new file mode 100644 index 000000000000..e7ca75d552c4 --- /dev/null +++ b/codegen-lite/src/test/resources/software/amazon/awssdk/codegen/lite/defaultsmode/defaults-mode-configuration.java @@ -0,0 +1,80 @@ +package software.amazon.awssdk.defaultsmode; + +import java.time.Duration; +import java.util.EnumMap; +import java.util.Map; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.utils.AttributeMap; + +/** + * Contains a collection of default configuration options for each DefaultsMode + */ +@SdkInternalApi +@Generated("software.amazon.awssdk:codegen") +public final class DefaultsModeConfiguration { + private static final AttributeMap STANDARD_DEFAULTS = AttributeMap.builder() + .put(SdkClientOption.DEFAULT_RETRY_MODE, RetryMode.STANDARD).build(); + + private static final AttributeMap STANDARD_HTTP_DEFAULTS = AttributeMap.builder() + .put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, Duration.ofMillis(2000)).build(); + + private static final AttributeMap MOBILE_DEFAULTS = AttributeMap.builder() + .put(SdkClientOption.DEFAULT_RETRY_MODE, RetryMode.ADAPTIVE).build(); + + private static final AttributeMap MOBILE_HTTP_DEFAULTS = AttributeMap.builder() + .put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, Duration.ofMillis(10000)).build(); + + private static final AttributeMap CROSS_REGION_DEFAULTS = AttributeMap.builder() + .put(SdkClientOption.DEFAULT_RETRY_MODE, RetryMode.STANDARD).build(); + + private static final AttributeMap CROSS_REGION_HTTP_DEFAULTS = AttributeMap.builder() + .put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, Duration.ofMillis(2800)).build(); + + private static final AttributeMap IN_REGION_DEFAULTS = AttributeMap.builder() + .put(SdkClientOption.DEFAULT_RETRY_MODE, RetryMode.STANDARD).build(); + + private static final AttributeMap IN_REGION_HTTP_DEFAULTS = AttributeMap.builder() + .put(SdkHttpConfigurationOption.CONNECTION_TIMEOUT, Duration.ofMillis(1000)).build(); + + private static final AttributeMap LEGACY_DEFAULTS = AttributeMap.empty(); + + private static final AttributeMap LEGACY_HTTP_DEFAULTS = AttributeMap.empty(); + + private static final Map DEFAULT_CONFIG_BY_MODE = new EnumMap<>(DefaultsMode.class); + + private static final Map DEFAULT_HTTP_CONFIG_BY_MODE = new EnumMap<>(DefaultsMode.class); + + static { + DEFAULT_CONFIG_BY_MODE.put(DefaultsMode.STANDARD, STANDARD_DEFAULTS); + DEFAULT_CONFIG_BY_MODE.put(DefaultsMode.MOBILE, MOBILE_DEFAULTS); + DEFAULT_CONFIG_BY_MODE.put(DefaultsMode.CROSS_REGION, CROSS_REGION_DEFAULTS); + DEFAULT_CONFIG_BY_MODE.put(DefaultsMode.IN_REGION, IN_REGION_DEFAULTS); + DEFAULT_CONFIG_BY_MODE.put(DefaultsMode.LEGACY, LEGACY_DEFAULTS); + DEFAULT_HTTP_CONFIG_BY_MODE.put(DefaultsMode.STANDARD, STANDARD_HTTP_DEFAULTS); + DEFAULT_HTTP_CONFIG_BY_MODE.put(DefaultsMode.MOBILE, MOBILE_HTTP_DEFAULTS); + DEFAULT_HTTP_CONFIG_BY_MODE.put(DefaultsMode.CROSS_REGION, CROSS_REGION_HTTP_DEFAULTS); + DEFAULT_HTTP_CONFIG_BY_MODE.put(DefaultsMode.IN_REGION, IN_REGION_HTTP_DEFAULTS); + DEFAULT_HTTP_CONFIG_BY_MODE.put(DefaultsMode.LEGACY, LEGACY_HTTP_DEFAULTS); + } + + private DefaultsModeConfiguration() { + } + + /** + * Return the default config options for a given defaults mode + */ + public static AttributeMap defaultConfig(DefaultsMode mode) { + return DEFAULT_CONFIG_BY_MODE.getOrDefault(mode, AttributeMap.empty()); + } + + /** + * Return the default config options for a given defaults mode + */ + public static AttributeMap defaultHttpConfig(DefaultsMode mode) { + return DEFAULT_HTTP_CONFIG_BY_MODE.getOrDefault(mode, AttributeMap.empty()); + } +} diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java index 3c3e242c2e70..c512ed49e1cd 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.awscore.client.builder; +import static software.amazon.awssdk.core.client.config.SdkClientOption.DEFAULTS_MODE; + import java.net.URI; import java.util.Arrays; import java.util.List; @@ -28,22 +30,27 @@ import software.amazon.awssdk.awscore.endpoint.DefaultServiceEndpointBuilder; import software.amazon.awssdk.awscore.eventstream.EventStreamInitialRequestInterceptor; import software.amazon.awssdk.awscore.interceptor.HelpfulUnknownHostExceptionInterceptor; +import software.amazon.awssdk.awscore.internal.defaultsmode.AutoDefaultsModeDiscovery; import software.amazon.awssdk.awscore.retry.AwsRetryPolicy; import software.amazon.awssdk.core.client.builder.SdkDefaultClientBuilder; import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.internal.defaultsmode.DefaultsModeResolver; import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.defaultsmode.DefaultsMode; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.internal.defaultsmode.DefaultsModeConfiguration; import software.amazon.awssdk.profiles.ProfileFile; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.regions.ServiceMetadata; import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain; import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.CollectionUtils; +import software.amazon.awssdk.utils.Logger; /** * An SDK-internal implementation of the methods in {@link AwsClientBuilder}, {@link AwsAsyncClientBuilder} and @@ -67,16 +74,21 @@ public abstract class AwsDefaultClientBuilder, ClientT> extends SdkDefaultClientBuilder implements AwsClientBuilder { + private static final Logger log = Logger.loggerFor(AwsClientBuilder.class); private static final String DEFAULT_ENDPOINT_PROTOCOL = "https"; + private final AutoDefaultsModeDiscovery autoDefaultsModeDiscovery; protected AwsDefaultClientBuilder() { super(); + autoDefaultsModeDiscovery = new AutoDefaultsModeDiscovery(); } @SdkTestInternalApi AwsDefaultClientBuilder(SdkHttpClient.Builder defaultHttpClientBuilder, - SdkAsyncHttpClient.Builder defaultAsyncHttpClientFactory) { + SdkAsyncHttpClient.Builder defaultAsyncHttpClientFactory, + AutoDefaultsModeDiscovery autoDefaultsModeDiscovery) { super(defaultHttpClientBuilder, defaultAsyncHttpClientFactory); + this.autoDefaultsModeDiscovery = autoDefaultsModeDiscovery; } /** @@ -103,6 +115,19 @@ protected final AttributeMap childHttpConfig() { return serviceHttpConfig(); } + /** + * Return HTTP related defaults with the following chain of priorities. + *
    + *
  1. Service-Specific Defaults
  2. + *
  3. Defaults vended by {@link DefaultsMode}
  4. + *
+ */ + @Override + protected final AttributeMap childHttpConfig(SdkClientConfiguration configuration) { + AttributeMap attributeMap = serviceHttpConfig(); + return attributeMap.merge(httpConfigFromDefaultsMode(configuration)); + } + /** * Optionally overridden by child classes to define service-specific HTTP configuration defaults. */ @@ -114,10 +139,10 @@ protected AttributeMap serviceHttpConfig() { protected final SdkClientConfiguration mergeChildDefaults(SdkClientConfiguration configuration) { SdkClientConfiguration config = mergeServiceDefaults(configuration); config = config.merge(c -> c.option(AwsAdvancedClientOption.ENABLE_DEFAULT_REGION_DETECTION, true) - .option(SdkAdvancedClientOption.DISABLE_HOST_PREFIX_INJECTION, false) - .option(AwsClientOption.SERVICE_SIGNING_NAME, signingName()) - .option(SdkClientOption.SERVICE_NAME, serviceName()) - .option(AwsClientOption.ENDPOINT_PREFIX, serviceEndpointPrefix())); + .option(SdkAdvancedClientOption.DISABLE_HOST_PREFIX_INJECTION, false) + .option(AwsClientOption.SERVICE_SIGNING_NAME, signingName()) + .option(SdkClientOption.SERVICE_NAME, serviceName()) + .option(AwsClientOption.ENDPOINT_PREFIX, serviceEndpointPrefix())); return mergeInternalDefaults(config); } @@ -143,6 +168,10 @@ protected final SdkClientConfiguration finalizeChildConfiguration(SdkClientConfi .option(AwsClientOption.AWS_REGION, resolveRegion(configuration)) .build(); + configuration = configuration.toBuilder() + .option(SdkClientOption.DEFAULTS_MODE, resolveDefaultsMode(configuration)) + .build(); + return configuration.toBuilder() .option(AwsClientOption.CREDENTIALS_PROVIDER, resolveCredentials(configuration)) .option(SdkClientOption.ENDPOINT, resolveEndpoint(configuration)) @@ -159,6 +188,14 @@ protected SdkClientConfiguration finalizeServiceConfiguration(SdkClientConfigura return configuration; } + /** + * Return the defaults specified for each {@link DefaultsMode} + */ + private AttributeMap httpConfigFromDefaultsMode(SdkClientConfiguration sdkClientConfiguration) { + DefaultsMode defaultsMode = sdkClientConfiguration.option(DEFAULTS_MODE); + return DefaultsModeConfiguration.defaultHttpConfig(defaultsMode); + } + /** * Resolve the signing region from the default-applied configuration. */ @@ -210,6 +247,24 @@ private Region regionFromDefaultProvider(SdkClientConfiguration config) { .getRegion(); } + private DefaultsMode resolveDefaultsMode(SdkClientConfiguration config) { + DefaultsMode defaultsMode = config.option(DEFAULTS_MODE) != null ? + config.option(DEFAULTS_MODE) : + DefaultsModeResolver.create() + .profileFile(() -> config.option(SdkClientOption.PROFILE_FILE)) + .profileName(config.option(SdkClientOption.PROFILE_NAME)) + .resolve(); + + if (defaultsMode == DefaultsMode.AUTO) { + defaultsMode = autoDefaultsModeDiscovery.discover(config.option(AwsClientOption.AWS_REGION)); + DefaultsMode finalDefaultsMode = defaultsMode; + log.debug(() -> String.format("Resolved %s client's AUTO configuration mode to %s", serviceName(), + finalDefaultsMode)); + } + + return defaultsMode; + } + /** * Resolve the credentials that should be used based on the customer's configuration. */ @@ -236,11 +291,18 @@ private RetryPolicy resolveAwsRetryPolicy(SdkClientConfiguration config) { RetryMode retryMode = RetryMode.resolver() .profileFile(() -> config.option(SdkClientOption.PROFILE_FILE)) .profileName(config.option(SdkClientOption.PROFILE_NAME)) - .defaultRetryMode(config.option(SdkClientOption.DEFAULT_RETRY_MODE)) + .defaultRetryMode(resolveDefaultRetryMode(config)) .resolve(); return AwsRetryPolicy.forRetryMode(retryMode); } + private RetryMode resolveDefaultRetryMode(SdkClientConfiguration config) { + DefaultsMode defaultsMode = config.option(DEFAULTS_MODE); + RetryMode retryMode = config.option(SdkClientOption.DEFAULT_RETRY_MODE); + return retryMode != null ? retryMode : + DefaultsModeConfiguration.defaultConfig(defaultsMode).get(SdkClientOption.DEFAULT_RETRY_MODE); + } + @Override public final BuilderT region(Region region) { clientConfiguration.option(AwsClientOption.AWS_REGION, region); diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/defaultsmode/AutoDefaultsModeDiscovery.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/defaultsmode/AutoDefaultsModeDiscovery.java index 868da56e1dd6..7ace28d39667 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/defaultsmode/AutoDefaultsModeDiscovery.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/defaultsmode/AutoDefaultsModeDiscovery.java @@ -31,7 +31,7 @@ * back to the {@link DefaultsMode#STANDARD} mode if the target mode cannot be determined. */ @SdkInternalApi -public final class AutoDefaultsModeDiscovery { +public class AutoDefaultsModeDiscovery { private static final String EC2_METADATA_REGION_PATH = "/latest/meta-data/placement/region"; private static final DefaultsMode FALLBACK_DEFAULTS_MODE = DefaultsMode.STANDARD; private static final String ANDROID_JAVA_VENDOR = "The Android Project"; diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultAwsClientBuilderTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultAwsClientBuilderTest.java index c32600c5f014..6d973f2467d9 100644 --- a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultAwsClientBuilderTest.java +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultAwsClientBuilderTest.java @@ -42,11 +42,12 @@ import org.mockito.runners.MockitoJUnitRunner; import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; import software.amazon.awssdk.auth.signer.Aws4Signer; -import software.amazon.awssdk.awscore.client.config.AwsClientOption; +import software.amazon.awssdk.awscore.internal.defaultsmode.AutoDefaultsModeDiscovery; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; import software.amazon.awssdk.core.signer.Signer; +import software.amazon.awssdk.defaultsmode.DefaultsMode; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.SdkHttpConfigurationOption; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; @@ -76,10 +77,14 @@ public class DefaultAwsClientBuilderTest { @Mock private SdkAsyncHttpClient.Builder defaultAsyncHttpClientFactory; + @Mock + private AutoDefaultsModeDiscovery autoModeDiscovery; + @Before public void setup() { when(defaultHttpClientBuilder.buildWithDefaults(any())).thenReturn(mock(SdkHttpClient.class)); when(defaultAsyncHttpClientFactory.buildWithDefaults(any())).thenReturn(mock(SdkAsyncHttpClient.class)); + when(autoModeDiscovery.discover(any())).thenReturn(DefaultsMode.IN_REGION); } @Test @@ -232,7 +237,7 @@ private class TestClientBuilder extends AwsDefaultClientBuilder { public TestClientBuilder() { - super(defaultHttpClientBuilder, null); + super(defaultHttpClientBuilder, null, autoModeDiscovery); } @Override @@ -273,7 +278,7 @@ private class TestAsyncClientBuilder extends AwsDefaultClientBuilder { public TestAsyncClientBuilder() { - super(defaultHttpClientBuilder, defaultAsyncHttpClientFactory); + super(defaultHttpClientBuilder, defaultAsyncHttpClientFactory, autoModeDiscovery); } @Override diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultsModeTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultsModeTest.java new file mode 100644 index 000000000000..f069f6bf35ba --- /dev/null +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/client/builder/DefaultsModeTest.java @@ -0,0 +1,246 @@ +/* + * 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.awscore.client.builder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static software.amazon.awssdk.awscore.client.config.AwsAdvancedClientOption.ENABLE_DEFAULT_REGION_DETECTION; +import static software.amazon.awssdk.core.client.config.SdkClientOption.DEFAULTS_MODE; +import static software.amazon.awssdk.core.client.config.SdkClientOption.DEFAULT_RETRY_MODE; +import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_POLICY; + +import java.time.Duration; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; +import software.amazon.awssdk.awscore.internal.defaultsmode.AutoDefaultsModeDiscovery; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.defaultsmode.DefaultsMode; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.internal.defaultsmode.DefaultsModeConfiguration; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.utils.AttributeMap; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultsModeTest { + + private static final AttributeMap SERVICE_DEFAULTS = AttributeMap + .builder() + .put(SdkHttpConfigurationOption.READ_TIMEOUT, Duration.ofSeconds(10)) + .build(); + + private static final String ENDPOINT_PREFIX = "test"; + private static final String SIGNING_NAME = "test"; + private static final String SERVICE_NAME = "test"; + + @Mock + private SdkHttpClient.Builder defaultHttpClientBuilder; + + @Mock + private SdkAsyncHttpClient.Builder defaultAsyncHttpClientBuilder; + + @Mock + private AutoDefaultsModeDiscovery autoModeDiscovery; + + @Test + public void defaultClient_shouldUseLegacyModeWithExistingDefaults() { + TestClient client = testClientBuilder() + .region(Region.US_WEST_2) + .httpClientBuilder((SdkHttpClient.Builder) serviceDefaults -> { + assertThat(serviceDefaults).isEqualTo(SERVICE_DEFAULTS); + return mock(SdkHttpClient.class); + }) + .build(); + + assertThat(client.clientConfiguration.option(DEFAULTS_MODE)).isEqualTo(DefaultsMode.LEGACY); + assertThat(client.clientConfiguration.option(RETRY_POLICY).retryMode()).isEqualTo(RetryMode.defaultRetryMode()); + } + + @Test + public void nonLegacyDefaultsMode_shouldApplySdkDefaultsAndHttpDefaults() { + DefaultsMode targetMode = DefaultsMode.IN_REGION; + + TestClient client = + testClientBuilder().region(Region.US_WEST_1) + .overrideConfiguration(o -> o.defaultsMode(targetMode)) + .httpClientBuilder((SdkHttpClient.Builder) serviceDefaults -> { + AttributeMap defaultHttpConfig = DefaultsModeConfiguration.defaultHttpConfig(targetMode); + AttributeMap mergedDefaults = SERVICE_DEFAULTS.merge(defaultHttpConfig); + assertThat(serviceDefaults).isEqualTo(mergedDefaults); + return mock(SdkHttpClient.class); + }).build(); + + assertThat(client.clientConfiguration.option(DEFAULTS_MODE)).isEqualTo(targetMode); + + AttributeMap attributes = DefaultsModeConfiguration.defaultConfig(targetMode); + + assertThat(client.clientConfiguration.option(RETRY_POLICY).retryMode()).isEqualTo(attributes.get(DEFAULT_RETRY_MODE)); + } + + @Test + public void nonLegacyDefaultsModeAsyncClient_shouldApplySdkDefaultsAndHttpDefaults() { + DefaultsMode targetMode = DefaultsMode.IN_REGION; + + TestAsyncClient client = + testAsyncClientBuilder().region(Region.US_WEST_1) + .overrideConfiguration(o -> o.defaultsMode(targetMode)) + .httpClientBuilder((SdkHttpClient.Builder) serviceDefaults -> { + AttributeMap defaultHttpConfig = DefaultsModeConfiguration.defaultHttpConfig(targetMode); + AttributeMap mergedDefaults = SERVICE_DEFAULTS.merge(defaultHttpConfig); + assertThat(serviceDefaults).isEqualTo(mergedDefaults); + return mock(SdkHttpClient.class); + }).build(); + + assertThat(client.clientConfiguration.option(DEFAULTS_MODE)).isEqualTo(targetMode); + + AttributeMap attributes = DefaultsModeConfiguration.defaultConfig(targetMode); + + assertThat(client.clientConfiguration.option(RETRY_POLICY).retryMode()).isEqualTo(attributes.get(DEFAULT_RETRY_MODE)); + } + + @Test + public void clientOverrideRetryMode_shouldTakePrecedence() { + TestClient client = + testClientBuilder().region(Region.US_WEST_1) + .overrideConfiguration(o -> o.defaultsMode(DefaultsMode.IN_REGION) + .retryPolicy(RetryMode.LEGACY)) + .build(); + assertThat(client.clientConfiguration.option(DEFAULTS_MODE)).isEqualTo(DefaultsMode.IN_REGION); + assertThat(client.clientConfiguration.option(RETRY_POLICY).retryMode()).isEqualTo(RetryMode.LEGACY); + } + + @Test + public void autoMode_shouldResolveDefaultsMode() { + DefaultsMode expectedMode = DefaultsMode.IN_REGION; + when(autoModeDiscovery.discover(any(Region.class))).thenReturn(expectedMode); + TestClient client = + testClientBuilder().region(Region.US_WEST_1) + .overrideConfiguration(o -> o.defaultsMode(DefaultsMode.AUTO)) + .build(); + + assertThat(client.clientConfiguration.option(DEFAULTS_MODE)).isEqualTo(expectedMode); + } + + private static class TestClient { + private final SdkClientConfiguration clientConfiguration; + + public TestClient(SdkClientConfiguration clientConfiguration) { + this.clientConfiguration = clientConfiguration; + } + } + + private AwsClientBuilder testClientBuilder() { + ClientOverrideConfiguration overrideConfig = + ClientOverrideConfiguration.builder() + .putAdvancedOption(ENABLE_DEFAULT_REGION_DETECTION, false) + .build(); + + return new TestClientBuilder().credentialsProvider(AnonymousCredentialsProvider.create()) + .overrideConfiguration(overrideConfig); + } + + private AwsClientBuilder testAsyncClientBuilder() { + ClientOverrideConfiguration overrideConfig = + ClientOverrideConfiguration.builder() + .putAdvancedOption(ENABLE_DEFAULT_REGION_DETECTION, false) + .build(); + + return new TestAsyncClientBuilder().credentialsProvider(AnonymousCredentialsProvider.create()) + .overrideConfiguration(overrideConfig); + } + + private class TestClientBuilder extends AwsDefaultClientBuilder + implements AwsClientBuilder { + + public TestClientBuilder() { + super(defaultHttpClientBuilder, defaultAsyncHttpClientBuilder, autoModeDiscovery); + } + + @Override + protected TestClient buildClient() { + return new TestClient(super.syncClientConfiguration()); + } + + @Override + protected String serviceEndpointPrefix() { + return ENDPOINT_PREFIX; + } + + @Override + protected String signingName() { + return SIGNING_NAME; + } + + @Override + protected String serviceName() { + return SERVICE_NAME; + } + + @Override + protected AttributeMap serviceHttpConfig() { + return SERVICE_DEFAULTS; + } + } + + private class TestAsyncClientBuilder extends AwsDefaultClientBuilder + implements AwsClientBuilder { + + public TestAsyncClientBuilder() { + super(defaultHttpClientBuilder, defaultAsyncHttpClientBuilder, autoModeDiscovery); + } + + @Override + protected TestAsyncClient buildClient() { + return new TestAsyncClient(super.asyncClientConfiguration()); + } + + @Override + protected String serviceEndpointPrefix() { + return ENDPOINT_PREFIX; + } + + @Override + protected String signingName() { + return SIGNING_NAME; + } + + @Override + protected String serviceName() { + return SERVICE_NAME; + } + + @Override + protected AttributeMap serviceHttpConfig() { + return SERVICE_DEFAULTS; + } + } + + private static class TestAsyncClient { + private final SdkClientConfiguration clientConfiguration; + + private TestAsyncClient(SdkClientConfiguration clientConfiguration) { + this.clientConfiguration = clientConfiguration; + } + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java index 40e9693d18fc..64d588b95cae 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java @@ -27,6 +27,7 @@ import static software.amazon.awssdk.core.client.config.SdkClientOption.API_CALL_TIMEOUT; import static software.amazon.awssdk.core.client.config.SdkClientOption.ASYNC_HTTP_CLIENT; import static software.amazon.awssdk.core.client.config.SdkClientOption.CRC32_FROM_COMPRESSED_DATA_ENABLED; +import static software.amazon.awssdk.core.client.config.SdkClientOption.DEFAULTS_MODE; import static software.amazon.awssdk.core.client.config.SdkClientOption.ENDPOINT_OVERRIDDEN; import static software.amazon.awssdk.core.client.config.SdkClientOption.EXECUTION_ATTRIBUTES; import static software.amazon.awssdk.core.client.config.SdkClientOption.EXECUTION_INTERCEPTORS; @@ -68,6 +69,7 @@ import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.core.util.SdkUserAgent; +import software.amazon.awssdk.defaultsmode.DefaultsMode; import software.amazon.awssdk.http.ExecutableHttpRequest; import software.amazon.awssdk.http.HttpExecuteRequest; import software.amazon.awssdk.http.SdkHttpClient; @@ -147,6 +149,7 @@ public final C build() { *
    *
  1. Customer Configuration
  2. *
  3. Service-Specific Defaults
  4. + *
  5. Defaults vended by {@link DefaultsMode}
  6. *
  7. Global Defaults
  8. *
*/ @@ -170,6 +173,7 @@ protected final SdkClientConfiguration syncClientConfiguration() { *
    *
  1. Customer Configuration
  2. *
  3. Implementation/Service-Specific Configuration
  4. + *
  5. Defaults vended by {@link DefaultsMode}
  6. *
  7. Global Default Configuration
  8. *
*/ @@ -275,8 +279,8 @@ private SdkHttpClient resolveSyncHttpClient(SdkClientConfiguration config) { "The httpClient and the httpClientBuilder can't both be configured."); return Either.fromNullable(config.option(SdkClientOption.SYNC_HTTP_CLIENT), httpClientBuilder) - .map(e -> e.map(NonManagedSdkHttpClient::new, b -> b.buildWithDefaults(childHttpConfig()))) - .orElseGet(() -> defaultHttpClientBuilder.buildWithDefaults(childHttpConfig())); + .map(e -> e.map(NonManagedSdkHttpClient::new, b -> b.buildWithDefaults(childHttpConfig(config)))) + .orElseGet(() -> defaultHttpClientBuilder.buildWithDefaults(childHttpConfig(config))); } /** @@ -286,13 +290,22 @@ private SdkAsyncHttpClient resolveAsyncHttpClient(SdkClientConfiguration config) Validate.isTrue(config.option(ASYNC_HTTP_CLIENT) == null || asyncHttpClientBuilder == null, "The asyncHttpClient and the asyncHttpClientBuilder can't both be configured."); return Either.fromNullable(config.option(ASYNC_HTTP_CLIENT), asyncHttpClientBuilder) - .map(e -> e.map(NonManagedSdkAsyncHttpClient::new, b -> b.buildWithDefaults(childHttpConfig()))) - .orElseGet(() -> defaultAsyncHttpClientBuilder.buildWithDefaults(childHttpConfig())); + .map(e -> e.map(NonManagedSdkAsyncHttpClient::new, b -> b.buildWithDefaults(childHttpConfig(config)))) + .orElseGet(() -> defaultAsyncHttpClientBuilder.buildWithDefaults(childHttpConfig(config))); } /** * Optionally overridden by child implementations to provide implementation-specific default HTTP configuration. */ + protected AttributeMap childHttpConfig(SdkClientConfiguration configuration) { + return childHttpConfig(); + } + + /** + * Optionally overridden by child implementations to provide implementation-specific default HTTP configuration. + * @deprecated use {@link #childHttpConfig(SdkClientConfiguration)} instead + */ + @Deprecated protected AttributeMap childHttpConfig() { return AttributeMap.empty(); } @@ -392,6 +405,7 @@ public final B overrideConfiguration(ClientOverrideConfiguration overrideConfig) overrideConfig.advancedOption(ENDPOINT_OVERRIDDEN_OVERRIDE).ifPresent(value -> { clientConfiguration.option(ENDPOINT_OVERRIDDEN, value); }); + clientConfiguration.option(DEFAULTS_MODE, overrideConfig.defaultsMode().orElse(null)); overrideConfig.advancedOption(SIGNER).ifPresent(s -> clientConfiguration.option(SIGNER_OVERRIDDEN, true)); return thisBuilder(); } 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 4a57d3885456..a2602105612f 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 @@ -210,7 +210,7 @@ public List metricPublishers() { * @see Builder#defaultsMode(DefaultsMode) */ public Optional defaultsMode() { - return Optional.of(defaultsMode); + return Optional.ofNullable(defaultsMode); } /** diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientConfiguration.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientConfiguration.java index c87e7b22f400..aabc7a5ba631 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientConfiguration.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientConfiguration.java @@ -79,6 +79,25 @@ public void close() { attributes.close(); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + SdkClientConfiguration that = (SdkClientConfiguration) o; + + return attributes.equals(that.attributes); + } + + @Override + public int hashCode() { + return attributes.hashCode(); + } + public static final class Builder implements CopyableBuilder { private final AttributeMap.Builder attributes; diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java index 01a45c683b8a..9930299d90ea 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java @@ -27,6 +27,7 @@ import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.defaultsmode.DefaultsMode; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.metrics.MetricPublisher; @@ -157,6 +158,11 @@ public final class SdkClientOption extends ClientOption { */ public static final SdkClientOption DEFAULT_RETRY_MODE = new SdkClientOption<>(RetryMode.class); + /** + * Option to specify the {@link DefaultsMode} + */ + public static final SdkClientOption DEFAULTS_MODE = new SdkClientOption<>(DefaultsMode.class); + private SdkClientOption(Class valueClass) { super(valueClass); } diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/config/SdkClientConfigurationTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/config/SdkClientConfigurationTest.java new file mode 100644 index 000000000000..c943b3eef096 --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/config/SdkClientConfigurationTest.java @@ -0,0 +1,29 @@ +/* + * 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.client.config; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; + +public class SdkClientConfigurationTest { + + @Test + public void equalsHashcode() { + EqualsVerifier.forClass(SdkClientConfiguration.class) + .withNonnullFields("attributes") + .verify(); + } +} diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/defaultsmode/DefaultsModeConfigurationTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/defaultsmode/DefaultsModeConfigurationTest.java new file mode 100644 index 000000000000..6a1e3e12faae --- /dev/null +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/defaultsmode/DefaultsModeConfigurationTest.java @@ -0,0 +1,40 @@ +/* + * 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 java.util.Arrays; +import org.junit.Test; +import software.amazon.awssdk.defaultsmode.DefaultsMode; +import software.amazon.awssdk.internal.defaultsmode.DefaultsModeConfiguration; +import software.amazon.awssdk.utils.AttributeMap; + +public class DefaultsModeConfigurationTest { + + @Test + public void defaultConfig_shouldPresentExceptLegacyAndAuto() { + Arrays.stream(DefaultsMode.values()).forEach(m -> { + if (m == DefaultsMode.LEGACY || m == DefaultsMode.AUTO) { + assertThat(DefaultsModeConfiguration.defaultConfig(m)).isEqualTo(AttributeMap.empty()); + assertThat(DefaultsModeConfiguration.defaultHttpConfig(m)).isEqualTo(AttributeMap.empty()); + } else { + assertThat(DefaultsModeConfiguration.defaultConfig(m)).isNotEqualTo(AttributeMap.empty()); + assertThat(DefaultsModeConfiguration.defaultHttpConfig(m)).isNotEqualTo(AttributeMap.empty()); + } + }); + } +} diff --git a/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpClient.java b/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpClient.java index cd2ac439cc89..b57130a6eb9b 100644 --- a/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpClient.java +++ b/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpClient.java @@ -70,8 +70,8 @@ default SdkHttpClient build() { } /** - * Create an {@link SdkHttpClient} with service specific defaults applied. Applying service defaults is optional - * and some options may not be supported by a particular implementation. + * Create an {@link SdkHttpClient} with service specific defaults and defaults from {@code DefaultsMode} applied. + * Applying service defaults is optional and some options may not be supported by a particular implementation. * * @param serviceDefaults Service specific defaults. Keys will be one of the constants defined in * {@link SdkHttpConfigurationOption}. diff --git a/http-client-spi/src/main/java/software/amazon/awssdk/http/async/SdkAsyncHttpClient.java b/http-client-spi/src/main/java/software/amazon/awssdk/http/async/SdkAsyncHttpClient.java index 3f7e8dcbd53b..a24f2c7c91a0 100644 --- a/http-client-spi/src/main/java/software/amazon/awssdk/http/async/SdkAsyncHttpClient.java +++ b/http-client-spi/src/main/java/software/amazon/awssdk/http/async/SdkAsyncHttpClient.java @@ -19,6 +19,7 @@ import software.amazon.awssdk.annotations.Immutable; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.http.SdkHttpConfigurationOption; import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.SdkAutoCloseable; import software.amazon.awssdk.utils.builder.SdkBuilder; diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/AsyncClientDefaultsModeTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/AsyncClientDefaultsModeTest.java new file mode 100644 index 000000000000..4c21e16d7bbe --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/AsyncClientDefaultsModeTest.java @@ -0,0 +1,42 @@ +/* + * 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.services.defaultsmode; + +import java.util.concurrent.CompletionException; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonAsyncClient; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonAsyncClientBuilder; +import software.amazon.awssdk.services.protocolrestjson.model.AllTypesResponse; + +public class AsyncClientDefaultsModeTest + extends ClientDefaultsModeTestSuite { + @Override + protected ProtocolRestJsonAsyncClientBuilder newClientBuilder() { + return ProtocolRestJsonAsyncClient.builder(); + } + + @Override + protected AllTypesResponse callAllTypes(ProtocolRestJsonAsyncClient client) { + try { + return client.allTypes().join(); + } catch (CompletionException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + + throw e; + } + } +} diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/ClientDefaultsModeTestSuite.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/ClientDefaultsModeTestSuite.java new file mode 100644 index 000000000000..9d99daee4ad9 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/ClientDefaultsModeTestSuite.java @@ -0,0 +1,95 @@ +/* + * Copyright 2010-2020 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.services.defaultsmode; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; +import static com.github.tomakehurst.wiremock.client.WireMock.containing; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; + +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import java.net.URI; +import org.junit.Rule; +import org.junit.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder; +import software.amazon.awssdk.core.retry.RetryMode; +import software.amazon.awssdk.defaultsmode.DefaultsMode; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.protocolrestjson.model.AllTypesResponse; + +/** + * Tests suites to verify {@link DefaultsMode} behavior. We currently just test SDK default configuration such as + * {@link RetryMode}; there is no easy way to test HTTP timeout option. + * + */ +public abstract class ClientDefaultsModeTestSuite> { + @Rule + public WireMockRule wireMock = new WireMockRule(0); + + @Test + public void legacyDefaultsMode_shouldUseLegacySetting() { + stubResponse(); + ClientT client = clientBuilder().overrideConfiguration(o -> o.retryPolicy(RetryMode.LEGACY)).build(); + callAllTypes(client); + + WireMock.verify(postRequestedFor(anyUrl()).withHeader("User-Agent", containing("cfg/retry-mode/legacy"))); + } + + @Test + public void standardDefaultsMode_shouldApplyStandardDefaults() { + stubResponse(); + ClientT client = clientBuilder().overrideConfiguration(o -> o.defaultsMode(DefaultsMode.STANDARD)).build(); + callAllTypes(client); + + WireMock.verify(postRequestedFor(anyUrl()).withHeader("User-Agent", containing("cfg/retry-mode/standard"))); + } + + @Test + public void retryModeOverridden_shouldTakePrecedence() { + stubResponse(); + ClientT client = + clientBuilder().overrideConfiguration(o -> o.defaultsMode(DefaultsMode.STANDARD).retryPolicy(RetryMode.LEGACY)).build(); + callAllTypes(client); + + WireMock.verify(postRequestedFor(anyUrl()).withHeader("User-Agent", containing("cfg/retry-mode/legacy"))); + } + + private BuilderT clientBuilder() { + return newClientBuilder().credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("akid", "skid"))) + .region(Region.US_EAST_1) + .endpointOverride(URI.create("http://localhost:" + wireMock.port())); + } + + protected abstract BuilderT newClientBuilder(); + + protected abstract AllTypesResponse callAllTypes(ClientT client); + + private void verifyRequestCount(int count) { + verify(count, anyRequestedFor(anyUrl())); + } + + private void stubResponse() { + stubFor(post(anyUrl()) + .willReturn(aResponse())); + } +} diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/SyncClientDefaultsModeTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/SyncClientDefaultsModeTest.java new file mode 100644 index 000000000000..106da961ce65 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/defaultsmode/SyncClientDefaultsModeTest.java @@ -0,0 +1,32 @@ +/* + * 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.services.defaultsmode; + +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonClient; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonClientBuilder; +import software.amazon.awssdk.services.protocolrestjson.model.AllTypesResponse; + +public class SyncClientDefaultsModeTest extends ClientDefaultsModeTestSuite { + @Override + protected ProtocolRestJsonClientBuilder newClientBuilder() { + return ProtocolRestJsonClient.builder(); + } + + @Override + protected AllTypesResponse callAllTypes(ProtocolRestJsonClient client) { + return client.allTypes(); + } +} diff --git a/utils/src/main/java/software/amazon/awssdk/utils/AttributeMap.java b/utils/src/main/java/software/amazon/awssdk/utils/AttributeMap.java index 32169bd3bc0c..7c182233e4ee 100644 --- a/utils/src/main/java/software/amazon/awssdk/utils/AttributeMap.java +++ b/utils/src/main/java/software/amazon/awssdk/utils/AttributeMap.java @@ -36,6 +36,7 @@ @SdkProtectedApi @Immutable public final class AttributeMap implements ToCopyableBuilder, SdkAutoCloseable { + private static final AttributeMap EMPTY = AttributeMap.builder().build(); private final Map, Object> attributes; private AttributeMap(Map, ?> attributes) { @@ -74,7 +75,7 @@ public AttributeMap merge(AttributeMap lowerPrecedence) { } public static AttributeMap empty() { - return builder().build(); + return EMPTY; } public AttributeMap copy() {