diff --git a/.changes/next-release/feature-AWSCodegenAWScore-bb730a0.json b/.changes/next-release/feature-AWSCodegenAWScore-bb730a0.json new file mode 100644 index 000000000000..36de314c08d2 --- /dev/null +++ b/.changes/next-release/feature-AWSCodegenAWScore-bb730a0.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS Codegen, AWS core", + "contributor": "ziyanli-amazon", + "description": "Add awsQueryCompatible trait support to service.\nWhen awsQueryCompatible trait is found, it's made available as an API property. When this property is found,\nthe error code is returned by looking up the mapping. This is a pre-requisite for migrating services from\nAWSQuery wire protocol to AWSJson." +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/IntermediateModelBuilder.java b/codegen/src/main/java/software/amazon/awssdk/codegen/IntermediateModelBuilder.java index e722b421fe92..3dde889be48d 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/IntermediateModelBuilder.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/IntermediateModelBuilder.java @@ -130,7 +130,7 @@ public IntermediateModel build() { IntermediateModel fullModel = new IntermediateModel( constructMetadata(service, customConfig), operations, shapes, customConfig, endpointOperation, paginators.getPagination(), namingStrategy, - waiters.getWaiters()); + waiters.getWaiters(), service.getAwsQueryCompatible()); customization.postprocess(fullModel); @@ -149,7 +149,8 @@ public IntermediateModel build() { endpointOperation, fullModel.getPaginators(), namingStrategy, - fullModel.getWaiters()); + fullModel.getWaiters(), + fullModel.getAwsQueryCompatible()); linkMembersToShapes(trimmedModel); linkOperationsToInputOutputShapes(trimmedModel); 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 4948ac406dae..853139862fcd 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 @@ -28,6 +28,7 @@ import software.amazon.awssdk.awscore.AwsResponse; import software.amazon.awssdk.awscore.AwsResponseMetadata; import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig; +import software.amazon.awssdk.codegen.model.service.AwsQueryCompatible; import software.amazon.awssdk.codegen.model.service.PaginatorDefinition; import software.amazon.awssdk.codegen.model.service.WaiterDefinition; import software.amazon.awssdk.codegen.naming.NamingStrategy; @@ -53,6 +54,9 @@ public final class IntermediateModel { @JsonIgnore private NamingStrategy namingStrategy; + @JsonIgnore + private Map awsQueryCompatible; + static { FILE_HEADER = loadDefaultFileHeader(); } @@ -71,7 +75,7 @@ public IntermediateModel(Metadata metadata, Map shapes, CustomizationConfig customizationConfig) { this(metadata, operations, shapes, customizationConfig, null, - Collections.emptyMap(), null, Collections.emptyMap()); + Collections.emptyMap(), null, Collections.emptyMap(), Collections.emptyMap()); } public IntermediateModel( @@ -82,7 +86,8 @@ public IntermediateModel( OperationModel endpointOperation, Map paginators, NamingStrategy namingStrategy, - Map waiters) { + Map waiters, + Map awsQueryCompatible) { this.metadata = metadata; this.operations = operations; this.shapes = shapes; @@ -91,6 +96,7 @@ public IntermediateModel( this.paginators = paginators; this.namingStrategy = namingStrategy; this.waiters = waiters; + this.awsQueryCompatible = awsQueryCompatible; } public Metadata getMetadata() { @@ -154,6 +160,9 @@ public Map getWaiters() { return waiters; } + public Map getAwsQueryCompatible() { + return awsQueryCompatible; + } public void setPaginators(Map paginators) { this.paginators = paginators; } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/service/AwsQueryCompatible.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/service/AwsQueryCompatible.java new file mode 100644 index 000000000000..3723c43d8471 --- /dev/null +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/service/AwsQueryCompatible.java @@ -0,0 +1,37 @@ +/* + * 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.model.service; + +/** + * Models the errorCode to use for errors to make the error compatible with an AWS Query protocol. + */ +public class AwsQueryCompatible { + private String errorCode; + + public AwsQueryCompatible() { + } + public AwsQueryCompatible(String errorCode) { + this.errorCode = errorCode; + } + + public String getErrorCode() { + return errorCode; + } + + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/service/ServiceModel.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/service/ServiceModel.java index dc9269c97113..f9635b0308f4 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/service/ServiceModel.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/service/ServiceModel.java @@ -25,6 +25,7 @@ public class ServiceModel { private Map shapes; private Map authorizers; + private Map awsQueryCompatible; private String documentation; public ServiceModel() { @@ -33,11 +34,13 @@ public ServiceModel() { public ServiceModel(ServiceMetadata metadata, Map operations, Map shapes, - Map authorizers) { + Map authorizers, + Map awsQueryCompatible) { this.metadata = metadata; this.operations = operations; this.shapes = shapes; this.authorizers = authorizers; + this.awsQueryCompatible = awsQueryCompatible; } public ServiceMetadata getMetadata() { @@ -99,4 +102,18 @@ public Map getAuthorizers() { public void setAuthorizers(Map authorizers) { this.authorizers = authorizers; } + + /** + * Provides a map of error code to compatibility model, used during error deserialization to ensure + * that errorCode is compatible with an AWS Query protocol + * + * @return the compatibility map, or null if none is specified in the model + */ + public Map getAwsQueryCompatible() { + return awsQueryCompatible; + } + + public void setAwsQueryCompatible(Map awsQueryCompatible) { + this.awsQueryCompatible = awsQueryCompatible; + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/specs/JsonProtocolSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/specs/JsonProtocolSpec.java index 09f075c33e1a..ee4675904d96 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/specs/JsonProtocolSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/specs/JsonProtocolSpec.java @@ -104,6 +104,9 @@ public MethodSpec initProtocolFactory(IntermediateModel model) { if (contentType != null) { methodSpec.addCode(".contentType($S)", contentType); } + if (model.getAwsQueryCompatible() != null) { + methodSpec.addCode(errorCodeMapping(model)); + } registerModeledExceptions(model, poetExtensions).forEach(methodSpec::addCode); methodSpec.addCode(";"); diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/specs/ProtocolSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/specs/ProtocolSpec.java index 95c9637e7b7c..ae6bf00ae25b 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/specs/ProtocolSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/specs/ProtocolSpec.java @@ -32,6 +32,7 @@ import software.amazon.awssdk.codegen.model.intermediate.ShapeModel; import software.amazon.awssdk.codegen.model.intermediate.ShapeType; import software.amazon.awssdk.codegen.model.service.AuthType; +import software.amazon.awssdk.codegen.model.service.AwsQueryCompatible; import software.amazon.awssdk.codegen.poet.PoetExtension; import software.amazon.awssdk.core.client.handler.SyncClientHandler; import software.amazon.awssdk.core.runtime.transform.AsyncStreamingRequestMarshaller; @@ -68,6 +69,17 @@ default List additionalMethods() { return new ArrayList<>(); } + default CodeBlock errorCodeMapping(IntermediateModel model) { + CodeBlock.Builder builder = CodeBlock.builder() + .add(".errorCodeMapping(software.amazon.awssdk.utils.ImmutableMap.builder()\n"); + model.getAwsQueryCompatible().keySet().stream().sorted().forEach(errorCode -> { + AwsQueryCompatible errorModel = model.getAwsQueryCompatible().get(errorCode); + builder.add(".put($S,$S)\n", errorCode, errorModel.getErrorCode()); + }); + builder.add(".build()"); + return builder.build(); + } + default List registerModeledExceptions(IntermediateModel model, PoetExtension poetExtensions) { return model.getShapes().values().stream() .filter(s -> s.getShapeType() == ShapeType.Exception) diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/IntermediateModelBuilderTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/IntermediateModelBuilderTest.java index 413c11d63749..d8367e70e757 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/IntermediateModelBuilderTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/IntermediateModelBuilderTest.java @@ -25,6 +25,7 @@ import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig; import software.amazon.awssdk.codegen.model.intermediate.IntermediateModel; import software.amazon.awssdk.codegen.model.intermediate.ShapeModel; +import software.amazon.awssdk.codegen.model.service.AwsQueryCompatible; import software.amazon.awssdk.codegen.model.service.ServiceModel; import software.amazon.awssdk.codegen.utils.ModelLoaderUtils; @@ -89,4 +90,20 @@ public void defaultEndpointDiscovery_false() { assertFalse(testModel.getEndpointOperation().get().isEndpointCacheRequired()); } + @Test + public void testAwsQueryCompatibleDeserialization() { + final File modelFile = new File(IntermediateModelBuilderTest.class + .getResource("poet/client/c2j/awsQueryCompatible/service-2.json").getFile()); + IntermediateModel testModel = new IntermediateModelBuilder( + C2jModels.builder() + .serviceModel(ModelLoaderUtils.loadModel(ServiceModel.class, modelFile)) + .customizationConfig(CustomizationConfig.create()) + .build()) + .build(); + + assertThat(testModel.getAwsQueryCompatible().values()) + .extracting(AwsQueryCompatible::getErrorCode) + .contains("fooErrorCode"); + } + } diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/IntermediateModelTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/IntermediateModelTest.java index d87cf898932f..42bc9e18ed14 100644 --- a/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/IntermediateModelTest.java +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/model/intermediate/IntermediateModelTest.java @@ -16,19 +16,34 @@ package software.amazon.awssdk.codegen.model.intermediate; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import java.io.File; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.Assert; import org.junit.jupiter.api.Test; import software.amazon.awssdk.codegen.C2jModels; import software.amazon.awssdk.codegen.IntermediateModelBuilder; import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig; +import software.amazon.awssdk.codegen.model.service.AwsQueryCompatible; import software.amazon.awssdk.codegen.model.service.ServiceMetadata; import software.amazon.awssdk.codegen.model.service.ServiceModel; import software.amazon.awssdk.codegen.utils.ModelLoaderUtils; public class IntermediateModelTest { + private static final String ERROR_CODE = "fooErrorCode"; + private static final String QUERY_ERROR_CODE = "queryErrorCode"; + private final AwsQueryCompatible model = new AwsQueryCompatible(ERROR_CODE); + private final Map awsQueryCompatible = new HashMap() { + { + put(QUERY_ERROR_CODE, model); + } + }; + @Test public void cannotFindShapeWhenNoShapesExist() { @@ -40,6 +55,7 @@ public void cannotFindShapeWhenNoShapesExist() { IntermediateModel testModel = new IntermediateModelBuilder( C2jModels.builder() .serviceModel(new ServiceModel(metadata, + Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap())) @@ -69,4 +85,29 @@ public void getShapeByNameAndC2jNameVerifiesC2JName() { .isInstanceOf(IllegalArgumentException.class) .hasMessage("C2J shape AnyShape with shape name PingResponse does not exist in the intermediate model."); } + + @Test + public void validateAwsQueryCompatible() { + + ServiceMetadata metadata = new ServiceMetadata(); + metadata.setProtocol(Protocol.REST_JSON.getValue()); + metadata.setServiceId("empty-service"); + metadata.setSignatureVersion("V4"); + + IntermediateModel testModel = new IntermediateModelBuilder( + C2jModels.builder() + .serviceModel(new ServiceModel(metadata, + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap(), + awsQueryCompatible)) + .customizationConfig(CustomizationConfig.create()) + .build()) + .build(); + + assertEquals(awsQueryCompatible, testModel.getAwsQueryCompatible()); + assertNotNull(testModel.getAwsQueryCompatible().get(QUERY_ERROR_CODE)); + AwsQueryCompatible awsQueryCompatible = testModel.getAwsQueryCompatible().get(QUERY_ERROR_CODE); + Assert.assertEquals(ERROR_CODE, awsQueryCompatible.getErrorCode()); + } } diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/model/service/AwsQueryCompatibleTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/model/service/AwsQueryCompatibleTest.java new file mode 100644 index 000000000000..fb638b3a32d1 --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/model/service/AwsQueryCompatibleTest.java @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.codegen.model.service; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class AwsQueryCompatibleTest { + + private static final String ERROR_CODE = "fooErrorCode"; + private AwsQueryCompatible model = new AwsQueryCompatible(ERROR_CODE); + + @Test + public void validateAwsQueryCompatibleAccessors() { + assertEquals(ERROR_CODE, model.getErrorCode()); + } + + @Test + public void validateAwsQueryCompatibleMutators() { + model.setErrorCode("newErrorCode"); + assertEquals("newErrorCode", model.getErrorCode()); + } + + @public void somethe() { + model.setErrorCode(""); + assertEquals("", ""); + } +} diff --git a/codegen/src/test/java/software/amazon/awssdk/codegen/model/service/ServiceModelTest.java b/codegen/src/test/java/software/amazon/awssdk/codegen/model/service/ServiceModelTest.java new file mode 100644 index 000000000000..64008a16893b --- /dev/null +++ b/codegen/src/test/java/software/amazon/awssdk/codegen/model/service/ServiceModelTest.java @@ -0,0 +1,59 @@ +/* + * 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.model.service; + + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.codegen.model.intermediate.Protocol; + +public class ServiceModelTest { + + private static final String ERROR_CODE = "fooErrorCode"; + private static final String QUERY_ERROR_CODE = "queryErrorCode"; + private final AwsQueryCompatible model = new AwsQueryCompatible(ERROR_CODE); + private final Map awsQueryCompatible = new HashMap() { + { + put(QUERY_ERROR_CODE, model); + } + }; + + @Test + public void validateAwsQueryCompatible() { + + + ServiceMetadata metadata = new ServiceMetadata(); + metadata.setProtocol(Protocol.REST_JSON.getValue()); + metadata.setServiceId("empty-service"); + metadata.setSignatureVersion("V4"); + + ServiceModel testModel = new ServiceModel(metadata, + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap(), + awsQueryCompatible); + + assertEquals(awsQueryCompatible, testModel.getAwsQueryCompatible()); + assertNotNull(testModel.getAwsQueryCompatible().get(QUERY_ERROR_CODE)); + AwsQueryCompatible awsQueryCompatible = testModel.getAwsQueryCompatible().get(QUERY_ERROR_CODE); + assertEquals(ERROR_CODE, awsQueryCompatible.getErrorCode()); + } +} diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/awsQueryCompatible/service-2.json b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/awsQueryCompatible/service-2.json new file mode 100644 index 000000000000..cd175451aa94 --- /dev/null +++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/client/c2j/awsQueryCompatible/service-2.json @@ -0,0 +1,54 @@ +{ + "version": "2.0", + "metadata": { + "apiVersion": "2010-05-08", + "endpointPrefix": "shared-output-service", + "globalEndpoint": "shared-output-service.amazonaws.com", + "protocol": "rest-json", + "serviceAbbreviation": "Shared Output Service", + "serviceFullName": "Shared output service", + "serviceId":"SharedOutputService", + "signatureVersion": "v4", + "uid": "shared-output-service-2010-05-08", + "xmlNamespace": "https://shared-output-service.amazonaws.com/doc/2010-05-08/" + }, + "operations": { + "Ping": { + "name": "ping", + "http": { + "method": "POST", + "requestUri": "/ping" + }, + "errors": [], + "output": { + "shape" : "PingOutput" + }, + "documentation": "

