diff --git a/.changes/next-release/feature-AWSSDKforJavav2-70eb163.json b/.changes/next-release/feature-AWSSDKforJavav2-70eb163.json new file mode 100644 index 000000000000..be555212977e --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-70eb163.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Cross region bucket access for S3 Client. This feature will allow users to access buckets of different region using a single cross region configured client." +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/CommonGeneratorTasks.java b/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/CommonGeneratorTasks.java index a6f32ea9fae0..42a06f582542 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/CommonGeneratorTasks.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/CommonGeneratorTasks.java @@ -28,6 +28,6 @@ class CommonGeneratorTasks extends CompositeGeneratorTask { new ModelClassGeneratorTasks(params), new PackageInfoGeneratorTasks(params), new BaseExceptionClassGeneratorTasks(params), - new ClientOptionsClassGeneratorTasks(params)); + new CommonInternalGeneratorTasks(params)); } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/ClientOptionsClassGeneratorTasks.java b/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/CommonInternalGeneratorTasks.java similarity index 61% rename from codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/ClientOptionsClassGeneratorTasks.java rename to codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/CommonInternalGeneratorTasks.java index cfb067a10ff7..7d407f582f7d 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/ClientOptionsClassGeneratorTasks.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/emitters/tasks/CommonInternalGeneratorTasks.java @@ -15,26 +15,35 @@ package software.amazon.awssdk.codegen.emitters.tasks; -import java.util.Collections; +import java.util.Arrays; import java.util.List; import software.amazon.awssdk.codegen.emitters.GeneratorTask; import software.amazon.awssdk.codegen.emitters.GeneratorTaskParams; import software.amazon.awssdk.codegen.emitters.PoetGeneratorTask; import software.amazon.awssdk.codegen.poet.client.SdkClientOptions; +import software.amazon.awssdk.codegen.poet.common.UserAgentUtilsSpec; -public class ClientOptionsClassGeneratorTasks extends BaseGeneratorTasks { +public class CommonInternalGeneratorTasks extends BaseGeneratorTasks { private final GeneratorTaskParams params; - public ClientOptionsClassGeneratorTasks(GeneratorTaskParams params) { + public CommonInternalGeneratorTasks(GeneratorTaskParams params) { super(params); this.params = params; } @Override protected List createTasks() throws Exception { - return Collections.singletonList( - new PoetGeneratorTask(clientOptionsDir(), params.getModel().getFileHeader(), new SdkClientOptions(params.getModel())) - ); + return Arrays.asList(createClientOptionTask(), createUserAgentTask()); + } + + private PoetGeneratorTask createClientOptionTask() { + return new PoetGeneratorTask(clientOptionsDir(), params.getModel().getFileHeader(), + new SdkClientOptions(params.getModel())); + } + + private PoetGeneratorTask createUserAgentTask() { + return new PoetGeneratorTask(clientOptionsDir(), params.getModel().getFileHeader(), + new UserAgentUtilsSpec(params.getModel())); } private String clientOptionsDir() { diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java index 6913bc6e83f8..596d44bcf14b 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java @@ -19,6 +19,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import software.amazon.awssdk.codegen.model.service.ClientContextParam; import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.traits.PayloadTrait; import software.amazon.awssdk.utils.AttributeMap; @@ -212,6 +213,20 @@ public class CustomizationConfig { */ private boolean delegateSyncClientClass; + /** + * Fully qualified name of a class that given the default sync client instance can return the final client instance, + * for instance by decorating the client with specific-purpose implementations of the client interface. + * See S3 customization.config for an example. + */ + private String syncClientDecorator; + + /** + * Fully qualified name of a class that given the default async client instance can return the final client instance, + * for instance by decorating the client with specific-purpose implementations of the client interface. + * See S3 customization.config for an example. + */ + private String asyncClientDecorator; + /** * Whether to skip generating endpoint tests from endpoint-tests.json */ @@ -236,6 +251,11 @@ public class CustomizationConfig { */ private boolean requiredTraitValidationEnabled = false; + /** + * Customization to attach map of Custom client param configs that can be set on a client builder. + */ + private Map customClientContextParams; + private CustomizationConfig() { } @@ -566,6 +586,22 @@ public void setDelegateAsyncClientClass(boolean delegateAsyncClientClass) { this.delegateAsyncClientClass = delegateAsyncClientClass; } + public String getSyncClientDecorator() { + return syncClientDecorator; + } + + public void setSyncClientDecorator(String syncClientDecorator) { + this.syncClientDecorator = syncClientDecorator; + } + + public String getAsyncClientDecorator() { + return asyncClientDecorator; + } + + public void setAsyncClientDecorator(String asyncClientDecorator) { + this.asyncClientDecorator = asyncClientDecorator; + } + public boolean isDelegateSyncClientClass() { return delegateSyncClientClass; } @@ -621,4 +657,12 @@ public boolean isRequiredTraitValidationEnabled() { public void setRequiredTraitValidationEnabled(boolean requiredTraitValidationEnabled) { this.requiredTraitValidationEnabled = requiredTraitValidationEnabled; } + + public Map getCustomClientContextParams() { + return customClientContextParams; + } + + public void setCustomClientContextParams(Map customClientContextParams) { + this.customClientContextParams = customClientContextParams; + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/ServiceConfig.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/ServiceConfig.java index b50be4ff45df..b0c0db862df2 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/ServiceConfig.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/ServiceConfig.java @@ -47,6 +47,8 @@ public class ServiceConfig { private boolean hasAccelerateModeEnabledProperty = false; + private boolean hasCrossRegionAccessEnabledProperty = false; + public String getClassName() { return className; } @@ -95,6 +97,14 @@ public void setHasPathStyleAccessEnabledProperty(boolean hasPathStyleAccessEnabl this.hasPathStyleAccessEnabledProperty = hasPathStyleAccessEnabledProperty; } + public boolean hasCrossRegionAccessEnabledProperty() { + return hasCrossRegionAccessEnabledProperty; + } + + public void setHasCrossRegionAccessEnabledProperty(boolean hasCrossRegionAccessEnabledProperty) { + this.hasCrossRegionAccessEnabledProperty = hasCrossRegionAccessEnabledProperty; + } + public boolean hasAccelerateModeEnabledProperty() { return hasAccelerateModeEnabledProperty; } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/IntermediateModel.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/IntermediateModel.java index 88300c9aacb2..28113a0d8b3d 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/IntermediateModel.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/intermediate/IntermediateModel.java @@ -235,6 +235,21 @@ public String getSdkResponseBaseClassName() { } } + public Optional syncClientDecoratorClassName() { + if (customizationConfig.getSyncClientDecorator() != null) { + return Optional.of(customizationConfig.getSyncClientDecorator()); + } + return Optional.empty(); + } + + public Optional asyncClientDecoratorClassName() { + String asyncClientDecorator = customizationConfig.getAsyncClientDecorator(); + if (customizationConfig.getAsyncClientDecorator() != null) { + return Optional.of(asyncClientDecorator); + } + return Optional.empty(); + } + public String getFileHeader() { return FILE_HEADER; } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/PoetExtension.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/PoetExtension.java index ba37f496c4c9..5061de088940 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/PoetExtension.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/PoetExtension.java @@ -73,6 +73,10 @@ public ClassName getServiceConfigClass() { + model.getMetadata().getServiceName() + "ServiceClientConfiguration"); } + public ClassName getUserAgentClass() { + return ClassName.get(model.getMetadata().getFullClientInternalPackageName(), "UserAgentUtils"); + } + /** * @param operationName Name of the operation * @return A Poet {@link ClassName} for the response type of a paginated operation in the base service package. diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/AsyncClientBuilderClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/AsyncClientBuilderClass.java index f8731d7dad07..509a30c6c8d7 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/AsyncClientBuilderClass.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/AsyncClientBuilderClass.java @@ -32,6 +32,7 @@ import software.amazon.awssdk.codegen.utils.AuthUtils; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.endpoints.EndpointProvider; public class AsyncClientBuilderClass implements ClassSpec { private final IntermediateModel model; @@ -79,7 +80,10 @@ public TypeSpec poetSpec() { builder.addMethod(bearerTokenProviderMethod()); } - return builder.addMethod(buildClientMethod()).build(); + builder.addMethod(buildClientMethod()); + builder.addMethod(initializeServiceClientConfigMethod()); + + return builder.build(); } private MethodSpec endpointDiscoveryEnabled() { @@ -119,26 +123,26 @@ private MethodSpec endpointProviderMethod() { } private MethodSpec buildClientMethod() { - return MethodSpec.methodBuilder("buildClient") - .addAnnotation(Override.class) - .addModifiers(Modifier.PROTECTED, Modifier.FINAL) - .returns(clientInterfaceName) - .addStatement("$T clientConfiguration = super.asyncClientConfiguration()", SdkClientConfiguration.class) - .addStatement("this.validateClientOptions(clientConfiguration)") - .addStatement("$T endpointOverride = null", URI.class) - .addCode("if (clientConfiguration.option($T.ENDPOINT_OVERRIDDEN) != null" - + "&& $T.TRUE.equals(clientConfiguration.option($T.ENDPOINT_OVERRIDDEN))) {" - + "endpointOverride = clientConfiguration.option($T.ENDPOINT);" - + "}", - SdkClientOption.class, Boolean.class, SdkClientOption.class, SdkClientOption.class) - .addStatement("$T serviceClientConfiguration = $T.builder()" - + ".overrideConfiguration(overrideConfiguration())" - + ".region(clientConfiguration.option($T.AWS_REGION))" - + ".endpointOverride(endpointOverride)" - + ".build()", - serviceConfigClassName, serviceConfigClassName, AwsClientOption.class) - .addStatement("return new $T(serviceClientConfiguration, clientConfiguration)", clientClassName) - .build(); + MethodSpec.Builder builder = MethodSpec.methodBuilder("buildClient") + .addAnnotation(Override.class) + .addModifiers(Modifier.PROTECTED, Modifier.FINAL) + .returns(clientInterfaceName) + .addStatement("$T clientConfiguration = super.asyncClientConfiguration()", + SdkClientConfiguration.class).addStatement("this.validateClientOptions" + + "(clientConfiguration)") + .addStatement("$T serviceClientConfiguration = initializeServiceClientConfig" + + "(clientConfiguration)", + serviceConfigClassName); + + builder.addStatement("$1T client = new $2T(serviceClientConfiguration, clientConfiguration)", + clientInterfaceName, clientClassName); + if (model.asyncClientDecoratorClassName().isPresent()) { + builder.addStatement("return new $T().decorate(client, clientConfiguration, clientContextParams.copy().build())", + PoetUtils.classNameFromFqcn(model.asyncClientDecoratorClassName().get())); + } else { + builder.addStatement("return client"); + } + return builder.build(); } private MethodSpec bearerTokenProviderMethod() { @@ -152,6 +156,29 @@ private MethodSpec bearerTokenProviderMethod() { .build(); } + private MethodSpec initializeServiceClientConfigMethod() { + return MethodSpec.methodBuilder("initializeServiceClientConfig").addModifiers(Modifier.PRIVATE) + .addParameter(SdkClientConfiguration.class, "clientConfig") + .returns(serviceConfigClassName) + .addStatement("$T endpointOverride = null", URI.class) + .addStatement("$T endpointProvider = clientConfig.option($T.ENDPOINT_PROVIDER)", + EndpointProvider.class, + SdkClientOption.class) + .addCode("if (clientConfig.option($T.ENDPOINT_OVERRIDDEN) != null" + + "&& $T.TRUE.equals(clientConfig.option($T.ENDPOINT_OVERRIDDEN))) {" + + "endpointOverride = clientConfig.option($T.ENDPOINT);" + + "}", + SdkClientOption.class, Boolean.class, SdkClientOption.class, SdkClientOption.class) + .addStatement("return $T.builder()" + + ".overrideConfiguration(overrideConfiguration())" + + ".region(clientConfig.option($T.AWS_REGION))" + + ".endpointOverride(endpointOverride)" + + ".endpointProvider(endpointProvider)" + + ".build()", + serviceConfigClassName, AwsClientOption.class) + .build(); + } + @Override public ClassName className() { return builderClassName; diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java index 1be4d730040e..b5fa79c68039 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java @@ -119,6 +119,12 @@ public TypeSpec poetSpec() { }); } + if (hasSdkClientContextParams()) { + model.getCustomizationConfig().getCustomClientContextParams().forEach((n, m) -> { + builder.addMethod(clientContextParamSetter(n, m)); + }); + } + if (model.getCustomizationConfig().getServiceConfig().getClassName() != null) { builder.addMethod(setServiceConfigurationMethod()) .addMethod(beanStyleSetServiceConfigurationMethod()); @@ -619,6 +625,12 @@ private boolean hasClientContextParams() { return clientContextParams != null && !clientContextParams.isEmpty(); } + private boolean hasSdkClientContextParams() { + return model.getCustomizationConfig() != null + && model.getCustomizationConfig().getCustomClientContextParams() != null + && !model.getCustomizationConfig().getCustomClientContextParams().isEmpty(); + } + private MethodSpec validateClientOptionsMethod() { MethodSpec.Builder builder = MethodSpec.methodBuilder("validateClientOptions") .addModifiers(PROTECTED, Modifier.STATIC) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderInterface.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderInterface.java index a65d8ae6398a..0aac740152bc 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderInterface.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderInterface.java @@ -80,6 +80,12 @@ public TypeSpec poetSpec() { }); } + if (hasSdkClientContextParams()) { + model.getCustomizationConfig().getCustomClientContextParams().forEach((n, m) -> { + builder.addMethod(clientContextParamSetter(n, m)); + }); + } + if (generateTokenProviderMethod()) { builder.addMethod(tokenProviderMethod()); } @@ -193,4 +199,10 @@ public ClassName className() { private boolean hasClientContextParams() { return model.getClientContextParams() != null && !model.getClientContextParams().isEmpty(); } + + private boolean hasSdkClientContextParams() { + return model.getCustomizationConfig() != null + && model.getCustomizationConfig().getCustomClientContextParams() != null + && !model.getCustomizationConfig().getCustomClientContextParams().isEmpty(); + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/SyncClientBuilderClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/SyncClientBuilderClass.java index 036589de04e8..bb5fd4f2208c 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/SyncClientBuilderClass.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/SyncClientBuilderClass.java @@ -32,6 +32,7 @@ import software.amazon.awssdk.codegen.utils.AuthUtils; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.endpoints.EndpointProvider; public class SyncClientBuilderClass implements ClassSpec { private final IntermediateModel model; @@ -79,7 +80,10 @@ public TypeSpec poetSpec() { builder.addMethod(tokenProviderMethodImpl()); } - return builder.addMethod(buildClientMethod()).build(); + builder.addMethod(buildClientMethod()); + builder.addMethod(initializeServiceClientConfigMethod()); + + return builder.build(); } private MethodSpec endpointDiscoveryEnabled() { @@ -119,26 +123,26 @@ private MethodSpec endpointProviderMethod() { private MethodSpec buildClientMethod() { - return MethodSpec.methodBuilder("buildClient") - .addAnnotation(Override.class) - .addModifiers(Modifier.PROTECTED, Modifier.FINAL) - .returns(clientInterfaceName) - .addStatement("$T clientConfiguration = super.syncClientConfiguration()", SdkClientConfiguration.class) - .addStatement("this.validateClientOptions(clientConfiguration)") - .addStatement("$T endpointOverride = null", URI.class) - .addCode("if (clientConfiguration.option($T.ENDPOINT_OVERRIDDEN) != null" - + "&& $T.TRUE.equals(clientConfiguration.option($T.ENDPOINT_OVERRIDDEN))) {" - + "endpointOverride = clientConfiguration.option($T.ENDPOINT);" - + "}", - SdkClientOption.class, Boolean.class, SdkClientOption.class, SdkClientOption.class) - .addStatement("$T serviceClientConfiguration = $T.builder()" - + ".overrideConfiguration(overrideConfiguration())" - + ".region(clientConfiguration.option($T.AWS_REGION))" - + ".endpointOverride(endpointOverride)" - + ".build()", - serviceConfigClassName, serviceConfigClassName, AwsClientOption.class) - .addStatement("return new $T(serviceClientConfiguration, clientConfiguration)", clientClassName) - .build(); + MethodSpec.Builder builder = MethodSpec.methodBuilder("buildClient") + .addAnnotation(Override.class) + .addModifiers(Modifier.PROTECTED, Modifier.FINAL) + .returns(clientInterfaceName) + .addStatement("$T clientConfiguration = super.syncClientConfiguration()", + SdkClientConfiguration.class) + .addStatement("this.validateClientOptions(clientConfiguration)") + .addStatement("$T serviceClientConfiguration = initializeServiceClientConfig" + + "(clientConfiguration)", + serviceConfigClassName); + + builder.addStatement("$1T client = new $2T(serviceClientConfiguration, clientConfiguration)", + clientInterfaceName, clientClassName); + if (model.syncClientDecoratorClassName().isPresent()) { + builder.addStatement("return new $T().decorate(client, clientConfiguration, clientContextParams.copy().build())", + PoetUtils.classNameFromFqcn(model.syncClientDecoratorClassName().get())); + } else { + builder.addStatement("return client"); + } + return builder.build(); } private MethodSpec tokenProviderMethodImpl() { @@ -152,6 +156,29 @@ private MethodSpec tokenProviderMethodImpl() { .build(); } + private MethodSpec initializeServiceClientConfigMethod() { + return MethodSpec.methodBuilder("initializeServiceClientConfig").addModifiers(Modifier.PRIVATE) + .addParameter(SdkClientConfiguration.class, "clientConfig") + .returns(serviceConfigClassName) + .addStatement("$T endpointOverride = null", URI.class) + .addStatement("$T endpointProvider = clientConfig.option($T.ENDPOINT_PROVIDER)", + EndpointProvider.class, + SdkClientOption.class) + .addCode("if (clientConfig.option($T.ENDPOINT_OVERRIDDEN) != null" + + "&& $T.TRUE.equals(clientConfig.option($T.ENDPOINT_OVERRIDDEN))) {" + + "endpointOverride = clientConfig.option($T.ENDPOINT);" + + "}", + SdkClientOption.class, Boolean.class, SdkClientOption.class, SdkClientOption.class) + .addStatement("return $T.builder()" + + ".overrideConfiguration(overrideConfiguration())" + + ".region(clientConfig.option($T.AWS_REGION))" + + ".endpointOverride(endpointOverride)" + + ".endpointProvider(endpointProvider)" + + ".build()", + serviceConfigClassName, AwsClientOption.class) + .build(); + } + @Override public ClassName className() { return builderClassName; diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientClass.java index 207befab0073..9b66f9a28259 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientClass.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientClass.java @@ -24,7 +24,6 @@ import static javax.lang.model.element.Modifier.STATIC; import static software.amazon.awssdk.codegen.internal.Constant.EVENT_PUBLISHER_PARAM_NAME; import static software.amazon.awssdk.codegen.poet.client.ClientClassUtils.addS3ArnableFieldCode; -import static software.amazon.awssdk.codegen.poet.client.ClientClassUtils.applyPaginatorUserAgentMethod; import static software.amazon.awssdk.codegen.poet.client.ClientClassUtils.applySignerOverrideMethod; import static software.amazon.awssdk.codegen.poet.client.SyncClientClass.getProtocolSpecs; @@ -155,10 +154,6 @@ protected void addAdditionalMethods(TypeSpec.Builder type) { .addMethod(protocolSpec.initProtocolFactory(model)) .addMethod(resolveMetricPublishersMethod()); - if (model.hasPaginators()) { - type.addMethod(applyPaginatorUserAgentMethod(poetExtensions, model)); - } - if (model.containsRequestSigners() || model.containsRequestEventStreams() || hasStreamingV4AuthOperations()) { type.addMethod(applySignerOverrideMethod(poetExtensions, model)); type.addMethod(isSignerOverriddenOnClientMethod()); @@ -196,9 +191,6 @@ protected List operations() { private Stream operations(OperationModel opModel) { List methods = new ArrayList<>(); methods.add(traditionalMethod(opModel)); - if (opModel.isPaginated()) { - methods.add(paginatedTraditionalMethod(opModel)); - } return methods.stream(); } @@ -420,14 +412,6 @@ protected MethodSpec.Builder operationBody(MethodSpec.Builder builder, Operation return builder; } - @Override - protected MethodSpec.Builder paginatedMethodBody(MethodSpec.Builder builder, OperationModel opModel) { - return builder.addModifiers(PUBLIC) - .addStatement("return new $T(this, applyPaginatorUserAgent($L))", - poetExtensions.getResponseClassForPaginatedAsyncOperation(opModel.getOperationName()), - opModel.getInput().getVariableName()); - } - @Override public ClassName className() { return className; diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientInterface.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientInterface.java index 3b06ba325db8..04f64c1b128c 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientInterface.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientInterface.java @@ -201,21 +201,34 @@ private MethodSpec builder() { */ protected Iterable operations() { return model.getOperations().values().stream() - .flatMap(this::operationsAndSimpleMethods) + .flatMap(this::operationsWithVariants) .sorted(Comparator.comparing(m -> m.name)) .collect(toList()); } - private Stream operationsAndSimpleMethods(OperationModel operationModel) { + private Stream operationsWithVariants(OperationModel operationModel) { List methods = new ArrayList<>(); - methods.addAll(traditionalMethods(operationModel)); - methods.addAll(overloadMethods(operationModel)); + methods.addAll(traditionalMethodWithConsumerVariant(operationModel)); + methods.addAll(overloadedMethods(operationModel)); methods.addAll(paginatedMethods(operationModel)); return methods.stream() // Add Deprecated annotation if needed to all overloads .map(m -> DeprecationUtils.checkDeprecated(operationModel, m)); } + /** + * Generates the traditional method for an operation (i.e. one that takes a request and returns a response). + */ + private List traditionalMethodWithConsumerVariant(OperationModel opModel) { + List methods = new ArrayList<>(); + String consumerBuilderJavadoc = consumerBuilderJavadoc(opModel, SimpleMethodOverload.NORMAL); + + methods.add(traditionalMethod(opModel)); + methods.add(ClientClassUtils.consumerBuilderVariant(methods.get(0), consumerBuilderJavadoc)); + + return methods; + } + private List paginatedMethods(OperationModel opModel) { List methods = new ArrayList<>(); @@ -251,7 +264,9 @@ protected MethodSpec paginatedTraditionalMethod(OperationModel opModel) { protected MethodSpec.Builder paginatedMethodBody(MethodSpec.Builder builder, OperationModel operationModel) { return builder.addModifiers(DEFAULT, PUBLIC) - .addStatement("throw new $T()", UnsupportedOperationException.class); + .addStatement("return new $T(this, $L)", + poetExtensions.getResponseClassForPaginatedAsyncOperation(operationModel.getOperationName()), + operationModel.getInput().getVariableName()); } private MethodSpec paginatedSimpleMethod(OperationModel opModel) { @@ -274,7 +289,7 @@ private MethodSpec paginatedSimpleMethod(OperationModel opModel) { * @param opModel Operation to generate simple methods for. * @return All simple method overloads for a given operation. */ - private List overloadMethods(OperationModel opModel) { + private List overloadedMethods(OperationModel opModel) { String consumerBuilderFileJavadoc = consumerBuilderJavadoc(opModel, SimpleMethodOverload.FILE); List methodOverloads = new ArrayList<>(); @@ -312,20 +327,6 @@ protected MethodSpec.Builder operationBody(MethodSpec.Builder builder, Operation .addStatement("throw new $T()", UnsupportedOperationException.class); } - /** - * Generates the traditional method for an operation (i.e. one that takes a request and returns a response). - */ - private List traditionalMethods(OperationModel opModel) { - List methods = new ArrayList<>(); - - methods.add(traditionalMethod(opModel)); - - String consumerBuilderJavadoc = consumerBuilderJavadoc(opModel, SimpleMethodOverload.NORMAL); - methods.add(ClientClassUtils.consumerBuilderVariant(methods.get(0), consumerBuilderJavadoc)); - - return methods; - } - protected MethodSpec traditionalMethod(OperationModel opModel) { ClassName responsePojoType = getPojoResponseType(opModel); ClassName requestType = ClassName.get(modelPackage, opModel.getInput().getVariableType()); diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/ClientClassUtils.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/ClientClassUtils.java index 046c9bacfc04..c31d4ab0345e 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/ClientClassUtils.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/ClientClassUtils.java @@ -40,15 +40,12 @@ import software.amazon.awssdk.codegen.model.service.HostPrefixProcessor; import software.amazon.awssdk.codegen.poet.PoetExtension; import software.amazon.awssdk.codegen.poet.PoetUtils; -import software.amazon.awssdk.core.ApiName; import software.amazon.awssdk.core.signer.Signer; -import software.amazon.awssdk.core.util.VersionInfo; import software.amazon.awssdk.utils.HostnameValidator; import software.amazon.awssdk.utils.StringUtils; import software.amazon.awssdk.utils.Validate; final class ClientClassUtils { - private static final String PAGINATOR_USER_AGENT = "PAGINATED"; private ClientClassUtils() { } @@ -85,40 +82,6 @@ static MethodSpec consumerBuilderVariant(MethodSpec spec, String javadoc) { return result.build(); } - static MethodSpec applyPaginatorUserAgentMethod(PoetExtension poetExtensions, IntermediateModel model) { - - TypeVariableName typeVariableName = - TypeVariableName.get("T", poetExtensions.getModelClass(model.getSdkRequestBaseClassName())); - - ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName - .get(ClassName.get(Consumer.class), ClassName.get(AwsRequestOverrideConfiguration.Builder.class)); - - CodeBlock codeBlock = CodeBlock.builder() - .addStatement("$T userAgentApplier = b -> b.addApiName($T.builder().version" - + "($T.SDK_VERSION).name($S).build())", - parameterizedTypeName, ApiName.class, - VersionInfo.class, - PAGINATOR_USER_AGENT) - .addStatement("$T overrideConfiguration =\n" - + " request.overrideConfiguration().map(c -> c.toBuilder()" - + ".applyMutation" - + "(userAgentApplier).build())\n" - + " .orElse((AwsRequestOverrideConfiguration.builder()" - + ".applyMutation" - + "(userAgentApplier).build()))", AwsRequestOverrideConfiguration.class) - .addStatement("return (T) request.toBuilder().overrideConfiguration" - + "(overrideConfiguration).build()") - .build(); - - return MethodSpec.methodBuilder("applyPaginatorUserAgent") - .addModifiers(Modifier.PRIVATE) - .addParameter(typeVariableName, "request") - .addTypeVariable(typeVariableName) - .addCode(codeBlock) - .returns(typeVariableName) - .build(); - } - static MethodSpec applySignerOverrideMethod(PoetExtension poetExtensions, IntermediateModel model) { String signerOverrideVariable = "signerOverride"; diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/DelegatingAsyncClientClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/DelegatingAsyncClientClass.java index e21fbda9f885..27e3cee5d046 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/DelegatingAsyncClientClass.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/DelegatingAsyncClientClass.java @@ -20,15 +20,21 @@ import static javax.lang.model.element.Modifier.ABSTRACT; import static javax.lang.model.element.Modifier.FINAL; import static javax.lang.model.element.Modifier.PRIVATE; +import static javax.lang.model.element.Modifier.PROTECTED; import static javax.lang.model.element.Modifier.PUBLIC; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeSpec; +import com.squareup.javapoet.TypeVariableName; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; import java.util.stream.Stream; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.codegen.model.config.customization.UtilitiesMethod; @@ -36,7 +42,6 @@ import software.amazon.awssdk.codegen.model.intermediate.OperationModel; import software.amazon.awssdk.codegen.poet.PoetExtension; import software.amazon.awssdk.codegen.poet.PoetUtils; -import software.amazon.awssdk.codegen.utils.PaginatorUtils; import software.amazon.awssdk.core.SdkClient; import software.amazon.awssdk.utils.Validate; @@ -88,14 +93,9 @@ protected void addFields(TypeSpec.Builder type) { @Override protected void addAdditionalMethods(TypeSpec.Builder type) { - MethodSpec delegate = MethodSpec.methodBuilder("delegate") - .addModifiers(PUBLIC) - .addStatement("return this.delegate") - .returns(SdkClient.class) - .build(); - type.addMethod(nameMethod()) - .addMethod(delegate); + .addMethod(delegateMethod()) + .addMethod(invokeMethod()); } private MethodSpec nameMethod() { @@ -107,6 +107,37 @@ private MethodSpec nameMethod() { .build(); } + private MethodSpec delegateMethod() { + return MethodSpec.methodBuilder("delegate") + .addModifiers(PUBLIC) + .addStatement("return this.delegate") + .returns(SdkClient.class) + .build(); + } + + private MethodSpec invokeMethod() { + TypeVariableName requestTypeVariableName = + TypeVariableName.get("T", poetExtensions.getModelClass(model.getSdkRequestBaseClassName())); + + TypeVariableName responseTypeVariableName = STREAMING_TYPE_VARIABLE; + + ParameterizedTypeName responseFutureTypeName = ParameterizedTypeName.get(ClassName.get(CompletableFuture.class), + responseTypeVariableName); + + ParameterizedTypeName functionTypeName = ParameterizedTypeName + .get(ClassName.get(Function.class), requestTypeVariableName, responseFutureTypeName); + + return MethodSpec.methodBuilder("invokeOperation") + .addModifiers(PROTECTED) + .addParameter(requestTypeVariableName, "request") + .addParameter(functionTypeName, "operation") + .addTypeVariable(requestTypeVariableName) + .addTypeVariable(responseTypeVariableName) + .returns(responseFutureTypeName) + .addStatement("return operation.apply(request)") + .build(); + } + @Override protected MethodSpec serviceClientConfigMethod() { return MethodSpec.methodBuilder("serviceClientConfiguration") @@ -139,9 +170,6 @@ protected List operations() { private Stream operations(OperationModel opModel) { List methods = new ArrayList<>(); methods.add(traditionalMethod(opModel)); - if (opModel.isPaginated()) { - methods.add(paginatedTraditionalMethod(opModel)); - } return methods.stream(); } @@ -164,21 +192,22 @@ protected MethodSpec.Builder operationBody(MethodSpec.Builder builder, Operation builder.addModifiers(PUBLIC) .addAnnotation(Override.class); - builder.addStatement("return delegate.$N($L)", + if (builder.parameters.isEmpty()) { + throw new IllegalStateException("All client methods must have an argument"); + } + + List parameters = new ArrayList<>(builder.parameters); + String requestParameter = parameters.remove(0).name; + String additionalParameters = String.format(", %s", parameters.stream().map(p -> p.name).collect(joining(", "))); + + builder.addStatement("return invokeOperation($N, request -> delegate.$N(request$N))", + requestParameter, opModel.getMethodName(), - builder.parameters.stream().map(p -> p.name).collect(joining(", "))); - return builder; - } + parameters.isEmpty() ? "" : additionalParameters); - @Override - protected MethodSpec.Builder paginatedMethodBody(MethodSpec.Builder builder, OperationModel opModel) { - String methodName = PaginatorUtils.getPaginatedMethodName(opModel.getMethodName()); - return builder.addModifiers(PUBLIC) - .addAnnotation(Override.class) - .addStatement("return delegate.$N($N)", methodName, opModel.getInput().getVariableName()); + return builder; } - @Override protected MethodSpec.Builder utilitiesOperationBody(MethodSpec.Builder builder) { return builder.addAnnotation(Override.class).addStatement("return delegate.$N()", UtilitiesMethod.METHOD_NAME); diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/DelegatingSyncClientClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/DelegatingSyncClientClass.java index 07278ab69287..e5c3bf7ac3e6 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/DelegatingSyncClientClass.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/DelegatingSyncClientClass.java @@ -16,16 +16,26 @@ package software.amazon.awssdk.codegen.poet.client; import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; import static javax.lang.model.element.Modifier.ABSTRACT; import static javax.lang.model.element.Modifier.FINAL; import static javax.lang.model.element.Modifier.PRIVATE; +import static javax.lang.model.element.Modifier.PROTECTED; import static javax.lang.model.element.Modifier.PUBLIC; +import static software.amazon.awssdk.codegen.poet.client.AsyncClientInterface.STREAMING_TYPE_VARIABLE; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeSpec; +import com.squareup.javapoet.TypeVariableName; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; +import java.util.function.Function; +import java.util.stream.Stream; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.codegen.docs.SimpleMethodOverload; import software.amazon.awssdk.codegen.model.config.customization.UtilitiesMethod; @@ -33,7 +43,6 @@ import software.amazon.awssdk.codegen.model.intermediate.OperationModel; import software.amazon.awssdk.codegen.poet.PoetExtension; import software.amazon.awssdk.codegen.poet.PoetUtils; -import software.amazon.awssdk.codegen.utils.PaginatorUtils; import software.amazon.awssdk.core.SdkClient; import software.amazon.awssdk.utils.Validate; @@ -89,14 +98,9 @@ protected void addConsumerMethod(List specs, MethodSpec spec, Simple @Override protected void addAdditionalMethods(TypeSpec.Builder type) { - MethodSpec delegate = MethodSpec.methodBuilder("delegate") - .addModifiers(PUBLIC) - .addStatement("return this.delegate") - .returns(SdkClient.class) - .build(); - type.addMethod(nameMethod()) - .addMethod(delegate); + .addMethod(delegateMethod()) + .addMethod(invokeMethod()); } @Override @@ -110,6 +114,23 @@ protected void addCloseMethod(TypeSpec.Builder type) { type.addMethod(method); } + @Override + protected List operations() { + return model.getOperations().values().stream() + // TODO Sync not supported for event streaming yet. Revisit after sync/async merge + .filter(o -> !o.hasEventStreamInput()) + .filter(o -> !o.hasEventStreamOutput()) + .flatMap(this::operations) + .sorted(Comparator.comparing(m -> m.name)) + .collect(toList()); + } + + private Stream operations(OperationModel opModel) { + List methods = new ArrayList<>(); + methods.add(traditionalMethod(opModel)); + return methods.stream(); + } + @Override public ClassName className() { return className; @@ -120,23 +141,30 @@ protected MethodSpec.Builder simpleMethodModifier(MethodSpec.Builder builder) { return builder.addAnnotation(Override.class); } + protected MethodSpec traditionalMethod(OperationModel opModel) { + MethodSpec.Builder builder = operationMethodSignature(model, opModel); + return operationBody(builder, opModel).build(); + } + @Override protected MethodSpec.Builder operationBody(MethodSpec.Builder builder, OperationModel opModel) { builder.addModifiers(PUBLIC) .addAnnotation(Override.class); - builder.addStatement("return delegate.$N($L)", + if (builder.parameters.isEmpty()) { + throw new IllegalStateException("All client methods must have an argument"); + } + + List operationParameters = new ArrayList<>(builder.parameters); + String requestParameter = operationParameters.remove(0).name; + String additionalParameters = String.format(", %s", operationParameters.stream().map(p -> p.name).collect(joining(", "))); + + builder.addStatement("return invokeOperation($N, request -> delegate.$N(request$N))", + requestParameter, opModel.getMethodName(), - builder.parameters.stream().map(p -> p.name).collect(joining(", "))); - return builder; - } + operationParameters.isEmpty() ? "" : additionalParameters); - @Override - protected MethodSpec.Builder paginatedMethodBody(MethodSpec.Builder builder, OperationModel opModel) { - String methodName = PaginatorUtils.getPaginatedMethodName(opModel.getMethodName()); - return builder.addModifiers(PUBLIC) - .addAnnotation(Override.class) - .addStatement("return delegate.$N($N)", methodName, opModel.getInput().getVariableName()); + return builder; } @Override @@ -167,6 +195,34 @@ private MethodSpec nameMethod() { .build(); } + private MethodSpec delegateMethod() { + return MethodSpec.methodBuilder("delegate") + .addModifiers(PUBLIC) + .addStatement("return this.delegate") + .returns(SdkClient.class) + .build(); + } + + private MethodSpec invokeMethod() { + TypeVariableName requestTypeVariableName = + TypeVariableName.get("T", poetExtensions.getModelClass(model.getSdkRequestBaseClassName())); + + TypeVariableName responseTypeVariableName = STREAMING_TYPE_VARIABLE; + + ParameterizedTypeName functionTypeName = ParameterizedTypeName + .get(ClassName.get(Function.class), requestTypeVariableName, responseTypeVariableName); + + return MethodSpec.methodBuilder("invokeOperation") + .addModifiers(PROTECTED) + .addParameter(requestTypeVariableName, "request") + .addParameter(functionTypeName, "operation") + .addTypeVariable(requestTypeVariableName) + .addTypeVariable(responseTypeVariableName) + .returns(responseTypeVariableName) + .addStatement("return operation.apply(request)") + .build(); + } + @Override protected MethodSpec serviceClientConfigMethod() { return MethodSpec.methodBuilder("serviceClientConfiguration") diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientClass.java index d664792b865f..310170284bfe 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientClass.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientClass.java @@ -21,7 +21,6 @@ import static javax.lang.model.element.Modifier.PUBLIC; import static javax.lang.model.element.Modifier.STATIC; import static software.amazon.awssdk.codegen.poet.client.ClientClassUtils.addS3ArnableFieldCode; -import static software.amazon.awssdk.codegen.poet.client.ClientClassUtils.applyPaginatorUserAgentMethod; import static software.amazon.awssdk.codegen.poet.client.ClientClassUtils.applySignerOverrideMethod; import com.squareup.javapoet.ClassName; @@ -34,10 +33,10 @@ import java.util.Collections; import java.util.List; import java.util.stream.Collectors; +import java.util.stream.Stream; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; import software.amazon.awssdk.awscore.client.config.AwsClientOption; -import software.amazon.awssdk.codegen.docs.SimpleMethodOverload; import software.amazon.awssdk.codegen.emitters.GeneratorTaskParams; import software.amazon.awssdk.codegen.model.config.customization.UtilitiesMethod; import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; @@ -50,7 +49,6 @@ import software.amazon.awssdk.codegen.poet.client.specs.ProtocolSpec; import software.amazon.awssdk.codegen.poet.client.specs.QueryProtocolSpec; import software.amazon.awssdk.codegen.poet.client.specs.XmlProtocolSpec; -import software.amazon.awssdk.codegen.utils.PaginatorUtils; import software.amazon.awssdk.core.RequestOverrideConfiguration; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; @@ -114,10 +112,6 @@ protected void addFields(TypeSpec.Builder type) { @Override protected void addAdditionalMethods(TypeSpec.Builder type) { - if (model.hasPaginators()) { - type.addMethod(applyPaginatorUserAgentMethod(poetExtensions, model)); - } - if (model.containsRequestSigners()) { type.addMethod(applySignerOverrideMethod(poetExtensions, model)); } @@ -210,14 +204,17 @@ protected List operations() { return model.getOperations().values().stream() .filter(o -> !o.hasEventStreamInput()) .filter(o -> !o.hasEventStreamOutput()) - .map(this::operationMethodSpecs) - .flatMap(List::stream) + .flatMap(this::operations) .collect(Collectors.toList()); } - private List operationMethodSpecs(OperationModel opModel) { + private Stream operations(OperationModel opModel) { List methods = new ArrayList<>(); + methods.add(traditionalMethod(opModel)); + return methods.stream(); + } + private MethodSpec traditionalMethod(OperationModel opModel) { MethodSpec.Builder method = SyncClientInterface.operationMethodSignature(model, opModel) .addAnnotation(Override.class) .addCode(ClientClassUtils.callApplySignerOverrideMethod(opModel)) @@ -297,34 +294,7 @@ private List operationMethodSpecs(OperationModel opModel) { .addStatement("metricPublishers.forEach(p -> p.publish(apiCallMetricCollector.collect()))") .endControlFlow(); - methods.add(method.build()); - - methods.addAll(paginatedMethods(opModel)); - - return methods; - } - - @Override - protected List paginatedMethods(OperationModel opModel) { - List paginatedMethodSpecs = new ArrayList<>(); - - if (opModel.isPaginated()) { - paginatedMethodSpecs.add(SyncClientInterface.operationMethodSignature(model, - opModel, - SimpleMethodOverload.PAGINATED, - PaginatorUtils.getPaginatedMethodName( - opModel.getMethodName())) - .addAnnotation(Override.class) - .returns(poetExtensions.getResponseClassForPaginatedSyncOperation( - opModel.getOperationName())) - .addStatement("return new $T(this, applyPaginatorUserAgent($L))", - poetExtensions.getResponseClassForPaginatedSyncOperation( - opModel.getOperationName()), - opModel.getInput().getVariableName()) - .build()); - } - - return paginatedMethodSpecs; + return method.build(); } @Override diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientInterface.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientInterface.java index 8c64f213eae0..5037d5eda1f2 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientInterface.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientInterface.java @@ -39,6 +39,7 @@ import java.util.Collections; import java.util.List; import java.util.function.Consumer; +import java.util.stream.Stream; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; @@ -176,16 +177,6 @@ private MethodSpec builder() { .build(); } - protected Iterable operations() { - return model.getOperations().values().stream() - // TODO Sync not supported for event streaming yet. Revisit after sync/async merge - .filter(o -> !o.hasEventStreamInput()) - .filter(o -> !o.hasEventStreamOutput()) - .map(this::operationMethodSpec) - .flatMap(List::stream) - .collect(toList()); - } - private MethodSpec serviceMetadata() { return MethodSpec.methodBuilder("serviceMetadata") .returns(ServiceMetadata.class) @@ -194,31 +185,36 @@ private MethodSpec serviceMetadata() { .build(); } - private List operationMethodSpec(OperationModel opModel) { - List methods = new ArrayList<>(); - - if (opModel.getInputShape().isSimpleMethod()) { - methods.add(simpleMethod(opModel)); - } - - methods.addAll(operation(opModel)); + protected Iterable operations() { + return model.getOperations().values().stream() + // TODO Sync not supported for event streaming yet. Revisit after sync/async merge + .filter(o -> !o.hasEventStreamInput()) + .filter(o -> !o.hasEventStreamOutput()) + .flatMap(this::operationsWithVariants) + .collect(toList()); + } - methods.addAll(streamingSimpleMethods(opModel)); + private Stream operationsWithVariants(OperationModel opModel) { + List methods = new ArrayList<>(); + methods.addAll(traditionalMethodWithConsumerVariant(opModel)); + methods.addAll(overloadedMethods(opModel)); methods.addAll(paginatedMethods(opModel)); return methods.stream() // Add Deprecated annotation if needed to all overloads - .map(m -> DeprecationUtils.checkDeprecated(opModel, m)) - .collect(toList()); + .map(m -> DeprecationUtils.checkDeprecated(opModel, m)); } - private MethodSpec simpleMethod(OperationModel opModel) { - ClassName requestType = ClassName.get(model.getMetadata().getFullModelPackageName(), - opModel.getInput().getVariableType()); - return operationSimpleMethodSignature(model, opModel, opModel.getMethodName()) - .addStatement("return $L($T.builder().build())", opModel.getMethodName(), requestType) - .addJavadoc(opModel.getDocs(model, ClientType.SYNC, SimpleMethodOverload.NO_ARG)) - .build(); + private List traditionalMethodWithConsumerVariant(OperationModel opModel) { + List methods = new ArrayList<>(); + + MethodSpec.Builder builder = operationMethodSignature(model, opModel); + MethodSpec method = operationBody(builder, opModel).build(); + methods.add(method); + + addConsumerMethod(methods, method, SimpleMethodOverload.NORMAL, opModel); + + return methods; } private static MethodSpec.Builder operationBaseSignature(IntermediateModel model, @@ -244,18 +240,6 @@ private static MethodSpec.Builder operationBaseSignature(IntermediateModel model return methodBuilder; } - private List operation(OperationModel opModel) { - List methods = new ArrayList<>(); - - MethodSpec.Builder builder = operationMethodSignature(model, opModel); - MethodSpec method = operationBody(builder, opModel).build(); - methods.add(method); - - addConsumerMethod(methods, method, SimpleMethodOverload.NORMAL, opModel); - - return methods; - } - protected MethodSpec.Builder operationBody(MethodSpec.Builder builder, OperationModel opModel) { return builder.addModifiers(DEFAULT) .addStatement("throw new $T()", UnsupportedOperationException.class); @@ -328,8 +312,10 @@ private MethodSpec paginatedSimpleMethod(OperationModel opModel) { } protected MethodSpec.Builder paginatedMethodBody(MethodSpec.Builder builder, OperationModel operationModel) { - return builder.addModifiers(DEFAULT) - .addStatement("throw new $T()", UnsupportedOperationException.class); + return builder.addModifiers(DEFAULT, PUBLIC) + .addStatement("return new $T(this, $L)", + poetExtensions.getResponseClassForPaginatedSyncOperation(operationModel.getOperationName()), + operationModel.getInput().getVariableName()); } private static void streamingMethod(MethodSpec.Builder methodBuilder, OperationModel opModel, TypeName responseType) { @@ -344,14 +330,20 @@ private static void streamingMethod(MethodSpec.Builder methodBuilder, OperationM } } - private List streamingSimpleMethods(OperationModel opModel) { + /** + * @param opModel Operation to generate simple methods for. + * @return All simple method overloads for a given operation. + */ + private List overloadedMethods(OperationModel opModel) { TypeName responseType = ClassName.get(model.getMetadata().getFullModelPackageName(), opModel.getReturnType().getReturnType()); ClassName requestType = ClassName.get(model.getMetadata().getFullModelPackageName(), opModel.getInput().getVariableType()); List simpleMethods = new ArrayList<>(); - + if (opModel.getInputShape().isSimpleMethod()) { + simpleMethods.add(simpleMethodWithNoArgs(opModel)); + } if (opModel.hasStreamingInput() && opModel.hasStreamingOutput()) { MethodSpec simpleMethod = streamingInputOutputFileSimpleMethod(opModel, responseType, requestType); simpleMethods.add(simpleMethod); @@ -378,6 +370,15 @@ private List streamingSimpleMethods(OperationModel opModel) { return simpleMethods; } + private MethodSpec simpleMethodWithNoArgs(OperationModel opModel) { + ClassName requestType = ClassName.get(model.getMetadata().getFullModelPackageName(), + opModel.getInput().getVariableType()); + return operationSimpleMethodSignature(model, opModel, opModel.getMethodName()) + .addStatement("return $L($T.builder().build())", opModel.getMethodName(), requestType) + .addJavadoc(opModel.getDocs(model, ClientType.SYNC, SimpleMethodOverload.NO_ARG)) + .build(); + } + protected void addConsumerMethod(List specs, MethodSpec spec, SimpleMethodOverload overload, OperationModel opModel) { String fileConsumerBuilderJavadoc = consumerBuilderJavadoc(opModel, overload); diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/common/UserAgentUtilsSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/common/UserAgentUtilsSpec.java new file mode 100644 index 000000000000..006c6592d09a --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/common/UserAgentUtilsSpec.java @@ -0,0 +1,118 @@ +/* + * 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.poet.common; + +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeSpec; +import com.squareup.javapoet.TypeVariableName; +import java.util.function.Consumer; +import javax.lang.model.element.Modifier; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; +import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; +import software.amazon.awssdk.codegen.poet.ClassSpec; +import software.amazon.awssdk.codegen.poet.PoetExtension; +import software.amazon.awssdk.codegen.poet.PoetUtils; +import software.amazon.awssdk.core.ApiName; +import software.amazon.awssdk.core.util.VersionInfo; + +public class UserAgentUtilsSpec implements ClassSpec { + + private static final String PAGINATOR_USER_AGENT = "PAGINATED"; + + protected final IntermediateModel model; + protected final PoetExtension poetExtensions; + + public UserAgentUtilsSpec(IntermediateModel model) { + this.model = model; + this.poetExtensions = new PoetExtension(model); + } + + @Override + public TypeSpec poetSpec() { + return TypeSpec.classBuilder(className()) + .addModifiers(Modifier.PUBLIC) + .addAnnotation(PoetUtils.generatedAnnotation()) + .addAnnotation(SdkInternalApi.class) + .addMethod(privateConstructor()) + .addMethod(applyUserAgentInfoMethod()) + .addMethod(applyPaginatorUserAgentMethod()) + .build(); + } + + @Override + public ClassName className() { + return poetExtensions.getUserAgentClass(); + } + + protected MethodSpec privateConstructor() { + return MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .build(); + } + + private MethodSpec applyUserAgentInfoMethod() { + + TypeVariableName typeVariableName = + TypeVariableName.get("T", poetExtensions.getModelClass(model.getSdkRequestBaseClassName())); + + ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName + .get(ClassName.get(Consumer.class), ClassName.get(AwsRequestOverrideConfiguration.Builder.class)); + + CodeBlock codeBlock = CodeBlock.builder() + .addStatement("$T overrideConfiguration =\n" + + " request.overrideConfiguration().map(c -> c.toBuilder()" + + ".applyMutation" + + "(userAgentApplier).build())\n" + + " .orElse((AwsRequestOverrideConfiguration.builder()" + + ".applyMutation" + + "(userAgentApplier).build()))", AwsRequestOverrideConfiguration.class) + .addStatement("return (T) request.toBuilder().overrideConfiguration" + + "(overrideConfiguration).build()") + .build(); + + return MethodSpec.methodBuilder("applyUserAgentInfo") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(typeVariableName, "request") + .addParameter(parameterizedTypeName, "userAgentApplier") + .addTypeVariable(typeVariableName) + .addCode(codeBlock) + .returns(typeVariableName) + .build(); + } + + private MethodSpec applyPaginatorUserAgentMethod() { + TypeVariableName typeVariableName = + TypeVariableName.get("T", poetExtensions.getModelClass(model.getSdkRequestBaseClassName())); + + return MethodSpec.methodBuilder("applyPaginatorUserAgent") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(typeVariableName, "request") + .addTypeVariable(typeVariableName) + .addStatement("return applyUserAgentInfo(request, b -> b.addApiName($T.builder()" + + ".version($T.SDK_VERSION)" + + ".name($S)" + + ".build()))", + ApiName.class, + VersionInfo.class, + PAGINATOR_USER_AGENT) + .returns(typeVariableName) + .build(); + } +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/model/ServiceClientConfigurationClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/model/ServiceClientConfigurationClass.java index 1d3d258ef01b..8d872211ea85 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/model/ServiceClientConfigurationClass.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/model/ServiceClientConfigurationClass.java @@ -31,6 +31,7 @@ import software.amazon.awssdk.codegen.poet.ClassSpec; import software.amazon.awssdk.codegen.poet.PoetUtils; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.endpoints.EndpointProvider; import software.amazon.awssdk.regions.Region; public class ServiceClientConfigurationClass implements ClassSpec { @@ -110,6 +111,13 @@ private TypeSpec builderInterfaceSpec() { .returns(className().nestedClass("Builder")) .addJavadoc("Configure the client override configuration") .build()) + .addMethod(MethodSpec.methodBuilder("endpointProvider") + .addAnnotation(Override.class) + .addModifiers(PUBLIC, ABSTRACT) + .addParameter(EndpointProvider.class, "endpointProvider") + .returns(className().nestedClass("Builder")) + .addJavadoc("Configure the endpointProvider") + .build()) .build(); } @@ -150,6 +158,14 @@ private TypeSpec builderImplSpec() { .addStatement("this.endpointOverride = endpointOverride") .addStatement("return this") .build()) + .addMethod(MethodSpec.methodBuilder("endpointProvider") + .addAnnotation(Override.class) + .addModifiers(PUBLIC) + .addParameter(EndpointProvider.class, "endpointProvider") + .returns(className().nestedClass("Builder")) + .addStatement("this.endpointProvider = endpointProvider") + .addStatement("return this") + .build()) .addMethod(MethodSpec.methodBuilder("build") .addAnnotation(Override.class) .addModifiers(PUBLIC) diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/AsyncResponseClassSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/AsyncResponseClassSpec.java index b6396e5581a6..9ef71caa5597 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/AsyncResponseClassSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/AsyncResponseClassSpec.java @@ -128,7 +128,10 @@ protected MethodSpec privateConstructor() { .addParameter(requestType(), REQUEST_MEMBER) .addParameter(boolean.class, LAST_PAGE_FIELD) .addStatement("this.$L = $L", CLIENT_MEMBER, CLIENT_MEMBER) - .addStatement("this.$L = $L", REQUEST_MEMBER, REQUEST_MEMBER) + .addStatement("this.$L = $T.applyPaginatorUserAgent($L)", + REQUEST_MEMBER, + poetExtensions.getUserAgentClass(), + REQUEST_MEMBER) .addStatement("this.$L = $L", LAST_PAGE_FIELD, LAST_PAGE_FIELD) .addStatement("this.$L = new $L()", NEXT_PAGE_FETCHER_MEMBER, nextPageFetcherClassName()) .build(); @@ -246,4 +249,5 @@ protected TypeSpec.Builder nextPageFetcherClass() { .addCode(nextPageMethodBody()) .build()); } + } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/SyncResponseClassSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/SyncResponseClassSpec.java index 5718ed479c8a..847eb6e80345 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/SyncResponseClassSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/paginators/SyncResponseClassSpec.java @@ -104,7 +104,10 @@ protected MethodSpec constructor() { .addParameter(getClientInterfaceName(), CLIENT_MEMBER) .addParameter(requestType(), REQUEST_MEMBER) .addStatement("this.$L = $L", CLIENT_MEMBER, CLIENT_MEMBER) - .addStatement("this.$L = $L", REQUEST_MEMBER, REQUEST_MEMBER) + .addStatement("this.$L = $T.applyPaginatorUserAgent($L)", + REQUEST_MEMBER, + poetExtensions.getUserAgentClass(), + REQUEST_MEMBER) .addStatement("this.$L = new $L()", NEXT_PAGE_FETCHER_MEMBER, nextPageFetcherClassName()) .build(); } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/ClientContextParamsClassSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/ClientContextParamsClassSpec.java index 46bad58f2542..f4923eb6b878 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/ClientContextParamsClassSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/ClientContextParamsClassSpec.java @@ -54,6 +54,12 @@ public TypeSpec poetSpec() { b.addField(paramDeclaration(n, m)); }); + if (model.getCustomizationConfig() != null && model.getCustomizationConfig().getCustomClientContextParams() != null) { + model.getCustomizationConfig().getCustomClientContextParams().forEach((n, m) -> { + b.addField(paramDeclaration(n, m)); + }); + } + return b.build(); } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointParametersClassSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointParametersClassSpec.java index 22d25f69ba6d..a330213aa873 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointParametersClassSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/rules/EndpointParametersClassSpec.java @@ -23,6 +23,8 @@ import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import java.util.Map; import javax.lang.model.element.Modifier; @@ -33,6 +35,8 @@ import software.amazon.awssdk.codegen.poet.ClassSpec; import software.amazon.awssdk.codegen.poet.PoetUtils; import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.utils.builder.CopyableBuilder; +import software.amazon.awssdk.utils.builder.ToCopyableBuilder; public class EndpointParametersClassSpec implements ClassSpec { private final IntermediateModel intermediateModel; @@ -53,6 +57,7 @@ public TypeSpec poetSpec() { .addType(builderInterfaceSpec()) .addType(builderImplSpec()) .addAnnotation(SdkPublicApi.class) + .addSuperinterface(toCopyableBuilderInterface()) .addModifiers(Modifier.PUBLIC, Modifier.FINAL); parameters().forEach((name, model) -> { @@ -60,6 +65,8 @@ public TypeSpec poetSpec() { b.addMethod(accessorMethod(name, model)); }); + b.addMethod(toBuilderMethod()); + return b.build(); } @@ -70,6 +77,7 @@ public ClassName className() { private TypeSpec builderInterfaceSpec() { TypeSpec.Builder b = TypeSpec.interfaceBuilder(builderInterfaceName()) + .addSuperinterface(copyableBuilderExtendsInterface()) .addModifiers(Modifier.PUBLIC); parameters().forEach((name, model) -> { @@ -89,6 +97,11 @@ private TypeSpec builderImplSpec() { .addModifiers(Modifier.PRIVATE, Modifier.STATIC) .addSuperinterface(builderInterfaceName()); + b.addMethod(MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .build()); + b.addMethod(toBuilderConstructor().build()); + parameters().forEach((name, model) -> { b.addField(fieldSpec(name, model).toBuilder().initializer(defaultValueCode(model)).build()); b.addMethod(builderSetterMethod(name, model)); @@ -183,6 +196,14 @@ private MethodSpec builderMethod() { .build(); } + private MethodSpec toBuilderMethod() { + return MethodSpec.methodBuilder("toBuilder") + .addModifiers(Modifier.PUBLIC) + .returns(builderInterfaceName()) + .addStatement("return new $T(this)", builderClassName()) + .build(); + } + private String variableName(String name) { return intermediateModel.getNamingStrategy().getVariableName(name); } @@ -224,4 +245,25 @@ private MethodSpec.Builder paramMethodBuilder(String name, ParameterModel model) } return b; } + + private MethodSpec.Builder toBuilderConstructor() { + MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder(); + constructorBuilder.addModifiers(Modifier.PRIVATE); + constructorBuilder.addParameter(className(), "builder"); + parameters().forEach((name, model) -> { + constructorBuilder.addStatement("this.$1N = builder.$1N", variableName(name)); + }); + return constructorBuilder; + } + + private TypeName toCopyableBuilderInterface() { + return ParameterizedTypeName.get(ClassName.get(ToCopyableBuilder.class), + className().nestedClass(builderInterfaceName().simpleName()), + className()); + } + + private TypeName copyableBuilderExtendsInterface() { + return ParameterizedTypeName.get(ClassName.get(CopyableBuilder.class), + builderInterfaceName(), className()); + } } diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java index b7b687f8c357..69ab37c3315d 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java @@ -146,6 +146,19 @@ public static IntermediateModel customContentTypeModels() { return new IntermediateModelBuilder(models).build(); } + public static IntermediateModel composedClientJsonServiceModels() { + File serviceModel = new File(ClientTestModels.class.getResource("client/c2j/rest-json/service-2.json").getFile()); + File customizationModel = + new File(ClientTestModels.class.getResource("client/c2j/composedclient/customization.config").getFile()); + CustomizationConfig customizationConfig = getCustomizationConfig(customizationModel); + C2jModels models = C2jModels.builder() + .serviceModel(getServiceModel(serviceModel)) + .customizationConfig(customizationConfig) + .build(); + + return new IntermediateModelBuilder(models).build(); + } + public static IntermediateModel internalConfigModels() { File serviceModel = new File(ClientTestModels.class.getResource("client/c2j/internalconfig/service-2.json").getFile()); File customizationModel = new File(ClientTestModels.class.getResource("client/c2j/internalconfig/customization.config").getFile()); diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BuilderClassTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BuilderClassTest.java index b111e47bf3c0..a1fcfa98150e 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BuilderClassTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BuilderClassTest.java @@ -68,6 +68,22 @@ public void syncClientBuilderClass() throws Exception { validateGeneration(SyncClientBuilderClass::new, "test-sync-client-builder-class.java"); } + @Test + public void syncComposedClientBuilderClass() throws Exception { + validateComposedClientGeneration(SyncClientBuilderClass::new, "test-composed-sync-client-builder-class.java"); + } + + @Test + public void syncComposedDefaultClientBuilderClass() throws Exception { + validateComposedClientGeneration(BaseClientBuilderClass::new, "test-composed-sync-default-client-builder.java"); + } + + @Test + public void syncHasCrossRegionAccessEnabledPropertyBuilderClass() throws Exception { + validateComposedClientGeneration(BaseClientBuilderInterface::new, "test-customcontextparams-sync-client-builder-class.java"); + } + + @Test public void asyncClientBuilderInterface() throws Exception { validateGeneration(AsyncClientBuilderInterface::new, "test-async-client-builder-interface.java"); @@ -78,10 +94,20 @@ public void asyncClientBuilderClass() throws Exception { validateGeneration(AsyncClientBuilderClass::new, "test-async-client-builder-class.java"); } + @Test + public void asyncComposedClientBuilderClass() throws Exception { + validateComposedClientGeneration(AsyncClientBuilderClass::new, "test-composed-async-client-builder-class.java"); + } + private void validateGeneration(Function generatorConstructor, String expectedClassName) { assertThat(generatorConstructor.apply(ClientTestModels.restJsonServiceModels()), generatesTo(expectedClassName)); } + private void validateComposedClientGeneration(Function generatorConstructor, + String expectedClassName) { + assertThat(generatorConstructor.apply(ClientTestModels.composedClientJsonServiceModels()), generatesTo(expectedClassName)); + } + private void validateBearerAuthGeneration(Function generatorConstructor, String expectedClassName) { assertThat(generatorConstructor.apply(ClientTestModels.bearerAuthServiceModels()), generatesTo(expectedClassName)); diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/poet/common/UserAgentClassSpecTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/common/UserAgentClassSpecTest.java new file mode 100644 index 000000000000..d2f00f7f76de --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/poet/common/UserAgentClassSpecTest.java @@ -0,0 +1,34 @@ +/* + * 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.poet.common; + +import static org.hamcrest.MatcherAssert.assertThat; +import static software.amazon.awssdk.codegen.poet.PoetMatchers.generatesTo; + +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.codegen.poet.ClassSpec; +import software.amazon.awssdk.codegen.poet.ClientTestModels; +import software.amazon.awssdk.codegen.poet.common.UserAgentUtilsSpec; + +public class UserAgentClassSpecTest { + + @Test + void testUserAgentClass() { + ClassSpec useragentspec = new UserAgentUtilsSpec(ClientTestModels.restJsonServiceModels()); + assertThat(useragentspec, generatesTo("test-user-agent-class.java")); + } + +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-async-client-builder-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-async-client-builder-class.java index 61101024dda9..9308dd1d2969 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-async-client-builder-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-async-client-builder-class.java @@ -7,6 +7,7 @@ import software.amazon.awssdk.awscore.client.config.AwsClientOption; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.endpoints.EndpointProvider; import software.amazon.awssdk.services.json.endpoints.JsonEndpointProvider; /** @@ -32,14 +33,20 @@ public DefaultJsonAsyncClientBuilder tokenProvider(SdkTokenProvider tokenProvide protected final JsonAsyncClient buildClient() { SdkClientConfiguration clientConfiguration = super.asyncClientConfiguration(); this.validateClientOptions(clientConfiguration); + JsonServiceClientConfiguration serviceClientConfiguration = initializeServiceClientConfig(clientConfiguration); + JsonAsyncClient client = new DefaultJsonAsyncClient(serviceClientConfiguration, clientConfiguration); + return client; + } + + private JsonServiceClientConfiguration initializeServiceClientConfig(SdkClientConfiguration clientConfig) { URI endpointOverride = null; - if (clientConfiguration.option(SdkClientOption.ENDPOINT_OVERRIDDEN) != null - && Boolean.TRUE.equals(clientConfiguration.option(SdkClientOption.ENDPOINT_OVERRIDDEN))) { - endpointOverride = clientConfiguration.option(SdkClientOption.ENDPOINT); + EndpointProvider endpointProvider = clientConfig.option(SdkClientOption.ENDPOINT_PROVIDER); + if (clientConfig.option(SdkClientOption.ENDPOINT_OVERRIDDEN) != null + && Boolean.TRUE.equals(clientConfig.option(SdkClientOption.ENDPOINT_OVERRIDDEN))) { + endpointOverride = clientConfig.option(SdkClientOption.ENDPOINT); } - JsonServiceClientConfiguration serviceClientConfiguration = JsonServiceClientConfiguration.builder() - .overrideConfiguration(overrideConfiguration()).region(clientConfiguration.option(AwsClientOption.AWS_REGION)) - .endpointOverride(endpointOverride).build(); - return new DefaultJsonAsyncClient(serviceClientConfiguration, clientConfiguration); + return JsonServiceClientConfiguration.builder().overrideConfiguration(overrideConfiguration()) + .region(clientConfig.option(AwsClientOption.AWS_REGION)).endpointOverride(endpointOverride) + .endpointProvider(endpointProvider).build(); } } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-composed-async-client-builder-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-composed-async-client-builder-class.java new file mode 100644 index 000000000000..a95cb03bd177 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-composed-async-client-builder-class.java @@ -0,0 +1,53 @@ +package software.amazon.awssdk.services.json; + +import java.net.URI; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider; +import software.amazon.awssdk.awscore.client.config.AwsClientOption; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.endpoints.EndpointProvider; +import software.amazon.awssdk.services.builder.AsyncClientDecorator; +import software.amazon.awssdk.services.json.endpoints.JsonEndpointProvider; + +/** + * Internal implementation of {@link JsonAsyncClientBuilder}. + */ +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +final class DefaultJsonAsyncClientBuilder extends DefaultJsonBaseClientBuilder implements + JsonAsyncClientBuilder { + @Override + public DefaultJsonAsyncClientBuilder endpointProvider(JsonEndpointProvider endpointProvider) { + clientConfiguration.option(SdkClientOption.ENDPOINT_PROVIDER, endpointProvider); + return this; + } + + @Override + public DefaultJsonAsyncClientBuilder tokenProvider(SdkTokenProvider tokenProvider) { + clientConfiguration.option(AwsClientOption.TOKEN_PROVIDER, tokenProvider); + return this; + } + + @Override + protected final JsonAsyncClient buildClient() { + SdkClientConfiguration clientConfiguration = super.asyncClientConfiguration(); + this.validateClientOptions(clientConfiguration); + JsonServiceClientConfiguration serviceClientConfiguration = initializeServiceClientConfig(clientConfiguration); + JsonAsyncClient client = new DefaultJsonAsyncClient(serviceClientConfiguration, clientConfiguration); + return new AsyncClientDecorator().decorate(client, clientConfiguration, clientContextParams.copy().build()); + } + + private JsonServiceClientConfiguration initializeServiceClientConfig(SdkClientConfiguration clientConfig) { + URI endpointOverride = null; + EndpointProvider endpointProvider = clientConfig.option(SdkClientOption.ENDPOINT_PROVIDER); + if (clientConfig.option(SdkClientOption.ENDPOINT_OVERRIDDEN) != null + && Boolean.TRUE.equals(clientConfig.option(SdkClientOption.ENDPOINT_OVERRIDDEN))) { + endpointOverride = clientConfig.option(SdkClientOption.ENDPOINT); + } + return JsonServiceClientConfiguration.builder().overrideConfiguration(overrideConfiguration()) + .region(clientConfig.option(AwsClientOption.AWS_REGION)).endpointOverride(endpointOverride) + .endpointProvider(endpointProvider).build(); + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-composed-sync-client-builder-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-composed-sync-client-builder-class.java new file mode 100644 index 000000000000..47887e3135c2 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-composed-sync-client-builder-class.java @@ -0,0 +1,53 @@ +package software.amazon.awssdk.services.json; + +import java.net.URI; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider; +import software.amazon.awssdk.awscore.client.config.AwsClientOption; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.endpoints.EndpointProvider; +import software.amazon.awssdk.services.builder.SyncClientDecorator; +import software.amazon.awssdk.services.json.endpoints.JsonEndpointProvider; + +/** + * Internal implementation of {@link JsonClientBuilder}. + */ +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +final class DefaultJsonClientBuilder extends DefaultJsonBaseClientBuilder implements + JsonClientBuilder { + @Override + public DefaultJsonClientBuilder endpointProvider(JsonEndpointProvider endpointProvider) { + clientConfiguration.option(SdkClientOption.ENDPOINT_PROVIDER, endpointProvider); + return this; + } + + @Override + public DefaultJsonClientBuilder tokenProvider(SdkTokenProvider tokenProvider) { + clientConfiguration.option(AwsClientOption.TOKEN_PROVIDER, tokenProvider); + return this; + } + + @Override + protected final JsonClient buildClient() { + SdkClientConfiguration clientConfiguration = super.syncClientConfiguration(); + this.validateClientOptions(clientConfiguration); + JsonServiceClientConfiguration serviceClientConfiguration = initializeServiceClientConfig(clientConfiguration); + JsonClient client = new DefaultJsonClient(serviceClientConfiguration, clientConfiguration); + return new SyncClientDecorator().decorate(client, clientConfiguration, clientContextParams.copy().build()); + } + + private JsonServiceClientConfiguration initializeServiceClientConfig(SdkClientConfiguration clientConfig) { + URI endpointOverride = null; + EndpointProvider endpointProvider = clientConfig.option(SdkClientOption.ENDPOINT_PROVIDER); + if (clientConfig.option(SdkClientOption.ENDPOINT_OVERRIDDEN) != null + && Boolean.TRUE.equals(clientConfig.option(SdkClientOption.ENDPOINT_OVERRIDDEN))) { + endpointOverride = clientConfig.option(SdkClientOption.ENDPOINT); + } + return JsonServiceClientConfiguration.builder().overrideConfiguration(overrideConfiguration()) + .region(clientConfig.option(AwsClientOption.AWS_REGION)).endpointOverride(endpointOverride) + .endpointProvider(endpointProvider).build(); + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-composed-sync-default-client-builder.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-composed-sync-default-client-builder.java new file mode 100644 index 000000000000..b352d7b685dd --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-composed-sync-default-client-builder.java @@ -0,0 +1,120 @@ +package software.amazon.awssdk.services.json; + +import java.util.ArrayList; +import java.util.List; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.auth.signer.Aws4Signer; +import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider; +import software.amazon.awssdk.auth.token.credentials.aws.DefaultAwsTokenProvider; +import software.amazon.awssdk.auth.token.signer.aws.BearerTokenSigner; +import software.amazon.awssdk.awscore.client.builder.AwsDefaultClientBuilder; +import software.amazon.awssdk.awscore.client.config.AwsClientOption; +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.ClasspathInterceptorChainFactory; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.signer.Signer; +import software.amazon.awssdk.services.json.endpoints.JsonClientContextParams; +import software.amazon.awssdk.services.json.endpoints.JsonEndpointProvider; +import software.amazon.awssdk.services.json.endpoints.internal.JsonEndpointAuthSchemeInterceptor; +import software.amazon.awssdk.services.json.endpoints.internal.JsonRequestSetEndpointInterceptor; +import software.amazon.awssdk.services.json.endpoints.internal.JsonResolveEndpointInterceptor; +import software.amazon.awssdk.utils.CollectionUtils; +import software.amazon.awssdk.utils.Validate; + +/** + * Internal base class for {@link DefaultJsonClientBuilder} and {@link DefaultJsonAsyncClientBuilder}. + */ +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +abstract class DefaultJsonBaseClientBuilder, C> extends AwsDefaultClientBuilder { + @Override + protected final String serviceEndpointPrefix() { + return "json-service-endpoint"; + } + + @Override + protected final String serviceName() { + return "Json"; + } + + @Override + protected final SdkClientConfiguration mergeServiceDefaults(SdkClientConfiguration config) { + return config.merge(c -> c.option(SdkClientOption.ENDPOINT_PROVIDER, defaultEndpointProvider()) + .option(SdkAdvancedClientOption.SIGNER, defaultSigner()) + .option(SdkClientOption.CRC32_FROM_COMPRESSED_DATA_ENABLED, false) + .option(SdkClientOption.SERVICE_CONFIGURATION, ServiceConfiguration.builder().build()) + .option(AwsClientOption.TOKEN_PROVIDER, defaultTokenProvider()) + .option(SdkAdvancedClientOption.TOKEN_SIGNER, defaultTokenSigner())); + } + + @Override + protected final SdkClientConfiguration finalizeServiceConfiguration(SdkClientConfiguration config) { + List endpointInterceptors = new ArrayList<>(); + endpointInterceptors.add(new JsonResolveEndpointInterceptor()); + endpointInterceptors.add(new JsonEndpointAuthSchemeInterceptor()); + endpointInterceptors.add(new JsonRequestSetEndpointInterceptor()); + ClasspathInterceptorChainFactory interceptorFactory = new ClasspathInterceptorChainFactory(); + List interceptors = interceptorFactory + .getInterceptors("software/amazon/awssdk/services/json/execution.interceptors"); + List additionalInterceptors = new ArrayList<>(); + interceptors = CollectionUtils.mergeLists(endpointInterceptors, interceptors); + interceptors = CollectionUtils.mergeLists(interceptors, additionalInterceptors); + interceptors = CollectionUtils.mergeLists(interceptors, config.option(SdkClientOption.EXECUTION_INTERCEPTORS)); + ServiceConfiguration.Builder serviceConfigBuilder = ((ServiceConfiguration) config + .option(SdkClientOption.SERVICE_CONFIGURATION)).toBuilder(); + serviceConfigBuilder.profileFile(serviceConfigBuilder.profileFileSupplier() != null ? serviceConfigBuilder + .profileFileSupplier() : config.option(SdkClientOption.PROFILE_FILE_SUPPLIER)); + serviceConfigBuilder.profileName(serviceConfigBuilder.profileName() != null ? serviceConfigBuilder.profileName() : config + .option(SdkClientOption.PROFILE_NAME)); + ServiceConfiguration finalServiceConfig = serviceConfigBuilder.build(); + return config.toBuilder().option(SdkClientOption.EXECUTION_INTERCEPTORS, interceptors) + .option(SdkClientOption.SERVICE_CONFIGURATION, finalServiceConfig).build(); + } + + private Signer defaultSigner() { + return Aws4Signer.create(); + } + + @Override + protected final String signingName() { + return "json-service"; + } + + private JsonEndpointProvider defaultEndpointProvider() { + return JsonEndpointProvider.defaultProvider(); + } + + public B customParameter(Boolean customParameter) { + clientContextParams.put(JsonClientContextParams.CUSTOM_PARAMETER, customParameter); + return thisBuilder(); + } + + public B serviceConfiguration(ServiceConfiguration serviceConfiguration) { + clientConfiguration.option(SdkClientOption.SERVICE_CONFIGURATION, serviceConfiguration); + return thisBuilder(); + } + + public void setServiceConfiguration(ServiceConfiguration serviceConfiguration) { + serviceConfiguration(serviceConfiguration); + } + + private SdkTokenProvider defaultTokenProvider() { + return DefaultAwsTokenProvider.create(); + } + + private Signer defaultTokenSigner() { + return BearerTokenSigner.create(); + } + + protected static void validateClientOptions(SdkClientConfiguration c) { + Validate.notNull(c.option(SdkAdvancedClientOption.SIGNER), + "The 'overrideConfiguration.advancedOption[SIGNER]' must be configured in the client builder."); + Validate.notNull(c.option(SdkAdvancedClientOption.TOKEN_SIGNER), + "The 'overrideConfiguration.advancedOption[TOKEN_SIGNER]' must be configured in the client builder."); + Validate.notNull(c.option(AwsClientOption.TOKEN_PROVIDER), + "The 'overrideConfiguration.advancedOption[TOKEN_PROVIDER]' must be configured in the client builder."); + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-customcontextparams-sync-client-builder-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-customcontextparams-sync-client-builder-class.java new file mode 100644 index 000000000000..3c39bb9fc9c0 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-customcontextparams-sync-client-builder-class.java @@ -0,0 +1,46 @@ +package software.amazon.awssdk.services.json; + +import java.util.function.Consumer; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider; +import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder; +import software.amazon.awssdk.services.json.endpoints.JsonEndpointProvider; + +/** + * This includes configuration specific to Json Service that is supported by both {@link JsonClientBuilder} and + * {@link JsonAsyncClientBuilder}. + */ +@Generated("software.amazon.awssdk:codegen") +public interface JsonBaseClientBuilder, C> extends AwsClientBuilder { + B serviceConfiguration(ServiceConfiguration serviceConfiguration); + + default B serviceConfiguration(Consumer serviceConfiguration) { + return serviceConfiguration(ServiceConfiguration.builder().applyMutation(serviceConfiguration).build()); + } + + /** + * Set the {@link JsonEndpointProvider} implementation that will be used by the client to determine the endpoint for + * each request. This is optional; if none is provided a default implementation will be used the SDK. + */ + default B endpointProvider(JsonEndpointProvider endpointProvider) { + throw new UnsupportedOperationException(); + } + + /** + * Enables this client to use Custom Parameter + */ + B customParameter(Boolean customParameter); + + /** + * Set the token provider to use for bearer token authorization. This is optional, if none is provided, the SDK will + * use {@link software.amazon.awssdk.auth.token.credentials.aws.DefaultAwsTokenProvider}. + *