ping

" + }, + "SecurePing": { + "name": "sping", + "http": { + "method": "POST", + "requestUri": "/sping" + }, + "errors": [], + "output": { + "shape" : "PingOutput" + }, + "documentation": "

secure ping

" + } + }, + "shapes": { + "PingOutput": { + "type": "structure", + "required": [], + "members": {} + } + }, + "awsQueryCompatible": { + "fooOperation": { + "errorCode": "fooErrorCode" + } + }, + "documentation": "A ping service that shared outputs" +} diff --git a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/BaseAwsJsonProtocolFactory.java b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/BaseAwsJsonProtocolFactory.java index 0df81bf8bc79..ebd1f5be396f 100644 --- a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/BaseAwsJsonProtocolFactory.java +++ b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/BaseAwsJsonProtocolFactory.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.protocols.json; import static java.util.Collections.unmodifiableList; +import static java.util.Collections.unmodifiableMap; import java.util.ArrayList; import java.util.Collections; @@ -59,6 +60,7 @@ public abstract class BaseAwsJsonProtocolFactory { private final AwsJsonProtocolMetadata protocolMetadata; private final List modeledExceptions; + private final Map awsQueryCompatibleErrorCodeMapping; private final Supplier defaultServiceExceptionSupplier; private final String customErrorCodeFieldName; private final SdkClientConfiguration clientConfiguration; @@ -67,6 +69,8 @@ public abstract class BaseAwsJsonProtocolFactory { protected BaseAwsJsonProtocolFactory(Builder builder) { this.protocolMetadata = builder.protocolMetadata.build(); this.modeledExceptions = unmodifiableList(builder.modeledExceptions); + this.awsQueryCompatibleErrorCodeMapping = builder.awsQueryCompatibleErrorCodeMapping == null ? null : + unmodifiableMap(builder.awsQueryCompatibleErrorCodeMapping); this.defaultServiceExceptionSupplier = builder.defaultServiceExceptionSupplier; this.customErrorCodeFieldName = builder.customErrorCodeFieldName; this.clientConfiguration = builder.clientConfiguration; @@ -124,6 +128,7 @@ public final HttpResponseHandler createErrorResponseHandler .jsonProtocolUnmarshaller(protocolUnmarshaller) .exceptions(modeledExceptions) .errorCodeParser(getSdkFactory().getErrorCodeParser(customErrorCodeFieldName)) + .awsQueryCompatibleErrorCodeMapping(awsQueryCompatibleErrorCodeMapping) .errorMessageParser(AwsJsonErrorMessageParser.DEFAULT_ERROR_MESSAGE_PARSER) .jsonFactory(getSdkFactory().getJsonFactory()) .defaultExceptionSupplier(defaultServiceExceptionSupplier) @@ -199,6 +204,7 @@ public abstract static class Builder { private Supplier defaultServiceExceptionSupplier; private String customErrorCodeFieldName; private SdkClientConfiguration clientConfiguration; + private Map awsQueryCompatibleErrorCodeMapping; protected Builder() { } @@ -214,6 +220,19 @@ public final SubclassT registerModeledException(ExceptionMetadata errorMetadata) return getSubclass(); } + /** + * Provides an error code mapping, which causes error codes in error responses to be mapped to an alternate + * error code. This mapping allows for an SDK client to be compatible with AWS Query. The given map is + * AWS Json error code to mapped error code. + * + * @param errorCodeMapping the error code mapping + * @return This builder for method chaining. + */ + public final SubclassT awsQueryCompatibleErrorCodeMapping(Map errorCodeMapping) { + this.awsQueryCompatibleErrorCodeMapping = errorCodeMapping; + return getSubclass(); + } + /** * A supplier for the services base exception builder. This is used when we can't identify any modeled * exception to unmarshall into. diff --git a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/unmarshall/AwsJsonProtocolErrorUnmarshaller.java b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/unmarshall/AwsJsonProtocolErrorUnmarshaller.java index 68c2e1ad571a..cc8ad8e643d6 100644 --- a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/unmarshall/AwsJsonProtocolErrorUnmarshaller.java +++ b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/unmarshall/AwsJsonProtocolErrorUnmarshaller.java @@ -17,6 +17,7 @@ import java.time.Duration; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkInternalApi; @@ -46,6 +47,7 @@ public final class AwsJsonProtocolErrorUnmarshaller implements HttpResponseHandl private final JsonFactory jsonFactory; private final Supplier defaultExceptionSupplier; private final ErrorCodeParser errorCodeParser; + private final Map awsQueryCompatibleErrorCodeMapping; private AwsJsonProtocolErrorUnmarshaller(Builder builder) { this.jsonProtocolUnmarshaller = builder.jsonProtocolUnmarshaller; @@ -54,6 +56,7 @@ private AwsJsonProtocolErrorUnmarshaller(Builder builder) { this.jsonFactory = builder.jsonFactory; this.defaultExceptionSupplier = builder.defaultExceptionSupplier; this.exceptions = builder.exceptions; + this.awsQueryCompatibleErrorCodeMapping = builder.awsQueryCompatibleErrorCodeMapping; } @Override @@ -72,12 +75,14 @@ private AwsServiceException unmarshall(SdkHttpFullResponse response, ExecutionAt SdkPojo sdkPojo = modeledExceptionMetadata.map(ExceptionMetadata::exceptionBuilderSupplier) .orElse(defaultExceptionSupplier) .get(); + String effectiveErrorCode = getEffectiveErrorCode(errorCode); AwsServiceException.Builder exception = ((AwsServiceException) jsonProtocolUnmarshaller .unmarshall(sdkPojo, response, jsonContent.getJsonNode())).toBuilder(); String errorMessage = errorMessageParser.parseErrorMessage(response, jsonContent.getJsonNode()); + exception.awsErrorDetails(extractAwsErrorDetails(response, executionAttributes, jsonContent, - errorCode, errorMessage)); + effectiveErrorCode, errorMessage)); exception.clockSkew(getClockSkew(executionAttributes)); // Status code and request id are sdk level fields exception.message(errorMessage); @@ -87,6 +92,13 @@ private AwsServiceException unmarshall(SdkHttpFullResponse response, ExecutionAt return exception.build(); } + private String getEffectiveErrorCode(String errorCode) { + if (awsQueryCompatibleErrorCodeMapping != null) { + return awsQueryCompatibleErrorCodeMapping.getOrDefault(errorCode, errorCode); + } + return errorCode; + } + private Duration getClockSkew(ExecutionAttributes executionAttributes) { Integer timeOffset = executionAttributes.getAttribute(SdkExecutionAttribute.TIME_OFFSET); return timeOffset == null ? null : Duration.ofSeconds(timeOffset); @@ -146,6 +158,7 @@ public static final class Builder { private JsonFactory jsonFactory; private Supplier defaultExceptionSupplier; private ErrorCodeParser errorCodeParser; + private Map awsQueryCompatibleErrorCodeMapping; private Builder() { } @@ -215,6 +228,11 @@ public Builder errorCodeParser(ErrorCodeParser errorCodeParser) { return this; } + public Builder awsQueryCompatibleErrorCodeMapping(Map awsQueryCompatibleErrorCodeMapping) { + this.awsQueryCompatibleErrorCodeMapping = awsQueryCompatibleErrorCodeMapping; + return this; + } + public AwsJsonProtocolErrorUnmarshaller build() { return new AwsJsonProtocolErrorUnmarshaller(this); }