+ * If the service, or any of its operations require Bearer Token Authorization, then the SDK will default to this + * token provider to retrieve the token to use for authorization. + *

+ * This provider works in conjunction with the + * {@code software.amazon.awssdk.core.client.config.SdkAdvancedClientOption.TOKEN_SIGNER} set on the client. By + * default it is {@link software.amazon.awssdk.auth.token.signer.aws.BearerTokenSigner}. + */ + B tokenProvider(SdkTokenProvider tokenProvider); +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-query-async-client-builder-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-query-async-client-builder-class.java new file mode 100644 index 000000000000..da01777df53b --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-query-async-client-builder-class.java @@ -0,0 +1,68 @@ +package software.amazon.awssdk.services.query; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider; +import software.amazon.awssdk.awscore.client.config.AwsClientOption; +import software.amazon.awssdk.codegen.internal.QueryProtocolCustomTestInterceptor; +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.endpoints.EndpointProvider; +import software.amazon.awssdk.protocols.query.interceptor.QueryParametersToBodyInterceptor; +import software.amazon.awssdk.services.query.endpoints.QueryEndpointProvider; +import software.amazon.awssdk.utils.CollectionUtils; + +/** + * Internal implementation of {@link QueryAsyncClientBuilder}. + */ +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +final class DefaultQueryAsyncClientBuilder extends DefaultQueryBaseClientBuilder + implements QueryAsyncClientBuilder { + @Override + public DefaultQueryAsyncClientBuilder endpointProvider(QueryEndpointProvider endpointProvider) { + clientConfiguration.option(SdkClientOption.ENDPOINT_PROVIDER, endpointProvider); + return this; + } + + @Override + public DefaultQueryAsyncClientBuilder tokenProvider(SdkTokenProvider tokenProvider) { + clientConfiguration.option(AwsClientOption.TOKEN_PROVIDER, tokenProvider); + return this; + } + + @Override + protected final QueryAsyncClient buildClient() { + SdkClientConfiguration clientConfiguration = super.asyncClientConfiguration(); + this.validateClientOptions(clientConfiguration); + QueryServiceClientConfiguration serviceClientConfiguration = initializeServiceClientConfig(clientConfiguration); + List interceptors = clientConfiguration.option(SdkClientOption.EXECUTION_INTERCEPTORS); + List queryParamsToBodyInterceptor = Collections + .singletonList(new QueryParametersToBodyInterceptor()); + List customizationInterceptors = new ArrayList<>(); + customizationInterceptors.add(new QueryProtocolCustomTestInterceptor()); + interceptors = CollectionUtils.mergeLists(queryParamsToBodyInterceptor, interceptors); + interceptors = CollectionUtils.mergeLists(customizationInterceptors, interceptors); + clientConfiguration = clientConfiguration.toBuilder().option(SdkClientOption.EXECUTION_INTERCEPTORS, interceptors) + .build(); + QueryAsyncClient client = new DefaultQueryAsyncClient(serviceClientConfiguration, clientConfiguration); + return client; + } + + private QueryServiceClientConfiguration initializeServiceClientConfig(SdkClientConfiguration clientConfig) { + URI endpointOverride = null; + EndpointProvider endpointProvider = clientConfig.option(SdkClientOption.ENDPOINT_PROVIDER); + if (clientConfig.option(SdkClientOption.ENDPOINT_OVERRIDDEN) != null + && Boolean.TRUE.equals(clientConfig.option(SdkClientOption.ENDPOINT_OVERRIDDEN))) { + endpointOverride = clientConfig.option(SdkClientOption.ENDPOINT); + } + return QueryServiceClientConfiguration.builder().overrideConfiguration(overrideConfiguration()) + .region(clientConfig.option(AwsClientOption.AWS_REGION)).endpointOverride(endpointOverride) + .endpointProvider(endpointProvider).build(); + } +} \ No newline at end of file diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-sync-client-builder-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-sync-client-builder-class.java index 64b443054e04..724519ba9aa3 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-sync-client-builder-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-sync-client-builder-class.java @@ -7,6 +7,7 @@ import software.amazon.awssdk.awscore.client.config.AwsClientOption; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.endpoints.EndpointProvider; import software.amazon.awssdk.services.json.endpoints.JsonEndpointProvider; /** @@ -32,14 +33,20 @@ public DefaultJsonClientBuilder tokenProvider(SdkTokenProvider tokenProvider) { protected final JsonClient buildClient() { SdkClientConfiguration clientConfiguration = super.syncClientConfiguration(); this.validateClientOptions(clientConfiguration); + JsonServiceClientConfiguration serviceClientConfiguration = initializeServiceClientConfig(clientConfiguration); + JsonClient client = new DefaultJsonClient(serviceClientConfiguration, clientConfiguration); + return client; + } + + private JsonServiceClientConfiguration initializeServiceClientConfig(SdkClientConfiguration clientConfig) { URI endpointOverride = null; - if (clientConfiguration.option(SdkClientOption.ENDPOINT_OVERRIDDEN) != null - && Boolean.TRUE.equals(clientConfiguration.option(SdkClientOption.ENDPOINT_OVERRIDDEN))) { - endpointOverride = clientConfiguration.option(SdkClientOption.ENDPOINT); + EndpointProvider endpointProvider = clientConfig.option(SdkClientOption.ENDPOINT_PROVIDER); + if (clientConfig.option(SdkClientOption.ENDPOINT_OVERRIDDEN) != null + && Boolean.TRUE.equals(clientConfig.option(SdkClientOption.ENDPOINT_OVERRIDDEN))) { + endpointOverride = clientConfig.option(SdkClientOption.ENDPOINT); } - JsonServiceClientConfiguration serviceClientConfiguration = JsonServiceClientConfiguration.builder() - .overrideConfiguration(overrideConfiguration()).region(clientConfiguration.option(AwsClientOption.AWS_REGION)) - .endpointOverride(endpointOverride).build(); - return new DefaultJsonClient(serviceClientConfiguration, clientConfiguration); + return JsonServiceClientConfiguration.builder().overrideConfiguration(overrideConfiguration()) + .region(clientConfig.option(AwsClientOption.AWS_REGION)).endpointOverride(endpointOverride) + .endpointProvider(endpointProvider).build(); } } diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/composedclient/customization.config b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/composedclient/customization.config new file mode 100644 index 000000000000..5f33dce1133f --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/composedclient/customization.config @@ -0,0 +1,20 @@ +{ + "serviceConfig": { + "className": "ServiceConfiguration", + "hasCrossRegionAccessEnabledProperty": true + }, + "utilitiesMethod": { + "returnType": "software.amazon.awssdk.services.json.JsonUtilities", + "createMethodParams": ["param1", "param2", "param3"] + }, + "syncClientDecorator": "software.amazon.awssdk.services.builder.SyncClientDecorator", + "asyncClientDecorator": "software.amazon.awssdk.services.builder.AsyncClientDecorator", + "asyncClientDecoratorClass": true, + "syncClientDecoratorClass": true, + "customClientContextParams":{ + "CustomParameter":{ + "documentation":"Enables this client to use Custom Parameter", + "type":"boolean" + } + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-abstract-async-client-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-abstract-async-client-class.java index a8e143b21cc3..783d45793ecb 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-abstract-async-client-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-abstract-async-client-class.java @@ -1,6 +1,7 @@ package software.amazon.awssdk.services.json; import java.util.concurrent.CompletableFuture; +import java.util.function.Function; import org.reactivestreams.Publisher; import software.amazon.awssdk.annotations.Generated; import software.amazon.awssdk.annotations.SdkPublicApi; @@ -25,6 +26,7 @@ import software.amazon.awssdk.services.json.model.GetWithoutRequiredMembersResponse; import software.amazon.awssdk.services.json.model.InputEventStream; import software.amazon.awssdk.services.json.model.InputEventStreamTwo; +import software.amazon.awssdk.services.json.model.JsonRequest; import software.amazon.awssdk.services.json.model.OperationWithChecksumRequiredRequest; import software.amazon.awssdk.services.json.model.OperationWithChecksumRequiredResponse; import software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest; @@ -39,8 +41,6 @@ import software.amazon.awssdk.services.json.model.StreamingInputOutputOperationResponse; import software.amazon.awssdk.services.json.model.StreamingOutputOperationRequest; import software.amazon.awssdk.services.json.model.StreamingOutputOperationResponse; -import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPublisher; -import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPublisher; import software.amazon.awssdk.utils.Validate; @Generated("software.amazon.awssdk:codegen") @@ -86,7 +86,7 @@ public JsonUtilities utilities() { */ @Override public CompletableFuture aPostOperation(APostOperationRequest aPostOperationRequest) { - return delegate.aPostOperation(aPostOperationRequest); + return invokeOperation(aPostOperationRequest, request -> delegate.aPostOperation(request)); } /** @@ -115,7 +115,7 @@ public CompletableFuture aPostOperation(APostOperationRe @Override public CompletableFuture aPostOperationWithOutput( APostOperationWithOutputRequest aPostOperationWithOutputRequest) { - return delegate.aPostOperationWithOutput(aPostOperationWithOutputRequest); + return invokeOperation(aPostOperationWithOutputRequest, request -> delegate.aPostOperationWithOutput(request)); } /** @@ -140,7 +140,7 @@ public CompletableFuture aPostOperationWithOut @Override public CompletableFuture bearerAuthOperation( BearerAuthOperationRequest bearerAuthOperationRequest) { - return delegate.bearerAuthOperation(bearerAuthOperationRequest); + return invokeOperation(bearerAuthOperationRequest, request -> delegate.bearerAuthOperation(request)); } /** @@ -165,7 +165,8 @@ public CompletableFuture bearerAuthOperation( @Override public CompletableFuture eventStreamOperation(EventStreamOperationRequest eventStreamOperationRequest, Publisher requestStream, EventStreamOperationResponseHandler asyncResponseHandler) { - return delegate.eventStreamOperation(eventStreamOperationRequest, requestStream, asyncResponseHandler); + return invokeOperation(eventStreamOperationRequest, + request -> delegate.eventStreamOperation(request, requestStream, asyncResponseHandler)); } /** @@ -192,7 +193,8 @@ public CompletableFuture eventStreamOperation(EventStreamOperationRequest public CompletableFuture eventStreamOperationWithOnlyInput( EventStreamOperationWithOnlyInputRequest eventStreamOperationWithOnlyInputRequest, Publisher requestStream) { - return delegate.eventStreamOperationWithOnlyInput(eventStreamOperationWithOnlyInputRequest, requestStream); + return invokeOperation(eventStreamOperationWithOnlyInputRequest, + request -> delegate.eventStreamOperationWithOnlyInput(request, requestStream)); } /** @@ -219,7 +221,8 @@ public CompletableFuture eventStreamO public CompletableFuture eventStreamOperationWithOnlyOutput( EventStreamOperationWithOnlyOutputRequest eventStreamOperationWithOnlyOutputRequest, EventStreamOperationWithOnlyOutputResponseHandler asyncResponseHandler) { - return delegate.eventStreamOperationWithOnlyOutput(eventStreamOperationWithOnlyOutputRequest, asyncResponseHandler); + return invokeOperation(eventStreamOperationWithOnlyOutputRequest, + request -> delegate.eventStreamOperationWithOnlyOutput(request, asyncResponseHandler)); } /** @@ -244,7 +247,7 @@ public CompletableFuture eventStreamOperationWithOnlyOutput( @Override public CompletableFuture getOperationWithChecksum( GetOperationWithChecksumRequest getOperationWithChecksumRequest) { - return delegate.getOperationWithChecksum(getOperationWithChecksumRequest); + return invokeOperation(getOperationWithChecksumRequest, request -> delegate.getOperationWithChecksum(request)); } /** @@ -273,7 +276,7 @@ public CompletableFuture getOperationWithCheck @Override public CompletableFuture getWithoutRequiredMembers( GetWithoutRequiredMembersRequest getWithoutRequiredMembersRequest) { - return delegate.getWithoutRequiredMembers(getWithoutRequiredMembersRequest); + return invokeOperation(getWithoutRequiredMembersRequest, request -> delegate.getWithoutRequiredMembers(request)); } /** @@ -299,7 +302,7 @@ public CompletableFuture getWithoutRequiredMe @Override public CompletableFuture operationWithChecksumRequired( OperationWithChecksumRequiredRequest operationWithChecksumRequiredRequest) { - return delegate.operationWithChecksumRequired(operationWithChecksumRequiredRequest); + return invokeOperation(operationWithChecksumRequiredRequest, request -> delegate.operationWithChecksumRequired(request)); } /** @@ -325,85 +328,8 @@ public CompletableFuture operationWithChe @Override public CompletableFuture paginatedOperationWithResultKey( PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) { - return delegate.paginatedOperationWithResultKey(paginatedOperationWithResultKeyRequest); - } - - /** - * Some paginated operation with result_key in paginators.json file
- *

- * This is a variant of - * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} - * operation. The return type is a custom publisher that can be subscribed to request a stream of response pages. - * SDK will internally handle making service calls for you. - *

- *

- * When the operation is called, an instance of this class is returned. At this point, no service calls are made yet - * and so there is no guarantee that the request is valid. If there are errors in your request, you will see the - * failures only after you start streaming the data. The subscribe method should be called as a request to start - * streaming data. For more info, see - * {@link org.reactivestreams.Publisher#subscribe(org.reactivestreams.Subscriber)}. Each call to the subscribe - * method will result in a new {@link org.reactivestreams.Subscription} i.e., a new contract to stream data from the - * starting request. - *

- * - *

- * The following are few ways to use the response class: - *

- * 1) Using the subscribe helper method - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPublisher publisher = client.paginatedOperationWithResultKeyPaginator(request);
-     * CompletableFuture future = publisher.subscribe(res -> { // Do something with the response });
-     * future.get();
-     * }
-     * 
- * - * 2) Using a custom subscriber - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPublisher publisher = client.paginatedOperationWithResultKeyPaginator(request);
-     * publisher.subscribe(new Subscriber() {
-     *
-     * public void onSubscribe(org.reactivestreams.Subscriber subscription) { //... };
-     *
-     *
-     * public void onNext(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyResponse response) { //... };
-     * });}
-     * 
- * - * As the response is a publisher, it can work well with third party reactive streams implementations like RxJava2. - *

- * Please notice that the configuration of MaxResults won't limit the number of results you get with the - * paginator. It only limits the number of results in each page. - *

- *

- * Note: If you prefer to have control on service calls, use the - * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} - * operation. - *

- * - * @param paginatedOperationWithResultKeyRequest - * @return A custom publisher that can be subscribed to request a stream of response pages.
- * The CompletableFuture returned by this method can be completed exceptionally with the following - * exceptions. - *
    - *
  • SdkException Base class for all exceptions that can be thrown by the SDK (both service and client). - * Can be used for catch all scenarios.
  • - *
  • SdkClientException If any client side error occurs such as an IO related failure, failure to get - * credentials, etc.
  • - *
  • JsonException Base class for all service exceptions. Unknown exceptions will be thrown as an instance - * of this type.
  • - *
- * @sample JsonAsyncClient.PaginatedOperationWithResultKey - * @see AWS API Documentation - */ - @Override - public PaginatedOperationWithResultKeyPublisher paginatedOperationWithResultKeyPaginator( - PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) { - return delegate.paginatedOperationWithResultKeyPaginator(paginatedOperationWithResultKeyRequest); + return invokeOperation(paginatedOperationWithResultKeyRequest, + request -> delegate.paginatedOperationWithResultKey(request)); } /** @@ -429,85 +355,8 @@ public PaginatedOperationWithResultKeyPublisher paginatedOperationWithResultKeyP @Override public CompletableFuture paginatedOperationWithoutResultKey( PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) { - return delegate.paginatedOperationWithoutResultKey(paginatedOperationWithoutResultKeyRequest); - } - - /** - * Some paginated operation without result_key in paginators.json file
- *

- * This is a variant of - * {@link #paginatedOperationWithoutResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyRequest)} - * operation. The return type is a custom publisher that can be subscribed to request a stream of response pages. - * SDK will internally handle making service calls for you. - *

- *

- * When the operation is called, an instance of this class is returned. At this point, no service calls are made yet - * and so there is no guarantee that the request is valid. If there are errors in your request, you will see the - * failures only after you start streaming the data. The subscribe method should be called as a request to start - * streaming data. For more info, see - * {@link org.reactivestreams.Publisher#subscribe(org.reactivestreams.Subscriber)}. Each call to the subscribe - * method will result in a new {@link org.reactivestreams.Subscription} i.e., a new contract to stream data from the - * starting request. - *

- * - *

- * The following are few ways to use the response class: - *

- * 1) Using the subscribe helper method - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPublisher publisher = client.paginatedOperationWithoutResultKeyPaginator(request);
-     * CompletableFuture future = publisher.subscribe(res -> { // Do something with the response });
-     * future.get();
-     * }
-     * 
- * - * 2) Using a custom subscriber - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPublisher publisher = client.paginatedOperationWithoutResultKeyPaginator(request);
-     * publisher.subscribe(new Subscriber() {
-     *
-     * public void onSubscribe(org.reactivestreams.Subscriber subscription) { //... };
-     *
-     *
-     * public void onNext(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyResponse response) { //... };
-     * });}
-     * 
- * - * As the response is a publisher, it can work well with third party reactive streams implementations like RxJava2. - *

- * Please notice that the configuration of MaxResults won't limit the number of results you get with the - * paginator. It only limits the number of results in each page. - *

- *

- * Note: If you prefer to have control on service calls, use the - * {@link #paginatedOperationWithoutResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyRequest)} - * operation. - *

- * - * @param paginatedOperationWithoutResultKeyRequest - * @return A custom publisher that can be subscribed to request a stream of response pages.
- * The CompletableFuture returned by this method can be completed exceptionally with the following - * exceptions. - *
    - *
  • SdkException Base class for all exceptions that can be thrown by the SDK (both service and client). - * Can be used for catch all scenarios.
  • - *
  • SdkClientException If any client side error occurs such as an IO related failure, failure to get - * credentials, etc.
  • - *
  • JsonException Base class for all service exceptions. Unknown exceptions will be thrown as an instance - * of this type.
  • - *
- * @sample JsonAsyncClient.PaginatedOperationWithoutResultKey - * @see AWS API Documentation - */ - @Override - public PaginatedOperationWithoutResultKeyPublisher paginatedOperationWithoutResultKeyPaginator( - PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) { - return delegate.paginatedOperationWithoutResultKeyPaginator(paginatedOperationWithoutResultKeyRequest); + return invokeOperation(paginatedOperationWithoutResultKeyRequest, + request -> delegate.paginatedOperationWithoutResultKey(request)); } /** @@ -551,7 +400,8 @@ public PaginatedOperationWithoutResultKeyPublisher paginatedOperationWithoutResu public CompletableFuture putOperationWithChecksum( PutOperationWithChecksumRequest putOperationWithChecksumRequest, AsyncRequestBody requestBody, AsyncResponseTransformer asyncResponseTransformer) { - return delegate.putOperationWithChecksum(putOperationWithChecksumRequest, requestBody, asyncResponseTransformer); + return invokeOperation(putOperationWithChecksumRequest, + request -> delegate.putOperationWithChecksum(request, requestBody, asyncResponseTransformer)); } /** @@ -581,7 +431,7 @@ public CompletableFuture putOperationWithChecksum( @Override public CompletableFuture streamingInputOperation( StreamingInputOperationRequest streamingInputOperationRequest, AsyncRequestBody requestBody) { - return delegate.streamingInputOperation(streamingInputOperationRequest, requestBody); + return invokeOperation(streamingInputOperationRequest, request -> delegate.streamingInputOperation(request, requestBody)); } /** @@ -617,8 +467,8 @@ public CompletableFuture streamingInputOperatio public CompletableFuture streamingInputOutputOperation( StreamingInputOutputOperationRequest streamingInputOutputOperationRequest, AsyncRequestBody requestBody, AsyncResponseTransformer asyncResponseTransformer) { - return delegate - .streamingInputOutputOperation(streamingInputOutputOperationRequest, requestBody, asyncResponseTransformer); + return invokeOperation(streamingInputOutputOperationRequest, + request -> delegate.streamingInputOutputOperation(request, requestBody, asyncResponseTransformer)); } /** @@ -649,7 +499,8 @@ public CompletableFuture streamingInputOutputOperation( public CompletableFuture streamingOutputOperation( StreamingOutputOperationRequest streamingOutputOperationRequest, AsyncResponseTransformer asyncResponseTransformer) { - return delegate.streamingOutputOperation(streamingOutputOperationRequest, asyncResponseTransformer); + return invokeOperation(streamingOutputOperationRequest, + request -> delegate.streamingOutputOperation(request, asyncResponseTransformer)); } @Override @@ -666,6 +517,11 @@ public SdkClient delegate() { return this.delegate; } + protected CompletableFuture invokeOperation(T request, + Function> operation) { + return operation.apply(request); + } + @Override public void close() { delegate.close(); diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-abstract-sync-client-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-abstract-sync-client-class.java index 4d6847402dc4..cc067f5eab5b 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-abstract-sync-client-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-abstract-sync-client-class.java @@ -1,11 +1,9 @@ package software.amazon.awssdk.services.json; -import java.nio.file.Path; +import java.util.function.Function; import software.amazon.awssdk.annotations.Generated; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.awscore.exception.AwsServiceException; -import software.amazon.awssdk.core.ResponseBytes; -import software.amazon.awssdk.core.ResponseInputStream; import software.amazon.awssdk.core.SdkClient; import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.sync.RequestBody; @@ -22,6 +20,7 @@ import software.amazon.awssdk.services.json.model.GetWithoutRequiredMembersResponse; import software.amazon.awssdk.services.json.model.InvalidInputException; import software.amazon.awssdk.services.json.model.JsonException; +import software.amazon.awssdk.services.json.model.JsonRequest; import software.amazon.awssdk.services.json.model.OperationWithChecksumRequiredRequest; import software.amazon.awssdk.services.json.model.OperationWithChecksumRequiredResponse; import software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest; @@ -36,8 +35,6 @@ import software.amazon.awssdk.services.json.model.StreamingInputOutputOperationResponse; import software.amazon.awssdk.services.json.model.StreamingOutputOperationRequest; import software.amazon.awssdk.services.json.model.StreamingOutputOperationResponse; -import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable; -import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyIterable; import software.amazon.awssdk.utils.Validate; @Generated("software.amazon.awssdk:codegen") @@ -69,14 +66,11 @@ public DelegatingJsonClient(JsonClient delegate) { * @sample JsonClient.APostOperation * @see AWS * API Documentation - * - * @deprecated This API is deprecated, use something else */ @Override - @Deprecated public APostOperationResponse aPostOperation(APostOperationRequest aPostOperationRequest) throws InvalidInputException, AwsServiceException, SdkClientException, JsonException { - return delegate.aPostOperation(aPostOperationRequest); + return invokeOperation(aPostOperationRequest, request -> delegate.aPostOperation(request)); } /** @@ -103,7 +97,7 @@ public APostOperationResponse aPostOperation(APostOperationRequest aPostOperatio public APostOperationWithOutputResponse aPostOperationWithOutput( APostOperationWithOutputRequest aPostOperationWithOutputRequest) throws InvalidInputException, AwsServiceException, SdkClientException, JsonException { - return delegate.aPostOperationWithOutput(aPostOperationWithOutputRequest); + return invokeOperation(aPostOperationWithOutputRequest, request -> delegate.aPostOperationWithOutput(request)); } /** @@ -125,7 +119,7 @@ public APostOperationWithOutputResponse aPostOperationWithOutput( @Override public BearerAuthOperationResponse bearerAuthOperation(BearerAuthOperationRequest bearerAuthOperationRequest) throws AwsServiceException, SdkClientException, JsonException { - return delegate.bearerAuthOperation(bearerAuthOperationRequest); + return invokeOperation(bearerAuthOperationRequest, request -> delegate.bearerAuthOperation(request)); } /** @@ -148,7 +142,7 @@ public BearerAuthOperationResponse bearerAuthOperation(BearerAuthOperationReques public GetOperationWithChecksumResponse getOperationWithChecksum( GetOperationWithChecksumRequest getOperationWithChecksumRequest) throws AwsServiceException, SdkClientException, JsonException { - return delegate.getOperationWithChecksum(getOperationWithChecksumRequest); + return invokeOperation(getOperationWithChecksumRequest, request -> delegate.getOperationWithChecksum(request)); } /** @@ -175,7 +169,7 @@ public GetOperationWithChecksumResponse getOperationWithChecksum( public GetWithoutRequiredMembersResponse getWithoutRequiredMembers( GetWithoutRequiredMembersRequest getWithoutRequiredMembersRequest) throws InvalidInputException, AwsServiceException, SdkClientException, JsonException { - return delegate.getWithoutRequiredMembers(getWithoutRequiredMembersRequest); + return invokeOperation(getWithoutRequiredMembersRequest, request -> delegate.getWithoutRequiredMembers(request)); } /** @@ -198,29 +192,7 @@ public GetWithoutRequiredMembersResponse getWithoutRequiredMembers( public OperationWithChecksumRequiredResponse operationWithChecksumRequired( OperationWithChecksumRequiredRequest operationWithChecksumRequiredRequest) throws AwsServiceException, SdkClientException, JsonException { - return delegate.operationWithChecksumRequired(operationWithChecksumRequiredRequest); - } - - /** - * Some paginated operation with result_key in paginators.json file - * - * @return Result of the PaginatedOperationWithResultKey operation returned by the service. - * @throws SdkException - * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for - * catch all scenarios. - * @throws SdkClientException - * If any client side error occurs such as an IO related failure, failure to get credentials, etc. - * @throws JsonException - * Base class for all service exceptions. Unknown exceptions will be thrown as an instance of this type. - * @sample JsonClient.PaginatedOperationWithResultKey - * @see #paginatedOperationWithResultKey(PaginatedOperationWithResultKeyRequest) - * @see AWS API Documentation - */ - @Override - public PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey() throws AwsServiceException, - SdkClientException, JsonException { - return paginatedOperationWithResultKey(PaginatedOperationWithResultKeyRequest.builder().build()); + return invokeOperation(operationWithChecksumRequiredRequest, request -> delegate.operationWithChecksumRequired(request)); } /** @@ -243,162 +215,8 @@ public PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey() public PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey( PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) throws AwsServiceException, SdkClientException, JsonException { - return delegate.paginatedOperationWithResultKey(paginatedOperationWithResultKeyRequest); - } - - /** - * Some paginated operation with result_key in paginators.json file
- *

- * This is a variant of - * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} - * operation. The return type is a custom iterable that can be used to iterate through all the pages. SDK will - * internally handle making service calls for you. - *

- *

- * When this operation is called, a custom iterable is returned but no service calls are made yet. So there is no - * guarantee that the request is valid. As you iterate through the iterable, SDK will start lazily loading response - * pages by making service calls until there are no pages left or your iteration stops. If there are errors in your - * request, you will see the failures only after you start iterating through the iterable. - *

- * - *

- * The following are few ways to iterate through the response pages: - *

- * 1) Using a Stream - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable responses = client.paginatedOperationWithResultKeyPaginator(request);
-     * responses.stream().forEach(....);
-     * }
-     * 
- * - * 2) Using For loop - * - *
-     * {
-     *     @code
-     *     software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable responses = client
-     *             .paginatedOperationWithResultKeyPaginator(request);
-     *     for (software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyResponse response : responses) {
-     *         // do something;
-     *     }
-     * }
-     * 
- * - * 3) Use iterator directly - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable responses = client.paginatedOperationWithResultKeyPaginator(request);
-     * responses.iterator().forEachRemaining(....);
-     * }
-     * 
- *

- * Please notice that the configuration of MaxResults won't limit the number of results you get with the - * paginator. It only limits the number of results in each page. - *

- *

- * Note: If you prefer to have control on service calls, use the - * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} - * operation. - *

- * - * @return A custom iterable that can be used to iterate through all the response pages. - * @throws SdkException - * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for - * catch all scenarios. - * @throws SdkClientException - * If any client side error occurs such as an IO related failure, failure to get credentials, etc. - * @throws JsonException - * Base class for all service exceptions. Unknown exceptions will be thrown as an instance of this type. - * @sample JsonClient.PaginatedOperationWithResultKey - * @see #paginatedOperationWithResultKeyPaginator(PaginatedOperationWithResultKeyRequest) - * @see AWS API Documentation - */ - @Override - public PaginatedOperationWithResultKeyIterable paginatedOperationWithResultKeyPaginator() throws AwsServiceException, - SdkClientException, JsonException { - return paginatedOperationWithResultKeyPaginator(PaginatedOperationWithResultKeyRequest.builder().build()); - } - - /** - * Some paginated operation with result_key in paginators.json file
- *

- * This is a variant of - * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} - * operation. The return type is a custom iterable that can be used to iterate through all the pages. SDK will - * internally handle making service calls for you. - *

- *

- * When this operation is called, a custom iterable is returned but no service calls are made yet. So there is no - * guarantee that the request is valid. As you iterate through the iterable, SDK will start lazily loading response - * pages by making service calls until there are no pages left or your iteration stops. If there are errors in your - * request, you will see the failures only after you start iterating through the iterable. - *

- * - *

- * The following are few ways to iterate through the response pages: - *

- * 1) Using a Stream - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable responses = client.paginatedOperationWithResultKeyPaginator(request);
-     * responses.stream().forEach(....);
-     * }
-     * 
- * - * 2) Using For loop - * - *
-     * {
-     *     @code
-     *     software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable responses = client
-     *             .paginatedOperationWithResultKeyPaginator(request);
-     *     for (software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyResponse response : responses) {
-     *         // do something;
-     *     }
-     * }
-     * 
- * - * 3) Use iterator directly - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable responses = client.paginatedOperationWithResultKeyPaginator(request);
-     * responses.iterator().forEachRemaining(....);
-     * }
-     * 
- *

- * Please notice that the configuration of MaxResults won't limit the number of results you get with the - * paginator. It only limits the number of results in each page. - *

- *

- * Note: If you prefer to have control on service calls, use the - * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} - * operation. - *

- * - * @param paginatedOperationWithResultKeyRequest - * @return A custom iterable that can be used to iterate through all the response pages. - * @throws SdkException - * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for - * catch all scenarios. - * @throws SdkClientException - * If any client side error occurs such as an IO related failure, failure to get credentials, etc. - * @throws JsonException - * Base class for all service exceptions. Unknown exceptions will be thrown as an instance of this type. - * @sample JsonClient.PaginatedOperationWithResultKey - * @see AWS API Documentation - */ - @Override - public PaginatedOperationWithResultKeyIterable paginatedOperationWithResultKeyPaginator( - PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) throws AwsServiceException, - SdkClientException, JsonException { - return delegate.paginatedOperationWithResultKeyPaginator(paginatedOperationWithResultKeyRequest); + return invokeOperation(paginatedOperationWithResultKeyRequest, + request -> delegate.paginatedOperationWithResultKey(request)); } /** @@ -421,85 +239,8 @@ public PaginatedOperationWithResultKeyIterable paginatedOperationWithResultKeyPa public PaginatedOperationWithoutResultKeyResponse paginatedOperationWithoutResultKey( PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) throws AwsServiceException, SdkClientException, JsonException { - return delegate.paginatedOperationWithoutResultKey(paginatedOperationWithoutResultKeyRequest); - } - - /** - * Some paginated operation without result_key in paginators.json file
- *

- * This is a variant of - * {@link #paginatedOperationWithoutResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyRequest)} - * operation. The return type is a custom iterable that can be used to iterate through all the pages. SDK will - * internally handle making service calls for you. - *

- *

- * When this operation is called, a custom iterable is returned but no service calls are made yet. So there is no - * guarantee that the request is valid. As you iterate through the iterable, SDK will start lazily loading response - * pages by making service calls until there are no pages left or your iteration stops. If there are errors in your - * request, you will see the failures only after you start iterating through the iterable. - *

- * - *

- * The following are few ways to iterate through the response pages: - *

- * 1) Using a Stream - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyIterable responses = client.paginatedOperationWithoutResultKeyPaginator(request);
-     * responses.stream().forEach(....);
-     * }
-     * 
- * - * 2) Using For loop - * - *
-     * {
-     *     @code
-     *     software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyIterable responses = client
-     *             .paginatedOperationWithoutResultKeyPaginator(request);
-     *     for (software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyResponse response : responses) {
-     *         // do something;
-     *     }
-     * }
-     * 
- * - * 3) Use iterator directly - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyIterable responses = client.paginatedOperationWithoutResultKeyPaginator(request);
-     * responses.iterator().forEachRemaining(....);
-     * }
-     * 
- *

- * Please notice that the configuration of MaxResults won't limit the number of results you get with the - * paginator. It only limits the number of results in each page. - *

- *

- * Note: If you prefer to have control on service calls, use the - * {@link #paginatedOperationWithoutResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyRequest)} - * operation. - *

- * - * @param paginatedOperationWithoutResultKeyRequest - * @return A custom iterable that can be used to iterate through all the response pages. - * @throws SdkException - * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for - * catch all scenarios. - * @throws SdkClientException - * If any client side error occurs such as an IO related failure, failure to get credentials, etc. - * @throws JsonException - * Base class for all service exceptions. Unknown exceptions will be thrown as an instance of this type. - * @sample JsonClient.PaginatedOperationWithoutResultKey - * @see AWS API Documentation - */ - @Override - public PaginatedOperationWithoutResultKeyIterable paginatedOperationWithoutResultKeyPaginator( - PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) throws AwsServiceException, - SdkClientException, JsonException { - return delegate.paginatedOperationWithoutResultKeyPaginator(paginatedOperationWithoutResultKeyRequest); + return invokeOperation(paginatedOperationWithoutResultKeyRequest, + request -> delegate.paginatedOperationWithoutResultKey(request)); } /** @@ -548,50 +289,8 @@ public PaginatedOperationWithoutResultKeyIterable paginatedOperationWithoutResul public ReturnT putOperationWithChecksum(PutOperationWithChecksumRequest putOperationWithChecksumRequest, RequestBody requestBody, ResponseTransformer responseTransformer) throws AwsServiceException, SdkClientException, JsonException { - return delegate.putOperationWithChecksum(putOperationWithChecksumRequest, requestBody, responseTransformer); - } - - /** - * Invokes the PutOperationWithChecksum operation. - * - * @param putOperationWithChecksumRequest - * @param sourcePath - * {@link Path} to file containing data to send to the service. File will be read entirely and may be read - * multiple times in the event of a retry. If the file does not exist or the current user does not have - * access to read it then an exception will be thrown. The service documentation for the request content is - * as follows ' - *

- * Object data. - *

- * ' - * @param destinationPath - * {@link Path} to file that response contents will be written to. The file must not exist or this method - * will throw an exception. If the file is not writable by the current user then an exception will be thrown. - * The service documentation for the response content is as follows ' - *

- * Object data. - *

- * '. - * @return The transformed result of the ResponseTransformer. - * @throws SdkException - * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for - * catch all scenarios. - * @throws SdkClientException - * If any client side error occurs such as an IO related failure, failure to get credentials, etc. - * @throws JsonException - * Base class for all service exceptions. Unknown exceptions will be thrown as an instance of this type. - * @sample JsonClient.PutOperationWithChecksum - * @see #putOperationWithChecksum(PutOperationWithChecksumRequest, RequestBody) - * @see #putOperationWithChecksum(PutOperationWithChecksumRequest, ResponseTransformer) - * @see AWS API Documentation - */ - @Override - public PutOperationWithChecksumResponse putOperationWithChecksum( - PutOperationWithChecksumRequest putOperationWithChecksumRequest, Path sourcePath, Path destinationPath) - throws AwsServiceException, SdkClientException, JsonException { - return putOperationWithChecksum(putOperationWithChecksumRequest, RequestBody.fromFile(sourcePath), - ResponseTransformer.toFile(destinationPath)); + return invokeOperation(putOperationWithChecksumRequest, + request -> delegate.putOperationWithChecksum(request, requestBody, responseTransformer)); } /** @@ -624,35 +323,7 @@ public PutOperationWithChecksumResponse putOperationWithChecksum( @Override public StreamingInputOperationResponse streamingInputOperation(StreamingInputOperationRequest streamingInputOperationRequest, RequestBody requestBody) throws AwsServiceException, SdkClientException, JsonException { - return delegate.streamingInputOperation(streamingInputOperationRequest, requestBody); - } - - /** - * Some operation with a streaming input - * - * @param streamingInputOperationRequest - * @param sourcePath - * {@link Path} to file containing data to send to the service. File will be read entirely and may be read - * multiple times in the event of a retry. If the file does not exist or the current user does not have - * access to read it then an exception will be thrown. The service documentation for the request content is - * as follows 'This be a stream' - * @return Result of the StreamingInputOperation operation returned by the service. - * @throws SdkException - * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for - * catch all scenarios. - * @throws SdkClientException - * If any client side error occurs such as an IO related failure, failure to get credentials, etc. - * @throws JsonException - * Base class for all service exceptions. Unknown exceptions will be thrown as an instance of this type. - * @sample JsonClient.StreamingInputOperation - * @see #streamingInputOperation(StreamingInputOperationRequest, RequestBody) - * @see AWS API Documentation - */ - @Override - public StreamingInputOperationResponse streamingInputOperation(StreamingInputOperationRequest streamingInputOperationRequest, - Path sourcePath) throws AwsServiceException, SdkClientException, JsonException { - return streamingInputOperation(streamingInputOperationRequest, RequestBody.fromFile(sourcePath)); + return invokeOperation(streamingInputOperationRequest, request -> delegate.streamingInputOperation(request, requestBody)); } /** @@ -694,42 +365,8 @@ public ReturnT streamingInputOutputOperation( StreamingInputOutputOperationRequest streamingInputOutputOperationRequest, RequestBody requestBody, ResponseTransformer responseTransformer) throws AwsServiceException, SdkClientException, JsonException { - return delegate.streamingInputOutputOperation(streamingInputOutputOperationRequest, requestBody, responseTransformer); - } - - /** - * Some operation with streaming input and streaming output - * - * @param streamingInputOutputOperationRequest - * @param sourcePath - * {@link Path} to file containing data to send to the service. File will be read entirely and may be read - * multiple times in the event of a retry. If the file does not exist or the current user does not have - * access to read it then an exception will be thrown. The service documentation for the request content is - * as follows 'This be a stream' - * @param destinationPath - * {@link Path} to file that response contents will be written to. The file must not exist or this method - * will throw an exception. If the file is not writable by the current user then an exception will be thrown. - * The service documentation for the response content is as follows 'This be a stream'. - * @return The transformed result of the ResponseTransformer. - * @throws SdkException - * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for - * catch all scenarios. - * @throws SdkClientException - * If any client side error occurs such as an IO related failure, failure to get credentials, etc. - * @throws JsonException - * Base class for all service exceptions. Unknown exceptions will be thrown as an instance of this type. - * @sample JsonClient.StreamingInputOutputOperation - * @see #streamingInputOutputOperation(StreamingInputOutputOperationRequest, RequestBody) - * @see #streamingInputOutputOperation(StreamingInputOutputOperationRequest, ResponseTransformer) - * @see AWS API Documentation - */ - @Override - public StreamingInputOutputOperationResponse streamingInputOutputOperation( - StreamingInputOutputOperationRequest streamingInputOutputOperationRequest, Path sourcePath, Path destinationPath) - throws AwsServiceException, SdkClientException, JsonException { - return streamingInputOutputOperation(streamingInputOutputOperationRequest, RequestBody.fromFile(sourcePath), - ResponseTransformer.toFile(destinationPath)); + return invokeOperation(streamingInputOutputOperationRequest, + request -> delegate.streamingInputOutputOperation(request, requestBody, responseTransformer)); } /** @@ -759,92 +396,10 @@ public StreamingInputOutputOperationResponse streamingInputOutputOperation( public ReturnT streamingOutputOperation(StreamingOutputOperationRequest streamingOutputOperationRequest, ResponseTransformer responseTransformer) throws AwsServiceException, SdkClientException, JsonException { - return delegate.streamingOutputOperation(streamingOutputOperationRequest, responseTransformer); - } - - /** - * Some operation with a streaming output - * - * @param streamingOutputOperationRequest - * @param destinationPath - * {@link Path} to file that response contents will be written to. The file must not exist or this method - * will throw an exception. If the file is not writable by the current user then an exception will be thrown. - * The service documentation for the response content is as follows 'This be a stream'. - * @return The transformed result of the ResponseTransformer. - * @throws SdkException - * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for - * catch all scenarios. - * @throws SdkClientException - * If any client side error occurs such as an IO related failure, failure to get credentials, etc. - * @throws JsonException - * Base class for all service exceptions. Unknown exceptions will be thrown as an instance of this type. - * @sample JsonClient.StreamingOutputOperation - * @see #streamingOutputOperation(StreamingOutputOperationRequest, ResponseTransformer) - * @see AWS API Documentation - */ - @Override - public StreamingOutputOperationResponse streamingOutputOperation( - StreamingOutputOperationRequest streamingOutputOperationRequest, Path destinationPath) throws AwsServiceException, - SdkClientException, JsonException { - return streamingOutputOperation(streamingOutputOperationRequest, ResponseTransformer.toFile(destinationPath)); - } - - /** - * Some operation with a streaming output - * - * @param streamingOutputOperationRequest - * @return A {@link ResponseInputStream} containing data streamed from service. Note that this is an unmanaged - * reference to the underlying HTTP connection so great care must be taken to ensure all data if fully read - * from the input stream and that it is properly closed. Failure to do so may result in sub-optimal behavior - * and exhausting connections in the connection pool. The unmarshalled response object can be obtained via - * {@link ResponseInputStream#response()}. The service documentation for the response content is as follows - * 'This be a stream'. - * @throws SdkException - * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for - * catch all scenarios. - * @throws SdkClientException - * If any client side error occurs such as an IO related failure, failure to get credentials, etc. - * @throws JsonException - * Base class for all service exceptions. Unknown exceptions will be thrown as an instance of this type. - * @sample JsonClient.StreamingOutputOperation - * @see #getObject(streamingOutputOperation, ResponseTransformer) - * @see AWS API Documentation - */ - @Override - public ResponseInputStream streamingOutputOperation( - StreamingOutputOperationRequest streamingOutputOperationRequest) throws AwsServiceException, SdkClientException, - JsonException { - return streamingOutputOperation(streamingOutputOperationRequest, ResponseTransformer.toInputStream()); + return invokeOperation(streamingOutputOperationRequest, + request -> delegate.streamingOutputOperation(request, responseTransformer)); } - /** - * Some operation with a streaming output - * - * @param streamingOutputOperationRequest - * @return A {@link ResponseBytes} that loads the data streamed from the service into memory and exposes it in - * convenient in-memory representations like a byte buffer or string. The unmarshalled response object can - * be obtained via {@link ResponseBytes#response()}. The service documentation for the response content is - * as follows 'This be a stream'. - * @throws SdkException - * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for - * catch all scenarios. - * @throws SdkClientException - * If any client side error occurs such as an IO related failure, failure to get credentials, etc. - * @throws JsonException - * Base class for all service exceptions. Unknown exceptions will be thrown as an instance of this type. - * @sample JsonClient.StreamingOutputOperation - * @see #getObject(streamingOutputOperation, ResponseTransformer) - * @see AWS API Documentation - */ - @Override - public ResponseBytes streamingOutputOperationAsBytes( - StreamingOutputOperationRequest streamingOutputOperationRequest) throws AwsServiceException, SdkClientException, - JsonException { - return streamingOutputOperation(streamingOutputOperationRequest, ResponseTransformer.toBytes()); - } /** * Creates an instance of {@link JsonUtilities} object with the configuration set on this client. @@ -863,6 +418,10 @@ public SdkClient delegate() { return this.delegate; } + protected ReturnT invokeOperation(T request, Function operation) { + return operation.apply(request); + } + @Override public final JsonServiceClientConfiguration serviceClientConfiguration() { return delegate.serviceClientConfiguration(); diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-aws-json-async-client-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-aws-json-async-client-class.java index e0ecd250b7ce..b03bc8eb84d2 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-aws-json-async-client-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-aws-json-async-client-class.java @@ -23,7 +23,6 @@ import software.amazon.awssdk.awscore.eventstream.EventStreamTaggedUnionJsonMarshaller; import software.amazon.awssdk.awscore.eventstream.EventStreamTaggedUnionPojoSupplier; import software.amazon.awssdk.awscore.exception.AwsServiceException; -import software.amazon.awssdk.core.ApiName; import software.amazon.awssdk.core.RequestOverrideConfiguration; import software.amazon.awssdk.core.SdkPojoBuilder; import software.amazon.awssdk.core.SdkResponse; @@ -44,7 +43,6 @@ import software.amazon.awssdk.core.protocol.VoidSdkResponse; import software.amazon.awssdk.core.runtime.transform.AsyncStreamingRequestMarshaller; import software.amazon.awssdk.core.signer.Signer; -import software.amazon.awssdk.core.util.VersionInfo; import software.amazon.awssdk.metrics.MetricCollector; import software.amazon.awssdk.metrics.MetricPublisher; import software.amazon.awssdk.metrics.NoOpMetricCollector; @@ -91,8 +89,6 @@ import software.amazon.awssdk.services.json.model.inputeventstream.DefaultInputEvent; import software.amazon.awssdk.services.json.model.inputeventstreamtwo.DefaultInputEventOne; import software.amazon.awssdk.services.json.model.inputeventstreamtwo.DefaultInputEventTwo; -import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPublisher; -import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPublisher; import software.amazon.awssdk.services.json.transform.APostOperationRequestMarshaller; import software.amazon.awssdk.services.json.transform.APostOperationWithOutputRequestMarshaller; import software.amazon.awssdk.services.json.transform.EventStreamOperationRequestMarshaller; @@ -739,83 +735,6 @@ public CompletableFuture paginatedOpera } } - /** - * Some paginated operation with result_key in paginators.json file
- *

- * This is a variant of - * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} - * operation. The return type is a custom publisher that can be subscribed to request a stream of response pages. - * SDK will internally handle making service calls for you. - *

- *

- * When the operation is called, an instance of this class is returned. At this point, no service calls are made yet - * and so there is no guarantee that the request is valid. If there are errors in your request, you will see the - * failures only after you start streaming the data. The subscribe method should be called as a request to start - * streaming data. For more info, see - * {@link org.reactivestreams.Publisher#subscribe(org.reactivestreams.Subscriber)}. Each call to the subscribe - * method will result in a new {@link org.reactivestreams.Subscription} i.e., a new contract to stream data from the - * starting request. - *

- * - *

- * The following are few ways to use the response class: - *

- * 1) Using the subscribe helper method - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPublisher publisher = client.paginatedOperationWithResultKeyPaginator(request);
-     * CompletableFuture future = publisher.subscribe(res -> { // Do something with the response });
-     * future.get();
-     * }
-     * 
- * - * 2) Using a custom subscriber - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPublisher publisher = client.paginatedOperationWithResultKeyPaginator(request);
-     * publisher.subscribe(new Subscriber() {
-     *
-     * public void onSubscribe(org.reactivestreams.Subscriber subscription) { //... };
-     *
-     *
-     * public void onNext(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyResponse response) { //... };
-     * });}
-     * 
- * - * As the response is a publisher, it can work well with third party reactive streams implementations like RxJava2. - *

- * Please notice that the configuration of MaxResults won't limit the number of results you get with the - * paginator. It only limits the number of results in each page. - *

- *

- * Note: If you prefer to have control on service calls, use the - * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} - * operation. - *

- * - * @param paginatedOperationWithResultKeyRequest - * @return A custom publisher that can be subscribed to request a stream of response pages.
- * The CompletableFuture returned by this method can be completed exceptionally with the following - * exceptions. - *
    - *
  • SdkException Base class for all exceptions that can be thrown by the SDK (both service and client). - * Can be used for catch all scenarios.
  • - *
  • SdkClientException If any client side error occurs such as an IO related failure, failure to get - * credentials, etc.
  • - *
  • JsonException Base class for all service exceptions. Unknown exceptions will be thrown as an instance - * of this type.
  • - *
- * @sample JsonAsyncClient.PaginatedOperationWithResultKey - * @see AWS API Documentation - */ - public PaginatedOperationWithResultKeyPublisher paginatedOperationWithResultKeyPaginator( - PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) { - return new PaginatedOperationWithResultKeyPublisher(this, applyPaginatorUserAgent(paginatedOperationWithResultKeyRequest)); - } - /** * Some paginated operation without result_key in paginators.json file * @@ -872,84 +791,6 @@ public CompletableFuture paginatedOp } } - /** - * Some paginated operation without result_key in paginators.json file
- *

- * This is a variant of - * {@link #paginatedOperationWithoutResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyRequest)} - * operation. The return type is a custom publisher that can be subscribed to request a stream of response pages. - * SDK will internally handle making service calls for you. - *

- *

- * When the operation is called, an instance of this class is returned. At this point, no service calls are made yet - * and so there is no guarantee that the request is valid. If there are errors in your request, you will see the - * failures only after you start streaming the data. The subscribe method should be called as a request to start - * streaming data. For more info, see - * {@link org.reactivestreams.Publisher#subscribe(org.reactivestreams.Subscriber)}. Each call to the subscribe - * method will result in a new {@link org.reactivestreams.Subscription} i.e., a new contract to stream data from the - * starting request. - *

- * - *

- * The following are few ways to use the response class: - *

- * 1) Using the subscribe helper method - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPublisher publisher = client.paginatedOperationWithoutResultKeyPaginator(request);
-     * CompletableFuture future = publisher.subscribe(res -> { // Do something with the response });
-     * future.get();
-     * }
-     * 
- * - * 2) Using a custom subscriber - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPublisher publisher = client.paginatedOperationWithoutResultKeyPaginator(request);
-     * publisher.subscribe(new Subscriber() {
-     *
-     * public void onSubscribe(org.reactivestreams.Subscriber subscription) { //... };
-     *
-     *
-     * public void onNext(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyResponse response) { //... };
-     * });}
-     * 
- * - * As the response is a publisher, it can work well with third party reactive streams implementations like RxJava2. - *

- * Please notice that the configuration of MaxResults won't limit the number of results you get with the - * paginator. It only limits the number of results in each page. - *

- *

- * Note: If you prefer to have control on service calls, use the - * {@link #paginatedOperationWithoutResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyRequest)} - * operation. - *

- * - * @param paginatedOperationWithoutResultKeyRequest - * @return A custom publisher that can be subscribed to request a stream of response pages.
- * The CompletableFuture returned by this method can be completed exceptionally with the following - * exceptions. - *
    - *
  • SdkException Base class for all exceptions that can be thrown by the SDK (both service and client). - * Can be used for catch all scenarios.
  • - *
  • SdkClientException If any client side error occurs such as an IO related failure, failure to get - * credentials, etc.
  • - *
  • JsonException Base class for all service exceptions. Unknown exceptions will be thrown as an instance - * of this type.
  • - *
- * @sample JsonAsyncClient.PaginatedOperationWithoutResultKey - * @see AWS API Documentation - */ - public PaginatedOperationWithoutResultKeyPublisher paginatedOperationWithoutResultKeyPaginator( - PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) { - return new PaginatedOperationWithoutResultKeyPublisher(this, - applyPaginatorUserAgent(paginatedOperationWithoutResultKeyRequest)); - } - /** * Some operation with a streaming input * @@ -1219,15 +1060,6 @@ private static List resolveMetricPublishers(SdkClientConfigurat return publishers; } - private T applyPaginatorUserAgent(T request) { - Consumer userAgentApplier = b -> b.addApiName(ApiName.builder() - .version(VersionInfo.SDK_VERSION).name("PAGINATED").build()); - AwsRequestOverrideConfiguration overrideConfiguration = request.overrideConfiguration() - .map(c -> c.toBuilder().applyMutation(userAgentApplier).build()) - .orElse((AwsRequestOverrideConfiguration.builder().applyMutation(userAgentApplier).build())); - return (T) request.toBuilder().overrideConfiguration(overrideConfiguration).build(); - } - private T applySignerOverride(T request, Signer signer) { if (request.overrideConfiguration().flatMap(c -> c.signer()).isPresent()) { return request; diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-aws-query-compatible-json-async-client-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-aws-query-compatible-json-async-client-class.java index e33761735db9..38fab697c390 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-aws-query-compatible-json-async-client-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-aws-query-compatible-json-async-client-class.java @@ -5,15 +5,12 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.annotations.Generated; import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; import software.amazon.awssdk.awscore.client.handler.AwsAsyncClientHandler; import software.amazon.awssdk.awscore.exception.AwsServiceException; -import software.amazon.awssdk.core.ApiName; import software.amazon.awssdk.core.RequestOverrideConfiguration; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; @@ -21,7 +18,6 @@ import software.amazon.awssdk.core.client.handler.ClientExecutionParams; import software.amazon.awssdk.core.http.HttpResponseHandler; import software.amazon.awssdk.core.metrics.CoreMetric; -import software.amazon.awssdk.core.util.VersionInfo; import software.amazon.awssdk.metrics.MetricCollector; import software.amazon.awssdk.metrics.MetricPublisher; import software.amazon.awssdk.metrics.NoOpMetricCollector; @@ -34,7 +30,6 @@ import software.amazon.awssdk.services.querytojsoncompatible.model.APostOperationResponse; import software.amazon.awssdk.services.querytojsoncompatible.model.InvalidInputException; import software.amazon.awssdk.services.querytojsoncompatible.model.QueryToJsonCompatibleException; -import software.amazon.awssdk.services.querytojsoncompatible.model.QueryToJsonCompatibleRequest; import software.amazon.awssdk.services.querytojsoncompatible.transform.APostOperationRequestMarshaller; import software.amazon.awssdk.utils.CompletableFutureUtils; import software.amazon.awssdk.utils.HostnameValidator; @@ -165,15 +160,6 @@ private static List resolveMetricPublishers(SdkClientConfigurat return publishers; } - private T applyPaginatorUserAgent(T request) { - Consumer userAgentApplier = b -> b.addApiName(ApiName.builder() - .version(VersionInfo.SDK_VERSION).name("PAGINATED").build()); - AwsRequestOverrideConfiguration overrideConfiguration = request.overrideConfiguration() - .map(c -> c.toBuilder().applyMutation(userAgentApplier).build()) - .orElse((AwsRequestOverrideConfiguration.builder().applyMutation(userAgentApplier).build())); - return (T) request.toBuilder().overrideConfiguration(overrideConfiguration).build(); - } - private HttpResponseHandler createErrorResponseHandler(BaseAwsJsonProtocolFactory protocolFactory, JsonOperationMetadata operationMetadata) { return protocolFactory.createErrorResponseHandler(operationMetadata); diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-aws-query-compatible-json-sync-client-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-aws-query-compatible-json-sync-client-class.java index 65123e1e4c5c..42c98a423c77 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-aws-query-compatible-json-sync-client-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-aws-query-compatible-json-sync-client-class.java @@ -2,13 +2,10 @@ import java.util.Collections; import java.util.List; -import java.util.function.Consumer; import software.amazon.awssdk.annotations.Generated; import software.amazon.awssdk.annotations.SdkInternalApi; -import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; import software.amazon.awssdk.awscore.client.handler.AwsSyncClientHandler; import software.amazon.awssdk.awscore.exception.AwsServiceException; -import software.amazon.awssdk.core.ApiName; import software.amazon.awssdk.core.RequestOverrideConfiguration; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; import software.amazon.awssdk.core.client.config.SdkClientOption; @@ -17,7 +14,6 @@ import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.http.HttpResponseHandler; import software.amazon.awssdk.core.metrics.CoreMetric; -import software.amazon.awssdk.core.util.VersionInfo; import software.amazon.awssdk.metrics.MetricCollector; import software.amazon.awssdk.metrics.MetricPublisher; import software.amazon.awssdk.metrics.NoOpMetricCollector; @@ -30,7 +26,6 @@ import software.amazon.awssdk.services.querytojsoncompatible.model.APostOperationResponse; import software.amazon.awssdk.services.querytojsoncompatible.model.InvalidInputException; import software.amazon.awssdk.services.querytojsoncompatible.model.QueryToJsonCompatibleException; -import software.amazon.awssdk.services.querytojsoncompatible.model.QueryToJsonCompatibleRequest; import software.amazon.awssdk.services.querytojsoncompatible.transform.APostOperationRequestMarshaller; import software.amazon.awssdk.utils.HostnameValidator; import software.amazon.awssdk.utils.Logger; @@ -114,15 +109,6 @@ public APostOperationResponse aPostOperation(APostOperationRequest aPostOperatio } } - private T applyPaginatorUserAgent(T request) { - Consumer userAgentApplier = b -> b.addApiName(ApiName.builder() - .version(VersionInfo.SDK_VERSION).name("PAGINATED").build()); - AwsRequestOverrideConfiguration overrideConfiguration = request.overrideConfiguration() - .map(c -> c.toBuilder().applyMutation(userAgentApplier).build()) - .orElse((AwsRequestOverrideConfiguration.builder().applyMutation(userAgentApplier).build())); - return (T) request.toBuilder().overrideConfiguration(overrideConfiguration).build(); - } - @Override public final String serviceName() { return SERVICE_NAME; diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-async-client-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-async-client-class.java index a232ac78b4e0..4c480ea950ee 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-async-client-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-async-client-class.java @@ -25,7 +25,6 @@ import software.amazon.awssdk.awscore.eventstream.EventStreamTaggedUnionPojoSupplier; import software.amazon.awssdk.awscore.eventstream.RestEventStreamAsyncResponseTransformer; import software.amazon.awssdk.awscore.exception.AwsServiceException; -import software.amazon.awssdk.core.ApiName; import software.amazon.awssdk.core.CredentialType; import software.amazon.awssdk.core.RequestOverrideConfiguration; import software.amazon.awssdk.core.SdkPojoBuilder; @@ -48,7 +47,6 @@ import software.amazon.awssdk.core.protocol.VoidSdkResponse; import software.amazon.awssdk.core.runtime.transform.AsyncStreamingRequestMarshaller; import software.amazon.awssdk.core.signer.Signer; -import software.amazon.awssdk.core.util.VersionInfo; import software.amazon.awssdk.metrics.MetricCollector; import software.amazon.awssdk.metrics.MetricPublisher; import software.amazon.awssdk.metrics.NoOpMetricCollector; @@ -98,8 +96,6 @@ import software.amazon.awssdk.services.json.model.inputeventstream.DefaultInputEvent; import software.amazon.awssdk.services.json.model.inputeventstreamtwo.DefaultInputEventOne; import software.amazon.awssdk.services.json.model.inputeventstreamtwo.DefaultInputEventTwo; -import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPublisher; -import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPublisher; import software.amazon.awssdk.services.json.transform.APostOperationRequestMarshaller; import software.amazon.awssdk.services.json.transform.APostOperationWithOutputRequestMarshaller; import software.amazon.awssdk.services.json.transform.BearerAuthOperationRequestMarshaller; @@ -817,83 +813,6 @@ public CompletableFuture paginatedOpera } } - /** - * Some paginated operation with result_key in paginators.json file
- *

- * This is a variant of - * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} - * operation. The return type is a custom publisher that can be subscribed to request a stream of response pages. - * SDK will internally handle making service calls for you. - *

- *

- * When the operation is called, an instance of this class is returned. At this point, no service calls are made yet - * and so there is no guarantee that the request is valid. If there are errors in your request, you will see the - * failures only after you start streaming the data. The subscribe method should be called as a request to start - * streaming data. For more info, see - * {@link org.reactivestreams.Publisher#subscribe(org.reactivestreams.Subscriber)}. Each call to the subscribe - * method will result in a new {@link org.reactivestreams.Subscription} i.e., a new contract to stream data from the - * starting request. - *

- * - *

- * The following are few ways to use the response class: - *

- * 1) Using the subscribe helper method - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPublisher publisher = client.paginatedOperationWithResultKeyPaginator(request);
-     * CompletableFuture future = publisher.subscribe(res -> { // Do something with the response });
-     * future.get();
-     * }
-     * 
- * - * 2) Using a custom subscriber - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyPublisher publisher = client.paginatedOperationWithResultKeyPaginator(request);
-     * publisher.subscribe(new Subscriber() {
-     *
-     * public void onSubscribe(org.reactivestreams.Subscriber subscription) { //... };
-     *
-     *
-     * public void onNext(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyResponse response) { //... };
-     * });}
-     * 
- * - * As the response is a publisher, it can work well with third party reactive streams implementations like RxJava2. - *

- * Please notice that the configuration of MaxResults won't limit the number of results you get with the - * paginator. It only limits the number of results in each page. - *

- *

- * Note: If you prefer to have control on service calls, use the - * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} - * operation. - *

- * - * @param paginatedOperationWithResultKeyRequest - * @return A custom publisher that can be subscribed to request a stream of response pages.
- * The CompletableFuture returned by this method can be completed exceptionally with the following - * exceptions. - *
    - *
  • SdkException Base class for all exceptions that can be thrown by the SDK (both service and client). - * Can be used for catch all scenarios.
  • - *
  • SdkClientException If any client side error occurs such as an IO related failure, failure to get - * credentials, etc.
  • - *
  • JsonException Base class for all service exceptions. Unknown exceptions will be thrown as an instance - * of this type.
  • - *
- * @sample JsonAsyncClient.PaginatedOperationWithResultKey - * @see AWS API Documentation - */ - public PaginatedOperationWithResultKeyPublisher paginatedOperationWithResultKeyPaginator( - PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) { - return new PaginatedOperationWithResultKeyPublisher(this, applyPaginatorUserAgent(paginatedOperationWithResultKeyRequest)); - } - /** * Some paginated operation without result_key in paginators.json file * @@ -950,84 +869,6 @@ public CompletableFuture paginatedOp } } - /** - * Some paginated operation without result_key in paginators.json file
- *

- * This is a variant of - * {@link #paginatedOperationWithoutResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyRequest)} - * operation. The return type is a custom publisher that can be subscribed to request a stream of response pages. - * SDK will internally handle making service calls for you. - *

- *

- * When the operation is called, an instance of this class is returned. At this point, no service calls are made yet - * and so there is no guarantee that the request is valid. If there are errors in your request, you will see the - * failures only after you start streaming the data. The subscribe method should be called as a request to start - * streaming data. For more info, see - * {@link org.reactivestreams.Publisher#subscribe(org.reactivestreams.Subscriber)}. Each call to the subscribe - * method will result in a new {@link org.reactivestreams.Subscription} i.e., a new contract to stream data from the - * starting request. - *

- * - *

- * The following are few ways to use the response class: - *

- * 1) Using the subscribe helper method - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPublisher publisher = client.paginatedOperationWithoutResultKeyPaginator(request);
-     * CompletableFuture future = publisher.subscribe(res -> { // Do something with the response });
-     * future.get();
-     * }
-     * 
- * - * 2) Using a custom subscriber - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyPublisher publisher = client.paginatedOperationWithoutResultKeyPaginator(request);
-     * publisher.subscribe(new Subscriber() {
-     *
-     * public void onSubscribe(org.reactivestreams.Subscriber subscription) { //... };
-     *
-     *
-     * public void onNext(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyResponse response) { //... };
-     * });}
-     * 
- * - * As the response is a publisher, it can work well with third party reactive streams implementations like RxJava2. - *

- * Please notice that the configuration of MaxResults won't limit the number of results you get with the - * paginator. It only limits the number of results in each page. - *

- *

- * Note: If you prefer to have control on service calls, use the - * {@link #paginatedOperationWithoutResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyRequest)} - * operation. - *

- * - * @param paginatedOperationWithoutResultKeyRequest - * @return A custom publisher that can be subscribed to request a stream of response pages.
- * The CompletableFuture returned by this method can be completed exceptionally with the following - * exceptions. - *
    - *
  • SdkException Base class for all exceptions that can be thrown by the SDK (both service and client). - * Can be used for catch all scenarios.
  • - *
  • SdkClientException If any client side error occurs such as an IO related failure, failure to get - * credentials, etc.
  • - *
  • JsonException Base class for all service exceptions. Unknown exceptions will be thrown as an instance - * of this type.
  • - *
- * @sample JsonAsyncClient.PaginatedOperationWithoutResultKey - * @see AWS API Documentation - */ - public PaginatedOperationWithoutResultKeyPublisher paginatedOperationWithoutResultKeyPaginator( - PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) { - return new PaginatedOperationWithoutResultKeyPublisher(this, - applyPaginatorUserAgent(paginatedOperationWithoutResultKeyRequest)); - } - /** * Invokes the PutOperationWithChecksum operation asynchronously. * @@ -1396,15 +1237,6 @@ private static List resolveMetricPublishers(SdkClientConfigurat return publishers; } - private T applyPaginatorUserAgent(T request) { - Consumer userAgentApplier = b -> b.addApiName(ApiName.builder() - .version(VersionInfo.SDK_VERSION).name("PAGINATED").build()); - AwsRequestOverrideConfiguration overrideConfiguration = request.overrideConfiguration() - .map(c -> c.toBuilder().applyMutation(userAgentApplier).build()) - .orElse((AwsRequestOverrideConfiguration.builder().applyMutation(userAgentApplier).build())); - return (T) request.toBuilder().overrideConfiguration(overrideConfiguration).build(); - } - private T applySignerOverride(T request, Signer signer) { if (request.overrideConfiguration().flatMap(c -> c.signer()).isPresent()) { return request; diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-async-client-interface.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-async-client-interface.java index ad8845778eb1..3ec88bc9ca0a 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-async-client-interface.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-async-client-interface.java @@ -838,7 +838,7 @@ default PaginatedOperationWithResultKeyPublisher paginatedOperationWithResultKey */ default PaginatedOperationWithResultKeyPublisher paginatedOperationWithResultKeyPaginator( PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) { - throw new UnsupportedOperationException(); + return new PaginatedOperationWithResultKeyPublisher(this, paginatedOperationWithResultKeyRequest); } /** @@ -1056,7 +1056,7 @@ default CompletableFuture paginatedO */ default PaginatedOperationWithoutResultKeyPublisher paginatedOperationWithoutResultKeyPaginator( PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) { - throw new UnsupportedOperationException(); + return new PaginatedOperationWithoutResultKeyPublisher(this, paginatedOperationWithoutResultKeyRequest); } /** diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-client-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-client-class.java index 544fe2e977aa..54019ade037a 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-client-class.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-client-class.java @@ -10,7 +10,6 @@ import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; import software.amazon.awssdk.awscore.client.handler.AwsSyncClientHandler; import software.amazon.awssdk.awscore.exception.AwsServiceException; -import software.amazon.awssdk.core.ApiName; import software.amazon.awssdk.core.CredentialType; import software.amazon.awssdk.core.RequestOverrideConfiguration; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; @@ -27,7 +26,6 @@ import software.amazon.awssdk.core.signer.Signer; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.core.sync.ResponseTransformer; -import software.amazon.awssdk.core.util.VersionInfo; import software.amazon.awssdk.metrics.MetricCollector; import software.amazon.awssdk.metrics.MetricPublisher; import software.amazon.awssdk.metrics.NoOpMetricCollector; @@ -63,8 +61,6 @@ import software.amazon.awssdk.services.json.model.StreamingInputOutputOperationResponse; import software.amazon.awssdk.services.json.model.StreamingOutputOperationRequest; import software.amazon.awssdk.services.json.model.StreamingOutputOperationResponse; -import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable; -import software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyIterable; import software.amazon.awssdk.services.json.transform.APostOperationRequestMarshaller; import software.amazon.awssdk.services.json.transform.APostOperationWithOutputRequestMarshaller; import software.amazon.awssdk.services.json.transform.BearerAuthOperationRequestMarshaller; @@ -459,84 +455,6 @@ public PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey( } } - /** - * Some paginated operation with result_key in paginators.json file
- *

- * This is a variant of - * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} - * operation. The return type is a custom iterable that can be used to iterate through all the pages. SDK will - * internally handle making service calls for you. - *

- *

- * When this operation is called, a custom iterable is returned but no service calls are made yet. So there is no - * guarantee that the request is valid. As you iterate through the iterable, SDK will start lazily loading response - * pages by making service calls until there are no pages left or your iteration stops. If there are errors in your - * request, you will see the failures only after you start iterating through the iterable. - *

- * - *

- * The following are few ways to iterate through the response pages: - *

- * 1) Using a Stream - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable responses = client.paginatedOperationWithResultKeyPaginator(request);
-     * responses.stream().forEach(....);
-     * }
-     * 
- * - * 2) Using For loop - * - *
-     * {
-     *     @code
-     *     software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable responses = client
-     *             .paginatedOperationWithResultKeyPaginator(request);
-     *     for (software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyResponse response : responses) {
-     *         // do something;
-     *     }
-     * }
-     * 
- * - * 3) Use iterator directly - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithResultKeyIterable responses = client.paginatedOperationWithResultKeyPaginator(request);
-     * responses.iterator().forEachRemaining(....);
-     * }
-     * 
- *

- * Please notice that the configuration of MaxResults won't limit the number of results you get with the - * paginator. It only limits the number of results in each page. - *

- *

- * Note: If you prefer to have control on service calls, use the - * {@link #paginatedOperationWithResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithResultKeyRequest)} - * operation. - *

- * - * @param paginatedOperationWithResultKeyRequest - * @return A custom iterable that can be used to iterate through all the response pages. - * @throws SdkException - * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for - * catch all scenarios. - * @throws SdkClientException - * If any client side error occurs such as an IO related failure, failure to get credentials, etc. - * @throws JsonException - * Base class for all service exceptions. Unknown exceptions will be thrown as an instance of this type. - * @sample JsonClient.PaginatedOperationWithResultKey - * @see AWS API Documentation - */ - @Override - public PaginatedOperationWithResultKeyIterable paginatedOperationWithResultKeyPaginator( - PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) throws AwsServiceException, - SdkClientException, JsonException { - return new PaginatedOperationWithResultKeyIterable(this, applyPaginatorUserAgent(paginatedOperationWithResultKeyRequest)); - } - /** * Some paginated operation without result_key in paginators.json file * @@ -584,85 +502,6 @@ public PaginatedOperationWithoutResultKeyResponse paginatedOperationWithoutResul } } - /** - * Some paginated operation without result_key in paginators.json file
- *

- * This is a variant of - * {@link #paginatedOperationWithoutResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyRequest)} - * operation. The return type is a custom iterable that can be used to iterate through all the pages. SDK will - * internally handle making service calls for you. - *

- *

- * When this operation is called, a custom iterable is returned but no service calls are made yet. So there is no - * guarantee that the request is valid. As you iterate through the iterable, SDK will start lazily loading response - * pages by making service calls until there are no pages left or your iteration stops. If there are errors in your - * request, you will see the failures only after you start iterating through the iterable. - *

- * - *

- * The following are few ways to iterate through the response pages: - *

- * 1) Using a Stream - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyIterable responses = client.paginatedOperationWithoutResultKeyPaginator(request);
-     * responses.stream().forEach(....);
-     * }
-     * 
- * - * 2) Using For loop - * - *
-     * {
-     *     @code
-     *     software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyIterable responses = client
-     *             .paginatedOperationWithoutResultKeyPaginator(request);
-     *     for (software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyResponse response : responses) {
-     *         // do something;
-     *     }
-     * }
-     * 
- * - * 3) Use iterator directly - * - *
-     * {@code
-     * software.amazon.awssdk.services.json.paginators.PaginatedOperationWithoutResultKeyIterable responses = client.paginatedOperationWithoutResultKeyPaginator(request);
-     * responses.iterator().forEachRemaining(....);
-     * }
-     * 
- *

- * Please notice that the configuration of MaxResults won't limit the number of results you get with the - * paginator. It only limits the number of results in each page. - *

- *

- * Note: If you prefer to have control on service calls, use the - * {@link #paginatedOperationWithoutResultKey(software.amazon.awssdk.services.json.model.PaginatedOperationWithoutResultKeyRequest)} - * operation. - *

- * - * @param paginatedOperationWithoutResultKeyRequest - * @return A custom iterable that can be used to iterate through all the response pages. - * @throws SdkException - * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for - * catch all scenarios. - * @throws SdkClientException - * If any client side error occurs such as an IO related failure, failure to get credentials, etc. - * @throws JsonException - * Base class for all service exceptions. Unknown exceptions will be thrown as an instance of this type. - * @sample JsonClient.PaginatedOperationWithoutResultKey - * @see AWS API Documentation - */ - @Override - public PaginatedOperationWithoutResultKeyIterable paginatedOperationWithoutResultKeyPaginator( - PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) throws AwsServiceException, - SdkClientException, JsonException { - return new PaginatedOperationWithoutResultKeyIterable(this, - applyPaginatorUserAgent(paginatedOperationWithoutResultKeyRequest)); - } - /** * Invokes the PutOperationWithChecksum operation. * @@ -949,15 +788,6 @@ public JsonUtilities utilities() { return JsonUtilities.create(param1, param2, param3); } - private T applyPaginatorUserAgent(T request) { - Consumer userAgentApplier = b -> b.addApiName(ApiName.builder() - .version(VersionInfo.SDK_VERSION).name("PAGINATED").build()); - AwsRequestOverrideConfiguration overrideConfiguration = request.overrideConfiguration() - .map(c -> c.toBuilder().applyMutation(userAgentApplier).build()) - .orElse((AwsRequestOverrideConfiguration.builder().applyMutation(userAgentApplier).build())); - return (T) request.toBuilder().overrideConfiguration(overrideConfiguration).build(); - } - private T applySignerOverride(T request, Signer signer) { if (request.overrideConfiguration().flatMap(c -> c.signer()).isPresent()) { return request; diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-client-interface.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-client-interface.java index bf5cba1dce17..47d774dcfadd 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-client-interface.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/test-json-client-interface.java @@ -391,27 +391,6 @@ default OperationWithChecksumRequiredResponse operationWithChecksumRequired( .applyMutation(operationWithChecksumRequiredRequest).build()); } - /** - * Some paginated operation with result_key in paginators.json file - * - * @return Result of the PaginatedOperationWithResultKey operation returned by the service. - * @throws SdkException - * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for - * catch all scenarios. - * @throws SdkClientException - * If any client side error occurs such as an IO related failure, failure to get credentials, etc. - * @throws JsonException - * Base class for all service exceptions. Unknown exceptions will be thrown as an instance of this type. - * @sample JsonClient.PaginatedOperationWithResultKey - * @see #paginatedOperationWithResultKey(PaginatedOperationWithResultKeyRequest) - * @see AWS API Documentation - */ - default PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey() throws AwsServiceException, - SdkClientException, JsonException { - return paginatedOperationWithResultKey(PaginatedOperationWithResultKeyRequest.builder().build()); - } - /** * Some paginated operation with result_key in paginators.json file * @@ -463,6 +442,27 @@ default PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey( .applyMutation(paginatedOperationWithResultKeyRequest).build()); } + /** + * Some paginated operation with result_key in paginators.json file + * + * @return Result of the PaginatedOperationWithResultKey operation returned by the service. + * @throws SdkException + * Base class for all exceptions that can be thrown by the SDK (both service and client). Can be used for + * catch all scenarios. + * @throws SdkClientException + * If any client side error occurs such as an IO related failure, failure to get credentials, etc. + * @throws JsonException + * Base class for all service exceptions. Unknown exceptions will be thrown as an instance of this type. + * @sample JsonClient.PaginatedOperationWithResultKey + * @see #paginatedOperationWithResultKey(PaginatedOperationWithResultKeyRequest) + * @see AWS API Documentation + */ + default PaginatedOperationWithResultKeyResponse paginatedOperationWithResultKey() throws AwsServiceException, + SdkClientException, JsonException { + return paginatedOperationWithResultKey(PaginatedOperationWithResultKeyRequest.builder().build()); + } + /** * Some paginated operation with result_key in paginators.json file
*

@@ -613,7 +613,7 @@ default PaginatedOperationWithResultKeyIterable paginatedOperationWithResultKeyP default PaginatedOperationWithResultKeyIterable paginatedOperationWithResultKeyPaginator( PaginatedOperationWithResultKeyRequest paginatedOperationWithResultKeyRequest) throws AwsServiceException, SdkClientException, JsonException { - throw new UnsupportedOperationException(); + return new PaginatedOperationWithResultKeyIterable(this, paginatedOperationWithResultKeyRequest); } /** @@ -825,7 +825,7 @@ default PaginatedOperationWithoutResultKeyResponse paginatedOperationWithoutResu default PaginatedOperationWithoutResultKeyIterable paginatedOperationWithoutResultKeyPaginator( PaginatedOperationWithoutResultKeyRequest paginatedOperationWithoutResultKeyRequest) throws AwsServiceException, SdkClientException, JsonException { - throw new UnsupportedOperationException(); + return new PaginatedOperationWithoutResultKeyIterable(this, paginatedOperationWithoutResultKeyRequest); } /** diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/common/test-user-agent-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/common/test-user-agent-class.java new file mode 100644 index 000000000000..fabddb0d0cb0 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/common/test-user-agent-class.java @@ -0,0 +1,29 @@ +package software.amazon.awssdk.services.json.internal; + +import java.util.function.Consumer; +import software.amazon.awssdk.annotations.Generated; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; +import software.amazon.awssdk.core.ApiName; +import software.amazon.awssdk.core.util.VersionInfo; +import software.amazon.awssdk.services.json.model.JsonRequest; + +@Generated("software.amazon.awssdk:codegen") +@SdkInternalApi +public class UserAgentUtils { + private UserAgentUtils() { + } + + public static T applyUserAgentInfo(T request, + Consumer userAgentApplier) { + AwsRequestOverrideConfiguration overrideConfiguration = request.overrideConfiguration() + .map(c -> c.toBuilder().applyMutation(userAgentApplier).build()) + .orElse((AwsRequestOverrideConfiguration.builder().applyMutation(userAgentApplier).build())); + return (T) request.toBuilder().overrideConfiguration(overrideConfiguration).build(); + } + + public static T applyPaginatorUserAgent(T request) { + return applyUserAgentInfo(request, + b -> b.addApiName(ApiName.builder().version(VersionInfo.SDK_VERSION).name("PAGINATED").build())); + } +} \ No newline at end of file diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/model/serviceclientconfiguration.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/model/serviceclientconfiguration.java index 1220fcb9e0c0..572604798a8b 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/model/serviceclientconfiguration.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/model/serviceclientconfiguration.java @@ -5,6 +5,7 @@ import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.awscore.AwsServiceClientConfiguration; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.endpoints.EndpointProvider; import software.amazon.awssdk.regions.Region; /** @@ -45,6 +46,13 @@ public interface Builder extends AwsServiceClientConfiguration.Builder { */ @Override Builder overrideConfiguration(ClientOverrideConfiguration clientOverrideConfiguration); + + /** + * Configure the endpointProvider + */ + @Override + Builder endpointProvider(EndpointProvider endpointProvider); + } private static final class BuilderImpl extends AwsServiceClientConfiguration.BuilderImpl implements Builder { @@ -73,6 +81,12 @@ public Builder endpointOverride(URI endpointOverride) { return this; } + @Override + public Builder endpointProvider(EndpointProvider endpointProvider) { + this.endpointProvider = endpointProvider; + return this; + } + @Override public JsonProtocolTestsServiceClientConfiguration build() { return new JsonProtocolTestsServiceClientConfiguration(this); diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithResultKeyIterable.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithResultKeyIterable.java index fcf33f2186d3..23c08dd0a57f 100644 --- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithResultKeyIterable.java +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/paginators/PaginatedOperationWithResultKeyIterable.java @@ -10,6 +10,7 @@ import software.amazon.awssdk.core.pagination.sync.SyncPageFetcher; import software.amazon.awssdk.core.util.PaginatorUtils; import software.amazon.awssdk.services.jsonprotocoltests.JsonProtocolTestsClient; +import software.amazon.awssdk.services.jsonprotocoltests.internal.UserAgentUtils; import software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyRequest; import software.amazon.awssdk.services.jsonprotocoltests.model.PaginatedOperationWithResultKeyResponse; import software.amazon.awssdk.services.jsonprotocoltests.model.SimpleStruct; @@ -83,7 +84,7 @@ public class PaginatedOperationWithResultKeyIterable implements SdkIterable { private final Region region; private final Boolean useDualStackEndpoint; @@ -88,7 +90,11 @@ public String operationContextParam() { return operationContextParam; } - public interface Builder { + public Builder toBuilder() { + return new BuilderImpl(this); + } + + public interface Builder extends CopyableBuilder { Builder region(Region region); Builder useDualStackEndpoint(Boolean useDualStackEndpoint); @@ -134,6 +140,22 @@ private static class BuilderImpl implements Builder { private String operationContextParam; + private BuilderImpl() { + } + + private BuilderImpl(QueryEndpointParams builder) { + this.region = builder.region; + this.useDualStackEndpoint = builder.useDualStackEndpoint; + this.useFIPSEndpoint = builder.useFIPSEndpoint; + this.endpointId = builder.endpointId; + this.defaultTrueParam = builder.defaultTrueParam; + this.defaultStringParam = builder.defaultStringParam; + this.deprecatedParam = builder.deprecatedParam; + this.booleanContextParam = builder.booleanContextParam; + this.stringContextParam = builder.stringContextParam; + this.operationContextParam = builder.operationContextParam; + } + @Override public Builder region(Region region) { this.region = region; diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/AwsServiceClientConfiguration.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/AwsServiceClientConfiguration.java index 30698fd6222a..97a20a1e31d5 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/AwsServiceClientConfiguration.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/AwsServiceClientConfiguration.java @@ -20,6 +20,7 @@ import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.core.SdkServiceClientConfiguration; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.endpoints.EndpointProvider; import software.amazon.awssdk.regions.Region; /** @@ -81,6 +82,9 @@ public interface Builder extends SdkServiceClientConfiguration.Builder { @Override Builder endpointOverride(URI endpointOverride); + @Override + Builder endpointProvider(EndpointProvider endpointProvider); + @Override AwsServiceClientConfiguration build(); } @@ -89,6 +93,7 @@ protected abstract static class BuilderImpl implements Builder { protected ClientOverrideConfiguration overrideConfiguration; protected Region region; protected URI endpointOverride; + protected EndpointProvider endpointProvider; protected BuilderImpl() { } @@ -97,6 +102,7 @@ protected BuilderImpl(AwsServiceClientConfiguration awsServiceClientConfiguratio this.overrideConfiguration = awsServiceClientConfiguration.overrideConfiguration(); this.region = awsServiceClientConfiguration.region(); this.endpointOverride = awsServiceClientConfiguration.endpointOverride().orElse(null); + this.endpointProvider = awsServiceClientConfiguration.endpointProvider().orElse(null); } @Override @@ -113,6 +119,12 @@ public final Region region() { public final URI endpointOverride() { return endpointOverride; } + + @Override + public final EndpointProvider endpointProvider() { + return endpointProvider; + } + } } diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java index 32daaba3f3fb..07cff8936c7c 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java @@ -43,6 +43,7 @@ import software.amazon.awssdk.core.internal.InternalCoreExecutionAttribute; import software.amazon.awssdk.core.internal.util.HttpChecksumResolver; import software.amazon.awssdk.core.signer.Signer; +import software.amazon.awssdk.endpoints.EndpointProvider; import software.amazon.awssdk.metrics.MetricCollector; @SdkInternalApi @@ -94,7 +95,8 @@ private AwsExecutionContextBuilder() { .putAttribute(SdkExecutionAttribute.OPERATION_NAME, executionParams.getOperationName()) .putAttribute(SdkExecutionAttribute.CLIENT_ENDPOINT, clientConfig.option(SdkClientOption.ENDPOINT)) .putAttribute(SdkExecutionAttribute.ENDPOINT_OVERRIDDEN, clientConfig.option(SdkClientOption.ENDPOINT_OVERRIDDEN)) - .putAttribute(SdkInternalExecutionAttribute.ENDPOINT_PROVIDER, clientConfig.option(SdkClientOption.ENDPOINT_PROVIDER)) + .putAttribute(SdkInternalExecutionAttribute.ENDPOINT_PROVIDER, + resolveEndpointProvider(originalRequest, clientConfig)) .putAttribute(SdkInternalExecutionAttribute.CLIENT_CONTEXT_PARAMS, clientConfig.option(SdkClientOption.CLIENT_CONTEXT_PARAMS)) .putAttribute(SdkInternalExecutionAttribute.DISABLE_HOST_PREFIX_INJECTION, @@ -204,4 +206,18 @@ private static boolean isAuthenticatedRequest(ExecutionAttributes executionAttri return executionAttributes.getOptionalAttribute(SdkInternalExecutionAttribute.IS_NONE_AUTH_TYPE_REQUEST).orElse(true); } + + /** + * Resolves the endpoint provider, with the request override configuration taking precedence over the + * provided default client clientConfig. + * @return The endpoint provider that will be used by the SDK to resolve endpoints. + */ + private static EndpointProvider resolveEndpointProvider(SdkRequest request, + SdkClientConfiguration clientConfig) { + return request.overrideConfiguration() + .flatMap(RequestOverrideConfiguration::endpointProvider) + .orElse(clientConfig.option(SdkClientOption.ENDPOINT_PROVIDER)); + } + + } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/RequestOverrideConfiguration.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/RequestOverrideConfiguration.java index 1dd67b4a6ceb..cb4daf65922a 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/RequestOverrideConfiguration.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/RequestOverrideConfiguration.java @@ -31,6 +31,7 @@ import software.amazon.awssdk.core.interceptor.ExecutionAttribute; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.signer.Signer; +import software.amazon.awssdk.endpoints.EndpointProvider; import software.amazon.awssdk.metrics.MetricPublisher; import software.amazon.awssdk.utils.CollectionUtils; import software.amazon.awssdk.utils.Validate; @@ -51,6 +52,8 @@ public abstract class RequestOverrideConfiguration { private final List metricPublishers; private final ExecutionAttributes executionAttributes; + private final EndpointProvider endpointProvider; + protected RequestOverrideConfiguration(Builder builder) { this.headers = CollectionUtils.deepUnmodifiableMap(builder.headers(), () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)); this.rawQueryParameters = CollectionUtils.deepUnmodifiableMap(builder.rawQueryParameters()); @@ -60,6 +63,7 @@ protected RequestOverrideConfiguration(Builder builder) { this.signer = builder.signer(); this.metricPublishers = Collections.unmodifiableList(new ArrayList<>(builder.metricPublishers())); this.executionAttributes = ExecutionAttributes.unmodifiableExecutionAttributes(builder.executionAttributes()); + this.endpointProvider = builder.endpointProvider(); } /** @@ -153,6 +157,14 @@ public ExecutionAttributes executionAttributes() { return executionAttributes; } + /** + * Returns the endpoint provider for resolving the endpoint for this request. This supersedes the + * endpoint provider set on the client. + */ + public Optional endpointProvider() { + return Optional.ofNullable(endpointProvider); + } + @Override public boolean equals(Object o) { if (this == o) { @@ -169,7 +181,8 @@ public boolean equals(Object o) { Objects.equals(apiCallAttemptTimeout, that.apiCallAttemptTimeout) && Objects.equals(signer, that.signer) && Objects.equals(metricPublishers, that.metricPublishers) && - Objects.equals(executionAttributes, that.executionAttributes); + Objects.equals(executionAttributes, that.executionAttributes) && + Objects.equals(endpointProvider, that.endpointProvider); } @Override @@ -183,6 +196,7 @@ public int hashCode() { hashCode = 31 * hashCode + Objects.hashCode(signer); hashCode = 31 * hashCode + Objects.hashCode(metricPublishers); hashCode = 31 * hashCode + Objects.hashCode(executionAttributes); + hashCode = 31 * hashCode + Objects.hashCode(endpointProvider); return hashCode; } @@ -412,6 +426,18 @@ default B putRawQueryParameter(String name, String value) { ExecutionAttributes executionAttributes(); + /** + * Sets the endpointProvider to use for resolving the endpoint of the request. This endpointProvider gets priority + * over the endpointProvider set on the client while resolving the endpoint for the requests. + * If this value is null, then the client level endpointProvider is used for resolving the endpoint. + * + * @param endpointProvider Endpoint Provider that will override the resolving the endpoint for the request. + * @return This object for method chaining + */ + B endpointProvider(EndpointProvider endpointProvider); + + EndpointProvider endpointProvider(); + /** * Create a new {@code SdkRequestOverrideConfiguration} with the properties set on this builder. * @@ -430,6 +456,9 @@ protected abstract static class BuilderImpl implements Builde private List metricPublishers = new ArrayList<>(); private ExecutionAttributes.Builder executionAttributesBuilder = ExecutionAttributes.builder(); + private EndpointProvider endpointProvider; + + protected BuilderImpl() { } @@ -442,6 +471,7 @@ protected BuilderImpl(RequestOverrideConfiguration sdkRequestOverrideConfig) { signer(sdkRequestOverrideConfig.signer().orElse(null)); metricPublishers(sdkRequestOverrideConfig.metricPublishers()); executionAttributes(sdkRequestOverrideConfig.executionAttributes()); + endpointProvider(sdkRequestOverrideConfig.endpointProvider); } @Override @@ -595,5 +625,21 @@ public ExecutionAttributes executionAttributes() { public void setExecutionAttributes(ExecutionAttributes executionAttributes) { executionAttributes(executionAttributes); } + + + @Override + public B endpointProvider(EndpointProvider endpointProvider) { + this.endpointProvider = endpointProvider; + return (B) this; + } + + public void setEndpointProvider(EndpointProvider endpointProvider) { + endpointProvider(endpointProvider); + } + + @Override + public EndpointProvider endpointProvider() { + return endpointProvider; + } } } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkServiceClientConfiguration.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkServiceClientConfiguration.java index 26674d396b96..8928ab52d3fd 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkServiceClientConfiguration.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkServiceClientConfiguration.java @@ -20,6 +20,7 @@ import java.util.Optional; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.endpoints.EndpointProvider; /** * Class to expose SDK service client settings to the user, e.g., ClientOverrideConfiguration @@ -30,9 +31,12 @@ public abstract class SdkServiceClientConfiguration { private final ClientOverrideConfiguration overrideConfiguration; private final URI endpointOverride; + private final EndpointProvider endpointProvider; + protected SdkServiceClientConfiguration(Builder builder) { this.overrideConfiguration = builder.overrideConfiguration(); this.endpointOverride = builder.endpointOverride(); + this.endpointProvider = builder.endpointProvider(); } /** @@ -53,6 +57,16 @@ public Optional endpointOverride() { return Optional.ofNullable(this.endpointOverride); } + /** + * + * @return The configured endpoint provider of the SdkClient. If the endpoint provider was not configured, the default + * endpoint provider will be returned. + */ + public Optional endpointProvider() { + return Optional.ofNullable(this.endpointProvider); + } + + @Override public boolean equals(Object o) { if (this == o) { @@ -64,13 +78,15 @@ public boolean equals(Object o) { SdkServiceClientConfiguration serviceClientConfiguration = (SdkServiceClientConfiguration) o; return Objects.equals(overrideConfiguration, serviceClientConfiguration.overrideConfiguration()) - && Objects.equals(endpointOverride, serviceClientConfiguration.endpointOverride().orElse(null)); + && Objects.equals(endpointOverride, serviceClientConfiguration.endpointOverride().orElse(null)) + && Objects.equals(endpointProvider, serviceClientConfiguration.endpointProvider().orElse(null)); } @Override public int hashCode() { int result = overrideConfiguration != null ? overrideConfiguration.hashCode() : 0; result = 31 * result + (endpointOverride != null ? endpointOverride.hashCode() : 0); + result = 31 * result + (endpointProvider != null ? endpointProvider.hashCode() : 0); return result; } @@ -88,6 +104,8 @@ public interface Builder { */ URI endpointOverride(); + EndpointProvider endpointProvider(); + /** * Configure the client override configuration */ @@ -98,6 +116,9 @@ public interface Builder { */ Builder endpointOverride(URI endpointOverride); + + Builder endpointProvider(EndpointProvider endpointProvider); + /** * Build the service client configuration using the configuration on this builder */ diff --git a/services/s3/src/it/java/software/amazon/awssdk/services/s3/crossregion/S3CrossRegionAsyncIntegrationTest.java b/services/s3/src/it/java/software/amazon/awssdk/services/s3/crossregion/S3CrossRegionAsyncIntegrationTest.java new file mode 100644 index 000000000000..6fd43016dc01 --- /dev/null +++ b/services/s3/src/it/java/software/amazon/awssdk/services/s3/crossregion/S3CrossRegionAsyncIntegrationTest.java @@ -0,0 +1,52 @@ +/* + * 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.s3.crossregion; + +import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import software.amazon.awssdk.services.s3.S3AsyncClient; + +public class S3CrossRegionAsyncIntegrationTest extends S3CrossRegionAsyncIntegrationTestBase { + private static final String BUCKET = temporaryBucketName(S3CrossRegionAsyncIntegrationTest.class); + + @BeforeAll + static void setUpClass() { + s3 = s3ClientBuilder().build(); + createBucket(BUCKET); + } + + @AfterAll + static void clearClass() { + deleteBucketAndAllContents(BUCKET); + } + + @BeforeEach + public void initialize() { + crossRegionS3Client = S3AsyncClient.builder() + .region(CROSS_REGION) + .crossRegionAccessEnabled(true) + .build(); + } + + @Override + protected String bucketName() { + return BUCKET; + } + +} diff --git a/services/s3/src/it/java/software/amazon/awssdk/services/s3/crossregion/S3CrossRegionAsyncIntegrationTestBase.java b/services/s3/src/it/java/software/amazon/awssdk/services/s3/crossregion/S3CrossRegionAsyncIntegrationTestBase.java new file mode 100644 index 000000000000..ee1b82d4da4a --- /dev/null +++ b/services/s3/src/it/java/software/amazon/awssdk/services/s3/crossregion/S3CrossRegionAsyncIntegrationTestBase.java @@ -0,0 +1,77 @@ +/* + * 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.s3.crossregion; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; +import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; +import software.amazon.awssdk.services.s3.model.HeadBucketResponse; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.S3Object; +import software.amazon.awssdk.services.s3.paginators.ListObjectsV2Publisher; + +public abstract class S3CrossRegionAsyncIntegrationTestBase extends S3CrossRegionIntegrationTestBase{ + protected S3AsyncClient crossRegionS3Client; + + @Override + protected List paginatedAPICall(ListObjectsV2Request listObjectsV2Request) { + List resultObjects = new ArrayList<>(); + ListObjectsV2Publisher publisher = crossRegionS3Client.listObjectsV2Paginator(listObjectsV2Request); + CompletableFuture subscribe = publisher.subscribe(response -> { + response.contents().forEach(a -> resultObjects.add(a)); + }); + subscribe.join(); + return resultObjects; + } + + @Override + protected DeleteObjectsResponse postObjectAPICall(DeleteObjectsRequest deleteObjectsRequest) { + return crossRegionS3Client.deleteObjects(deleteObjectsRequest).join(); + } + + @Override + protected HeadBucketResponse headAPICall(HeadBucketRequest headBucketRequest) { + return crossRegionS3Client.headBucket(headBucketRequest).join(); + } + + @Override + protected DeleteObjectResponse deleteObjectAPICall(DeleteObjectRequest deleteObjectRequest) { + return crossRegionS3Client.deleteObject(deleteObjectRequest).join(); + } + + @Override + protected PutObjectResponse putAPICall(PutObjectRequest putObjectRequest, String testString) { + return crossRegionS3Client.putObject(putObjectRequest, AsyncRequestBody.fromString(testString)).join(); + } + + @Override + protected ResponseBytes getAPICall(GetObjectRequest getObjectRequest) { + return crossRegionS3Client.getObject(getObjectRequest, AsyncResponseTransformer.toBytes()).join(); + } +} diff --git a/services/s3/src/it/java/software/amazon/awssdk/services/s3/crossregion/S3CrossRegionCrtIntegrationTest.java b/services/s3/src/it/java/software/amazon/awssdk/services/s3/crossregion/S3CrossRegionCrtIntegrationTest.java new file mode 100644 index 000000000000..29c5d075be8d --- /dev/null +++ b/services/s3/src/it/java/software/amazon/awssdk/services/s3/crossregion/S3CrossRegionCrtIntegrationTest.java @@ -0,0 +1,51 @@ +/* + * 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.s3.crossregion; + +import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import software.amazon.awssdk.services.s3.S3AsyncClient; + +public class S3CrossRegionCrtIntegrationTest extends S3CrossRegionAsyncIntegrationTestBase { + private static final String BUCKET = temporaryBucketName(S3CrossRegionCrtIntegrationTest.class); + + @BeforeAll + static void setUpClass() { + s3 = s3ClientBuilder().build(); + createBucket(BUCKET); + } + + @AfterAll + static void clearClass() { + deleteBucketAndAllContents(BUCKET); + } + + @BeforeEach + public void initialize() { + crossRegionS3Client = S3AsyncClient.crtBuilder() + .region(CROSS_REGION) + .crossRegionAccessEnabled(true) + .build(); + } + @Override + protected String bucketName() { + return BUCKET; + } + +} diff --git a/services/s3/src/it/java/software/amazon/awssdk/services/s3/crossregion/S3CrossRegionIntegrationTestBase.java b/services/s3/src/it/java/software/amazon/awssdk/services/s3/crossregion/S3CrossRegionIntegrationTestBase.java new file mode 100644 index 000000000000..a8d391251bd5 --- /dev/null +++ b/services/s3/src/it/java/software/amazon/awssdk/services/s3/crossregion/S3CrossRegionIntegrationTestBase.java @@ -0,0 +1,141 @@ +/* + * 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.s3.crossregion; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3IntegrationTestBase; +import software.amazon.awssdk.services.s3.model.ChecksumAlgorithm; +import software.amazon.awssdk.services.s3.model.ChecksumMode; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; +import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; +import software.amazon.awssdk.services.s3.model.HeadBucketResponse; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.S3Object; + +public abstract class S3CrossRegionIntegrationTestBase extends S3IntegrationTestBase { + + public static final String X_AMZ_BUCKET_REGION = "x-amz-bucket-region"; + + protected static final Region CROSS_REGION = Region.of("eu-central-1"); + + private static final String KEY = "key"; + + @Test + void getApi_CrossRegionCall() { + s3.putObject(p -> p.bucket(bucketName()).checksumAlgorithm(ChecksumAlgorithm.CRC32).key(KEY), RequestBody.fromString( + "TEST_STRING")); + GetObjectRequest getObjectRequest = + GetObjectRequest.builder().bucket(bucketName()).checksumMode(ChecksumMode.ENABLED).key(KEY).build(); + ResponseBytes response = getAPICall(getObjectRequest); + assertThat(new String(response.asByteArray())).isEqualTo("TEST_STRING"); + } + + @Test + void putApi_CrossRegionCall() { + s3.putObject(p -> p.bucket(bucketName()).checksumAlgorithm(ChecksumAlgorithm.CRC32).key(KEY), RequestBody.fromString( + "TEST_STRING")); + PutObjectRequest putObjectRequest = + PutObjectRequest.builder().bucket(bucketName()).checksumAlgorithm(ChecksumAlgorithm.CRC32).key(KEY).build(); + PutObjectResponse response = putAPICall(putObjectRequest, "TEST_STRING"); + assertThat(response.checksumCRC32()).isEqualTo("S9ke8w=="); + } + + @Test + void deleteApi_CrossRegionCall() { + s3.putObject(p -> p.bucket(bucketName()).checksumAlgorithm(ChecksumAlgorithm.CRC32).key(KEY), RequestBody.fromString( + "TEST_STRING")); + DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder().bucket(bucketName()).key(KEY).build(); + DeleteObjectResponse response = deleteObjectAPICall(deleteObjectRequest); + assertThat(response).isNotNull(); + } + + @Test + void postApi_CrossRegionCall() { + s3.putObject(p -> p.bucket(bucketName()).checksumAlgorithm(ChecksumAlgorithm.CRC32).key(KEY), RequestBody.fromString( + "TEST_STRING")); + s3.putObject(p -> p.bucket(bucketName()).checksumAlgorithm(ChecksumAlgorithm.CRC32).key(KEY + "_1"), + RequestBody.fromString("TEST_STRING")); + DeleteObjectsRequest deleteObjectsRequest = + DeleteObjectsRequest.builder().bucket(bucketName()).delete(d -> d.objects(o -> o.key(KEY), o -> o.key(KEY + "_1"))).build(); + DeleteObjectsResponse response = postObjectAPICall(deleteObjectsRequest); + assertThat(response).isNotNull(); + } + + @Test + void cachedRegionGetsUsed_when_CrossRegionCall() { + putAPICall(PutObjectRequest.builder().bucket(bucketName()).checksumAlgorithm(ChecksumAlgorithm.CRC32).key(KEY).build(), + "TEST_STRING"); + GetObjectRequest getObjectRequest = + GetObjectRequest.builder().bucket(bucketName()).checksumMode(ChecksumMode.ENABLED).key(KEY).build(); + ResponseBytes response = getAPICall(getObjectRequest); + assertThat(new String(response.asByteArray())).isEqualTo("TEST_STRING"); + } + + @Test + void paginatedApi_CrossRegionCall() { + s3.deleteObject(p -> p.bucket(bucketName()).key(KEY)); + int maxKeys = 3; + int totalKeys = maxKeys * 2 ; + IntStream.range(0, totalKeys ) + .forEach( + i -> + s3.putObject(p -> p.bucket(bucketName()).checksumAlgorithm(ChecksumAlgorithm.CRC32).key(KEY + "_" + i), + RequestBody.fromString("TEST_STRING")) + ); + ListObjectsV2Request listObjectsV2Request = ListObjectsV2Request.builder().bucket(bucketName()).maxKeys(maxKeys).build(); + List s3ObjectList = paginatedAPICall(listObjectsV2Request); + assertThat(s3ObjectList).hasSize(totalKeys); + IntStream.range(0, totalKeys ).forEach(i -> s3.deleteObject(p -> p.bucket(bucketName()).key(KEY + "_" + i))); + } + + @Test + void headApi_CrossRegionCall() { + s3.putObject(p -> p.bucket(bucketName()).checksumAlgorithm(ChecksumAlgorithm.CRC32).key(KEY), RequestBody.fromString( + "TEST_STRING")); + HeadBucketRequest headBucketRequest = HeadBucketRequest.builder().bucket(bucketName()).build(); + HeadBucketResponse response = headAPICall(headBucketRequest); + assertThat(response).isNotNull(); + } + + protected abstract List paginatedAPICall(ListObjectsV2Request listObjectsV2Request); + + protected abstract DeleteObjectsResponse postObjectAPICall(DeleteObjectsRequest deleteObjectsRequest); + + protected abstract HeadBucketResponse headAPICall(HeadBucketRequest headBucketRequest); + + protected abstract DeleteObjectResponse deleteObjectAPICall(DeleteObjectRequest deleteObjectRequest); + + protected abstract PutObjectResponse putAPICall(PutObjectRequest putObjectRequest, String testString); + + protected abstract ResponseBytes getAPICall(GetObjectRequest getObjectRequest); + + protected abstract String bucketName(); + +} diff --git a/services/s3/src/it/java/software/amazon/awssdk/services/s3/crossregion/S3CrossRegionSyncIntegrationTest.java b/services/s3/src/it/java/software/amazon/awssdk/services/s3/crossregion/S3CrossRegionSyncIntegrationTest.java new file mode 100644 index 000000000000..e7e68b6dac63 --- /dev/null +++ b/services/s3/src/it/java/software/amazon/awssdk/services/s3/crossregion/S3CrossRegionSyncIntegrationTest.java @@ -0,0 +1,107 @@ +/* + * 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.s3.crossregion; + +import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.core.sync.ResponseTransformer; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectResponse; +import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; +import software.amazon.awssdk.services.s3.model.HeadBucketResponse; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; +import software.amazon.awssdk.services.s3.model.S3Object; + +public class S3CrossRegionSyncIntegrationTest extends S3CrossRegionIntegrationTestBase { + private static final String BUCKET = temporaryBucketName(S3CrossRegionSyncIntegrationTest.class); + private S3Client crossRegionS3Client; + + @BeforeAll + static void setUpClass() { + s3 = s3ClientBuilder().build(); + createBucket(BUCKET); + } + + @AfterAll + static void clearClass() { + deleteBucketAndAllContents(BUCKET); + } + + @BeforeEach + public void initialize() { + crossRegionS3Client = S3Client.builder() + .region(CROSS_REGION) + .crossRegionAccessEnabled(true) + .build(); + } + + @Override + protected List paginatedAPICall(ListObjectsV2Request listObjectsV2Request) { + List resultS3Object = new ArrayList<>(); + Iterator v2ResponseIterator = + crossRegionS3Client.listObjectsV2Paginator(listObjectsV2Request).iterator(); + while (v2ResponseIterator.hasNext()) { + v2ResponseIterator.next().contents().forEach(a -> resultS3Object.add(a)); + } + return resultS3Object; + } + + @Override + protected DeleteObjectsResponse postObjectAPICall(DeleteObjectsRequest deleteObjectsRequest) { + return crossRegionS3Client.deleteObjects(deleteObjectsRequest); + } + + @Override + protected HeadBucketResponse headAPICall(HeadBucketRequest headBucketRequest) { + return crossRegionS3Client.headBucket(headBucketRequest); + } + + @Override + protected DeleteObjectResponse deleteObjectAPICall(DeleteObjectRequest deleteObjectRequest) { + return crossRegionS3Client.deleteObject(deleteObjectRequest); + } + + @Override + protected PutObjectResponse putAPICall(PutObjectRequest putObjectRequest, String testString) { + return crossRegionS3Client.putObject(putObjectRequest, RequestBody.fromString(testString)); + } + + @Override + protected ResponseBytes getAPICall(GetObjectRequest getObjectRequest) { + return crossRegionS3Client.getObject(getObjectRequest, ResponseTransformer.toBytes()); + } + + @Override + protected String bucketName() { + return BUCKET; + } +} diff --git a/services/s3/src/it/java/software/amazon/awssdk/services/s3/crt/S3CrossRegionCrtIntegrationTest.java b/services/s3/src/it/java/software/amazon/awssdk/services/s3/crt/S3CrossRegionCrtIntegrationTest.java new file mode 100644 index 000000000000..953c6e4b4f4b --- /dev/null +++ b/services/s3/src/it/java/software/amazon/awssdk/services/s3/crt/S3CrossRegionCrtIntegrationTest.java @@ -0,0 +1,156 @@ +/* + * 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.s3.crt; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.awssdk.services.s3.crt.S3CrtClientCopyIntegrationTest.randomBytes; +import static software.amazon.awssdk.services.s3.utils.ChecksumUtils.computeCheckSum; +import static software.amazon.awssdk.testutils.service.S3BucketUtils.temporaryBucketName; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadLocalRandom; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.core.sync.ResponseTransformer; +import software.amazon.awssdk.crt.CrtResource; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.S3IntegrationTestBase; +import software.amazon.awssdk.services.s3.model.CopyObjectResponse; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.testutils.RandomTempFile; +import software.amazon.awssdk.testutils.service.AwsTestBase; + +public class S3CrossRegionCrtIntegrationTest extends S3IntegrationTestBase { + public static final Region CROSS_REGION = Region.EU_CENTRAL_1; + private static final String BUCKET = temporaryBucketName(S3CrossRegionCrtIntegrationTest.class); + private static final String KEY = "key"; + private static final String ORIGINAL_OBJ = "test_file.dat"; + private static final String COPIED_OBJ = "test_file_copy.dat"; + private static final long OBJ_SIZE = ThreadLocalRandom.current().nextLong(8 * 1024, 16 * 1024 + 1); + private static S3AsyncClient crtClient; + private static File file; + private static ExecutorService executorService; + + @BeforeAll + public static void setup() throws Exception { + S3IntegrationTestBase.setUp(); + S3IntegrationTestBase.createBucket(BUCKET); + crtClient = S3AsyncClient.crtBuilder() + .region(CROSS_REGION) + .crossRegionAccessEnabled(true) + .credentialsProvider(AwsTestBase.CREDENTIALS_PROVIDER_CHAIN) + .build(); + file = new RandomTempFile(10_000); + S3IntegrationTestBase.s3.putObject(PutObjectRequest.builder() + .bucket(BUCKET) + .key(KEY) + .build(), file.toPath()); + executorService = Executors.newFixedThreadPool(2); + } + + @AfterAll + public static void cleanup() { + crtClient.close(); + S3IntegrationTestBase.deleteBucketAndAllContents(BUCKET); + executorService.shutdown(); + CrtResource.waitForNoResources(); + } + + @Test + void crossRegionClient_getObject() throws IOException { + byte[] bytes = + crtClient.getObject(b -> b.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join().asByteArray(); + assertThat(bytes).isEqualTo(Files.readAllBytes(file.toPath())); + } + + @Test + void putObjectNoSuchBucket() { + assertThatThrownBy(() -> crtClient.getObject(GetObjectRequest.builder().bucket("nonExistingTestBucket" + UUID.randomUUID()).key(KEY).build(), + AsyncResponseTransformer.toBytes()).get()) + .hasCauseInstanceOf(S3Exception.class) + .satisfies(throwable -> assertThat(throwable.getCause()).satisfies(cause -> assertThat(((S3Exception) cause).statusCode()).isEqualTo(404))); + } + + @Test + void copy_copiedObject_hasSameContent() { + byte[] originalContent = randomBytes(OBJ_SIZE); + createOriginalObject(originalContent, ORIGINAL_OBJ); + copyObject(ORIGINAL_OBJ, COPIED_OBJ); + validateCopiedObject(originalContent, ORIGINAL_OBJ); + } + + private void copyObject(String original, String destination) { + CompletableFuture future = crtClient.copyObject(c -> c + .sourceBucket(BUCKET) + .sourceKey(original) + .destinationBucket(BUCKET) + .destinationKey(destination)); + + CopyObjectResponse copyObjectResponse = future.join(); + assertThat(copyObjectResponse.responseMetadata().requestId()).isNotNull(); + assertThat(copyObjectResponse.sdkHttpResponse()).isNotNull(); + } + + @Test + void putObject_byteBufferBody_objectSentCorrectly() { + byte[] data = new byte[16384]; + new Random().nextBytes(data); + ByteBuffer byteBuffer = ByteBuffer.wrap(data); + + AsyncRequestBody body = AsyncRequestBody.fromByteBuffer(byteBuffer); + + crtClient.putObject(r -> r.bucket(BUCKET).key(KEY), body).join(); + + ResponseBytes responseBytes = S3IntegrationTestBase.s3.getObject(r -> r.bucket(BUCKET).key(KEY), + ResponseTransformer.toBytes()); + + byte[] expectedSum = computeCheckSum(byteBuffer); + + assertThat(computeCheckSum(responseBytes.asByteBuffer())).isEqualTo(expectedSum); + } + + private void validateCopiedObject(byte[] originalContent, String originalKey) { + ResponseBytes copiedObject = s3.getObject(r -> r.bucket(BUCKET) + .key(originalKey), + ResponseTransformer.toBytes()); + assertThat(computeCheckSum(copiedObject.asByteBuffer())).isEqualTo(computeCheckSum(ByteBuffer.wrap(originalContent))); + } + + private void createOriginalObject(byte[] originalContent, String originalKey) { + crtClient.putObject(r -> r.bucket(BUCKET) + .key(originalKey), + AsyncRequestBody.fromBytes(originalContent)).join(); + } + +} diff --git a/services/s3/src/it/java/software/amazon/awssdk/services/s3/crt/S3CrtClientCopyIntegrationTest.java b/services/s3/src/it/java/software/amazon/awssdk/services/s3/crt/S3CrtClientCopyIntegrationTest.java index d0f92bb5b29a..f4d2b34c1cdf 100644 --- a/services/s3/src/it/java/software/amazon/awssdk/services/s3/crt/S3CrtClientCopyIntegrationTest.java +++ b/services/s3/src/it/java/software/amazon/awssdk/services/s3/crt/S3CrtClientCopyIntegrationTest.java @@ -166,7 +166,7 @@ private void validateCopiedObject(byte[] originalContent, String originalKey) { assertThat(computeCheckSum(copiedObject.asByteBuffer())).isEqualTo(computeCheckSum(ByteBuffer.wrap(originalContent))); } - private static byte[] randomBytes(long size) { + public static byte[] randomBytes(long size) { byte[] bytes = new byte[Math.toIntExact(size)]; ThreadLocalRandom.current().nextBytes(bytes); return bytes; diff --git a/services/s3/src/it/java/software/amazon/awssdk/services/s3/crt/S3CrtGetObjectIntegrationTest.java b/services/s3/src/it/java/software/amazon/awssdk/services/s3/crt/S3CrtGetObjectIntegrationTest.java index 5206a53448c6..a7ea924c5fe4 100644 --- a/services/s3/src/it/java/software/amazon/awssdk/services/s3/crt/S3CrtGetObjectIntegrationTest.java +++ b/services/s3/src/it/java/software/amazon/awssdk/services/s3/crt/S3CrtGetObjectIntegrationTest.java @@ -23,19 +23,15 @@ import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import software.amazon.awssdk.core.async.AsyncResponseTransformer; -import software.amazon.awssdk.core.async.SdkPublisher; import software.amazon.awssdk.crt.CrtResource; -import software.amazon.awssdk.http.async.SimpleSubscriber; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.S3IntegrationTestBase; -import software.amazon.awssdk.services.s3.internal.crt.S3CrtAsyncClient; import software.amazon.awssdk.services.s3.model.GetObjectResponse; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.testutils.RandomTempFile; @@ -97,35 +93,4 @@ void getObject_customResponseTransformer() { } - private static final class TestResponseTransformer implements AsyncResponseTransformer { - private CompletableFuture future; - - @Override - public CompletableFuture prepare() { - future = new CompletableFuture<>(); - return future; - } - - @Override - public void onResponse(GetObjectResponse response) { - assertThat(response).isNotNull(); - } - - @Override - public void onStream(SdkPublisher publisher) { - publisher.subscribe(new SimpleSubscriber(b -> { - }) { - @Override - public void onComplete() { - super.onComplete(); - future.complete(null); - } - }); - } - - @Override - public void exceptionOccurred(Throwable error) { - future.completeExceptionally(error); - } - } } diff --git a/services/s3/src/it/java/software/amazon/awssdk/services/s3/crt/TestResponseTransformer.java b/services/s3/src/it/java/software/amazon/awssdk/services/s3/crt/TestResponseTransformer.java new file mode 100644 index 000000000000..6b7e8848af45 --- /dev/null +++ b/services/s3/src/it/java/software/amazon/awssdk/services/s3/crt/TestResponseTransformer.java @@ -0,0 +1,57 @@ +/* + * 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.s3.crt; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.ByteBuffer; +import java.util.concurrent.CompletableFuture; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.core.async.SdkPublisher; +import software.amazon.awssdk.http.async.SimpleSubscriber; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; + +public final class TestResponseTransformer implements AsyncResponseTransformer { + private CompletableFuture future; + + @Override + public CompletableFuture prepare() { + future = new CompletableFuture<>(); + return future; + } + + @Override + public void onResponse(GetObjectResponse response) { + assertThat(response).isNotNull(); + } + + @Override + public void onStream(SdkPublisher publisher) { + publisher.subscribe(new SimpleSubscriber(b -> { + }) { + @Override + public void onComplete() { + super.onComplete(); + future.complete(null); + } + }); + } + + @Override + public void exceptionOccurred(Throwable error) { + future.completeExceptionally(error); + } +} \ No newline at end of file diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3CrtAsyncClientBuilder.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3CrtAsyncClientBuilder.java index ee2434cc9ac4..e3131ba7aca7 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3CrtAsyncClientBuilder.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3CrtAsyncClientBuilder.java @@ -212,9 +212,27 @@ default S3CrtAsyncClientBuilder retryConfiguration(Consumer Configures whether cross-region bucket access is enabled for clients using the configuration. + *

The following behavior is used when this mode is enabled: + *

    + *
  1. This method allows enabling or disabling cross-region bucket access for clients. When cross-region bucket + * access is enabled, requests that do not act on an existing bucket (e.g., createBucket API) will be routed to the + * region configured on the client
  2. + *
  3. The first time a request is made that references an existing bucket (e.g., putObject API), a request will be + * made to the client-configured region. If the bucket does not exist in this region, the service will include the + * actual region in the error responses. Subsequently, the API will be called using the correct region obtained + * from the error response.
  4. + *
  5. This location may be cached in the client for subsequent requests to the same bucket.
  6. + *
+ *

Enabling this mode has several drawbacks, as it can increase latency if the bucket's location is physically far + * from the location of the request.Therefore, it is strongly advised, whenever possible, to know the location of your + * buckets and create a region-specific client to access them + * + * @param crossRegionAccessEnabled Whether cross region bucket access should be enabled. + * @return The builder object for method chaining. + */ + S3CrtAsyncClientBuilder crossRegionAccessEnabled(boolean crossRegionAccessEnabled); @Override S3AsyncClient build(); diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java index 3e9c32b0ff4b..6fcc596ced2b 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java @@ -499,7 +499,6 @@ private AttributeMap createClientContextParams() { !s3Configuration.multiRegionEnabled()); params.put(S3ClientContextParams.FORCE_PATH_STYLE, s3Configuration.pathStyleAccessEnabled()); params.put(S3ClientContextParams.ACCELERATE, s3Configuration.accelerateModeEnabled()); - return params.build(); } diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/client/S3AsyncClientDecorator.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/client/S3AsyncClientDecorator.java new file mode 100644 index 000000000000..2dbb61091da2 --- /dev/null +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/client/S3AsyncClientDecorator.java @@ -0,0 +1,49 @@ +/* + * 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.s3.internal.client; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.endpoints.S3ClientContextParams; +import software.amazon.awssdk.services.s3.internal.crossregion.S3CrossRegionAsyncClient; +import software.amazon.awssdk.utils.AttributeMap; +import software.amazon.awssdk.utils.ConditionalDecorator; + +@SdkInternalApi +public class S3AsyncClientDecorator { + + public S3AsyncClientDecorator() { + } + + public S3AsyncClient decorate(S3AsyncClient base, + SdkClientConfiguration clientConfiguration, + AttributeMap clientContextParams) { + List> decorators = new ArrayList<>(); + decorators.add(ConditionalDecorator.create(isCrossRegionEnabledAsync(clientContextParams), + S3CrossRegionAsyncClient::new)); + return ConditionalDecorator.decorate(base, decorators); + } + + private Predicate isCrossRegionEnabledAsync(AttributeMap clientContextParams) { + Boolean crossRegionEnabled = clientContextParams.get(S3ClientContextParams.CROSS_REGION_ACCESS_ENABLED); + return client -> crossRegionEnabled != null && crossRegionEnabled.booleanValue(); + } + +} diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/client/S3SyncClientDecorator.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/client/S3SyncClientDecorator.java new file mode 100644 index 000000000000..0aa80cd5e253 --- /dev/null +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/client/S3SyncClientDecorator.java @@ -0,0 +1,49 @@ +/* + * 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.s3.internal.client; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.endpoints.S3ClientContextParams; +import software.amazon.awssdk.services.s3.internal.crossregion.S3CrossRegionSyncClient; +import software.amazon.awssdk.utils.AttributeMap; +import software.amazon.awssdk.utils.ConditionalDecorator; + +@SdkInternalApi +public class S3SyncClientDecorator { + + public S3SyncClientDecorator() { + } + + public S3Client decorate(S3Client base, + SdkClientConfiguration clientConfiguration, + AttributeMap clientContextParams) { + List> decorators = new ArrayList<>(); + decorators.add(ConditionalDecorator.create(isCrossRegionEnabledSync(clientContextParams), + S3CrossRegionSyncClient::new)); + + return ConditionalDecorator.decorate(base, decorators); + } + + private Predicate isCrossRegionEnabledSync(AttributeMap clientContextParams) { + Boolean crossRegionEnabled = clientContextParams.get(S3ClientContextParams.CROSS_REGION_ACCESS_ENABLED); + return client -> crossRegionEnabled != null && crossRegionEnabled.booleanValue(); + } +} diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crossregion/S3CrossRegionAsyncClient.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crossregion/S3CrossRegionAsyncClient.java new file mode 100644 index 000000000000..f8efd780b351 --- /dev/null +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crossregion/S3CrossRegionAsyncClient.java @@ -0,0 +1,148 @@ +/* + * 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.s3.internal.crossregion; + +import static software.amazon.awssdk.services.s3.internal.crossregion.utils.CrossRegionUtils.getBucketRegionFromException; +import static software.amazon.awssdk.services.s3.internal.crossregion.utils.CrossRegionUtils.isS3RedirectException; +import static software.amazon.awssdk.services.s3.internal.crossregion.utils.CrossRegionUtils.requestWithDecoratedEndpointProvider; +import static software.amazon.awssdk.services.s3.internal.crossregion.utils.CrossRegionUtils.updateUserAgentInConfig; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; +import java.util.function.Function; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.DelegatingS3AsyncClient; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.services.s3.model.S3Request; +import software.amazon.awssdk.utils.CompletableFutureUtils; + +@SdkInternalApi +public final class S3CrossRegionAsyncClient extends DelegatingS3AsyncClient { + + private final Map bucketToRegionCache = new ConcurrentHashMap<>(); + + public S3CrossRegionAsyncClient(S3AsyncClient s3Client) { + super(s3Client); + } + + @Override + protected CompletableFuture invokeOperation( + T request, Function> operation) { + + Optional bucket = request.getValueForField("Bucket", String.class); + + AwsRequestOverrideConfiguration overrideConfiguration = updateUserAgentInConfig(request); + T userAgentUpdatedRequest = (T) request.toBuilder().overrideConfiguration(overrideConfiguration).build(); + + if (!bucket.isPresent()) { + return operation.apply(userAgentUpdatedRequest); + } + String bucketName = bucket.get(); + + CompletableFuture returnFuture = new CompletableFuture<>(); + CompletableFuture apiOperationFuture = bucketToRegionCache.containsKey(bucketName) ? + operation.apply( + requestWithDecoratedEndpointProvider( + userAgentUpdatedRequest, + () -> bucketToRegionCache.get(bucketName), + serviceClientConfiguration().endpointProvider().get() + ) + ) : + operation.apply(userAgentUpdatedRequest); + + apiOperationFuture.whenComplete(redirectToCrossRegionIfRedirectException(operation, + userAgentUpdatedRequest, + bucketName, + returnFuture)); + return returnFuture; + } + + private BiConsumer redirectToCrossRegionIfRedirectException( + Function> operation, + T userAgentUpdatedRequest, String bucketName, + CompletableFuture returnFuture) { + + return (response, throwable) -> { + if (throwable != null) { + if (isS3RedirectException(throwable)) { + bucketToRegionCache.remove(bucketName); + requestWithCrossRegion(userAgentUpdatedRequest, operation, bucketName, returnFuture, throwable); + } else { + returnFuture.completeExceptionally(throwable); + } + } else { + returnFuture.complete(response); + } + }; + } + + private void requestWithCrossRegion(T request, + Function> operation, + String bucketName, + CompletableFuture returnFuture, + Throwable throwable) { + + Optional bucketRegionFromException = getBucketRegionFromException((S3Exception) throwable.getCause()); + if (bucketRegionFromException.isPresent()) { + sendRequestWithRightRegion(request, operation, bucketName, returnFuture, bucketRegionFromException.get()); + } else { + fetchRegionAndSendRequest(request, operation, bucketName, returnFuture); + } + } + + private void fetchRegionAndSendRequest(T request, + Function> operation, + String bucketName, + CompletableFuture returnFuture) { + // // TODO: Need to change codegen of Delegating Client to avoid the cast, have taken a backlog item to fix this. + ((S3AsyncClient) delegate()).headBucket(b -> b.bucket(bucketName)).whenComplete((response, + throwable) -> { + if (throwable != null) { + if (isS3RedirectException(throwable)) { + bucketToRegionCache.remove(bucketName); + Optional bucketRegion = getBucketRegionFromException((S3Exception) throwable.getCause()); + if (bucketRegion.isPresent()) { + sendRequestWithRightRegion(request, operation, bucketName, returnFuture, bucketRegion.get()); + } else { + returnFuture.completeExceptionally(throwable); + } + } else { + returnFuture.completeExceptionally(throwable); + } + } + }); + } + + private void sendRequestWithRightRegion(T request, + Function> operation, + String bucketName, + CompletableFuture returnFuture, + String region) { + bucketToRegionCache.put(bucketName, Region.of(region)); + CompletableFuture newFuture = operation.apply( + requestWithDecoratedEndpointProvider(request, + () -> Region.of(region), + serviceClientConfiguration().endpointProvider().get())); + CompletableFutureUtils.forwardResultTo(newFuture, returnFuture); + CompletableFutureUtils.forwardExceptionTo(returnFuture, newFuture); + } +} \ No newline at end of file diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crossregion/S3CrossRegionSyncClient.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crossregion/S3CrossRegionSyncClient.java new file mode 100644 index 000000000000..10ac10af5816 --- /dev/null +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crossregion/S3CrossRegionSyncClient.java @@ -0,0 +1,107 @@ +/* + * 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.s3.internal.crossregion; + +import static software.amazon.awssdk.services.s3.internal.crossregion.utils.CrossRegionUtils.getBucketRegionFromException; +import static software.amazon.awssdk.services.s3.internal.crossregion.utils.CrossRegionUtils.isS3RedirectException; +import static software.amazon.awssdk.services.s3.internal.crossregion.utils.CrossRegionUtils.requestWithDecoratedEndpointProvider; +import static software.amazon.awssdk.services.s3.internal.crossregion.utils.CrossRegionUtils.updateUserAgentInConfig; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.DelegatingS3Client; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.services.s3.model.S3Request; + +/** + * Decorator S3 Sync client that will fetch the region name whenever there is Redirect 301 error due to cross region bucket + * access. + */ +@SdkInternalApi +public final class S3CrossRegionSyncClient extends DelegatingS3Client { + + private final Map bucketToRegionCache = new ConcurrentHashMap<>(); + + public S3CrossRegionSyncClient(S3Client s3Client) { + super(s3Client); + } + + private static Optional bucketNameFromRequest(T request) { + return request.getValueForField("Bucket", String.class); + } + + @Override + protected ReturnT invokeOperation(T request, Function operation) { + + Optional bucketRequest = bucketNameFromRequest(request); + + AwsRequestOverrideConfiguration overrideConfiguration = updateUserAgentInConfig(request); + T userAgentUpdatedRequest = (T) request.toBuilder().overrideConfiguration(overrideConfiguration).build(); + + + if (!bucketRequest.isPresent()) { + return operation.apply(userAgentUpdatedRequest); + } + String bucketName = bucketRequest.get(); + try { + if (bucketToRegionCache.containsKey(bucketName)) { + return operation.apply( + requestWithDecoratedEndpointProvider(userAgentUpdatedRequest, + () -> bucketToRegionCache.get(bucketName), + serviceClientConfiguration().endpointProvider().get())); + } + return operation.apply(userAgentUpdatedRequest); + } catch (S3Exception exception) { + if (isS3RedirectException(exception)) { + updateCacheFromRedirectException(exception, bucketName); + return operation.apply( + requestWithDecoratedEndpointProvider( + userAgentUpdatedRequest, + () -> bucketToRegionCache.computeIfAbsent(bucketName, this::fetchBucketRegion), + serviceClientConfiguration().endpointProvider().get())); + } + throw exception; + } + } + + private void updateCacheFromRedirectException(S3Exception exception, String bucketName) { + Optional regionStr = getBucketRegionFromException(exception); + // If redirected, clear previous values due to region change. + bucketToRegionCache.remove(bucketName); + regionStr.ifPresent(region -> bucketToRegionCache.put(bucketName, Region.of(region))); + } + + private Region fetchBucketRegion(String bucketName) { + try { + ((S3Client) delegate()).headBucket(HeadBucketRequest.builder().bucket(bucketName).build()); + } catch (S3Exception exception) { + if (isS3RedirectException(exception)) { + return Region.of(getBucketRegionFromException(exception).orElseThrow(() -> exception)); + } + throw exception; + } + return null; + } + + +} diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crossregion/endpointprovider/BucketEndpointProvider.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crossregion/endpointprovider/BucketEndpointProvider.java new file mode 100644 index 000000000000..ce89341753b5 --- /dev/null +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crossregion/endpointprovider/BucketEndpointProvider.java @@ -0,0 +1,50 @@ +/* + * 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.s3.internal.crossregion.endpointprovider; + +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.endpoints.S3EndpointParams; +import software.amazon.awssdk.services.s3.endpoints.S3EndpointProvider; + +/** + * Decorator S3EndpointProvider which updates the region with the one that is supplied during its instantiation. + */ +@SdkInternalApi +public class BucketEndpointProvider implements S3EndpointProvider { + private final S3EndpointProvider delegateEndPointProvider; + private final Supplier regionSupplier; + + private BucketEndpointProvider(S3EndpointProvider delegateEndPointProvider, Supplier regionSupplier) { + this.delegateEndPointProvider = delegateEndPointProvider; + this.regionSupplier = regionSupplier; + } + + public static BucketEndpointProvider create(S3EndpointProvider delegateEndPointProvider, Supplier regionSupplier) { + return new BucketEndpointProvider(delegateEndPointProvider, regionSupplier); + } + + @Override + public CompletableFuture resolveEndpoint(S3EndpointParams endpointParams) { + Region crossRegion = regionSupplier.get(); + return delegateEndPointProvider.resolveEndpoint( + crossRegion != null ? endpointParams.copy(c -> c.region(crossRegion)) : endpointParams); + } +} + diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crossregion/utils/CrossRegionUtils.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crossregion/utils/CrossRegionUtils.java new file mode 100644 index 000000000000..5c4413997671 --- /dev/null +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crossregion/utils/CrossRegionUtils.java @@ -0,0 +1,84 @@ +/* + * 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.s3.internal.crossregion.utils; + + +import java.util.Optional; +import java.util.concurrent.CompletionException; +import java.util.function.Consumer; +import java.util.function.Supplier; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; +import software.amazon.awssdk.core.ApiName; +import software.amazon.awssdk.endpoints.EndpointProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.endpoints.S3EndpointProvider; +import software.amazon.awssdk.services.s3.internal.crossregion.endpointprovider.BucketEndpointProvider; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.services.s3.model.S3Request; + +@SdkInternalApi +public final class CrossRegionUtils { + public static final int REDIRECT_STATUS_CODE = 301; + public static final String AMZ_BUCKET_REGION_HEADER = "x-amz-bucket-region"; + private static final ApiName API_NAME = ApiName.builder().version("cross-region").name("hll").build(); + private static final Consumer USER_AGENT_APPLIER = b -> b.addApiName(API_NAME); + + + private CrossRegionUtils() { + } + + public static Optional getBucketRegionFromException(S3Exception exception) { + return exception.awsErrorDetails() + .sdkHttpResponse() + .firstMatchingHeader(AMZ_BUCKET_REGION_HEADER); + } + + public static boolean isS3RedirectException(Throwable exception) { + Throwable exceptionToBeChecked = exception instanceof CompletionException ? exception.getCause() : exception ; + return exceptionToBeChecked instanceof S3Exception + && ((S3Exception) exceptionToBeChecked).statusCode() == REDIRECT_STATUS_CODE; + } + + + @SuppressWarnings("unchecked") + public static T requestWithDecoratedEndpointProvider(T request, Supplier regionSupplier, + EndpointProvider clientEndpointProvider) { + AwsRequestOverrideConfiguration requestOverrideConfig = + request.overrideConfiguration().orElseGet(() -> AwsRequestOverrideConfiguration.builder().build()); + + S3EndpointProvider delegateEndpointProvider = (S3EndpointProvider) requestOverrideConfig.endpointProvider() + .orElse(clientEndpointProvider); + return (T) request.toBuilder() + .overrideConfiguration( + requestOverrideConfig.toBuilder() + .endpointProvider( + BucketEndpointProvider.create(delegateEndpointProvider, regionSupplier)) + .build()) + .build(); + } + + public static AwsRequestOverrideConfiguration updateUserAgentInConfig(T request) { + AwsRequestOverrideConfiguration overrideConfiguration = + request.overrideConfiguration().map(c -> c.toBuilder() + .applyMutation(USER_AGENT_APPLIER) + .build()) + .orElse(AwsRequestOverrideConfiguration.builder() + .applyMutation(USER_AGENT_APPLIER) + .build()); + return overrideConfiguration; + } +} diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClient.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClient.java index 860ac509932e..936ab7b94fac 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClient.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClient.java @@ -18,6 +18,7 @@ import static software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute.SDK_HTTP_EXECUTION_ATTRIBUTES; import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.HTTP_CHECKSUM; import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.OPERATION_NAME; +import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.SIGNING_REGION; import static software.amazon.awssdk.services.s3.internal.crt.S3NativeClientConfiguration.DEFAULT_PART_SIZE_IN_BYTES; import java.net.URI; @@ -27,6 +28,7 @@ import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute; import software.amazon.awssdk.awscore.AwsRequest; import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; import software.amazon.awssdk.core.SdkRequest; @@ -101,6 +103,7 @@ private static S3AsyncClient initializeS3AsyncClient(DefaultS3CrtClientBuilder b .overrideConfiguration(overrideConfigurationBuilder.build()) .accelerate(builder.accelerate) .forcePathStyle(builder.forcePathStyle) + .crossRegionAccessEnabled(builder.crossRegionAccessEnabled) .httpClientBuilder(initializeS3CrtAsyncHttpClient(builder)) .build(); } @@ -149,6 +152,7 @@ public static final class DefaultS3CrtClientBuilder implements S3CrtAsyncClientB private List executionInterceptors; private S3CrtRetryConfiguration retryConfiguration; + private boolean crossRegionAccessEnabled; public AwsCredentialsProvider credentialsProvider() { return credentialsProvider; @@ -178,6 +182,10 @@ public Long readBufferSizeInBytes() { return readBufferSizeInBytes; } + public boolean crossRegionAccessEnabled() { + return crossRegionAccessEnabled; + } + @Override public S3CrtAsyncClientBuilder credentialsProvider(AwsCredentialsProvider credentialsProvider) { this.credentialsProvider = credentialsProvider; @@ -259,6 +267,12 @@ public S3CrtAsyncClientBuilder retryConfiguration(S3CrtRetryConfiguration retryC return this; } + @Override + public S3CrtAsyncClientBuilder crossRegionAccessEnabled(boolean crossRegionAccessEnabled) { + this.crossRegionAccessEnabled = crossRegionAccessEnabled; + return this; + } + @Override public S3CrtAsyncClient build() { return new DefaultS3CrtAsyncClient(this); @@ -280,6 +294,7 @@ public void afterMarshalling(Context.AfterMarshalling context, builder.put(OPERATION_NAME, executionAttributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME)) .put(HTTP_CHECKSUM, executionAttributes.getAttribute(SdkInternalExecutionAttribute.HTTP_CHECKSUM)) + .put(SIGNING_REGION, executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNING_REGION)) .build(); // For putObject and getObject, we rely on CRT to perform checksum validation diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3CrtAsyncHttpClient.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3CrtAsyncHttpClient.java index 9681c8c14f94..149471f30179 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3CrtAsyncHttpClient.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3CrtAsyncHttpClient.java @@ -20,6 +20,7 @@ import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.HTTP_CHECKSUM; import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.METAREQUEST_PAUSE_OBSERVABLE; import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.OPERATION_NAME; +import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.SIGNING_REGION; import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely; import java.net.URI; @@ -31,6 +32,7 @@ import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; import software.amazon.awssdk.core.interceptor.trait.HttpChecksum; +import software.amazon.awssdk.crt.auth.signing.AwsSigningConfig; import software.amazon.awssdk.crt.http.HttpHeader; import software.amazon.awssdk.crt.http.HttpRequest; import software.amazon.awssdk.crt.s3.ChecksumConfig; @@ -43,6 +45,7 @@ import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.async.AsyncExecuteRequest; import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.Logger; import software.amazon.awssdk.utils.NumericUtils; @@ -117,6 +120,7 @@ public CompletableFuture execute(AsyncExecuteRequest asyncRequest) { HttpChecksum httpChecksum = asyncRequest.httpExecutionAttributes().getAttribute(HTTP_CHECKSUM); ResumeToken resumeToken = asyncRequest.httpExecutionAttributes().getAttribute(CRT_PAUSE_RESUME_TOKEN); + Region signingRegion = asyncRequest.httpExecutionAttributes().getAttribute(SIGNING_REGION); ChecksumConfig checksumConfig = checksumConfig(httpChecksum, requestType, s3NativeClientConfiguration.checksumValidationEnabled()); @@ -130,6 +134,13 @@ public CompletableFuture execute(AsyncExecuteRequest asyncRequest) { .withResponseHandler(responseHandler) .withResumeToken(resumeToken); + // Create a new SigningConfig object only if the signing region has changed from the previously configured region. + if (signingRegion != null && !s3ClientOptions.getRegion().equals(signingRegion.id())) { + requestOptions.withSigningConfig( + AwsSigningConfig.getDefaultS3SigningConfig(signingRegion.id(), + s3ClientOptions.getCredentialsProvider())); + } + S3MetaRequest s3MetaRequest = crtS3Client.makeMetaRequest(requestOptions); S3MetaRequestPauseObservable observable = asyncRequest.httpExecutionAttributes().getAttribute(METAREQUEST_PAUSE_OBSERVABLE); @@ -144,6 +155,7 @@ public CompletableFuture execute(AsyncExecuteRequest asyncRequest) { return executeFuture; } + private static URI getEndpoint(URI uri) { return invokeSafely(() -> new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), null, null, null)); } diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3InternalSdkHttpExecutionAttribute.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3InternalSdkHttpExecutionAttribute.java index 763cc874cc86..f7b817ab9ad2 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3InternalSdkHttpExecutionAttribute.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/crt/S3InternalSdkHttpExecutionAttribute.java @@ -19,6 +19,7 @@ import software.amazon.awssdk.core.interceptor.trait.HttpChecksum; import software.amazon.awssdk.crt.s3.ResumeToken; import software.amazon.awssdk.http.SdkHttpExecutionAttribute; +import software.amazon.awssdk.regions.Region; @SdkInternalApi public final class S3InternalSdkHttpExecutionAttribute extends SdkHttpExecutionAttribute { @@ -37,6 +38,9 @@ public final class S3InternalSdkHttpExecutionAttribute extends SdkHttpExecuti public static final S3InternalSdkHttpExecutionAttribute CRT_PAUSE_RESUME_TOKEN = new S3InternalSdkHttpExecutionAttribute<>(ResumeToken.class); + public static final S3InternalSdkHttpExecutionAttribute SIGNING_REGION = + new S3InternalSdkHttpExecutionAttribute<>(Region.class); + private S3InternalSdkHttpExecutionAttribute(Class valueClass) { super(valueClass); } diff --git a/services/s3/src/main/resources/codegen-resources/customization.config b/services/s3/src/main/resources/codegen-resources/customization.config index 18756b83c79a..1a1efb76c5f4 100644 --- a/services/s3/src/main/resources/codegen-resources/customization.config +++ b/services/s3/src/main/resources/codegen-resources/customization.config @@ -152,7 +152,8 @@ "hasUseArnRegionProperty": true, "hasMultiRegionEnabledProperty": true, "hasPathStyleAccessEnabledProperty":true, - "hasAccelerateModeEnabledProperty":true + "hasAccelerateModeEnabledProperty":true, + "hasCrossRegionAccessEnabledProperty":true }, "skipEndpointTests": { "Invalid access point ARN: Not S3": "Test assumes UseArnRegion is true but SDK defaults to false", @@ -232,6 +233,8 @@ ], "delegateAsyncClientClass": true, "delegateSyncClientClass": true, + "syncClientDecorator": "software.amazon.awssdk.services.s3.internal.client.S3SyncClientDecorator", + "asyncClientDecorator": "software.amazon.awssdk.services.s3.internal.client.S3AsyncClientDecorator", "useGlobalEndpoint": true, "interceptors": [ "software.amazon.awssdk.services.s3.internal.handlers.PutObjectInterceptor", @@ -248,5 +251,11 @@ "software.amazon.awssdk.services.s3.internal.handlers.GetObjectInterceptor", "software.amazon.awssdk.services.s3.internal.handlers.CopySourceInterceptor" ], - "requiredTraitValidationEnabled": true + "requiredTraitValidationEnabled": true, + "customClientContextParams":{ + "CrossRegionAccessEnabled":{ + "documentation":"Enables cross-region bucket access for this client", + "type":"boolean" + } + } } diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/S3ConfigurationTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/S3ConfigurationTest.java index d9a7ed6da7e0..c2ecc2d0107d 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/S3ConfigurationTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/S3ConfigurationTest.java @@ -115,4 +115,5 @@ public void useArnRegionEnabled_enabledInCProfile_shouldResolveToConfigCorrectly assertThat(config.useArnRegionEnabled()).isEqualTo(false); } + } diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/ClientDecorationFactoryTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/ClientDecorationFactoryTest.java new file mode 100644 index 000000000000..6a6b86896457 --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/ClientDecorationFactoryTest.java @@ -0,0 +1,88 @@ +/* + * 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.s3.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.endpoints.S3ClientContextParams; +import software.amazon.awssdk.services.s3.internal.client.S3AsyncClientDecorator; +import software.amazon.awssdk.services.s3.internal.client.S3SyncClientDecorator; +import software.amazon.awssdk.services.s3.internal.crossregion.S3CrossRegionAsyncClient; +import software.amazon.awssdk.services.s3.internal.crossregion.S3CrossRegionSyncClient; +import software.amazon.awssdk.utils.AttributeMap; + +public class ClientDecorationFactoryTest { + + AttributeMap.Builder clientContextParams = AttributeMap.builder(); + + @ParameterizedTest + @MethodSource("syncTestCases") + + void syncClientTest(AttributeMap clientContextParams, Class clazz, boolean isClass) { + S3SyncClientDecorator decorator = new S3SyncClientDecorator(); + S3Client decorateClient = decorator.decorate(S3Client.create(), null, clientContextParams); + if (isClass) { + assertThat(decorateClient).isInstanceOf(clazz); + } else { + assertThat(decorateClient).isNotInstanceOf(clazz); + } + } + + @ParameterizedTest + @MethodSource("asyncTestCases") + void asyncClientTest(AttributeMap clientContextParams, Class clazz, boolean isClass) { + S3AsyncClientDecorator decorator = new S3AsyncClientDecorator(); + S3AsyncClient decoratedClient = decorator.decorate(S3AsyncClient.create(), + null ,clientContextParams); + if (isClass) { + assertThat(decoratedClient).isInstanceOf(clazz); + } else { + assertThat(decoratedClient).isNotInstanceOf(clazz); + } + } + + + private static Stream syncTestCases() { + return Stream.of( + Arguments.of(AttributeMap.builder().build(), S3CrossRegionSyncClient.class, false), + Arguments.of(AttributeMap.builder().put(S3ClientContextParams.CROSS_REGION_ACCESS_ENABLED, false).build(), + S3CrossRegionSyncClient.class, false), + Arguments.of(AttributeMap.builder().put(S3ClientContextParams.CROSS_REGION_ACCESS_ENABLED, true).build(), + S3CrossRegionSyncClient.class, true) + ); + } + + private static Stream asyncTestCases() { + return Stream.of( + Arguments.of(AttributeMap.builder().build(), + S3CrossRegionAsyncClient.class, + false), + Arguments.of(AttributeMap.builder().put(S3ClientContextParams.CROSS_REGION_ACCESS_ENABLED, false).build(), + S3CrossRegionAsyncClient.class, + false), + Arguments.of(AttributeMap.builder().put(S3ClientContextParams.CROSS_REGION_ACCESS_ENABLED, true).build() + , S3CrossRegionAsyncClient.class, + true) + ); + } +} diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crossregion/S3CrossRegionAsyncClientRedirectTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crossregion/S3CrossRegionAsyncClientRedirectTest.java new file mode 100644 index 000000000000..b3859a682c03 --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crossregion/S3CrossRegionAsyncClientRedirectTest.java @@ -0,0 +1,143 @@ +/* + * 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.s3.internal.crossregion; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.function.Consumer; +import org.junit.jupiter.api.BeforeEach; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; +import software.amazon.awssdk.services.s3.model.ListBucketsRequest; +import software.amazon.awssdk.services.s3.model.ListBucketsResponse; +import software.amazon.awssdk.services.s3.model.ListObjectsRequest; +import software.amazon.awssdk.services.s3.model.ListObjectsResponse; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.utils.CompletableFutureUtils; + +public class S3CrossRegionAsyncClientRedirectTest extends S3DecoratorRedirectTestBase { + private static S3AsyncClient mockDelegateAsyncClient; + private S3AsyncClient decoratedS3AsyncClient; + + @BeforeEach + public void setup() { + mockDelegateAsyncClient = Mockito.mock(S3AsyncClient.class); + decoratedS3AsyncClient = new S3CrossRegionAsyncClient(mockDelegateAsyncClient); + } + + @Override + protected void stubRedirectSuccessSuccess() { + when(mockDelegateAsyncClient.listObjects(any(ListObjectsRequest.class))) + .thenReturn(CompletableFutureUtils.failedFuture(new CompletionException(redirectException(301, CROSS_REGION.id(), null, null)))) + .thenReturn(CompletableFuture.completedFuture(ListObjectsResponse.builder().contents(S3_OBJECTS).build())) + .thenReturn(CompletableFuture.completedFuture(ListObjectsResponse.builder().contents(S3_OBJECTS).build())); + } + + @Override + protected ListObjectsResponse apiCallToService() throws Throwable { + try{ + return decoratedS3AsyncClient.listObjects(i -> i.bucket(CROSS_REGION_BUCKET)).join(); + }catch (CompletionException exception){ + throw exception.getCause(); + } + } + + @Override + protected void verifyTheApiServiceCall(int times, ArgumentCaptor requestArgumentCaptor) { + verify(mockDelegateAsyncClient, times(times)).listObjects(requestArgumentCaptor.capture()); + } + + @Override + protected void stubServiceClientConfiguration() { + when(mockDelegateAsyncClient.serviceClientConfiguration()).thenReturn(CONFIGURED_ENDPOINT_PROVIDER); + } + + @Override + protected void stubClientAPICallWithFirstRedirectThenSuccessWithRegionInErrorResponse() { + when(mockDelegateAsyncClient.listObjects(any(ListObjectsRequest.class))) + .thenReturn(CompletableFutureUtils.failedFuture(new CompletionException(redirectException(301, CROSS_REGION.id(), null, + null)))) + .thenReturn(CompletableFuture.completedFuture(ListObjectsResponse.builder().contents(S3_OBJECTS).build() + )); + } + + @Override + protected void verifyNoBucketApiCall(int times, ArgumentCaptor requestArgumentCaptor) { + verify(mockDelegateAsyncClient, times(times)).listBuckets(requestArgumentCaptor.capture()); + } + + @Override + protected ListBucketsResponse noBucketCallToService() throws Throwable { + return decoratedS3AsyncClient.listBuckets(ListBucketsRequest.builder().build()).join(); + } + + @Override + protected void stubApiWithNoBucketField() { + when(mockDelegateAsyncClient.listBuckets(any(ListBucketsRequest.class))) + .thenReturn(CompletableFutureUtils.failedFuture(new CompletionException(redirectException(301, CROSS_REGION.id(), null, + "Redirect")))) + .thenReturn(CompletableFuture.completedFuture(ListBucketsResponse.builder().build() + )); + } + + @Override + protected void stubHeadBucketRedirect() { + when(mockDelegateAsyncClient.headBucket(any(HeadBucketRequest.class))) + .thenReturn(CompletableFutureUtils.failedFuture(new CompletionException(redirectException(301,CROSS_REGION.id(), null, null)))); + when(mockDelegateAsyncClient.headBucket(any(Consumer.class))) + .thenReturn(CompletableFutureUtils.failedFuture(new CompletionException(redirectException(301,CROSS_REGION.id(), null, null)))); + } + + @Override + protected void stubRedirectWithNoRegionAndThenSuccess() { + when(mockDelegateAsyncClient.listObjects(any(ListObjectsRequest.class))) + .thenReturn(CompletableFutureUtils.failedFuture(new CompletionException(redirectException(301, null, null, null)))) + .thenReturn(CompletableFuture.completedFuture(ListObjectsResponse.builder().contents(S3_OBJECTS).build())) + .thenReturn(CompletableFuture.completedFuture(ListObjectsResponse.builder().contents(S3_OBJECTS).build())); + } + + @Override + protected void stubRedirectThenError() { + when(mockDelegateAsyncClient.listObjects(any(ListObjectsRequest.class))) + .thenReturn(CompletableFutureUtils.failedFuture(new CompletionException(redirectException(301, CROSS_REGION.id(), null, + null)))) + .thenReturn(CompletableFutureUtils.failedFuture(new CompletionException(redirectException(400, null, + "InvalidArgument", "Invalid id")))); + } + + @Override + protected void verifyHeadBucketServiceCall(int times) { + verify(mockDelegateAsyncClient, times(times)).headBucket(any(Consumer.class)); + } + + @Override + protected void verifyNoBucketCall() { + assertThatExceptionOfType(CompletionException.class) + .isThrownBy( + () -> noBucketCallToService()) + + .withCauseInstanceOf(S3Exception.class) + .withMessage("software.amazon.awssdk.services.s3.model.S3Exception: Redirect (Service: S3, Status Code: 301, Request ID: 1, Extended Request ID: A1)"); + } +} diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crossregion/S3CrossRegionAsyncClientTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crossregion/S3CrossRegionAsyncClientTest.java new file mode 100644 index 000000000000..382411840030 --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crossregion/S3CrossRegionAsyncClientTest.java @@ -0,0 +1,391 @@ +/* + * 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.s3.internal.crossregion; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static software.amazon.awssdk.services.s3.internal.crossregion.S3DecoratorRedirectTestBase.CHANGED_CROSS_REGION; +import static software.amazon.awssdk.services.s3.internal.crossregion.S3DecoratorRedirectTestBase.CROSS_REGION; +import static software.amazon.awssdk.services.s3.internal.crossregion.S3DecoratorRedirectTestBase.OVERRIDE_CONFIGURED_REGION; +import static software.amazon.awssdk.services.s3.internal.crossregion.S3DecoratorRedirectTestBase.X_AMZ_BUCKET_REGION; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute; +import software.amazon.awssdk.endpoints.EndpointProvider; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpFullResponse; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.S3AsyncClientBuilder; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3Configuration; +import software.amazon.awssdk.services.s3.S3ServiceClientConfiguration; +import software.amazon.awssdk.services.s3.endpoints.internal.DefaultS3EndpointProvider; +import software.amazon.awssdk.services.s3.internal.crossregion.endpointprovider.BucketEndpointProvider; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.services.s3.paginators.ListObjectsV2Publisher; +import software.amazon.awssdk.testutils.service.http.MockAsyncHttpClient; +import software.amazon.awssdk.utils.StringInputStream; +import software.amazon.awssdk.utils.StringUtils; + +class S3CrossRegionAsyncClientTest { + + private static final String RESPONSE = "response"; + private static final String BUCKET = "bucket"; + private static final String KEY = "key"; + private static final String TOKEN = "token"; + private MockAsyncHttpClient mockAsyncHttpClient ; + private CaptureInterceptor captureInterceptor; + private S3AsyncClient s3Client; + + @BeforeEach + void before() { + mockAsyncHttpClient = new MockAsyncHttpClient(); + captureInterceptor = new CaptureInterceptor(); + s3Client = clientBuilder().build(); + } + + public static Stream stubResponses() { + Consumer redirectStubConsumer = mockSyncHttpClient -> + mockSyncHttpClient.stubResponses(customHttpResponse(301, CROSS_REGION.id()), successHttpResponse()); + + Consumer successStubConsumer = mockSyncHttpClient -> + mockSyncHttpClient.stubResponses(successHttpResponse(), successHttpResponse()); + + return Stream.of( + Arguments.of(redirectStubConsumer, BucketEndpointProvider.class), + Arguments.of(successStubConsumer, DefaultS3EndpointProvider.class) + ); + } + + @ParameterizedTest + @MethodSource("stubResponses") + void standardOp_crossRegionClient_noOverrideConfig_SuccessfullyIntercepts(Consumer stubConsumer, + Class endpointProviderType) { + stubConsumer.accept(mockAsyncHttpClient); + S3AsyncClient crossRegionClient = new S3CrossRegionAsyncClient(s3Client); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join(); + assertThat(captureInterceptor.endpointProvider).isInstanceOf(endpointProviderType); + } + + @ParameterizedTest + @MethodSource("stubResponses") + void standardOp_crossRegionClient_existingOverrideConfig_SuccessfullyIntercepts(Consumer stubConsumer, + Class endpointProviderType) { + stubConsumer.accept(mockAsyncHttpClient); + S3AsyncClient crossRegionClient = new S3CrossRegionAsyncClient(s3Client); + GetObjectRequest request = GetObjectRequest.builder() + .bucket(BUCKET) + .key(KEY) + .overrideConfiguration(o -> o.putHeader("someheader", "somevalue")) + .build(); + crossRegionClient.getObject(request, AsyncResponseTransformer.toBytes()).join(); + assertThat(captureInterceptor.endpointProvider).isInstanceOf(endpointProviderType); + assertThat(mockAsyncHttpClient.getLastRequest().headers().get("someheader")).isNotNull(); + } + + @ParameterizedTest + @MethodSource("stubResponses") + void paginatedOp_crossRegionClient_DoesIntercept(Consumer stubConsumer, + Class endpointProviderType) throws Exception { + stubConsumer.accept(mockAsyncHttpClient); + S3AsyncClient crossRegionClient = new S3CrossRegionAsyncClient(s3Client); + ListObjectsV2Publisher publisher = + crossRegionClient.listObjectsV2Paginator(r -> r.bucket(BUCKET).continuationToken(TOKEN).build()); + CompletableFuture future = publisher.subscribe(ListObjectsV2Response::contents); + future.get(); + assertThat(captureInterceptor.endpointProvider).isInstanceOf(endpointProviderType); + } + + @ParameterizedTest + @MethodSource("stubResponses") + void crossRegionClient_createdWithWrapping_SuccessfullyIntercepts(Consumer stubConsumer, + Class endpointProviderType) { + stubConsumer.accept(mockAsyncHttpClient); + S3AsyncClient crossRegionClient = clientBuilder().crossRegionAccessEnabled(true).build(); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join(); + assertThat(captureInterceptor.endpointProvider).isInstanceOf(endpointProviderType); + } + + @Test + void crossRegionClient_CallsHeadObject_when_regionNameNotPresentInFallBackCall(){ + mockAsyncHttpClient.reset(); + mockAsyncHttpClient.stubResponses(customHttpResponse(301, null), + customHttpResponse(301, CROSS_REGION.id()), + successHttpResponse(), successHttpResponse()); + S3AsyncClient crossRegionClient = + clientBuilder().endpointOverride(null).region(OVERRIDE_CONFIGURED_REGION).crossRegionAccessEnabled(true).build(); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join(); + assertThat(captureInterceptor.endpointProvider).isInstanceOf(BucketEndpointProvider.class); + + List requests = mockAsyncHttpClient.getRequests(); + assertThat(requests).hasSize(3); + + assertThat(requests.stream().map(req -> req.host().substring(10,req.host().length() - 14 )).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(OVERRIDE_CONFIGURED_REGION.id(), + OVERRIDE_CONFIGURED_REGION.id(), + CROSS_REGION.id())); + + assertThat(requests.stream().map(req -> req.method()).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(SdkHttpMethod.GET, + SdkHttpMethod.HEAD, + SdkHttpMethod.GET)); + + // Resetting the mock client to capture the new API request for second S3 Call. + mockAsyncHttpClient.reset(); + mockAsyncHttpClient.stubResponses(successHttpResponse(), successHttpResponse()); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join(); + List postCacheRequests = mockAsyncHttpClient.getRequests(); + + assertThat(postCacheRequests.stream() + .map(req -> req.host().substring(10,req.host().length() - 14 )) + .collect(Collectors.toList())) + .isEqualTo(Arrays.asList(CROSS_REGION.id())); + assertThat(postCacheRequests.stream().map(req -> req.method()).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(SdkHttpMethod.GET)); + assertThat(captureInterceptor.endpointProvider).isInstanceOf(BucketEndpointProvider.class); + + } + + @Test + void crossRegionClient_CallsHeadObjectErrors_shouldTerminateTheAPI() { + mockAsyncHttpClient.stubResponses(customHttpResponse(301, null ), + customHttpResponse(400, null ), + successHttpResponse(), successHttpResponse()); + S3AsyncClient crossRegionClient = + clientBuilder().endpointOverride(null).region(OVERRIDE_CONFIGURED_REGION) + .crossRegionAccessEnabled(true).build(); + + assertThatExceptionOfType(CompletionException.class) + .isThrownBy(() -> crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join()) + .withMessageContaining("software.amazon.awssdk.services.s3.model.S3Exception: null (Service: S3, Status Code: 400, Request ID: null)"); + + List requests = mockAsyncHttpClient.getRequests(); + assertThat(requests).hasSize(2); + + assertThat(requests.stream().map(req -> req.host().substring(10,req.host().length() - 14 )).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(OVERRIDE_CONFIGURED_REGION.toString(), + OVERRIDE_CONFIGURED_REGION.toString())); + + assertThat(requests.stream().map(req -> req.method()).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(SdkHttpMethod.GET, + SdkHttpMethod.HEAD)); + } + + @Test + void crossRegionClient_CallsHeadObjectWithNoRegion_shouldTerminateHeadBucketAPI() { + mockAsyncHttpClient.stubResponses(customHttpResponse(301, null ), + customHttpResponse(301, null ), + successHttpResponse(), successHttpResponse()); + S3AsyncClient crossRegionClient = + clientBuilder().endpointOverride(null).region(OVERRIDE_CONFIGURED_REGION) + .crossRegionAccessEnabled(true).build(); + + assertThatExceptionOfType(CompletionException.class) + .isThrownBy(() -> crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join()) + .withMessageContaining("software.amazon.awssdk.services.s3.model.S3Exception: null (Service: S3, Status Code: 301, Request ID: null)") + .withCauseInstanceOf(S3Exception.class).withRootCauseExactlyInstanceOf(S3Exception.class); + + List requests = mockAsyncHttpClient.getRequests(); + assertThat(requests).hasSize(2); + + assertThat(requests.stream().map(req -> req.host().substring(10,req.host().length() - 14 )).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(OVERRIDE_CONFIGURED_REGION.toString(), + OVERRIDE_CONFIGURED_REGION.toString())); + + assertThat(requests.stream().map(req -> req.method()).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(SdkHttpMethod.GET, + SdkHttpMethod.HEAD)); + } + + + @Test + void crossRegionClient_cancelsTheThread_when_futureIsCancelled(){ + mockAsyncHttpClient.reset(); + mockAsyncHttpClient.stubResponses(customHttpResponse(301, null), + customHttpResponse(301, CROSS_REGION.id()), + successHttpResponse(), successHttpResponse()); + S3AsyncClient crossRegionClient = + clientBuilder().endpointOverride(null).region(OVERRIDE_CONFIGURED_REGION).crossRegionAccessEnabled(true).build(); + CompletableFuture> completableFuture = crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY) + , AsyncResponseTransformer.toBytes()); + + completableFuture.cancel(true); + assertThat(completableFuture.isCancelled()).isTrue(); + } + + @Test + void crossRegionClient_when_redirectsAfterCaching() { + mockAsyncHttpClient.stubResponses(customHttpResponse(301, CROSS_REGION.id()), + successHttpResponse(), + successHttpResponse(), + customHttpResponse(301, CHANGED_CROSS_REGION.id()), + successHttpResponse()); + S3AsyncClient crossRegionClient = + clientBuilder().endpointOverride(null).region(OVERRIDE_CONFIGURED_REGION).crossRegionAccessEnabled(true).build(); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join(); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join(); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join(); + + assertThat(captureInterceptor.endpointProvider).isInstanceOf(BucketEndpointProvider.class); + + List requests = mockAsyncHttpClient.getRequests(); + assertThat(requests).hasSize(5); + + assertThat(requests.stream().map(req -> req.host().substring(10,req.host().length() - 14 )).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(OVERRIDE_CONFIGURED_REGION.toString(), + CROSS_REGION.toString(), + CROSS_REGION.toString(), + CROSS_REGION.toString(), + CHANGED_CROSS_REGION.toString())); + + assertThat(requests.stream().map(req -> req.method()).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(SdkHttpMethod.GET,SdkHttpMethod.GET,SdkHttpMethod.GET,SdkHttpMethod.GET,SdkHttpMethod.GET)); + } + + @Test + void crossRegionClient_when_redirectsAfterCaching_withFallBackRedirectWithNoRegion() { + mockAsyncHttpClient.stubResponses(customHttpResponse(301, null ), + customHttpResponse(301, CROSS_REGION.id()), + successHttpResponse(), + successHttpResponse(), + customHttpResponse(301, null), + customHttpResponse(301, CHANGED_CROSS_REGION.id()), + successHttpResponse()); + S3AsyncClient crossRegionClient = + clientBuilder().endpointOverride(null).region(OVERRIDE_CONFIGURED_REGION).crossRegionAccessEnabled(true).build(); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join(); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join(); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join(); + + assertThat(captureInterceptor.endpointProvider).isInstanceOf(BucketEndpointProvider.class); + + List requests = mockAsyncHttpClient.getRequests(); + assertThat(requests).hasSize(7); + + assertThat(requests.stream().map(req -> req.host().substring(10,req.host().length() - 14 )).collect(Collectors.toList())) + .isEqualTo(Arrays.asList( + OVERRIDE_CONFIGURED_REGION.toString(), OVERRIDE_CONFIGURED_REGION.toString(), CROSS_REGION.toString(), + CROSS_REGION.toString(), + CROSS_REGION.toString(), OVERRIDE_CONFIGURED_REGION.toString(), + CHANGED_CROSS_REGION.toString())); + + assertThat(requests.stream().map(req -> req.method()).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(SdkHttpMethod.GET, SdkHttpMethod.HEAD, SdkHttpMethod.GET, + SdkHttpMethod.GET, + SdkHttpMethod.GET, SdkHttpMethod.HEAD, SdkHttpMethod.GET)); + } + + + @Test + void standardOp_crossRegionClient_containUserAgent() { + mockAsyncHttpClient.stubResponses(successHttpResponse()); + + S3AsyncClient crossRegionClient = clientBuilder().crossRegionAccessEnabled(true).build(); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join(); + assertThat(mockAsyncHttpClient.getLastRequest().firstMatchingHeader("User-Agent").get()).contains("hll/cross-region"); + } + + @Test + void standardOp_crossRegionClient_FromContextParamBuilder_containUserAgent(){ + mockAsyncHttpClient.stubResponses(successHttpResponse()); + S3AsyncClient crossRegionClient = clientBuilder().crossRegionAccessEnabled(true).build(); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join(); + assertThat(mockAsyncHttpClient.getLastRequest().firstMatchingHeader("User-Agent").get()).contains("hll/cross-region"); + } + + @ParameterizedTest + @MethodSource("stubResponses") + void crossRegionClient_fromParamBuilder_createdWithWrapping_SuccessfullyIntercepts(Consumer stubConsumer, + Class endpointProviderType) { + stubConsumer.accept(mockAsyncHttpClient); + S3AsyncClient crossRegionClient = clientBuilder().crossRegionAccessEnabled(true).build(); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join(); + assertThat(captureInterceptor.endpointProvider).isInstanceOf(endpointProviderType); + } + + @Test + void standardOp_simpleClient_doesNotContainCrossRegionUserAgent() { + mockAsyncHttpClient.stubResponses(successHttpResponse()); + S3AsyncClient crossRegionClient = clientBuilder().crossRegionAccessEnabled(false).build(); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY), AsyncResponseTransformer.toBytes()).join(); + assertThat(mockAsyncHttpClient.getLastRequest().firstMatchingHeader("User-Agent").get()).doesNotContain("hll/cross-region"); + } + + private S3AsyncClientBuilder clientBuilder() { + return S3AsyncClient.builder() + .httpClient(mockAsyncHttpClient) + .endpointOverride(URI.create("http://localhost")) + .overrideConfiguration(c -> c.addExecutionInterceptor(captureInterceptor)); + } + + private static final class CaptureInterceptor implements ExecutionInterceptor { + private EndpointProvider endpointProvider; + + @Override + public void beforeMarshalling(Context.BeforeMarshalling context, ExecutionAttributes executionAttributes) { + endpointProvider = executionAttributes.getAttribute(SdkInternalExecutionAttribute.ENDPOINT_PROVIDER); + } + } + + + public static HttpExecuteResponse successHttpResponse() { + return HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder() + .statusCode(200) + .build()) + .responseBody(AbortableInputStream.create(new StringInputStream(RESPONSE))) + .build(); + } + + public static HttpExecuteResponse customHttpResponse(int statusCode, String bucket_region) { + SdkHttpFullResponse.Builder httpResponseBuilder = SdkHttpResponse.builder(); + if (StringUtils.isNotBlank(bucket_region)) { + httpResponseBuilder.appendHeader(X_AMZ_BUCKET_REGION, bucket_region); + } + return HttpExecuteResponse.builder() + .response(httpResponseBuilder.statusCode(statusCode).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(RESPONSE))) + .build(); + } +} diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crossregion/S3CrossRegionSyncClientRedirectTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crossregion/S3CrossRegionSyncClientRedirectTest.java new file mode 100644 index 000000000000..d295f098354c --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crossregion/S3CrossRegionSyncClientRedirectTest.java @@ -0,0 +1,128 @@ +/* + * 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.s3.internal.crossregion; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; +import software.amazon.awssdk.services.s3.model.HeadBucketResponse; +import software.amazon.awssdk.services.s3.model.ListBucketsRequest; +import software.amazon.awssdk.services.s3.model.ListBucketsResponse; +import software.amazon.awssdk.services.s3.model.ListObjectsRequest; +import software.amazon.awssdk.services.s3.model.ListObjectsResponse; +import software.amazon.awssdk.services.s3.model.S3Exception; + +public class S3CrossRegionSyncClientRedirectTest extends S3DecoratorRedirectTestBase { + + private static S3Client mockDelegateClient; + private S3Client decoratedS3Client; + + @BeforeEach + public void setup() { + mockDelegateClient = Mockito.mock(S3Client.class); + decoratedS3Client = new S3CrossRegionSyncClient(mockDelegateClient); + } + + @Override + protected void verifyNoBucketCall() { + assertThatExceptionOfType(S3Exception.class) + .isThrownBy( + () -> noBucketCallToService()) + .withMessage("Redirect (Service: S3, Status Code: 301, Request ID: 1, " + + "Extended Request ID: A1)"); + } + + @Override + protected void verifyNoBucketApiCall(int times, ArgumentCaptor requestArgumentCaptor) { + verify(mockDelegateClient, times(times)).listBuckets(requestArgumentCaptor.capture()); + } + + @Override + protected ListBucketsResponse noBucketCallToService() { + return decoratedS3Client.listBuckets(ListBucketsRequest.builder().build()); + } + + @Override + protected void stubApiWithNoBucketField() { + when(mockDelegateClient.listBuckets(any(ListBucketsRequest.class))) + .thenThrow(redirectException(301, CROSS_REGION.id(), null, "Redirect")) + .thenReturn(ListBucketsResponse.builder().build()); + } + + @Override + protected void stubHeadBucketRedirect() { + when(mockDelegateClient.headBucket(any(HeadBucketRequest.class))) + .thenThrow(redirectException(301, CROSS_REGION.id(), null, null)) + .thenReturn(HeadBucketResponse.builder().build()); + } + + @Override + protected void stubRedirectWithNoRegionAndThenSuccess() { + when(mockDelegateClient.listObjects(any(ListObjectsRequest.class))) + .thenThrow(redirectException(301, null, null, null)) + .thenReturn(ListObjectsResponse.builder().contents(S3_OBJECTS).build()); + } + + @Override + protected void stubRedirectThenError() { + when(mockDelegateClient.listObjects(any(ListObjectsRequest.class))) + .thenThrow(redirectException(301, CROSS_REGION.id(), null, null)) + .thenThrow(redirectException(400, null, "InvalidArgument", "Invalid id")); + } + + @Override + protected void stubRedirectSuccessSuccess() { + when(mockDelegateClient.listObjects(any(ListObjectsRequest.class))) + .thenThrow(redirectException(301, CROSS_REGION.id(), null, null)) + .thenReturn(ListObjectsResponse.builder().contents(S3_OBJECTS).build()) + .thenReturn(ListObjectsResponse.builder().contents(S3_OBJECTS).build()); + } + + @Override + protected ListObjectsResponse apiCallToService() { + return decoratedS3Client.listObjects(i -> i.bucket(CROSS_REGION_BUCKET)); + } + + @Override + protected void verifyTheApiServiceCall(int times, ArgumentCaptor requestArgumentCaptor) { + verify(mockDelegateClient, times(times)).listObjects(requestArgumentCaptor.capture()); + } + + @Override + protected void verifyHeadBucketServiceCall(int times) { + verify(mockDelegateClient, times(times)).headBucket(any(HeadBucketRequest.class)); + } + + @Override + protected void stubServiceClientConfiguration() { + when(mockDelegateClient.serviceClientConfiguration()).thenReturn(CONFIGURED_ENDPOINT_PROVIDER); + } + + @Override + protected void stubClientAPICallWithFirstRedirectThenSuccessWithRegionInErrorResponse() { + when(mockDelegateClient.listObjects(any(ListObjectsRequest.class))) + .thenThrow(redirectException(301, CROSS_REGION.id(), null, null)) + .thenReturn(ListObjectsResponse.builder().contents(S3_OBJECTS).build()); + } +} diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crossregion/S3CrossRegionSyncClientTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crossregion/S3CrossRegionSyncClientTest.java new file mode 100644 index 000000000000..f576d09d317e --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crossregion/S3CrossRegionSyncClientTest.java @@ -0,0 +1,374 @@ +/* + * 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.s3.internal.crossregion; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static software.amazon.awssdk.services.s3.internal.crossregion.S3CrossRegionAsyncClientTest.customHttpResponse; +import static software.amazon.awssdk.services.s3.internal.crossregion.S3CrossRegionAsyncClientTest.successHttpResponse; +import static software.amazon.awssdk.services.s3.internal.crossregion.S3DecoratorRedirectTestBase.CHANGED_CROSS_REGION; +import static software.amazon.awssdk.services.s3.internal.crossregion.S3DecoratorRedirectTestBase.CROSS_REGION; +import static software.amazon.awssdk.services.s3.internal.crossregion.S3DecoratorRedirectTestBase.OVERRIDE_CONFIGURED_REGION; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute; +import software.amazon.awssdk.endpoints.EndpointProvider; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3ClientBuilder; +import software.amazon.awssdk.services.s3.endpoints.internal.DefaultS3EndpointProvider; +import software.amazon.awssdk.services.s3.internal.crossregion.endpointprovider.BucketEndpointProvider; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Response; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.services.s3.paginators.ListObjectsV2Iterable; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; + +class S3CrossRegionSyncClientTest { + + private static final String BUCKET = "bucket"; + private static final String KEY = "key"; + private static final String TOKEN = "token"; + + private MockSyncHttpClient mockSyncHttpClient ; + private CaptureInterceptor captureInterceptor; + private S3Client defaultS3Client; + + @BeforeEach + void before() { + mockSyncHttpClient = new MockSyncHttpClient(); + captureInterceptor = new CaptureInterceptor(); + defaultS3Client = clientBuilder().build(); + } + + + private static Stream stubResponses() { + Consumer redirectStubConsumer = mockSyncHttpClient -> + mockSyncHttpClient.stubResponses(customHttpResponse(301, CROSS_REGION.id()), successHttpResponse()); + + Consumer successStubConsumer = mockSyncHttpClient -> + mockSyncHttpClient.stubResponses(successHttpResponse(), successHttpResponse()); + + return Stream.of( + Arguments.of(redirectStubConsumer, BucketEndpointProvider.class), + Arguments.of(successStubConsumer, DefaultS3EndpointProvider.class) + ); + } + + private static Stream stubOverriddenEndpointProviderResponses() { + Consumer redirectStubConsumer = mockSyncHttpClient -> + mockSyncHttpClient.stubResponses(customHttpResponse(301, CROSS_REGION.id()), successHttpResponse()); + + Consumer successStubConsumer = mockSyncHttpClient -> + mockSyncHttpClient.stubResponses(successHttpResponse(), successHttpResponse()); + + return Stream.of( + Arguments.of(redirectStubConsumer, BucketEndpointProvider.class, CROSS_REGION), + Arguments.of(successStubConsumer, TestEndpointProvider.class, OVERRIDE_CONFIGURED_REGION) + ); + } + + @ParameterizedTest + @MethodSource("stubResponses") + void standardOp_crossRegionClient_noOverrideConfig_SuccessfullyIntercepts(Consumer stubConsumer, + Class endpointProviderType) { + stubConsumer.accept(mockSyncHttpClient); + S3Client crossRegionClient = new S3CrossRegionSyncClient(defaultS3Client); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY)); + assertThat(captureInterceptor.endpointProvider).isInstanceOf(endpointProviderType); + } + + @ParameterizedTest + @MethodSource("stubResponses") + void standardOp_crossRegionClient_existingOverrideConfig_SuccessfullyIntercepts(Consumer stubConsumer, + Class endpointProviderType) { + stubConsumer.accept(mockSyncHttpClient); + S3Client crossRegionClient = new S3CrossRegionSyncClient(defaultS3Client); + GetObjectRequest request = GetObjectRequest.builder() + .bucket(BUCKET) + .key(KEY) + .overrideConfiguration(o -> o.putHeader("someheader", "somevalue")) + .build(); + crossRegionClient.getObject(request); + assertThat(captureInterceptor.endpointProvider).isInstanceOf(endpointProviderType); + assertThat(mockSyncHttpClient.getLastRequest().headers().get("someheader")).isNotNull(); + } + + @ParameterizedTest + @MethodSource("stubResponses") + void paginatedOp_crossRegionClient_DoesNotIntercept(Consumer stubConsumer, + Class endpointProviderType) { + stubConsumer.accept(mockSyncHttpClient); + S3Client crossRegionClient = new S3CrossRegionSyncClient(defaultS3Client); + ListObjectsV2Iterable iterable = + crossRegionClient.listObjectsV2Paginator(r -> r.bucket(BUCKET).continuationToken(TOKEN).build()); + iterable.forEach(ListObjectsV2Response::contents); + assertThat(captureInterceptor.endpointProvider).isInstanceOf(endpointProviderType); + } + + @ParameterizedTest + @MethodSource("stubResponses") + void crossRegionClient_createdWithWrapping_SuccessfullyIntercepts(Consumer stubConsumer, + Class endpointProviderType) { + stubConsumer.accept(mockSyncHttpClient); + + + S3Client crossRegionClient = clientBuilder().crossRegionAccessEnabled(true).build(); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY)); + assertThat(captureInterceptor.endpointProvider).isInstanceOf(endpointProviderType); + } + + @ParameterizedTest + @MethodSource("stubOverriddenEndpointProviderResponses") + void standardOp_crossRegionClient_takesCustomEndpointProviderInRequest(Consumer stubConsumer, + Class endpointProviderType, + Region region) { + stubConsumer.accept(mockSyncHttpClient); + S3Client crossRegionClient = clientBuilder().crossRegionAccessEnabled(true) + .endpointProvider(new TestEndpointProvider()) + .region(OVERRIDE_CONFIGURED_REGION) + .build(); + GetObjectRequest request = GetObjectRequest.builder() + .bucket(BUCKET) + .key(KEY) + .overrideConfiguration(o -> o.putHeader("someheader", "somevalue") + .endpointProvider(new TestEndpointProvider())) + .build(); + crossRegionClient.getObject(request); + assertThat(captureInterceptor.endpointProvider).isInstanceOf(endpointProviderType); + assertThat(mockSyncHttpClient.getLastRequest().headers().get("someheader")).isNotNull(); + assertThat(mockSyncHttpClient.getLastRequest().encodedPath()).contains("test_prefix_"); + assertThat(mockSyncHttpClient.getLastRequest().host()).contains(region.id()); + } + + @ParameterizedTest + @MethodSource("stubOverriddenEndpointProviderResponses") + void standardOp_crossRegionClient_takesCustomEndpointProviderInClient(Consumer stubConsumer, + Class endpointProviderType, + Region region) { + stubConsumer.accept(mockSyncHttpClient); + S3Client crossRegionClient = clientBuilder().crossRegionAccessEnabled(true) + .endpointProvider(new TestEndpointProvider()) + .region(OVERRIDE_CONFIGURED_REGION) + .build(); + GetObjectRequest request = GetObjectRequest.builder() + .bucket(BUCKET) + .key(KEY) + .overrideConfiguration(o -> o.putHeader("someheader", "somevalue")) + .build(); + crossRegionClient.getObject(request); + assertThat(captureInterceptor.endpointProvider).isInstanceOf(endpointProviderType); + assertThat(mockSyncHttpClient.getLastRequest().headers().get("someheader")).isNotNull(); + assertThat(mockSyncHttpClient.getLastRequest().encodedPath()).contains("test_prefix_"); + assertThat(mockSyncHttpClient.getLastRequest().host()).contains(region.id()); + } + + @Test + void crossRegionClient_CallsHeadObject_when_regionNameNotPresentInFallBackCall() { + mockSyncHttpClient.stubResponses(customHttpResponse(301, null ), + customHttpResponse(301, CROSS_REGION.id() ), + successHttpResponse(), successHttpResponse()); + S3Client crossRegionClient = + clientBuilder().endpointOverride(null).region(OVERRIDE_CONFIGURED_REGION).crossRegionAccessEnabled(true).build(); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY)); + assertThat(captureInterceptor.endpointProvider).isInstanceOf(BucketEndpointProvider.class); + + List requests = mockSyncHttpClient.getRequests(); + assertThat(requests).hasSize(3); + + assertThat(requests.stream().map(req -> req.host().substring(10,req.host().length() - 14 )).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(OVERRIDE_CONFIGURED_REGION.toString(), + OVERRIDE_CONFIGURED_REGION.toString(), + CROSS_REGION.id())); + + assertThat(requests.stream().map(req -> req.method()).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(SdkHttpMethod.GET, + SdkHttpMethod.HEAD, + SdkHttpMethod.GET)); + + // Resetting the mock client to capture the new API request for second S3 Call. + mockSyncHttpClient.reset(); + mockSyncHttpClient.stubResponses(successHttpResponse(), successHttpResponse()); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY)); + List postCacheRequests = mockSyncHttpClient.getRequests(); + + assertThat(postCacheRequests.stream() + .map(req -> req.host().substring(10,req.host().length() - 14 )) + .collect(Collectors.toList())) + .isEqualTo(Arrays.asList(CROSS_REGION.id())); + assertThat(postCacheRequests.stream().map(req -> req.method()).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(SdkHttpMethod.GET)); + assertThat(captureInterceptor.endpointProvider).isInstanceOf(BucketEndpointProvider.class); + + } + + @Test + void crossRegionClient_when_redirectsAfterCaching() { + mockSyncHttpClient.stubResponses(customHttpResponse(301, CROSS_REGION.id()), + successHttpResponse(), + successHttpResponse(), + customHttpResponse(301, CHANGED_CROSS_REGION.id()), + successHttpResponse()); + S3Client crossRegionClient = + clientBuilder().endpointOverride(null).region(OVERRIDE_CONFIGURED_REGION).crossRegionAccessEnabled(true).build(); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY)); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY)); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY)); + assertThat(captureInterceptor.endpointProvider).isInstanceOf(BucketEndpointProvider.class); + + List requests = mockSyncHttpClient.getRequests(); + assertThat(requests).hasSize(5); + + assertThat(requests.stream().map(req -> req.host().substring(10,req.host().length() - 14 )).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(OVERRIDE_CONFIGURED_REGION.toString(), + CROSS_REGION.toString(), + CROSS_REGION.toString(), + CROSS_REGION.toString(), + CHANGED_CROSS_REGION.toString())); + + assertThat(requests.stream().map(req -> req.method()).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(SdkHttpMethod.GET,SdkHttpMethod.GET,SdkHttpMethod.GET,SdkHttpMethod.GET,SdkHttpMethod.GET)); + } + + @Test + void crossRegionClient_when_redirectsAfterCaching_withFallBackRedirectWithNoRegion() { + mockSyncHttpClient.stubResponses(customHttpResponse(301, null ), + customHttpResponse(301, CROSS_REGION.id() ), + successHttpResponse(), + successHttpResponse(), + customHttpResponse(301, null), + customHttpResponse(301, CHANGED_CROSS_REGION.id()), + successHttpResponse()); + S3Client crossRegionClient = + clientBuilder().endpointOverride(null).region(OVERRIDE_CONFIGURED_REGION).crossRegionAccessEnabled(true).build(); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY)); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY)); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY)); + assertThat(captureInterceptor.endpointProvider).isInstanceOf(BucketEndpointProvider.class); + + List requests = mockSyncHttpClient.getRequests(); + assertThat(requests).hasSize(7); + + assertThat(requests.stream().map(req -> req.host().substring(10,req.host().length() - 14 )).collect(Collectors.toList())) + .isEqualTo(Arrays.asList( + OVERRIDE_CONFIGURED_REGION.toString(), OVERRIDE_CONFIGURED_REGION.toString(), CROSS_REGION.toString(), + CROSS_REGION.toString(), + CROSS_REGION.toString(), OVERRIDE_CONFIGURED_REGION.toString(), + CHANGED_CROSS_REGION.toString())); + + assertThat(requests.stream().map(req -> req.method()).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(SdkHttpMethod.GET, SdkHttpMethod.HEAD, SdkHttpMethod.GET, + SdkHttpMethod.GET, + SdkHttpMethod.GET, SdkHttpMethod.HEAD, SdkHttpMethod.GET)); + } + + @Test + void crossRegionClient_CallsHeadObjectErrors_shouldTerminateTheAPI() { + mockSyncHttpClient.stubResponses(customHttpResponse(301, null ), + customHttpResponse(400, null ), + successHttpResponse(), successHttpResponse()); + S3Client crossRegionClient = + clientBuilder().endpointOverride(null).region(OVERRIDE_CONFIGURED_REGION).crossRegionAccessEnabled(true).build(); + + assertThatExceptionOfType(S3Exception.class) + .isThrownBy(() -> crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY))) + .withMessageContaining("Status Code: 400"); + + List requests = mockSyncHttpClient.getRequests(); + assertThat(requests).hasSize(2); + + assertThat(requests.stream().map(req -> req.host().substring(10,req.host().length() - 14 )).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(OVERRIDE_CONFIGURED_REGION.toString(), + OVERRIDE_CONFIGURED_REGION.toString())); + + assertThat(requests.stream().map(req -> req.method()).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(SdkHttpMethod.GET, + SdkHttpMethod.HEAD)); + } + + @Test + void crossRegionClient_CallsHeadObjectWithNoRegion_shouldTerminateHeadBucketAPI() { + mockSyncHttpClient.stubResponses(customHttpResponse(301, null ), + customHttpResponse(301, null ), + successHttpResponse(), successHttpResponse()); + S3Client crossRegionClient = + clientBuilder().endpointOverride(null).region(OVERRIDE_CONFIGURED_REGION).crossRegionAccessEnabled(true).build(); + + assertThatExceptionOfType(S3Exception.class) + .isThrownBy(() -> crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY))) + .withMessageContaining("Status Code: 301"); + + List requests = mockSyncHttpClient.getRequests(); + assertThat(requests).hasSize(2); + + assertThat(requests.stream().map(req -> req.host().substring(10,req.host().length() - 14 )).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(OVERRIDE_CONFIGURED_REGION.toString(), + OVERRIDE_CONFIGURED_REGION.toString())); + + assertThat(requests.stream().map(req -> req.method()).collect(Collectors.toList())) + .isEqualTo(Arrays.asList(SdkHttpMethod.GET, + SdkHttpMethod.HEAD)); + } + + + @Test + void standardOp_crossRegionClient_containUserAgent() { + mockSyncHttpClient.stubResponses(successHttpResponse()); + S3Client crossRegionClient = clientBuilder().crossRegionAccessEnabled(true).build(); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY)); + assertThat(mockSyncHttpClient.getLastRequest().firstMatchingHeader("User-Agent").get()).contains("hll/cross-region"); + } + + @Test + void standardOp_simpleClient_doesNotContainCrossRegionUserAgent() { + mockSyncHttpClient.stubResponses(successHttpResponse()); + S3Client crossRegionClient = clientBuilder().crossRegionAccessEnabled(false).build(); + crossRegionClient.getObject(r -> r.bucket(BUCKET).key(KEY)); + assertThat(mockSyncHttpClient.getLastRequest().firstMatchingHeader("User-Agent").get()) + .doesNotContain("hll/cross-region"); + } + + private S3ClientBuilder clientBuilder() { + return S3Client.builder() + .httpClient(mockSyncHttpClient) + .overrideConfiguration(c -> c.addExecutionInterceptor(captureInterceptor)); + } + + private static final class CaptureInterceptor implements ExecutionInterceptor { + + private EndpointProvider endpointProvider; + + @Override + public void beforeMarshalling(Context.BeforeMarshalling context, ExecutionAttributes executionAttributes) { + endpointProvider = executionAttributes.getAttribute(SdkInternalExecutionAttribute.ENDPOINT_PROVIDER); + } + } +} diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crossregion/S3DecoratorRedirectTestBase.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crossregion/S3DecoratorRedirectTestBase.java new file mode 100644 index 000000000000..10d0709b377f --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crossregion/S3DecoratorRedirectTestBase.java @@ -0,0 +1,214 @@ +/* + * 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.s3.internal.crossregion; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import software.amazon.awssdk.awscore.exception.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.endpoints.EndpointProvider; +import software.amazon.awssdk.http.SdkHttpFullResponse; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3ServiceClientConfiguration; +import software.amazon.awssdk.services.s3.endpoints.S3EndpointProvider; +import software.amazon.awssdk.services.s3.internal.crossregion.endpointprovider.BucketEndpointProvider; +import software.amazon.awssdk.services.s3.model.ListBucketsRequest; +import software.amazon.awssdk.services.s3.model.ListBucketsResponse; +import software.amazon.awssdk.services.s3.model.ListObjectsRequest; +import software.amazon.awssdk.services.s3.model.ListObjectsResponse; +import software.amazon.awssdk.services.s3.model.S3Exception; +import software.amazon.awssdk.services.s3.model.S3Object; + +public abstract class S3DecoratorRedirectTestBase { + + public static final String X_AMZ_BUCKET_REGION = "x-amz-bucket-region"; + protected static final String CROSS_REGION_BUCKET = "anyBucket"; + protected static final Region CROSS_REGION = Region.EU_CENTRAL_1; + protected static final Region CHANGED_CROSS_REGION = Region.US_WEST_1; + + public static final Region OVERRIDE_CONFIGURED_REGION = Region.US_WEST_2; + + protected static final List S3_OBJECTS = Collections.singletonList(S3Object.builder().key("keyObject").build()); + + protected static final S3ServiceClientConfiguration CONFIGURED_ENDPOINT_PROVIDER = + S3ServiceClientConfiguration.builder().endpointProvider(S3EndpointProvider.defaultProvider()).build(); + + @Test + void decoratorAttemptsToRetryWithRegionNameInErrorResponse() throws Throwable { + stubServiceClientConfiguration(); + stubClientAPICallWithFirstRedirectThenSuccessWithRegionInErrorResponse(); + // Assert retrieved listObject + ListObjectsResponse listObjectsResponse = apiCallToService(); + assertThat(listObjectsResponse.contents()).isEqualTo(S3_OBJECTS); + + ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(ListObjectsRequest.class); + verifyTheApiServiceCall(2, requestArgumentCaptor); + + assertThat(requestArgumentCaptor.getAllValues().get(0).overrideConfiguration().get().endpointProvider()).isNotPresent(); + verifyTheEndPointProviderOverridden(1, requestArgumentCaptor, CROSS_REGION.id()); + + verifyHeadBucketServiceCall(0); + } + + @Test + void decoratorUsesCache_when_CrossRegionAlreadyPresent() throws Throwable { + stubServiceClientConfiguration(); + stubRedirectSuccessSuccess(); + + ListObjectsResponse listObjectsResponse = apiCallToService(); + assertThat(listObjectsResponse.contents()).isEqualTo(S3_OBJECTS); + + ListObjectsResponse listObjectsResponseSecondCall = apiCallToService(); + assertThat(listObjectsResponseSecondCall.contents()).isEqualTo(S3_OBJECTS); + + ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(ListObjectsRequest.class); + verifyTheApiServiceCall(3, requestArgumentCaptor); + + assertThat(requestArgumentCaptor.getAllValues().get(0).overrideConfiguration().get().endpointProvider()).isNotPresent(); + verifyTheEndPointProviderOverridden(1, requestArgumentCaptor, CROSS_REGION.id()); + verifyTheEndPointProviderOverridden(2, requestArgumentCaptor, CROSS_REGION.id()); + verifyHeadBucketServiceCall(0); + } + + /** + * Call is redirected to actual end point + * The redirected call fails because of incorrect parameters passed + * This exception should be reported correctly + */ + @Test + void apiCallFailure_when_CallFailsAfterRedirection() { + stubServiceClientConfiguration(); + stubRedirectThenError(); + assertThatExceptionOfType(S3Exception.class) + .isThrownBy(() -> apiCallToService()) + .withMessageContaining("Invalid id (Service: S3, Status Code: 400, Request ID: 1, Extended Request ID: A1)"); + verifyHeadBucketServiceCall(0); + } + + @Test + void headBucketCalled_when_RedirectDoesNotHasRegionName() throws Throwable { + stubServiceClientConfiguration(); + stubRedirectWithNoRegionAndThenSuccess(); + stubHeadBucketRedirect(); + ListObjectsResponse listObjectsResponse = apiCallToService(); + assertThat(listObjectsResponse.contents()).isEqualTo(S3_OBJECTS); + + ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(ListObjectsRequest.class); + verifyTheApiServiceCall(2, requestArgumentCaptor); + + assertThat(requestArgumentCaptor.getAllValues().get(0).overrideConfiguration().get().endpointProvider()).isNotPresent(); + verifyTheEndPointProviderOverridden(1, requestArgumentCaptor, CROSS_REGION.id()); + verifyHeadBucketServiceCall(1); + } + + @Test + void headBucketCalledAndCached__when_RedirectDoesNotHasRegionName() throws Throwable { + stubServiceClientConfiguration(); + stubRedirectWithNoRegionAndThenSuccess(); + stubHeadBucketRedirect(); + ListObjectsResponse listObjectsResponse = apiCallToService(); + assertThat(listObjectsResponse.contents()).isEqualTo(S3_OBJECTS); + + ArgumentCaptor preCacheCaptor = ArgumentCaptor.forClass(ListObjectsRequest.class); + verifyTheApiServiceCall(2, preCacheCaptor); + // We need to get the BucketEndpointProvider in order to update the cache + verifyTheEndPointProviderOverridden(1, preCacheCaptor, CROSS_REGION.id()); + listObjectsResponse = apiCallToService(); + assertThat(listObjectsResponse.contents()).isEqualTo(S3_OBJECTS); + // We need to captor again so that we get the args used in second API Call + ArgumentCaptor overAllPostCacheCaptor = ArgumentCaptor.forClass(ListObjectsRequest.class); + verifyTheApiServiceCall(3, overAllPostCacheCaptor); + assertThat(overAllPostCacheCaptor.getAllValues().get(0).overrideConfiguration().get().endpointProvider()).isNotPresent(); + verifyTheEndPointProviderOverridden(1, overAllPostCacheCaptor, CROSS_REGION.id()); + verifyTheEndPointProviderOverridden(2, overAllPostCacheCaptor, CROSS_REGION.id()); + verifyHeadBucketServiceCall(1); + } + + @Test + void requestsAreNotOverridden_when_NoBucketInRequest() throws Throwable { + stubServiceClientConfiguration(); + stubApiWithNoBucketField(); + stubHeadBucketRedirect(); + verifyNoBucketCall(); + ArgumentCaptor requestArgumentCaptor = ArgumentCaptor.forClass(ListBucketsRequest.class); + verifyHeadBucketServiceCall(0); + verifyNoBucketApiCall(1, requestArgumentCaptor); + assertThat(requestArgumentCaptor.getAllValues().get(0).overrideConfiguration().get().endpointProvider()).isNotPresent(); + verifyHeadBucketServiceCall(0); + } + + protected abstract void verifyNoBucketCall(); + + protected abstract void verifyNoBucketApiCall(int i, ArgumentCaptor requestArgumentCaptor); + + protected abstract ListBucketsResponse noBucketCallToService() throws Throwable; + + protected abstract void stubApiWithNoBucketField(); + + protected abstract void stubHeadBucketRedirect(); + + protected abstract void stubRedirectWithNoRegionAndThenSuccess(); + + protected abstract void stubRedirectThenError(); + + protected abstract void stubRedirectSuccessSuccess(); + + protected AwsServiceException redirectException(int statusCode, String region, String errorCode, String errorMessage) { + SdkHttpFullResponse.Builder sdkHttpFullResponseBuilder = SdkHttpFullResponse.builder(); + if (region != null) { + sdkHttpFullResponseBuilder.appendHeader(X_AMZ_BUCKET_REGION, region); + } + return S3Exception.builder() + .statusCode(statusCode) + .requestId("1") + .extendedRequestId("A1") + .awsErrorDetails(AwsErrorDetails.builder() + .errorMessage(errorMessage) + .sdkHttpResponse(sdkHttpFullResponseBuilder.build()) + .errorCode(errorCode) + .serviceName("S3") + .build()) + .build(); + } + + void verifyTheEndPointProviderOverridden(int attempt, + ArgumentCaptor requestArgumentCaptor, + String expectedRegion) throws Exception { + EndpointProvider overridenEndpointProvider = + requestArgumentCaptor.getAllValues().get(attempt).overrideConfiguration().get().endpointProvider().get(); + assertThat(overridenEndpointProvider).isInstanceOf(BucketEndpointProvider.class); + assertThat(((S3EndpointProvider) overridenEndpointProvider).resolveEndpoint(e -> e.region(Region.US_WEST_2) + .bucket(CROSS_REGION_BUCKET) + .build()) + .get().url().getHost()) + .isEqualTo("s3." + expectedRegion + ".amazonaws.com"); + } + + protected abstract ListObjectsResponse apiCallToService() throws Throwable; + + protected abstract void verifyTheApiServiceCall(int times, ArgumentCaptor requestArgumentCaptor); + + protected abstract void verifyHeadBucketServiceCall(int times); + + protected abstract void stubServiceClientConfiguration(); + + protected abstract void stubClientAPICallWithFirstRedirectThenSuccessWithRegionInErrorResponse(); +} diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crossregion/TestEndpointProvider.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crossregion/TestEndpointProvider.java new file mode 100644 index 000000000000..e7ab65e6fdde --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crossregion/TestEndpointProvider.java @@ -0,0 +1,31 @@ +/* + * 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.s3.internal.crossregion; + + +import java.util.concurrent.CompletableFuture; +import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.services.s3.endpoints.S3EndpointParams; +import software.amazon.awssdk.services.s3.endpoints.S3EndpointProvider; + +public class TestEndpointProvider implements S3EndpointProvider { + S3EndpointProvider s3EndpointProvider = S3EndpointProvider.defaultProvider(); + @Override + public CompletableFuture resolveEndpoint(S3EndpointParams endpointParams) { + return s3EndpointProvider.resolveEndpoint(endpointParams.copy(c -> c.bucket("test_prefix_"+endpointParams.bucket()))); + + } +} \ No newline at end of file diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClientTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClientTest.java index 5c0da7b22f9f..ee44bf18839f 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClientTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/crt/DefaultS3CrtAsyncClientTest.java @@ -29,8 +29,10 @@ import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute; +import software.amazon.awssdk.services.s3.DelegatingS3AsyncClient; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.endpoints.S3ClientContextParams; +import software.amazon.awssdk.services.s3.internal.crossregion.S3CrossRegionAsyncClient; import software.amazon.awssdk.utils.AttributeMap; class DefaultS3CrtAsyncClientTest { @@ -92,4 +94,23 @@ void invalidConfig_shouldThrowException(long value) { .hasMessageContaining( "positive"); } + + @Test + void crtClient_with_crossRegionAccessEnabled_asTrue(){ + S3AsyncClient crossRegionCrtClient = S3AsyncClient.crtBuilder().crossRegionAccessEnabled(true).build(); + assertThat(crossRegionCrtClient).isInstanceOf(DefaultS3CrtAsyncClient.class); + assertThat(((DelegatingS3AsyncClient)crossRegionCrtClient).delegate()).isInstanceOf(S3CrossRegionAsyncClient.class); + } + + @Test + void crtClient_with_crossRegionAccessEnabled_asFalse(){ + S3AsyncClient crossRegionDisabledCrtClient = S3AsyncClient.crtBuilder().crossRegionAccessEnabled(false).build(); + assertThat(crossRegionDisabledCrtClient).isInstanceOf(DefaultS3CrtAsyncClient.class); + assertThat(((DelegatingS3AsyncClient)crossRegionDisabledCrtClient).delegate()).isNotInstanceOf(S3CrossRegionAsyncClient.class); + + S3AsyncClient defaultCrtClient = S3AsyncClient.crtBuilder().build(); + assertThat(defaultCrtClient).isInstanceOf(DefaultS3CrtAsyncClient.class); + assertThat(((DelegatingS3AsyncClient)defaultCrtClient).delegate()).isNotInstanceOf(S3CrossRegionAsyncClient.class); + } + } diff --git a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/customresponsemetadata/customization.config b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/customresponsemetadata/customization.config index 9ede2184d4e0..f766add4301c 100644 --- a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/customresponsemetadata/customization.config +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/customresponsemetadata/customization.config @@ -20,5 +20,7 @@ "REQUEST_ID": "x-foobar-id" }, "skipEndpointTestGeneration": true, - "requiredTraitValidationEnabled": true + "requiredTraitValidationEnabled": true, + "delegateAsyncClientClass": true, + "delegateSyncClientClass": true } diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/delegatingclients/DelegatingAsyncClientTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/delegatingclients/DelegatingAsyncClientTest.java new file mode 100644 index 000000000000..755a681b59c9 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/delegatingclients/DelegatingAsyncClientTest.java @@ -0,0 +1,136 @@ +/* + * 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.delegatingclients; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.services.protocolrestjson.DelegatingProtocolRestJsonAsyncClient; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonAsyncClient; +import software.amazon.awssdk.services.protocolrestjson.model.AllTypesRequest; +import software.amazon.awssdk.services.protocolrestjson.model.PaginatedOperationWithResultKeyRequest; +import software.amazon.awssdk.services.protocolrestjson.model.PaginatedOperationWithResultKeyResponse; +import software.amazon.awssdk.services.protocolrestjson.model.ProtocolRestJsonRequest; +import software.amazon.awssdk.services.protocolrestjson.paginators.PaginatedOperationWithResultKeyPublisher; +import software.amazon.awssdk.testutils.service.http.MockAsyncHttpClient; +import software.amazon.awssdk.utils.StringInputStream; + +public class DelegatingAsyncClientTest { + + private static final String INTERCEPTED_HEADER = "intercepted-header"; + private static final String INTERCEPTED_HEADER_VALUE = "intercepted-value"; + private static final String RESPONSE = "response"; + + MockAsyncHttpClient mockAsyncHttpClient = new MockAsyncHttpClient(); + ProtocolRestJsonAsyncClient defaultClient = ProtocolRestJsonAsyncClient.builder() + .httpClient(mockAsyncHttpClient) + .endpointOverride(URI.create("http://localhost")) + .build(); + ProtocolRestJsonAsyncClient decoratingClient = new DecoratingClient(defaultClient); + + @BeforeEach + public void before() { + mockAsyncHttpClient.stubNextResponse( + HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(RESPONSE))) + .build()); + } + + @Test + public void standardOp_Request_standardFutureResponse_delegatingClient_SuccessfullyIntercepts() { + decoratingClient.allTypes(AllTypesRequest.builder().stringMember("test").build()).join(); + validateIsDecorated(); + } + + @Test + public void standardOp_ConsumerRequest_standardFutureResponse_delegatingClient_SuccessfullyIntercepts() { + decoratingClient.allTypes(r -> r.stringMember("test")).join(); + validateIsDecorated(); + } + + @Test + public void paginatedOp_Request_standardFutureResponse_delegatingClient_SuccessfullyIntercepts() { + decoratingClient.paginatedOperationWithResultKey( + PaginatedOperationWithResultKeyRequest.builder().nextToken("token").build()) + .join(); + validateIsDecorated(); + } + + @Test + public void paginatedOp_ConsumerRequest_standardFutureResponse_delegatingClient_SuccessfullyIntercepts() { + decoratingClient.paginatedOperationWithResultKey(r -> r.nextToken("token")).join(); + validateIsDecorated(); + } + + @Test + public void paginatedOp_Request_publisherResponse_delegatingClient_DoesNotIntercept() throws Exception { + PaginatedOperationWithResultKeyPublisher publisher = + decoratingClient.paginatedOperationWithResultKeyPaginator(PaginatedOperationWithResultKeyRequest.builder() + .nextToken("token") + .build()); + CompletableFuture future = publisher.subscribe(PaginatedOperationWithResultKeyResponse::items); + future.get(); + validateIsDecorated(); + } + + @Test + public void paginatedOp_ConsumerRequest_publisherResponse_delegatingClient_DoesNotIntercept() throws Exception { + PaginatedOperationWithResultKeyPublisher publisher = + decoratingClient.paginatedOperationWithResultKeyPaginator(r -> r.nextToken("token").build()); + CompletableFuture future = publisher.subscribe(PaginatedOperationWithResultKeyResponse::items); + future.get(); + validateIsDecorated(); + } + + private void validateIsDecorated() { + SdkHttpRequest lastRequest = mockAsyncHttpClient.getLastRequest(); + assertThat(lastRequest.headers().get(INTERCEPTED_HEADER)).isNotNull(); + assertThat(lastRequest.headers().get(INTERCEPTED_HEADER).get(0)).isEqualTo(INTERCEPTED_HEADER_VALUE); + } + + private static final class DecoratingClient extends DelegatingProtocolRestJsonAsyncClient { + + DecoratingClient(ProtocolRestJsonAsyncClient client) { + super(client); + } + + @Override + protected CompletableFuture + invokeOperation(T request, Function> operation) { + return operation.apply(decorateRequest(request)); + } + + @SuppressWarnings("unchecked") + private T decorateRequest(T request) { + AwsRequestOverrideConfiguration alin = AwsRequestOverrideConfiguration.builder() + .putHeader(INTERCEPTED_HEADER, INTERCEPTED_HEADER_VALUE) + .build(); + return (T) request.toBuilder() + .overrideConfiguration(alin) + .build(); + } + } +} diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/delegatingclients/DelegatingSyncClientTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/delegatingclients/DelegatingSyncClientTest.java new file mode 100644 index 000000000000..49c2cfca53bf --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/delegatingclients/DelegatingSyncClientTest.java @@ -0,0 +1,139 @@ +/* + * 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.delegatingclients; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.util.Iterator; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; +import software.amazon.awssdk.http.AbortableInputStream; +import software.amazon.awssdk.http.HttpExecuteResponse; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.services.protocolrestjson.DelegatingProtocolRestJsonAsyncClient; +import software.amazon.awssdk.services.protocolrestjson.DelegatingProtocolRestJsonClient; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonAsyncClient; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonClient; +import software.amazon.awssdk.services.protocolrestjson.model.AllTypesRequest; +import software.amazon.awssdk.services.protocolrestjson.model.PaginatedOperationWithResultKeyRequest; +import software.amazon.awssdk.services.protocolrestjson.model.PaginatedOperationWithResultKeyResponse; +import software.amazon.awssdk.services.protocolrestjson.model.ProtocolRestJsonRequest; +import software.amazon.awssdk.services.protocolrestjson.paginators.PaginatedOperationWithResultKeyIterable; +import software.amazon.awssdk.services.protocolrestjson.paginators.PaginatedOperationWithResultKeyPublisher; +import software.amazon.awssdk.testutils.service.http.MockSyncHttpClient; +import software.amazon.awssdk.utils.StringInputStream; + +public class DelegatingSyncClientTest { + + private static final String INTERCEPTED_HEADER = "intercepted-header"; + private static final String INTERCEPTED_HEADER_VALUE = "intercepted-value"; + private static final String RESPONSE = "response"; + + MockSyncHttpClient mockSyncHttpClient = new MockSyncHttpClient(); + ProtocolRestJsonClient defaultClient = ProtocolRestJsonClient.builder() + .httpClient(mockSyncHttpClient) + .endpointOverride(URI.create("http://localhost")) + .build(); + ProtocolRestJsonClient decoratingClient = new DecoratingClient(defaultClient); + + @BeforeEach + public void before() { + mockSyncHttpClient.stubNextResponse( + HttpExecuteResponse.builder() + .response(SdkHttpResponse.builder().statusCode(200).build()) + .responseBody(AbortableInputStream.create(new StringInputStream(RESPONSE))) + .build()); + } + + @Test + public void standardOp_Request_standardFutureResponse_delegatingClient_SuccessfullyIntercepts() { + decoratingClient.allTypes(AllTypesRequest.builder().stringMember("test").build()); + validateIsDecorated(); + } + + @Test + public void standardOp_ConsumerRequest_standardFutureResponse_delegatingClient_SuccessfullyIntercepts() { + decoratingClient.allTypes(r -> r.stringMember("test")); + validateIsDecorated(); + } + + @Test + public void paginatedOp_Request_standardFutureResponse_delegatingClient_SuccessfullyIntercepts() { + decoratingClient.paginatedOperationWithResultKey(PaginatedOperationWithResultKeyRequest.builder() + .nextToken("token") + .build()); + validateIsDecorated(); + } + + @Test + public void paginatedOp_ConsumerRequest_standardFutureResponse_delegatingClient_SuccessfullyIntercepts() { + decoratingClient.paginatedOperationWithResultKey(r -> r.nextToken("token")); + validateIsDecorated(); + } + + @Test + public void paginatedOp_Request_publisherResponse_delegatingClient_SuccessfullyIntercepts() { + PaginatedOperationWithResultKeyIterable iterable = + decoratingClient.paginatedOperationWithResultKeyPaginator(PaginatedOperationWithResultKeyRequest.builder() + .nextToken("token") + .build()); + iterable.forEach(PaginatedOperationWithResultKeyResponse::items); + validateIsDecorated(); + } + + @Test + public void paginatedOp_ConsumerRequest_publisherResponse_delegatingClient_SuccessfullyIntercepts() { + PaginatedOperationWithResultKeyIterable iterable = + decoratingClient.paginatedOperationWithResultKeyPaginator(r -> r.nextToken("token").build()); + iterable.forEach(PaginatedOperationWithResultKeyResponse::items); + validateIsDecorated(); + } + + private void validateIsDecorated() { + SdkHttpRequest lastRequest = mockSyncHttpClient.getLastRequest(); + assertThat(lastRequest.headers().get(INTERCEPTED_HEADER)).isNotNull(); + assertThat(lastRequest.headers().get(INTERCEPTED_HEADER).get(0)).isEqualTo(INTERCEPTED_HEADER_VALUE); + } + + private static final class DecoratingClient extends DelegatingProtocolRestJsonClient { + + DecoratingClient(ProtocolRestJsonClient client) { + super(client); + } + + @Override + protected ReturnT + invokeOperation(T request, Function operation) { + return operation.apply(decorateRequest(request)); + } + + @SuppressWarnings("unchecked") + private T decorateRequest(T request) { + AwsRequestOverrideConfiguration alin = AwsRequestOverrideConfiguration.builder() + .putHeader(INTERCEPTED_HEADER, + INTERCEPTED_HEADER_VALUE) + .build(); + return (T) request.toBuilder() + .overrideConfiguration(alin) + .build(); + } + } +} diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/endpointproviders/EndpointInterceptorTests.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/endpointproviders/EndpointInterceptorTests.java index e9e10177cc8d..4ab603f270bb 100644 --- a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/endpointproviders/EndpointInterceptorTests.java +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/endpointproviders/EndpointInterceptorTests.java @@ -27,12 +27,14 @@ import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute; import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.endpoints.EndpointProvider; import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.restjsonendpointproviders.RestJsonEndpointProvidersAsyncClient; import software.amazon.awssdk.services.restjsonendpointproviders.RestJsonEndpointProvidersAsyncClientBuilder; import software.amazon.awssdk.services.restjsonendpointproviders.RestJsonEndpointProvidersClient; import software.amazon.awssdk.services.restjsonendpointproviders.RestJsonEndpointProvidersClientBuilder; +import software.amazon.awssdk.services.restjsonendpointproviders.endpoints.RestJsonEndpointProvidersEndpointProvider; public class EndpointInterceptorTests { diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/endpointproviders/RequestOverrideEndpointProviderTests.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/endpointproviders/RequestOverrideEndpointProviderTests.java new file mode 100644 index 000000000000..075976d136c8 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/endpointproviders/RequestOverrideEndpointProviderTests.java @@ -0,0 +1,164 @@ +/* + * 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.endpointproviders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import org.junit.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; +import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute; +import software.amazon.awssdk.endpoints.Endpoint; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.restjsonendpointproviders.RestJsonEndpointProvidersAsyncClient; +import software.amazon.awssdk.services.restjsonendpointproviders.RestJsonEndpointProvidersAsyncClientBuilder; +import software.amazon.awssdk.services.restjsonendpointproviders.RestJsonEndpointProvidersClient; +import software.amazon.awssdk.services.restjsonendpointproviders.RestJsonEndpointProvidersClientBuilder; +import software.amazon.awssdk.services.restjsonendpointproviders.endpoints.RestJsonEndpointProvidersEndpointParams; +import software.amazon.awssdk.services.restjsonendpointproviders.endpoints.RestJsonEndpointProvidersEndpointProvider; + +public class RequestOverrideEndpointProviderTests { + + @Test + public void sync_endpointOverridden_equals_requestOverride() { + CapturingInterceptor interceptor = new CapturingInterceptor(); + RestJsonEndpointProvidersClient client = syncClientBuilder() + .overrideConfiguration(o -> o.addExecutionInterceptor(interceptor) + .putAdvancedOption(SdkAdvancedClientOption.DISABLE_HOST_PREFIX_INJECTION, true)) + .build(); + + assertThatThrownBy(() -> client.operationWithHostPrefix( + r -> r.overrideConfiguration(o -> o.endpointProvider(new CustomEndpointProvider(Region.AWS_GLOBAL)))) + + ) + .hasMessageContaining("stop"); + Endpoint endpoint = interceptor.executionAttributes().getAttribute(SdkInternalExecutionAttribute.RESOLVED_ENDPOINT); + assertThat(endpoint.url().getHost()).isEqualTo("restjson.aws-global.amazonaws.com"); + } + + @Test + public void sync_endpointOverridden_equals_ClientsWhenNoRequestOverride() { + CapturingInterceptor interceptor = new CapturingInterceptor(); + RestJsonEndpointProvidersClient client = syncClientBuilder().endpointProvider(new CustomEndpointProvider(Region.EU_WEST_2)) + .overrideConfiguration(o -> o.addExecutionInterceptor(interceptor) + .putAdvancedOption(SdkAdvancedClientOption.DISABLE_HOST_PREFIX_INJECTION, true)) + .build(); + + assertThatThrownBy(() -> client.operationWithHostPrefix(r -> {})).hasMessageContaining("stop"); + + Endpoint endpoint = interceptor.executionAttributes().getAttribute(SdkInternalExecutionAttribute.RESOLVED_ENDPOINT); + + assertThat(endpoint.url().getHost()).isEqualTo("restjson.eu-west-2.amazonaws.com"); + } + + + @Test + public void async_endpointOverridden_equals_requestOverride() { + + + CapturingInterceptor interceptor = new CapturingInterceptor(); + RestJsonEndpointProvidersAsyncClient client = asyncClientBuilder() + .overrideConfiguration(o -> o.addExecutionInterceptor(interceptor) + .putAdvancedOption(SdkAdvancedClientOption.DISABLE_HOST_PREFIX_INJECTION, true)) + .build(); + + assertThatThrownBy(() -> client.operationWithHostPrefix( + r -> r.overrideConfiguration(o -> o.endpointProvider(new CustomEndpointProvider(Region.AWS_GLOBAL))) + ).join()) + .hasMessageContaining("stop"); + + Endpoint endpoint = interceptor.executionAttributes().getAttribute(SdkInternalExecutionAttribute.RESOLVED_ENDPOINT); + + assertThat(endpoint.url().getHost()).isEqualTo("restjson.aws-global.amazonaws.com"); + } + + + @Test + public void async_endpointOverridden_equals_ClientsWhenNoRequestOverride() { + + + CapturingInterceptor interceptor = new CapturingInterceptor(); + RestJsonEndpointProvidersAsyncClient client = asyncClientBuilder().endpointProvider(new CustomEndpointProvider(Region.EU_WEST_2)) + .overrideConfiguration(o -> o.addExecutionInterceptor(interceptor) + .putAdvancedOption(SdkAdvancedClientOption.DISABLE_HOST_PREFIX_INJECTION, true)) + .build(); + + assertThatThrownBy(() -> client.operationWithHostPrefix(r -> {}).join()) + .hasMessageContaining("stop"); + + Endpoint endpoint = interceptor.executionAttributes().getAttribute(SdkInternalExecutionAttribute.RESOLVED_ENDPOINT); + + assertThat(endpoint.url().getHost()).isEqualTo("restjson.eu-west-2.amazonaws.com"); + } + + public static class CapturingInterceptor implements ExecutionInterceptor { + + private ExecutionAttributes executionAttributes; + + @Override + public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) { + this.executionAttributes = executionAttributes; + throw new CaptureCompletedException("stop"); + } + + public ExecutionAttributes executionAttributes() { + return executionAttributes; + } + + public class CaptureCompletedException extends RuntimeException { + CaptureCompletedException(String message) { + super(message); + } + } + } + private RestJsonEndpointProvidersClientBuilder syncClientBuilder() { + return RestJsonEndpointProvidersClient.builder() + .region(Region.US_WEST_2) + .credentialsProvider( + StaticCredentialsProvider.create( + AwsBasicCredentials.create("akid", "skid"))); + } + + private RestJsonEndpointProvidersAsyncClientBuilder asyncClientBuilder() { + return RestJsonEndpointProvidersAsyncClient.builder() + .region(Region.US_WEST_2) + .credentialsProvider( + StaticCredentialsProvider.create( + AwsBasicCredentials.create("akid", "skid"))); + } + + + class CustomEndpointProvider implements RestJsonEndpointProvidersEndpointProvider{ + final RestJsonEndpointProvidersEndpointProvider endpointProvider; + final Region overridingRegion; + CustomEndpointProvider (Region region){ + this.endpointProvider = RestJsonEndpointProvidersEndpointProvider.defaultProvider(); + this.overridingRegion = region; + } + @Override + public CompletableFuture resolveEndpoint(RestJsonEndpointProvidersEndpointParams endpointParams) { + return endpointProvider.resolveEndpoint(endpointParams.toBuilder().region(overridingRegion).build()); + } + } +} diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/serviceclientconfiguration/ServiceClientConfigurationTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/serviceclientconfiguration/ServiceClientConfigurationTest.java index 6a0058400f27..1d682af68ec6 100644 --- a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/serviceclientconfiguration/ServiceClientConfigurationTest.java +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/serviceclientconfiguration/ServiceClientConfigurationTest.java @@ -21,9 +21,11 @@ import java.time.Duration; import org.junit.jupiter.api.Test; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.endpoints.EndpointProvider; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.protocolrestxml.ProtocolRestXmlAsyncClient; import software.amazon.awssdk.services.protocolrestxml.ProtocolRestXmlClient; +import software.amazon.awssdk.services.protocolrestxml.endpoints.ProtocolRestXmlEndpointProvider; public class ServiceClientConfigurationTest { @@ -91,6 +93,26 @@ public void syncClient_serviceClientConfiguration_withoutOverrideConfiguration_s assertThat(client.serviceClientConfiguration().overrideConfiguration().metricPublishers()).isEmpty(); } + @Test + public void syncClientWithEndpointProvider_serviceClientConfiguration_shouldReturnCorrectEndpointProvider() { + ProtocolRestXmlEndpointProvider clientEndpointProvider = ProtocolRestXmlEndpointProvider.defaultProvider(); + ProtocolRestXmlClient client = ProtocolRestXmlClient.builder() + .endpointProvider(clientEndpointProvider) + .build(); + + EndpointProvider endpointProvider = client.serviceClientConfiguration().endpointProvider().orElse(null); + assertThat(endpointProvider).isEqualTo(clientEndpointProvider); + } + + @Test + public void syncClientWithoutEndpointProvider_serviceClientConfiguration_shouldReturnDefaultEndpointProvider() { + ProtocolRestXmlClient client = ProtocolRestXmlClient.builder() + .build(); + + EndpointProvider endpointProvider = client.serviceClientConfiguration().endpointProvider().orElse(null); + assertThat(endpointProvider instanceof ProtocolRestXmlEndpointProvider).isTrue(); + } + @Test public void asyncClient_serviceClientConfiguration_shouldReturnCorrectRegion() { ProtocolRestXmlAsyncClient client = ProtocolRestXmlAsyncClient.builder() @@ -155,4 +177,25 @@ public void asyncClient_serviceClientConfiguration_withoutOverrideConfiguration_ assertThat(client.serviceClientConfiguration().overrideConfiguration().metricPublishers()).isEmpty(); } + + @Test + public void asyncClientWithEndpointProvider_serviceClientConfiguration_shouldReturnCorrectEndpointProvider() { + ProtocolRestXmlEndpointProvider clientEndpointProvider = ProtocolRestXmlEndpointProvider.defaultProvider(); + ProtocolRestXmlAsyncClient client = ProtocolRestXmlAsyncClient.builder() + .endpointProvider(clientEndpointProvider) + .build(); + + EndpointProvider endpointProvider = client.serviceClientConfiguration().endpointProvider().orElse(null); + assertThat(endpointProvider).isEqualTo(clientEndpointProvider); + } + + @Test + public void asyncClientWithoutEndpointProvider_serviceClientConfiguration_shouldReturnDefault() { + ProtocolRestXmlAsyncClient client = ProtocolRestXmlAsyncClient.builder() + .build(); + + EndpointProvider endpointProvider = client.serviceClientConfiguration().endpointProvider().orElse(null); + assertThat(endpointProvider instanceof ProtocolRestXmlEndpointProvider).isTrue(); + } + } diff --git a/utils/src/main/java/software/amazon/awssdk/utils/ConditionalDecorator.java b/utils/src/main/java/software/amazon/awssdk/utils/ConditionalDecorator.java new file mode 100644 index 000000000000..2e5a009c51f0 --- /dev/null +++ b/utils/src/main/java/software/amazon/awssdk/utils/ConditionalDecorator.java @@ -0,0 +1,67 @@ +/* + * 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.utils; + +import java.util.List; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; +import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.utils.internal.DefaultConditionalDecorator; + +/** + * An interface that defines a class that contains a transform for another type as well as a condition for whether + * that transform should be applied. + * + * @param A type that can be decorated, or transformed, through applying a function. + */ +@FunctionalInterface +@SdkProtectedApi +public interface ConditionalDecorator { + + default Predicate predicate() { + return t -> true; + } + + + UnaryOperator transform(); + + static ConditionalDecorator create(Predicate predicate, UnaryOperator transform) { + DefaultConditionalDecorator.Builder builder = new DefaultConditionalDecorator.Builder<>(); + return builder.predicate(predicate).transform(transform).build(); + } + + /** + * This function will transform an initially supplied value with provided transforming, or decorating, functions that are + * conditionally and sequentially applied. For each pair of condition and transform: if the condition evaluates to true, the + * transform will be applied to the incoming value and the output from the transform is the input to the next transform. + *

+ * If the supplied collection is ordered, the function is guaranteed to apply the transforms in the order in which they appear + * in the collection. + * + * @param initialValue The untransformed start value + * @param decorators A list of condition to transform + * @param The type of the value + * @return A single transformed value that is the result of applying all transforms evaluated to true + */ + static T decorate(T initialValue, List> decorators) { + return decorators.stream() + .filter(d -> d.predicate().test(initialValue)) + .reduce(initialValue, + (element, decorator) -> decorator.transform().apply(element), + (el1, el2) -> { throw new IllegalStateException("Should not reach here, combine function not " + + "needed unless executed in parallel."); }); + } +} diff --git a/utils/src/main/java/software/amazon/awssdk/utils/internal/DefaultConditionalDecorator.java b/utils/src/main/java/software/amazon/awssdk/utils/internal/DefaultConditionalDecorator.java new file mode 100644 index 000000000000..ab894edb6f13 --- /dev/null +++ b/utils/src/main/java/software/amazon/awssdk/utils/internal/DefaultConditionalDecorator.java @@ -0,0 +1,90 @@ +/* + * 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.utils.internal; + +import java.util.Objects; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.utils.ConditionalDecorator; + +@SdkInternalApi +public final class DefaultConditionalDecorator implements ConditionalDecorator { + private final Predicate predicate; + private final UnaryOperator transform; + + DefaultConditionalDecorator(Builder builder) { + this.predicate = builder.predicate; + this.transform = builder.transform; + } + + public static Builder builder() { + return new Builder<>(); + } + + @Override + public Predicate predicate() { + return predicate; + } + + @Override + public UnaryOperator transform() { + return transform; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DefaultConditionalDecorator)) { + return false; + } + + DefaultConditionalDecorator that = (DefaultConditionalDecorator) o; + + if (!Objects.equals(predicate, that.predicate)) { + return false; + } + return Objects.equals(transform, that.transform); + } + + @Override + public int hashCode() { + int result = predicate != null ? predicate.hashCode() : 0; + result = 31 * result + (transform != null ? transform.hashCode() : 0); + return result; + } + + public static final class Builder { + private Predicate predicate; + private UnaryOperator transform; + + public Builder predicate(Predicate predicate) { + this.predicate = predicate; + return this; + } + + public Builder transform(UnaryOperator transform) { + this.transform = transform; + return this; + } + + public ConditionalDecorator build() { + return new DefaultConditionalDecorator<>(this); + } + } +} diff --git a/utils/src/test/java/software/amazon/awssdk/utils/ConditionalDecoratorTest.java b/utils/src/test/java/software/amazon/awssdk/utils/ConditionalDecoratorTest.java new file mode 100644 index 000000000000..4543df7f1f88 --- /dev/null +++ b/utils/src/test/java/software/amazon/awssdk/utils/ConditionalDecoratorTest.java @@ -0,0 +1,64 @@ +/* + * 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.utils; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Test; + +public class ConditionalDecoratorTest { + + @Test + void basicTransform_directlyCalled_isSuccessful() { + ConditionalDecorator decorator = ConditionalDecorator.create(i -> true, i -> i + 1); + assertThat(decorator.transform().apply(3)).isEqualTo(4); + } + + @Test + void listOfOrderedTransforms_singleTransformAlwaysTrue_isSuccessful() { + ConditionalDecorator d1 = ConditionalDecorator.create(i -> true, i -> i + 1); + assertThat(ConditionalDecorator.decorate(2, Collections.singletonList(d1))).isEqualTo(3); + } + + @Test + void listOfOrderedTransforms_alwaysTrue_isSuccessful() { + ConditionalDecorator d1 = ConditionalDecorator.create(i -> true, i -> i + 1); + ConditionalDecorator d2 = ConditionalDecorator.create(i -> true, i -> i * 2); + assertThat(ConditionalDecorator.decorate(2, Arrays.asList(d1, d2))).isEqualTo(6); + } + + @Test + void listOfOrderedTransformsInReverse_alwaysTrue_isSuccessful() { + ConditionalDecorator d1 = ConditionalDecorator.create(i -> true, i -> i + 1); + ConditionalDecorator d2 = ConditionalDecorator.create(i -> true, i -> i * 2); + assertThat(ConditionalDecorator.decorate(2, Arrays.asList(d2, d1))).isEqualTo(5); + } + + @Test + void listOfOrderedTransforms_onlyAddsEvenNumbers_isSuccessful() { + List> decorators = + IntStream.range(0, 9) + .>mapToObj(i -> ConditionalDecorator.create(j -> i % 2 == 0, + j -> j + i)) + .collect(Collectors.toList()); + assertThat(ConditionalDecorator.decorate(0, decorators)).isEqualTo(20); + } +}